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

в 17:21, , рубрики: framework, php, php 5.4, singleton, software engineering, метки: , , ,

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

Источник


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


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