3

I'm programming in PHP procedurally (is this even a word?) for about five years now and decided to try an OOP approach but ran into some concept/design problems. Let's say you have some modules in the program, every module has the possibility to list, add, edit and delete an entity. An entity can be..dunno, user, client, product etc.

How would you design the classes to manipulate these entityes?

Two possibilities came in my mind:

  • create classes for every entity with methods like getUsersList, addUser, editUser, delUser
    This approach seems resource consumingbecause in the script for the listing you only need the getUsersList and maybe delUser methods, while in the add user popup script, you only need the addUser method and in the edit user popup script only the editUser method. So, you have to instanciate an object and only use two or one of it's methods...
  • create general classes: listing, add, edit and delete and extend them for every entity this way you only have to instanciate one class at a time (the one you really need)

Thanks in advance,

Trufa
  • 39,971
  • 43
  • 126
  • 190
Catalin
  • 858
  • 5
  • 16
  • 1
    *(related)* [Learning PHP Class](http://stackoverflow.com/questions/2206387/learning-php-class/2206835#2206835) – Gordon Jan 26 '11 at 14:35

4 Answers4

5

I would create an interface defining your list, add, edit, and delete methods. This gives you a class "template". If your classes (User, Client, Product, etc.) implement this interface, then the methods in the interface must be defined in those classes.

This will give you a similar "API" to access all the functionality of every class that implements your interface. Since each of your listed objects contains different data, the details of the methods will be different, and thus separate, but the interface will be the same.

Aside:

Your inclusion of "list" in your list of methods concerns me a little. It seems to imply that you are seeing your objects as collections of Users, Clients, Products, etc, where there should most likely be a User class that represents a single user, a Client class that represents a single client, etc.

On the other hand, "list" may be handled as a static method - a method that can be called without an instance of the class.

$bob = new User('bob');
$bob->add(); // Adds bob to the database
$fred = new User('fred');
$fred->add(); // Adds fred to the database

$users = User::list(); // Gives an array of all the users in the database

That's how I would handle things, anyway.

Ryan Kinal
  • 17,414
  • 6
  • 46
  • 63
  • User and Users should be separate entities, as you might want to do `User->delete()`, but you may also want `Users->delete()` to remove multiple users... and as you said, `Users` should be a collection, should be iterable and should have `length()` property..as opposed to a single `User` – Quamis Jan 26 '11 at 14:30
  • Depending on what you want `User->add`, `Client->add`, and `Product->add()` to look like, it may not be part of your interface, as classes have to implement interface methods with the *exact* method signature. – Ryan Kinal Jan 26 '11 at 14:32
2

You will need to create a solid architecture and framework for managing your data model. This is not easy and will only get more complex as the data model grows. I would highly recommend using a PHP framework (Symfony, CakePHP, etc), or at least, an ORM (Doctrine, Propel, etc).

If you still want to roll your own, I would start with an architecture similar to below.

You will want a DbRecord class that is used for individual record operations (like saving, deleting, etc). This DbRecord class will be extended by specific entities and will provide the foundation for basic entity operations.

class DbRecord {
    public function save() {
        // save logic (create or update)
    }

    public function delete() {
        // delete logic
    }

    // other record methods
}

class User extends DbRecord {
    private $name;
    private $email;

    public function setName($name_) {
        $this->name = $name_;
    }

    public function setEmail($email_) {
        $this->email = $email_;
    }
}

From which, you can perform individual record operations:

$user = new User();
$user->setName('jim');
$user->setEmail('jim@domain.com');

$user->save();

You will now want a DbTable class that is used for bulk operations on the table (like reading all entities, etc).

class DbTable {
    public function readAll() {
        // read all
    }

    public function deleteAll() {
        // delete all logic
    }

    public function fetch($sql) {
        // fetch logic
    }

    // other table methods
}

class UserTable extends DbTable {
    public function validateAllUsers() {
        // validation logic
    }

    // ...
}

From which, you can perform bulk/table operations:

$userTable = new UserTable();
$users = $userTable->readAll();

foreach ($users as $user) {
    // etc
}

Code architecture is the key to a website scaling properly. It is important to divide the data model into the appropriate classes and hierarchy.

Again, as your website grows, it can get very complicated to manage the data model manually. It is then when you will really see the benefit of a PHP framework or ORM.

NOTE: DbRecord and DbTable are arbitrary names - use w/e name you like.

Stephen Watkins
  • 25,047
  • 15
  • 66
  • 100
1

Use your first method, where you create a reusable object with methods. It is not a waste of time as you only code it once.

class User {
    function __construct() { /* Constructor code */ }
    function load($id) { ... }
    function save() { ... }
    function delete() { ... }
}
Josh K
  • 28,364
  • 20
  • 86
  • 132
  • Yes, but isn't it a waste of resources? Instanciating the whole object and keeping it in memory when you only need half or less of it? Try scaling this to a much larger scale.. – Catalin Jan 26 '11 at 14:10
  • @Catalin: OOP by default is more bulky then procedural code. Look at ORM's, they are horribly inefficient. However it speeds development and makes the code cleaner. – Josh K Jan 26 '11 at 14:17
  • Yes, thought so, but thinking about it...you could just create a naming rule for functions and work procedurally..create an user.utils.php with functions like user_load();user_save(); user_delete(); – Catalin Jan 26 '11 at 14:25
  • @Catalin: Sure you could, however then they wouldn't be interacting on the same object. You would have to pass a second object around between the functions. – Josh K Jan 26 '11 at 15:15
1

You're on the right track with 'general classes' (also called base classes, or abstract classes in case their behaviour NEEDS to be complemented by child classes before they can be put to use).

The OOP approach would be to put all behavior that is common to all entities in the base classes.

If you use something akin to ActiveRecord, you already have a general (abstract) interface for create-update-delete operations. Use that to your advantage, and let your base classes operate ONLY on those interface methods. They don't need to know they are updating a Product, or a a User, they just need to know they can call the update() method on an entity.

But even without using something quite feature-heavy like an AR framework (check out Doctrine if you're interested in a very flexible ORM..) you can use interfaces to abstract behavior.

Let me give you a more elaborate example...


/**
 * Interface for all entities to use
 */
interface Entity {
    static function newEntity();
    static function fetch($id);
    function save();
    function setProperties(array $properties);
    function delete();
}


/**
 * A concrete product entity which implements the interface
 */
class Product implements Entity {
    public $productId;
    public $name;
    public $price;
    public $description;

    /**
     * Factory method to create a new Product
     *
     * @param integer $id Optional, if you have auto-increment keys you don't need to set it
     * @return Product
     */
    public static function newEntity($id=NULL) {
        $product = new Product();
        $product->productId = $id;
        return $product;
    }

    /**
     * Factory method to fetch an existing entity from the database
     *
     * @param integer $id
     * @return Product
     */
    public static function fetch($id) {
        // make select with supplied id
        // let $row be resultset
        if (!$row) {
            return NULL; // you might devise different strategies for handling not-found cases; in this case you need to check if fetch returned NULL
        }

        $product = new Product();
        $product->productId = $id;
        $product->name = $row['name'];
        $product->price = $row['price'];
        $product->description = $row['description'];
        return $product;
    }

    /**
     * Update properties from a propreties array
     * @param array $properties
     * @return void
     */
    public function setProperties(array $properties) {
        $this->name = $properties['name'];
        $this->price = $properties['price'];
        $this->description = $properties['description'];
    }

    public function save() {
        // save current product properties to database
    }

    public function delete() {
        // delete product with $this->productId from database
    }
}

/**
 * An abstract CRUD controller for entities
 */
abstract class EntityCrudController {
    protected $entityClass = 'UNDEFINED'; // Override this property in child controllers to define the entity class name
    protected $editTemplate = NULL; // Override this to set an edit template for the specific entity
    protected $templateEngine; // Pseudo-Templating engine for this example

    /**
     *  Display the edit form for this entity
     * @param integer $entityId
     * @return string
     */
    public function editAction($entityId) {
        // Fetch entity - this is not the most clean way to fetch, you should probably consider building a factory that encapsulates this.
        $entity = call_user_func($this->entityClass, 'fetch', $entityId);

        // Assign entity to your edit template, in this example I'm assuming we're using a template engine similar to Smarty
        // You can generate the HTML output in any other way you might like to use.
        $this->templateEngine->setTemplate($this->editTemplate);
        $this->templateEngine->assign('entity', $entity);
        return $this->template->render();
    }

    /**
     * Update an existing entity
     *
     * @param integer $entityId
     * @param array $postArray
     * @return string
     */
    public function updateAction($entityId, array $formArray) {
        // Be sure to validate form data first here, if there are errors call $this->editAction() instead and be sure to set some error information
        $entity = call_user_func($this->entityClass, 'fetch', $entityId);
        $entity->setProperties($formArray);
        $entity->save();

        // Again, using our imaginary templating engine to display...
        $this->templateEngine->setTemplate($this->editTemplate);
        $this->templateEngine->assign('entity', $entity);
        $this->templateEngine->assign('message', 'Saved successfully!');
        return $this->template->render();
    }

    // Devise similar generic methods for newAction/insertAction here
}


/**
 * Concrete controller class for products
 * This controller doesn't do much more than extend the abstract controller and override the 2 relevant properties.
 */
class ProductCrudController extends EntityCrudController {
    protected $entityClass = 'Product';
    protected $editTemplate = 'editProduct.tpl';
}

// Usage example:

// Display edit form:
$controller = new ProductCrudController();
$htmlOutput = $controller->editAction(1);

// Save product:
$htmlOutput = $controller->updateAction(1, array('name' => 'Test Product', 'price' => '9.99', 'description' => 'This is a test product'));

Of course, there is much to improve.. e.g. you generally don't want to make a query everytime you call fetch() on an entity, but instead only query once and store the resulting object in an IdentityMap, which also ensures data integrity.

Hope this helps, got a bit more than I intended, but I think it's commendable you try to tackle this without throwing a framework on the problem :)

Manuel Strausz
  • 226
  • 1
  • 7