A web developer's blog. PHP, MySQL, CakePHP, Zend Framework, Wordpress, Code Igniter, Django, Python, CSS, Javascript, jQuery, Knockout.js, and other web development topics.

A Follow-up on the Zend Framework Quickstart Tutorial: The Model

The model is probably the most difficult to concept to grasp in MVC (Read Surviving The Deep End). This post is very rough and un-editted and is based on my experience.

I have gone through the new Zend Framework Quickstart Tutorial just a few days ago, and I found it hard to adapt to the new “data mapper” implementation in found in the models. Further reading will tell you that a Data Mapper is responsible for mapping a domain object to the database.

Quoted from MartinFowler.com

Objects and relational databases have different mechanisms for structuring data. Many parts of an object, such as collections and inheritance, aren’t present in relational databases. When you build an object model with a lot of business logic it’s valuable to use these mechanisms to better organize the data and the behavior that goes with it. Doing so leads to variant schemas; that is, the object schema and the relational schema don’t match up.

You still need to transfer data between the two schemas, and this data transfer becomes a complexity in its own right. If the in-memory objects know about the relational database structure, changes in one tend to ripple to the other.

The Data Mapper is a layer of software that separates the in-memory objects from the database. Its responsibility is to transfer data between the two and also to isolate them from each other. With Data Mapper the in-memory objects needn’t know even that there’s a database present; they need no SQL interface code, and certainly no knowledge of the database schema. (The database schema is always ignorant of the objects that use it.) Since it’s a form of Mapper (473), Data Mapper itself is even unknown to the domain layer.

In my example, I will be using a Model named “Group”. The group will contain an Id, Name, and Description. The SQL for this table will look something like this:

CREATE TABLE  `test_databases`.`group` (
    `id` INT NOT NULL AUTO_INCREMENT ,
    `name` VARCHAR( 100 ) NOT NULL ,
    `description_text` VARCHAR( 255 ) NOT NULL ,
    PRIMARY KEY (  `id` )
)

The Group Model
My group model that is found in application/models/Group.php contains something like this:

class Default_Model_Group 
{
    protected $_id; //a field
    protected $_name; //a field
    protected $_description_text; //a field
    protected $_mapper; //the mapper
 
    public function __construct(array $options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        }
    }
 
    //__set() and __get() are built-in in PHP
    //they are called Magic Methods
    //http://us2.php.net/manual/en/language.oop5.magic.php
    public function __set($name, $value)
    {
        //Section A
        //What we are trying to do here is remove the underscores and camelCase the $name
        //so that we can call the $name as a Method within our class
        $pieces = explode('_', $name);
        foreach($pieces AS $key => $row) {
            $pieces[$key] = ucfirst($row);
        }
 
        //Section B
        //so $method below would contain:
        //$method = 'setDescriptionText' when you $obj->description_text = 'My Desc...'
        //we will look into this more in the following paragraphs...
        $name = implode('',$pieces);
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid group property');
        }
 
        //Section C
        //The code below will call the ff:
        //$this->setDescriptionText($value);
        //because $method is equal to "setDescriptionText".
        //setDescriptionText was created in Section A of this code snippet
        $this->$method($value);
    }
 
    //__get has the same idea as __set
    public function __get($name)
    {
        //again, we strip the underscores and camelCase
        $pieces = explode('_', $name);
        foreach($pieces AS $key => $row) {
            $pieces[$key] = ucfirst($row);
        }
        $name = implode('',$pieces);
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid group property');
        }
        //then we call the method...
        //For example: $this->getDescriptionText()
        return $this->$method();
    }
 
    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }
 
    public function setName($text)
    {
        $this->_name = (string) $text;
        return $this;
    }
 
    public function getDescriptionText()
    {
        return $this->_description_text;
    }
 
    public function setDescriptionText($text)
    {
        $this->_description_text = (string) $text;
        return $this;
    }
 
    public function getName()
    {
        return $this->_name;
    }
 
    public function setId($id)
    {
        $this->_twitter_id = (int) $id;
        return $this;
    }
 
    public function getId()
    {
        return $this->_id;
    }
 
    {
        $this->_mapper = $mapper;
        return $this;
    }
 
    public function getMapper()
    {
        if (null === $this->_mapper) {
            $this->setMapper(new Default_Model_GroupMapper());
        }
        return $this->_mapper;
    }
 
    public function save()
    {
        $this->getMapper()->save($this);
    }
 
    public function find($id)
    {
        $this->getMapper()->find($id, $this);
        return $this;
    }
 
    public function fetchAll()
    {
        return $this->getMapper()->fetchAll();
    }
 
    public function fetchGroup($id)
    {
        return $this->getMapper()->fetchGroup($id);
    }
}

Wow, I didn’t expect it to be that long. Anyway, here is our Mapper.

This is also found inside: application/models/GroupMapper.php

<?php
 
class Default_Model_GroupMapper
{
    protected $_dbTable;
 
    public function setDbTable($dbTable)
    {
        if (is_string($dbTable)) {
            $dbTable = new $dbTable();
        }
        if (!$dbTable instanceof Zend_Db_Table_Abstract) {
            throw new Exception('Invalid table data gateway provided');
        }
        $this->_dbTable = $dbTable;
        return $this;
    }
 
