Reputation: 366
New to PHP & OOP so bear with me... I'm in the optimistic early stages of designing and writing my first PHP & OOP website after a lifetime of writing crappy M$ VBA rubbish.
I have a class "User" which has a save method with the relevant database calls etc... (actually I have DB and DBUtils classes that handles the connection and CRUD - my business classes just call select, update, delete methods on DB Utils and pass associative arrays of data)
Anywho... My "User" class is extended by an "Admin" class which has some additional properties over and above "User" yada yada...
What is the best way to deal with the save method on "Admin"? I understand that if I add a save method to the Admin class it will supercede the one on User but I don't want it to. I want to write the save method on Admin to only deal with the properties etc that are specific to the Admin objects and for the properties inherited from "User" to be dealt with in the User save method.
Does that make sense? Is it a specific OOP pattern I'm looking for? Any help or guidance with how I should design and structure this code would be appreciated.
EDIT: Whoa! Thanks for all the answers below. Not sure which is my preferred yet. I will have to do some playing around...
Upvotes: 1
Views: 1752
Reputation: 58444
You main issue stems from fact that you have been ignoring one of core ideas in OOP: single responsibility principle .. then again, it seems like everyone who provided "answers" have no ideas what SRP is either.
What you refer to as "business logic" should be kept separate from storage related operation. Neither the instance of User
no Admin
should be aware oh how the storage is performed, because it is not business logic.
$entity = new Admin;
$mapper = new AdminMapper( $db );
$entity->setName('Wintermute');
$mapper->fetch( $entity );
//retrieve data for admin with that name
$entity->setName('Neuromancer');
$mapper->store( $entity );
// rename and save
What you see above is an extremely simplified application of data mapper pattern. The goal of this pattern is to separate business and storage logic.
If, instead of instantiating the mapper directly $mapper = new AdminMapper( $db )
, it would be provided by an injected factory $mapper = $this->mapperFactory->build('Admin')
(as it should be in proper codebase), you would have no indication about the storage medium. The data could be stored either in an SQL DB or file, or some remote REST API. If interface for mapper stays the same, you can replace it whenever you need.
The use of factory would let you avoid tight coupling to specific class names and, in case of mappers for SQL databases, let you inject in every instance same DB connection.
To learn a bit more about it, you could read this, this and this.
But, if you are seriously thinking about studying OOP, then reading this book is mandatory.
Upvotes: 5
Reputation: 22812
You are trying to implement the Active record pattern.
An approach in object persistence is to provide a common ancestor class [e.g. BasicEntity
] every subclass extends, which builds queries based on a given data schema:
class BasicEntity
{
protected $tablename;
protected $schema;
public function update()
{
$fields = "";
$placeholders = "";
foreach($this -> schema as $field => $type)
{
// you join the fields here to get something like ('username', 'email', 'enabled', 'createdAt', 'password')
// then you write your PDO statement providing placeholders like (:?, :?, :?, :?, :?)
// you'll have to bind parameters based on their $type [int, string, date]
}
$query = sprintf(
"UPDATE %s SET VALUES(%s) = %s",
$this -> tablename,
$fields,
$placeholders
);
// execute statement here, handle exceptions, and so...
}
}
So your User
class will be like:
class User extends BasicEntity
{
protected $id;
protected $username;
protected $email;
protected $password;
protected $enabled;
protected $createdAt;
public function __construct()
{
$this -> tablename = '_user';
$this -> schema = array(
'id' => 'int',
'username' => 'string',
'email' => 'string',
'password' => 'string',
'enabled' => 'int',
'createdAt' => 'datetime'
);
}
}
And your Admin
class:
class Admin extends User
{
protected $additionalProperty;
public function __construct()
{
parent::__construct();
$this -> schema['additionalProperty'] = 'string';
}
}
Calling update()
will build the right query based on class schema. This approach works with at a low complexity level, because you'll notice that:
additionalProperty
];To resolve the first, you need composition of objects, so you don't make your main table grow much [it just gets a reference to an external AdditionalPropertyList
entity, for example].
To resolve the second, you have to keep the schema in external files or using inline annotations.
To resolve the third, you'll have to write your own ORM [Object Relational Mapping], or much better switch to an existing one.
Anyway out of the learning benefits, I'd stand on the shoulder of giants and I'd pick a framework if you plan to build a scalable and maintainable application.
Upvotes: 3
Reputation: 251082
When you write your save method in the Admin class, you can perform the additional work after deferring to the parent object for the standard save...
class Admin extends User
{
// Other methods
public function save()
{
parent::save();
// now save your additional fields wherever they need to go
}
}
On a side note, while using object to represent the domain, such as User and Admin, is fine - it sometimes pays off to have the persistence logic elsewhere, for example by using The Repository Pattern. This prevents your object from being tied to the persistence mechanism.
Upvotes: 3
Reputation: 76413
Despite being new to OO, it does sound like you've hit on one of the major shortcomings of PHP's object model. What you want to do could be easily achieved by means of method overloading (the ability to have several methods with the same name, but that take different parameters, the actual member function that is invoked depends on the parameters).
You can work around this issue by checking the params in your save
method in the Admin
class:
class Admin
{
public function save(Model_Abstract $model = null)//type hinting
{
if ($model instanceof Model_Admin)
{
//do admin insert/update stuff here
return;
}
parent::save($model);//call regular method
}
}
Upvotes: 1
Reputation: 21856
There are many ways to solve this issue. One of them is having a protected method that can be overridden in a extended class to provide data from that extended class;
A Example:
class User
{
public function Save()
{
$savestuff = $this->GetProperties();
// do save acctions
}
protected function GetProperties()
{
return 'user prop';
}
}
class Admin Extends User
{
protected function GetProperties()
{
return 'admin prop';
}
}
Upvotes: 2