Right, your object is, like Rene said, just a container of functions. Though, in theory, that's what objects are for (group functionality together), the way you went about this, you're actually using classes as modules. It's not what you call SOLID code.
Edit:
Perhaps you might want to scroll down first, because throughout the answer I've used rather messy examples to explain things, but at the bottom I've added a really simple example that might help you to understand the access modifiers and inheritance better.
Anyhow, I'm not going into details on how you should refactor this to be SOLID compliant, that's part of the learning curve. The simple fact of the matter is that, yes OOP is confusing at first, and at first you might not even need constructors, access modifiers (public, protected or private), let alone destructors. Especially if you haven't come to terms with inheritance. But let me explain a few things that, hopefully, will clarify one or two things.
Class methods share state:
That's just a fancy way of saying that class methods have access to variables, without having them passed as arguments. Of course, not just any variable will do: it's the properties of the class that are accessible. In your snippet, you're passing $conn
to various methods, like you would if they were functions. The difference is, that classes can be assigned properties, too.
class RefactoredFunctions
{
private $conn = null;
public function setConn($conn)
{
$this->conn = $conn;
}
public function retrievePassword($userNAme)
{
//your code here, but replace $conn with $this->conn
}
}
Now you can call the retrievePassword
method, without having to pass the $conn
param over and over again. Though, this code would fail, if $conn
wasn't set prior to calling retrievePassword
. How can we fix that?
Enter constructors. A constructor is a method, like any other, except for the fact that it can't be called manually. It's called automatically, when an instance of the class is created. You could define a constructor that expects an argument, and pass the connection to it, ensuring the $conn
property will be set:
public function __constructor($conn)
{
$this->conn = $conn;
}
//using this class:
$instance = new RefactoredFunctions($conn);
$instance->retrievePassword('userFoo');//conn is set through constructor!
That's all a constructor does (9/10 times): setting properties your class requires to work properly. These properties are sometimes called the dependencies of that class. The class depends on them.
Now, an argument that is called $conn
is clearly supposed to be a DB connection in your case, but how do we make sure nobody passes a string? Simple: we use type-hinting, another benefit of OOP:
public function __construct(PDO $conn)
{
$this->conn = $conn;
}
//usage
$instance= new RefactoredFunctions(new PDO());//works fine
$error = new RefactoredFunctions('foobar');//fatal error string is not PDO instance
This makes your code more failsafe. Use type-hints to avoid bugs. That's an order!
Now, all this time, I've been using $this->conn
which is a property declared as private. Why? Simple. I've used type-hints to ensure the value of $this->conn
is guaranteed to be an instance of PDO
, but if I would have declared public $conn
, I'm able to access the property (and thus assign it a new value) simply bypassing the setter/constructor:
//assume public $conn
$instance= new RefactoredFunctions(new PDO());//works fine
$instance->retrievePassword('userFoo');//still OK
$instance->conn = 'reassign conn';
$instance->retrievePassword('userFoo');//ERROR!
Now, with private properties:
$instance= new RefactoredFunctions(new PDO());//works fine
$instance->conn = 'reassign conn';//ERROR!!
$instance->retrievePassword('userFoo');//will never get this far
What's the benefit of this? Simple: the bug is easy to find. Imagine if I would've reassinged the conn
property by accident, and then call a method 50 lines further down the script, or pass an instance to another class (defined in another file), and call the method there. I'd get an error, with a file & line number that contains the call, not the cause (reassignment statement).
OK, you got this far, and this is still making sense? On to protected
... Where does that tie in?
Well, the more objects you have, chances are some of them share functionality. Rather than writing the same methods several times, we extend classes from each other. So far, we have:
class RefactoredFunctions
{
private $conn = null;
public function __construct(PDO $conn)
{
$this->conn = $conn;
}
public function setConn(PDO $conn)
{
$this->conn = $conn;
return $this;
}
public function retrievePassword($userNAme)
{
//your code here, but replace $conn with $this->conn
}
}
Now, suppose I wanted a DB class per table (quite common). I have this base class, that holds a connection instance, and some methods to query a DB. What if I moved the constructor, $conn
property and setConnection
method to a generic class, and create child classes per table? that would save me 2 methods/class to write. If I have 10 tables, that's 20 methods. Of course I'll do that:
class BaseTable
{
private $conn = null;
private $tblName = null;//to hold the table their linked to
public function __construct(PDO $conn)
{
$this->conn = $conn;
}
public function setConn(PDO $conn)
{
$this->conn = $conn;
return $this;
}
public function setTable($tblName)
{//set table name for each instance
$this->tblName = $tblName;
return $this;
}
}
//a child
class UserTbl extends BaseTable
{
public function getUsers()
{
$this->conn->prepare('SELECT * FROM '.$this->table);
//etc...
}
}
//usage:
$users = new UserTbl(new PDO());
$users->getUsers();
Ok, so I didn't have to write those properties and methods, but the code above won't work! Why? because private
means the properties/methods are only visible to the class in which they were defined, in this case BaseTable
, not the child class. protected
means that properties/methods can be accessed in both the class that defined them, and its children. So, would you make the $conn
property protected? Of course NOT.
There's a number of reasons why you wouldn't do this, but I might go into those in a future edit. ATM, we have some more important things to focus on.
Have you noticed how the connection is guaranteed to be available thanks to the constructor, but the tablename isn't? yet, we're using the table name in our queries. How do we fix that? You have 3 options:
- Make
$tblName
protected and assign it a value in the child's definition, and remove the setTable
method
- override the parent constructor (less secure)
- A combination of the first two
So, if in the base class, we now have protected $tblName = null;
you can write this, in the child:
protected $tblName = 'users';
Job done. The second approach would look like this:
//in child
public function __construct(PDO $connection, $tblName)
{
$this->tblName = $tblName;
parent::__construct($connection);//explicit call required
}
To combine the two would require using abstract
, final
and all that, here's an example of this, but I'm not going into all that now.
All this time, we still have to deal with the private $conn
being invisible to the child classes. How would you deal with that? The answer to that question is surprizingly simple:
//in BaseTable
protected function getConnection()
{
return $this->conn;
}
This protected method is visible to the children, and returns the connection, so all we have to do now is write:
$this->getConnection()->prepare('SELECT ...');
instead of $this->conn
. There we are, problem solved, you should be able to get these classes working now.
Note that there's a lot of things left to be done. For example the getUsers
methods is nothing but a select * from tbl
query. We could just as well have written that in our BaseTable
class, but using another name and access modifier:
protected function getAll()
{//protected: it's for internal use only
$res = $this->conn('SELECT * FROM '.$this->tblName);
return $res->fetchAll();
}
Then, in each child class:
//UsersTable:
public function getUsers()
{
return $this->getAll();
}
//LogsTable
public function getLogs()
{
return $this->getAll();
}
You could even add arguments in the child methods, and process the return value of the getAll
method accordingly:
protected function getAll()
{
return $this->conn->query('SELECT * FROM '.$this->table);
}
//UsersTable:
public function getUsers($asArray = false)
{
$asArray = $asArray ? PDO::FETCH_ASSOC : PDO::FETCH_OBJ;
return $this->getAll()->fetchAll($asArray);
}
One last thought on inheritance: Assume the classes BasteTable
and UserTable extends BaseTable
. When using type-hinting, this is good to know:
$base = new BaseTable();
$user = new UserTable();
if ($base instanceof BaseTable) echo 'Yes';//will echo, of course
if ($user instanceof UserTable) echo 'Duh';//of course, this is true
That's as you'd expect it, but:
if ($base instanceof UserTable) echo 'WTF';//You won't see this
if ($user instanceof BaseTable) echo 'Yes';//will echo!!
So all instances of UserTable
are instances of BaseTable
, too, but not all instances of BaseTable
are instances of UserTable
. Now this might seem odd, at first but think of it like this:
class Room
{
public $door;//can be opened from outside
protected $window;//can only be opened from within ANY room, all rooms have to access this
}
class Bathroom extends Room
{
private $mirror;//this room has a mirror, can only be accessed in a bathroom
}
class Bedroom extends Room
{
private $bed;//this one has a bed, can only be used when inside a bedroom
}
Now you see: both a bedroom and a bathroom are rooms, but a bedroom is no bathroom, hence:
$bath = new Bathroom;//instanceof Room, not instanceof Bedroom
$bed = new Bedroom;//instanceof Room, not instanceof Bathroom
$genericRoom = new Room;//instanceof Room
//not specified which, so no mirror or bed, so not instance of any child
I hope this clears one or two things up for you. If you have a follow up question, don't hesitate to ask. I'm not going to add any more info, as this is quite a lot to deal with already