    public function getDbTable()
    {
        if (null === $this->_dbTable) {
            $this->setDbTable('Default_Model_DbTable_Group');
        }
        return $this->_dbTable;
    }
 
    public function save(Default_Model_Group $obj)
    {
        $data = array(
            'name'   => $obj->getName(),
            'description_text' => $obj->getDescriptionText()
        );
 
        if (null === ($id = $obj->getId())) {
            unset($data['id']);
            $this->getDbTable()->insert($data);
        } else {
            $this->getDbTable()->update($data, array('id = ?' => $id));
        }
    }
 
    public function find($id, Default_Model_Group $obj)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return;
        }
        $row = $result->current();
        $twitter->setId($row->id)
                  ->setName($row->name)
                  ->setDescriptionText($row->description_text);
    }
 
    public function fetchAll()
    {
        $resultSet = $this->getDbTable()->fetchAll();
        $entries   = array();
        foreach ($resultSet as $row) {
            $entry = new Default_Model_Group();
            $entry->setId($row->id)
                  ->setName($row->name)
                  ->setDescriptionText($row->description_text);
                  ->setMapper($this);
            $entries[] = $entry;
        }
        return $entries;
    }
 
    public function fetchGroup($id)
    {
        $quoted = $this->getDbTable()->getAdapter()->quote($id,'INTEGER');
        $resultSet = $this->getDbTable()->fetchAll('id = '.$quoted);
        return $resultSet;
    }
}

And finally, our Default_Model_DbTable_Group that is found in: application/models/DbTable/Group.php has only the following lines…

<?php
 
class Twitter_Model_DbTable_Group extends Zend_Db_Table_Abstract 
{
    protected $_name = 'group';
}

In my controller, I would just do something like this:

<?php
 
class GroupController extends Zend_Controller_Action
{
    protected $_flashMessenger = null;
 
    public function init()
    {
        /* Initialize action controller here */
        $this->_flash_messenger = $this->_helper->FlashMessenger;
    }
 
    public function indexAction()
    {
        // action body
    }
 
    public function saveAction()
    {
        $this->_helper->viewRenderer->setNoRender();
        $this->_helper->layout()->disableLayout();
 
        $group_name = $this->getRequest()->getPost('group_name');
        $group_desc = $this->getRequest()->getPost('group_desc');
 
        $group_model = new Default_Model_Group();
        $group_model->name= $group_name; //this is what I have been trying to tell from Section A above
        $group_model->description_text= $group_desc; //and this too!!!
        $group_model->save();
 
        $this->_flash_messenger->addMessage('You have added group: <b>'.$group_name.'</b>');
        $this->_redirect('/group'); 
    }
}
This entry was posted in General and tagged , , , , , , , . Bookmark the permalink.

5 Responses to A Follow-up on the Zend Framework Quickstart Tutorial: The Model

  1. Thai Bui says:

    I’ve noticed a lots of mistake in your article, I’ll come from bottom to top:
    - function saveAction(), in line $group_model->save($data), where is $data come from? it should be $this->save() instead right, cause you already set the attributes for the entity.
    - function init(), line $this->_helper->FlashMessenger;, again, where is FlashMessenger?
    - And what is your intention when split your model into 3 different classes (DAO, DTO and Business Object?).
    In my opinion, which such a simple case like that, just 1 or 2 classes is enough, for example:
    class Group extends Zend_Db_Table {
    protected $_name = ‘group’;

    // The most simple case, with out validation
    public function addNewGroup($group){
    $this->insert($group);
    }

    // A bit more complext, pass and object that already validated
    // The object
    public function addNewGroup(Default_Model_Group $group){
    $this->insert(array(
    ‘name’ => $group->getName(),
    ‘description’ => $group->getDescription()
    ));
    }
    }

    Then in the controller, it’s more simple than your code, just receive the data, and pass it to the Default_Model_Group or impact it directly into Group without validation.

  2. Wenbert says:

    Thanks for taking your time to comment on the mistakes of this article Thai.

    I have corrected the

    //$group_model->save($data);
    $group_model->save();

    For the init() method, I think FlashMessenger is a built-in view helper that you can use out-of-box. I am using ZF 1.8.x and the FlashMessenger seemed to work when I:

    $this->_flash_messenger = $this->_helper->FlashMessenger;

    I agree with you. Most of the cases I have encountered, 2 classes is enough. One is a class that extends Zend_Db_Table and another that extends Zend_Db_Table_Abstract. This article is sort of like a follow-up on the Zend Framework Quickstart which uses 3 different classes for one model.

    It is always good to know other ways of implementing models. I do not have in-depth knowledge of the different design patterns, so this article also serves as a “note” to me when I try to remember things. It is part of my learning process.

  3. Wenbert says:

    … the comments CSS is messed up. I will fix this later. Have to work ;-)

  4. Pingback: Construyendo capas Models sólidas en MVC Zend Framework « Olagato’s blog

  5. Marcio says:

    Thanks a lot for this article. There is something that I don’t understand. It seems that, when, on your Mapper you have: setDbTable($dbTable) that $dbTable seems to be ready to receive ANY table name, however, that Mapper should be related to ONE table only or am I wrong ? Please advice.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>