Реализация шаблона Identity Map в Yii Framework

в 8:26, , рубрики: patterns, php, yii, метки: , ,

Доброго времени суток, читатели!

Проблема

При работе с базой данных или просто с объектами, доступными из разных частей вашего приложения, есть опасность, что объекты, которые, казалось бы, должны быть равны вовсе таковыми не являются.

Например, допустим, у нас есть некоторая модель ActiveRecord — Expence и вот такой код:

$modelOne = Expence::model()->findByPk(10);
$modelTwo = Expence::model()->findByPk(10);
var_dump($modelOne === $modelTwo); // Вернет false

Таким образом, меняя одну модель мы никоим образом не затронем вторую(что логично, так как они ссылаются на разные объекты).

$modelOne->someField = "Data";
$modelOne->save();
/// ...какой-то код...
echo $modelTwo->someField; // Содержит старое значение
$modelTwo->save(); // Затираем ранее записаные данные

Решение

Для решения данной проблемы воспользуемся шаблоном проектирования, названным Мартином Фаулером Identity Map.
Его идея заключается в том, чтобы отслеживать наличие в приложении объектов имеющий один и тот же идентификатор. Таким образом, если мы запрашиваем модель с id равным 5 в двух разных местах программы, мы получим ссылку на один и тот же объект.

Реализация

К сожалению, Yii не следит за тем, запрашивали ли мы какой либо объект из базы или нет, поэтому придется написать свой собственный класс.


<?php
	/**
	 * Singleton class to manipulate instances of models (e.g. 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;
	    }
	       
	}
	

С помощью методов addRecord и getRecord мы добавляем и получаем модель из своеобразного реестра моделей(в качестве которого выступает ассоциативный массив вида «имякласса.ид» => объект).

Теперь необходимо заставить Yii создавать объект, если он еще не был получен, или возвращать имеющийся из массива $objects. Будем пытаться сделать это средствами самого Yii, не создавая лишних прослоек после CActiveRecord. Конечно, хотелось бы после при выполнении Expence::model()->findByPk(10) выдавать полученный ранее объект без запроса к базе, но в Yii нет механизма перехвата данного запроса. Да, есть CActiveRecord::beforeFind(), но из него невозможно получить данные о запросе, в частности после findByPk(). Экземпляр модели создается в методе CActiveRecord::instantiate(). Переопределим его в нашей модели.


<?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;
	    }
	//...какой-то код 
	}

Вот и все, теперь

$modelOne = Expence::model()->findByPk(10);
$modelTwo = Expence::model()->findByPk(10);
var_dump($modelOne === $modelTwo); // true

Ссылки:

Автор: organium


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js