Реализация шаблона проектирования Singleton на PHP 5.4

—

от автора

Казалось бы, из всех шаблонов проектирования что может быть проще всем известного синглетона. Во многих классических примерах реализации на разных языках программирования она может занимать всего пару десятков строк а того и меньше.
image
Так получилось, что я реализую этот шаблон вот уже второй год начиная с первого выхода 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *