1

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...

Kenilik
  • 266
  • 7
  • 16
  • 2
    Even if you override the `save` method in the Admin class, you can still call the one in the parent class (i.e. User) by doing `parent::save()`. – Kemal Fadillah Sep 24 '12 at 11:42
  • 1
    Why not take an ready ORM? For example, my favourite: [RedBean](http://redbeanphp.com/#/Tutorial) It's super easy to use and offers quick development. – Mārtiņš Briedis Sep 24 '12 at 11:42

5 Answers5

5

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.

Community
  • 1
  • 1
tereško
  • 56,151
  • 24
  • 92
  • 147
  • "then again, it seems like everyone who provided "answers" have no ideas what SRP is either" - to be fair I believe I covered this. Also, the choice between the repository pattern and an active object pattern doesn't mean something isn't object-oriented - given that we don't know if there is anything other than persistence in these classes it may not violate SRP. – Fenton Sep 25 '12 at 11:31
  • @tereško I'm interested in that for my app, for this question you propose 2 different mappers AdminMapper and UserMapper? how would you implement them: the 1st one extending 2nd, overriding its save() method? –  Sep 25 '12 at 20:50
  • @cyril , why this obsession with extending classes ?! If you 3 methods: `save()`, `open()` and `delete()`, and they are completely different for `AdminMapper` and `UserMapper` then what is the point in extending ? – tereško Sep 25 '12 at 21:29
3

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.

Fenton
  • 206,497
  • 63
  • 356
  • 369
3

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:

  • if you extend your entities [on the same table!], you need to provide empty table fields even for rows that don't have such class fields [in this case, additionalProperty];
  • if your schema changes [e.g. you change a variable name], you have to hardcode it into class constructor, making it harder to maintain;
  • if you want to handle relationship between entities, it will be a big pain to write proper joins in every SELECT statement, unless you just write a lot of separate queries, tearing performance down.

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.

Community
  • 1
  • 1
moonwave99
  • 19,895
  • 2
  • 38
  • 62
2

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';
  }
} 
JvdBerg
  • 21,117
  • 8
  • 31
  • 54
1

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
    }
}
Elias Van Ootegem
  • 67,812
  • 9
  • 101
  • 138