Sunday, March 6, 2011

How to Design Domain Layer Objects to Represent Multiple Objects & Single Object in Zend Framework?

I'm working on creating a domain layer in Zend Framework that is separate from the data access layer. The Data Access Layer is composed to two main objects, a Table Data Gateway and a Row Data Gateway. As per Bill Karwin's reply to this earlier question I now have the following code for my domain Person object:

class Model_Row_Person
{
    protected $_gateway;

    public function __construct(Zend_Db_Table_Row $gateway)
    {
     $this->_gateway = $gateway;
    }

    public function login($userName, $password)
    {

    }

    public function setPassword($password)
    {

    }
}

However, this only works with an individual row. I also need to create a domain object that can represent the entire table and (presumably) can be used to iterate through all of the Person's in the table and return the appropriate type of person (admin, buyer, etc) object for use. Basically, I envision something like the following:

class Model_Table_Person implements SeekableIterator, Countable, ArrayAccess
{
    protected $_gateway;

    public function __construct(Model_DbTable_Person $gateway)
    {
     $this->_gateway = $gateway;
    }

    public function current()
    {
     $current = $this->_gateway->fetchRow($this->_pointer);

     return $this->_getUser($current);
    }

    private function _getUser(Zend_Db_Table_Row $current)
    {
     switch($current->userType)
     {
      case 'admin':
       return new Model_Row_Administrator($current);
       break;
      case 'associate':
       return new Model_Row_Associate($current);
       break;
     }
    }
}

Is this is good/bad way to handle this particular problem? What improvements or adjustments should I make to the overall design?

Thanks in advance for your comments and criticisms.

From stackoverflow
  • I had in mind that you would use the Domain Model class to completely hide the fact that you're using a database table for persistence. So passing a Table object or a Row object should be completely under the covers:

    <?php
    require_once 'Zend/Loader.php';
    Zend_Loader::registerAutoload();
    
    $db = Zend_Db::factory('mysqli', array('dbname'=>'test',
        'username'=>'root', 'password'=>'xxxx'));
    Zend_Db_Table_Abstract::setDefaultAdapter($db);
    
    class Table_Person extends Zend_Db_Table_Abstract
    {
        protected $_name = 'person';
    }
    
    class Model_Person
    {
        /** @var Zend_Db_Table */
        protected static $table = null;
    
        /** @var Zend_Db_Table_Row */
        protected $person;
    
        public static function init() {
            if (self::$table == null) {
                self::$table = new Table_Person();
            }
        }
    
        protected static function factory(Zend_Db_Table_Row $personRow) {
            $personClass = 'Model_Person_' . ucfirst($personRow->person_type);
            return new $personClass($personRow);
        }
    
        public static function get($id) {
            self::init();
            $personRow = self::$table->find($id)->current();
            return self::factory($personRow);
        }
    
        public static function getCollection() {
            self::init();
            $personRowset = self::$table->fetchAll();
            $personArray = array();
            foreach ($personRowset as $person) {
                $personArray[] = self::factory($person);
            }
            return $personArray;
        }
    
        // protected constructor can only be called from this class, e.g. factory()
        protected function __construct(Zend_Db_Table_Row $personRow) {
            $this->person = $personRow;
        }
    
        public function login($password) {
            if ($this->person->password_hash ==
                hash('sha256', $this->person->password_salt . $password)) {
                return true;
            } else {
                return false;
            }
    
        }
    
        public function setPassword($newPassword) {
            $this->person->password_hash = hash('sha256',
                $this->person->password_salt . $newPassword);
            $this->person->save();
        }
    }
    
    class Model_Person_Admin extends Model_Person { }
    class Model_Person_Associate extends Model_Person { }
    
    $person = Model_Person::get(1);
    print "Got object of type ".get_class($person)."\n";
    $person->setPassword('potrzebie');
    
    $people = Model_Person::getCollection();
    print "Got ".count($people)." people objects:\n";
    foreach ($people as $i => $person) {
        print "\t$i: ".get_class($person)."\n";
    }
    

    "I thought static methods were bad which is why I was trying to create the table level methods as instance methods."

    I don't buy into any blanket statement that static is always bad, or singletons are always bad, or goto is always bad, or what have you. People who make such unequivocal statements are looking to oversimplify the issues. Use the language tools appropriately and they'll be good to you.

    That said, there's often a tradeoff when you choose one language construct, it makes it easier to do some things while it's harder to do other things. People often point to static making it difficult to write unit test code, and also PHP has some annoying deficiencies related to static and subclassing. But there are also advantages, as we see in this code. You have to judge for yourself whether the advantages outweigh the disadvantages, on a case by case basis.

    "Would Zend Framework support a Finder class?"

    I don't think that's necessary.

    "Is there a particular reason that you renamed the find method to be get in the model class?"

    I named the method get() just to be distinct from find(). The "getter" paradigm is associated with OO interfaces, while "finders" are traditionally associated with database stuff. We're trying to design the Domain Model to pretend there's no database involved.

    "And would you use continue to use the same logic to implement specific getBy and getCollectionBy methods?"

    I'd resist creating a generic getBy() method, because it's tempting to make it accept a generic SQL expression, and then pass it on to the data access objects verbatim. This couples the usage of our Domain Model to the underlying database representation.

    Bill Karwin : BTW, I used this as an exercise to learn how to code PHP in the new NetBeans 6.5. It rocks!
    Noah Goodrich : Two Questions: 1) I thought static methods were bad which is why I was trying to create the table level methods as instance methods. Would Zend Framework support a Finder class?
    Noah Goodrich : 2) Is there a particular reason that you renamed the find method to be get in the model class? And would you use continue to use the same logic to implement specific getBy and getCollectionBy methods?
    Noah Goodrich : I guess that was really three questions :-)
    Noah Goodrich : Thank you very much for your responses. To clarify the final questions, I am thinking to do actually the opposite, I will want to build a function getCollectionByUserType('admin') for example or getByUserName('albus'). Is it a problem to create a static method for each use case?
    Bill Karwin : Oh I think it's fine to make methods to do specific operations, I was just cautioning against a too-generic method like getBy('username = \'albus\'')
    Noah Goodrich : And you don't think it would cause bloat to create a bunch of static get functions?
    Bill Karwin : There's probably a relatively short list of things that make sense to do with a given Domain Model. And internally, those methods can call into a common protected method, which reduces code duplication.
    Noah Goodrich : Thanks again for all your help on this Bill. Your suggestions have been terrific!
    Bill Karwin : Cool! Glad to help.

0 comments:

Post a Comment