- PVSM.RU - https://www.pvsm.ru -

Реализация шаблона проектирования 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 TraitsTSingleton;
	
	/**
	 * @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 LibCDb
{
	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 = LibCDb::getMyChild()[0];

		// Safe variant
		$db = LibCb::getMyChildSafe()[0];
	}
}

Пожалуй все. Сама реализация паттерна тестировалась, поэтому при желании можно смело брать и пользоваться. Благодарю за внимание.

P.S.
Все же я был оптимистом, говоря, что это был конечный вариант реализации. Во время написания статьи примесь пополнилась еще двумя методами.

Автор: AccessGranted

Источник [1]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/framework/19291

Ссылки в тексте:

[1] Источник: http://habrahabr.ru/post/157615/