Catalin
Catalin

Reputation: 868

OOP approach in PHP

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:

Thanks in advance,

Upvotes: 3

Views: 1924

Answers (4)

Manuel Strausz
Manuel Strausz

Reputation: 226

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 :)

Upvotes: 1

Stephen Watkins
Stephen Watkins

Reputation: 25775

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('[email protected]');

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

Upvotes: 2

Ryan Kinal
Ryan Kinal

Reputation: 17732

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.

Upvotes: 5

Josh K
Josh K

Reputation: 28893

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() { ... }
}

Upvotes: 1

Related Questions