Так получилось, что я реализую этот шаблон вот уже второй год начиная с первого выхода PHP 5.3 в 2009 году. В то время у его предшественника версии 5.2 не было позднего статического связывания и для создания экземпляра класса в метод приходилось передавать его имя, что казалось мне архинеудобным.
С выходом PHP 5.4, взглянув еще раз на старую реализацию и на новые возможности языка, я переписал этот шаблон еще раз получив — как мне казалось тогда и кажется сейчас — конечный вариант.
Подробности реализации ниже.
Сразу бы хотелось отметить основные особенности:
- Параметрическое порождение. Позволяет создавать экземпляры классов используя сигнатуру вызова метода ::getInstance. Каждой сигнатуре будет соответствовать свой экземпляр класса. По умолчанию такой тип порождения отключен. Включается в дочерних классах переопределением метода ::useParametricInstantiation.
- Получение дочернего объекта по имени родительского класса. Позволяет ссылаться на дочерние классы из родительских а также из других классов не зная их имени.
- Создание дочернего класса по имени родительского класса. Аналогично второму пункту, только в случае если дочерний объект не был создан к моменту вызова метода — создаст его.
Теперь сама реализация на PHP 5.4 в качестве примеси, последнего нововведения этого языка. Размер кода небольшой, поэтому привожу его здесь.
<?php /** * Trait TSingleton. * An implementation of the Singleton design pattern. */ namespace Traits; const SINGLETON_GLOBAL_VARS_PREFIX = 'singleton'; trait TSingleton { /** * Do not allow creating object by the new operator. * * @final * @access private * @return void */ final private function __construct() { } /** * Do not allow cloning object. * * @final * @access private * @return void */ final private function __clone() { } /** * Called when class is being instantiated. * * @access protected * @return void */ protected function onCreate() { } /** * Returns true if child class has a parent specified by the mask. * * @param string $child * @param string $parentMask * @final * @static * @access public * @return boolean */ final static function hasParentClass($child, $parentMask) { $currentClass = get_parent_class($child); if (!$currentClass) return false; do { if (strpos($currentClass, $parentMask) !== false) return true; } while ($currentClass = get_parent_class($currentClass)); return false; } /** * Returns instance of child class using its parent' class name specified by the mask. * * @param string $parentMask Any substring of parent fully qualified class name. * @final * @static * @access public * @return array|null */ final static function getObjectByParent($parentMask) { foreach ($GLOBALS as $name => $value) { if(strpos($name, SINGLETON_GLOBAL_VARS_PREFIX) === false) continue; foreach ($value as $object) { if (self::hasParentClass($object, $parentMask)) return $value; } } return null; } /** * Finds an object by the mask of its parent's class namе. If not found the * method will create it. * * @param string $parentMask * @param array $initArgs * @final * @static * @access public * @return array|null */ final static function getObjectByParentSafe($parentMask, $initArgs = []) { $child = self::getObjectByParent($parentMask); if ($child !== null) return $child; // Look up all declared classes. $result = []; foreach (get_declared_classes() as $class) { if (self::hasParentClass($class, $parentMask)) { $result[] = call_user_func_array(($class . '::getInstance'), $initArgs); } } return count($result) ? $result : null; } /** * Returns child object of the parent class that called the method. * * @see TSingleton::getObjectByParent * @final * @static * @access public * @return array */ final static function getMyChild() { return self::getObjectByParent(get_called_class()); } /** * Safe variant of ::getMyChild. * * @see TSingleton::getObjectByParentSafe * @final * @static * @access public * @return array */ final static function getMyChildSafe() { $initArgs = func_get_args(); return self::getObjectByParent(get_called_class(), $initArgs); } /** * Returns class instance. * * @static * @final * @access public * @return TSingleton */ final static function getInstance() { static $objPool = []; $argsArray = func_get_args(); $class = get_called_class(); if (static::useParametricInstantiation() && count($argsArray)) { $fingerprint = ''; foreach ($argsArray as $arg) { if (is_array($arg) || is_object($arg)) $fingerprint .= serialize($arg); else $fingerprint .= $arg; } $key = md5($class . $fingerprint); } else // Use class name as a key. $key = $class; if (isset($objPool[$key])) return $objPool[$key]; $instance = new $class(); $varname = SINGLETON_GLOBAL_VARS_PREFIX . $class; if (isset($GLOBALS[$varname])) $GLOBALS[$varname][] = $instance; else $GLOBALS[$varname] = [$instance]; $objPool[$key] = $instance; call_user_func_array([$instance, 'onCreate'], $argsArray); return $instance; } /** * Enables or disables the parametric class instantiation. Disabled by default. * * @access public * @static * @return boolean */ static function useParametricInstantiation() { return false; } }
Метод TSingleton::useParametricInstantiation запрещает или разрешает параметрическое порождение. Как было упомянуто выше и видно из реализации по умолчанию она отключена.
Пример абстрактного класс, реализующий интерфейс взаимодействия с базой данных использующего примесь TSingleton:
namespace Lib; abstract class CDb { use Traits\TSingleton; /** * @var PDO PDO connection object */ private $pdoConnect; /** * Returns connection string to main database. * * @return string */ abstract static function getDefaultNode(); /** * @return string */ abstract function getDSNByNodeId($nodeId); /** * @return void */ final protected function onCreate() { $args = func_get_args(); $nodeId = $args[0]; try { $dsn = $this->getDSNByNodeId($nodeId); $this->pdoConnect = new PDO($dsn, $this->getUsername(), $this->getPass(), $this->getPDODriverOptions()); } catch(\PDOException $pdoExcep) { } } /** * @return CDb */ final static function connect($node = null) { if ($node === null) $node = static::getDefaultNode(); return static::getInstance($node); } /** * @return true */ final static function useParametricInstantiation() { return true; } }
Надеюсь никого не утомило еще читать PHP код. Осталось немного. Дочерний класс MyCDb:
namespace MyApp; class MyCDb extends \Lib\CDb { const DEFAULT_NODE = 1; const SHARD_1 = 2; /** * @return string */ function getDSNByNodeId($nodeId) { switch ($nodeId) { case self::DEFAULT_NODE: return 'mysql:host=main.database.local;dbname=mydb'; case self::SHARD_1: return 'mysql:host=shard1.database.local;dbname=mydb'; } } /** * @return integer */ function getDefaultNode() { return self::DEFAULT_NODE; } /** * @return string */ function getUsername() { return 'root'; } /** * @return string */ function getPass() { 'mypass'; } /** * @return array */ function getPDODriverOptions() { return [\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY]; } } // Connection to the main database $db = MyCDb::connect(); // Connection to the shard 1 $dbShard1 = MyCDb::connect(MyCDb::SHARD_1); // $db !== $dbShard1 $dbShard2 = MyCDb::connect(MyCDb::SHARD_1); // $dbShard1 === $dbShard2
Из примера выше видно, что с помощью параметрического порождения у нас есть удобная возможность получать доступ к той или иной базе данных. Разумеется область применения такого типа порождения этим не ограничивается.
Пример получения дочернего класса:
abstract class Model { function query() { // Returns instance of MyCDb class. $db = \Lib\CDb::getMyChild()[0]; // Safe variant $db = \Lib\Cb::getMyChildSafe()[0]; } }
Пожалуй все. Сама реализация паттерна тестировалась, поэтому при желании можно смело брать и пользоваться. Благодарю за внимание.
P.S.
Все же я был оптимистом, говоря, что это был конечный вариант реализации. Во время написания статьи примесь пополнилась еще двумя методами.
ссылка на оригинал статьи http://habrahabr.ru/post/157615/
Добавить комментарий