0

I have a PHP MVC application using Zend Framework. As presented in the quickstart, I use 3 layers for the model part :

  • Model (business logic)
  • Data mapper
  • Table data gateway (or data access object, i.e. one class per SQL table)

The model is UML designed and totally independent of the DB.

My problem is : I can't have multiple instances of the same "instance/record".

For example : if I get, for example, the user "Chuck Norris" with id=5, this will create a new model instance wich members will be filled by the data mapper (the data mapper query the table data gateway that query the DB). Then, if I change the name to "Duck Norras", don't save it in DB right away, and re-load the same user in another variable, I have "synchronisation" problems... (different instances for the same "record")

Right now, I use the Multiton / Identity Map pattern : like Singleton, but multiple instances indexed by a key (wich is the user ID in our example). But this is complicating my developpement a lot, and my testings too.

How to do it right ?

Matthieu Napoli
  • 48,448
  • 45
  • 173
  • 261

3 Answers3

1

Identity Map

Edit

In response to this comment:

If I have a "select * from X", how can I skip getting the already loaded records ?

You can't in the query itself, but you can in the logic that loads the rows into entity objects. In pseudo-code:

class Person {}

class PersonMapper {
  protected $identity_map = array();
  function load($row) {
    if (!isset($this->identity_map[$row['id']])) {
      $person = new Person();
      foreach ($row as $key => $value) {
        $person->$key = $value;
      }
      $this->identity_map[$row['id']] = $person;
    }
    return $this->identity_map[$row['id']];
  }
}

class MappingIterator {
  function __construct($resultset, $mapper) {
    $this->resultset = $resultset;
    $this->mapper = $mapper;
  }
  function next() {
    $row = next($this->resultset);
    if ($row) {
      return $this->mapper->load($row);
    }
  }
}

In practice, you'd probably want your MappingIterator to implement Iterator, but I skipped it for brevity.

troelskn
  • 115,121
  • 27
  • 131
  • 155
  • ... and the problem is when I need to execute specific queries (select where ...), I have to bypass the Identity map wich can cause multiple instance for the same "identity" – Matthieu Napoli Mar 23 '10 at 15:04
  • Sounds like you're implementing it in an odd way then. If you use the identity map in the hydration phase, you can just skip those records that are already in the map. – troelskn Mar 24 '10 at 09:43
  • If I have a "select * from X", how can I skip getting the already loaded records ? – Matthieu Napoli Mar 24 '10 at 12:39
0

Keep all loaded model instances in "live model pool". When you load/query a model, first check if it has been already loaded into pool (use primary key or similar concept). If so, return the object (or a reference) from pool. This way all your references point to the same object. My terminology may be incorrect but hopefully you get the idea. Basically the pool acts as a cache between business logic and database.

jholster
  • 5,066
  • 1
  • 27
  • 20
  • Yes I get the idea, this is exactly what I've been doing. But the problem is when you want, for example, the list of all users, you can't use the "pool", so you have to do a SQL query, but then you will have multiple instances for the same user. The solution would be to always check the pool, but this is a lot of work, not practical... And you have to always keep that in mind or you'll have a debugging nightmare – Matthieu Napoli Mar 23 '10 at 14:27
  • So basically we are talking about advantages / disadvantes of an ORM. It's not necessary a debugging nightmare if implemented properly, but there will always be a performance hit. The pool checking part is of course done transparently in your ORM layer, so you don't have to worry about that in the business logic layer. A related [question][1]. [1]: http://stackoverflow.com/questions/108699/good-php-orm-library – jholster Mar 24 '10 at 11:59
  • Your link directed me to Xyster (http://xyster.libreworks.net/documentation/guide/xyster.orm.relation.html), wich is EXACTLY what I am trying to develop, but, of course, in better. THANK YOU – Matthieu Napoli Mar 24 '10 at 13:59
  • I'm glad to hear that. Personally I've been searching for the Holy Grail of PHP ORM for ages and been developing few ones, but still haven't found it ;) – jholster Mar 24 '10 at 21:40
  • Well after looking at Xyster, it seems to be a "one-man" project. But I read that Doctrine 2 will do exactly that, so i'll wait... – Matthieu Napoli Mar 25 '10 at 10:10
0

Multiton

Best option if you want to use a variety of singletons in your project.

<?php
abstract class FactoryAbstract {
    protected static $instances = array();
    public static function getInstance() {
        $className = static::getClassName();
        if (!(self::$instances[$className] instanceof $className)) {
            self::$instances[$className] = new $className();
        }
        return self::$instances[$className];
    }
    public static function removeInstance() {
        $className = static::getClassName();
        if (array_key_exists($className, self::$instances)) {
            unset(self::$instances[$className]);
        }
    }
    final protected static function getClassName() {
        return get_called_class();
    }
    protected function __construct() { }
    final protected function __clone() { }
}
abstract class Factory extends FactoryAbstract {
    final public static function getInstance() {
        return parent::getInstance();
    }
    final public static function removeInstance() {
        parent::removeInstance();
    }
}
// using:
class FirstProduct extends Factory {
    public $a = [];
}
class SecondProduct extends FirstProduct {
}
FirstProduct::getInstance()->a[] = 1;
SecondProduct::getInstance()->a[] = 2;
FirstProduct::getInstance()->a[] = 3;
SecondProduct::getInstance()->a[] = 4;
print_r(FirstProduct::getInstance()->a);
// array(1, 3)
print_r(SecondProduct::getInstance()->a);
// array(2, 4)