📜 ⬆️ ⬇️

Implementing an Identity Map Template in the Yii Framework

Good day, habrasoobschestvo!

Problem


When working with a database or simply with objects that are accessible from different parts of your application, there is a danger that objects that would seem to be equal at all are not.

For example, let's say we have some ActiveRecord model - Expence and here is the code:
$modelOne = Expence::model()->findByPk(10); $modelTwo = Expence::model()->findByPk(10); var_dump($modelOne === $modelTwo); //  false 

Thus, changing one model, we will in no way touch the second (which is logical, since they refer to different objects).
 $modelOne->someField = "Data"; $modelOne->save(); /// ...- ... echo $modelTwo->someField; //    $modelTwo->save(); //     


')

Decision


To solve this problem, we will use a design template called Martin Fowler Identity Map .
His idea is to track the presence in the application of objects having the same identifier. Thus, if we request a model with id equal to 5 in two different places in the program, we will get a link to the same object.

Implementation


Unfortunately, Yii does not monitor whether we have requested any object from the database or not, so we will have to write our own class.
 <?php /** * Singleton class to manipulate instances of models (eg CActiveRecord). * * @author Yuriy Ratanov <organium@gmail.com> */ class ObjectWatcher { /** * Current instance of ObjectWatcher * @var ObjectWatcher */ private static $_instance; /** * Array of objects to work with. * @var array */ private $objects = array(); /** * Geting instance of ObjectWatcher. * @return ObjectWatcher */ static function getInstance(){ if(!isset(self::$_instance)){ self::$_instance = new ObjectWatcher; } return self::$_instance; } /** * Getting instance of the object existing in the current application. * @param string $className * @param int $id * @return mixed null or object of the class $className with an id = $id if it exists. */ static function getRecord($className, $id) { $inst = self::getInstance(); $key = "$className.$id"; if(isset($inst->objects[$key])){ return $inst->objects[$key]; } return null; } /** * Adding object to ObjectWatcher registry. * @param $obj * @param int $id */ static function addRecord($obj, $id) { $inst = self::getInstance(); $inst->objects[$inst->getKey($obj, $id)] = $obj; } function getKey($obj, $id){ return get_class($obj).'.'.$id; } } 


Using the addRecord and getRecord methods, we add and obtain a model from a kind of register of models (which is an associative array of the form “nameclass.id” => object).

Now it is necessary to force Yii to create an object, if it has not yet been received, or to return the existing one from the $ objects array. We will try to do this with the help of Yii itself, without creating any extra layers after CActiveRecord. Of course, I would like later, when running Expence :: model () -> findByPk (10), to return the object obtained earlier without a query to the database, but Yii does not have an interception mechanism for this query. Yes, there is CActiveRecord :: beforeFind (), but it is impossible to get information about the request from it, in particular, after findByPk (). An instance of the model is created in the CActiveRecord :: instantiate () method. Redefine it in our model.
 <?php class Expence extends CActiveRecord { //...-  protected function instantiate($attributes) { if(($record = ObjectWatcher::getRecord(get_class($this), $attributes['id'])) != false){ //   ,       $model = $record; }else{//           $model = parent::instantiate($attributes); ObjectWatcher::addRecord($model, $attributes['id']);//     } return $model; } //...-  } 

That's all now
 $modelOne = Expence::model()->findByPk(10); $modelTwo = Expence::model()->findByPk(10); var_dump($modelOne === $modelTwo); // true 


References:

Source: https://habr.com/ru/post/143497/


All Articles