From 8b962b27d6b1a701870088eb501c9079702775d3 Mon Sep 17 00:00:00 2001 From: madmatt Date: Sat, 31 Oct 2015 17:15:51 +1300 Subject: [PATCH] New features to Singleton allowing class overriding and passing constructor args - Allows a call to ::getInstance() to return a different class to that expected This feature means that unit tests can override functionality that doesn't require testing. For example, to test the \eBot\Match\Match class, we want to override the \eTools\Rcon\CSGO class so that we don't need to entirely mock the RCON connection, and we can instead test the \eTools\Rcon\CSGO class in a different way later. - Allows a call to ::getInstance() to include constructor arguments This feature means that we can create singletons for objects that require constructor arguments to be provided (e.g. \eTools\Rcon\CSGO). --- src/eTools/Utils/Singleton.php | 41 ++++++++++++- tests/eTools/Utils/SingletonTest.php | 91 ++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 tests/eTools/Utils/SingletonTest.php diff --git a/src/eTools/Utils/Singleton.php b/src/eTools/Utils/Singleton.php index 1020177a..dae42b49 100644 --- a/src/eTools/Utils/Singleton.php +++ b/src/eTools/Utils/Singleton.php @@ -13,14 +13,53 @@ abstract class Singleton { protected static $instances = array(); + private static $override_instances = array(); + + /** + * Returns a singleton object of a given class. Extend this class, then call \YourClass::getInstance() from anywhere + * in the application. + * + * If an override class is registered, that will be returned instead. Thi + * + * @return \stdClass A singleton of the requested class + */ public static function getInstance() { $class = get_called_class(); + $args = func_get_args(); + + // Check if an override is set, and replace the class name to load if it is + if (isset(self::$override_instances[$class])) { + $class = self::$override_instances[$class]; + } + + // Create a singleton instance of the class if it doesn't already exist if (!isset(self::$instances[$class])) { - self::$instances[$class] = new $class(); + if(is_array($args) && sizeof($args) > 0) { + $reflectionClass = new \ReflectionClass($class); + self::$instances[$class] = $reflectionClass->newInstanceArgs($args); + } else { + self::$instances[$class] = new $class(); + } } + return self::$instances[$class]; } + /** + * Configures an override class, that can later be returned by self::getInstance(). + * + * @param string $oldClass The class name to replace + * @param string $newClass The class to replace this with + * @return void + */ + public static function set_override_instance($oldClass, $newClass) { + self::$override_instances[$oldClass] = $newClass; + } + + public static function clear_instances() { + self::$instances = array(); + } + protected function __construct() { } diff --git a/tests/eTools/Utils/SingletonTest.php b/tests/eTools/Utils/SingletonTest.php new file mode 100644 index 00000000..c13139a1 --- /dev/null +++ b/tests/eTools/Utils/SingletonTest.php @@ -0,0 +1,91 @@ +assertEquals(false, $obj->getCheck()); + $obj->toggleCheck(); + + /** @var SingletonSubclass $obj2 */ + $obj2 = SingletonSubclass::getInstance(); + $this->assertEquals(true, $obj2->getCheck()); + + $obj2->toggleCheck(); + $this->assertEquals(false, $obj->getCheck()); + } + + public function testGetInstanceSetsConstructorArgs() { + /** @var SingletonSubclass $obj */ + $obj = SingletonSubclass::getInstance('arg1', 'arg2'); + $this->assertEquals('arg1', $obj->getConstructorArg1()); + $this->assertEquals('arg2', $obj->getConstructorArg2()); + } + + /** + * This test ensures that the Singleton::set_override_instance() method overrides classes correctly + */ + public function testOverrideInstances() { + // Before setting an override, we should get the normal class + $singletonSubclass = SingletonSubclass::getInstance(); + $this->assertInstanceOf('eTools\Tests\Utils\SingletonSubclass', $singletonSubclass); + + // Set the override and try again, we should now get the overridden method + Singleton::set_override_instance( + 'eTools\Tests\Utils\SingletonSubclass', + 'eTools\Tests\Utils\SingletonOverrideSubclass' + ); + + // Ensure it hasn't changed the existing instance somehow + $this->assertInstanceOf('eTools\Tests\Utils\SingletonSubclass', $singletonSubclass); + $this->assertInstanceOf('eTools\Tests\Utils\SingletonOverrideSubclass', SingletonSubclass::getInstance()); + + // Ensure override works every time once it's set + $this->assertInstanceOf('eTools\Tests\Utils\SingletonOverrideSubclass', SingletonSubclass::getInstance()); + } +} + +/** + * Class SingletonSubclass + * @package eTools\Tests\Utils + * + * This is a sample class - it should be used only for testing + */ +class SingletonSubclass extends Singleton { + private $check = false; + private $constructorArg1 = null; + private $constructorArg2 = null; + + public function __construct($arg1 = null, $arg2 = null) { + $this->constructorArg1 = $arg1; + $this->constructorArg2 = $arg2; + } + + public function toggleCheck() { + $this->check = !$this->check; + } + + public function getCheck() { + return $this->check; + } + + public function getConstructorArg1() { + return $this->constructorArg1; + } + + public function getConstructorArg2() { + return $this->constructorArg2; + } +} + +class SingletonOverrideSubclass extends Singleton {} \ No newline at end of file