From da58b05becc3ec3741b58b3e841a2ed5b36ca2bb Mon Sep 17 00:00:00 2001 From: "Kirill \"Nemoden\" K" Date: Tue, 7 Jun 2016 11:47:36 +1000 Subject: [PATCH 1/2] sharedConstructor option added --- Dice.php | 45 ++++++++++++++++++++++++++++ tests/SharedConstructorTest.php | 24 +++++++++++++++ tests/TestData/SharedConstructor.php | 26 ++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 tests/SharedConstructorTest.php create mode 100644 tests/TestData/SharedConstructor.php diff --git a/Dice.php b/Dice.php index 9f18535..e1f7b92 100644 --- a/Dice.php +++ b/Dice.php @@ -21,6 +21,11 @@ class Dice { */ private $instances = []; + /** + * @var Stores any instances marked as 'sharedConstructor' so create() can return the same instance + */ + private $sharedConstructor = []; + /** * Add a rule $rule to the class $name * @param string $name The name of the class to add the rule for @@ -61,6 +66,12 @@ public function create($name, array $args = [], array $share = []) { // Is there a shared instance set? Return it. Better here than a closure for this, calling a closure is slower. if (!empty($this->instances[$name])) return $this->instances[$name]; + $rules = $this->getRule($name); + if (!empty($rules['sharedConstructor']) && isset($this->sharedConstructor[$name])) { + $snapshot = $this->snapshotArray($args); + if (!empty($this->sharedConstructor[$name][$snapshot])) return $this->sharedConstructor[$name][$snapshot]; + } + // Create a closure for creating the object if there isn't one already if (empty($this->cache[$name])) $this->cache[$name] = $this->getClosure($name, $this->getRule($name)); @@ -91,6 +102,18 @@ private function getClosure($name, array $rule) { if ($constructor) $constructor->invokeArgs($this->instances[$name], $params($args, $share)); return $this->instances[$name]; }; + else if (!empty($rule['sharedConstructor'])) $closure = function (array $args, array $share) use ($class, $name, $constructor, $params) { + //Shared instance: create the class without calling the constructor (and write to \$name and $name, see issue #68) + $snapshot = $this->snapshotArray($args); + if (!isset($this->sharedConstructor[$name])) { + $this->sharedConstructor[$name] = []; + } + $this->sharedConstructor[$name][$snapshot] = $this->sharedConstructor[ltrim($name, '\\')][$snapshot] = $class->newInstanceWithoutConstructor(); + + //Now call this constructor after constructing all the dependencies. This avoids problems with cyclic references (issue #7) + if ($constructor) $constructor->invokeArgs($this->sharedConstructor[$name][$snapshot], $params($args, $share)); + return $this->sharedConstructor[$name][$snapshot]; + }; else if ($params) $closure = function (array $args, array $share) use ($class, $params) { // This class has depenencies, call the $params closure to generate them based on $args and $share return new $class->name(...$params($args, $share)); @@ -184,4 +207,26 @@ private function getParams(\ReflectionMethod $method, array $rule) { return $parameters; }; } + + /** + * Creates an unique hash for an array + * @param array $args + * @return string unique hash for an array + */ + private function snapshotArray(array $args = []) + { + $res = ""; + foreach ($args as $key => $arg) { + if (is_object($arg)) { + $res .= spl_object_hash($arg); + } elseif (is_array($arg)) { + $res .= $this->snapshotArray($arg); + } else { + $res .= $arg; + } + $res = $key . $res; + } + return $res; + #return md5($res); + } } diff --git a/tests/SharedConstructorTest.php b/tests/SharedConstructorTest.php new file mode 100644 index 0000000..e831bcb --- /dev/null +++ b/tests/SharedConstructorTest.php @@ -0,0 +1,24 @@ +dice->addRule('Greeter', $rule); + + $greeter = $this->dice->create('Greeter'); + $actual = $greeter->greet("John Doe"); + $expected = "Hi, John Doe!"; + $this->assertEquals($expected, $actual); + $actual = $greeter->greet("John Doe"); + $expected = "From cache: Hi, John Doe!"; + $this->assertEquals($expected, $actual); + + $greeter2 = $this->dice->create('Greeter'); + $actual = $greeter2->greet("John Doe"); + $expected = "From cache: Hi, John Doe!"; + $this->assertEquals($expected, $actual); + $this->assertSame($greeter, $greeter2); + } +} diff --git a/tests/TestData/SharedConstructor.php b/tests/TestData/SharedConstructor.php new file mode 100644 index 0000000..1502ea5 --- /dev/null +++ b/tests/TestData/SharedConstructor.php @@ -0,0 +1,26 @@ + + * $ Date: Tue 07 Jun 2016 11:31:59 AM VLAT $ + */ + +class Greeter +{ + protected $greeting; + + protected $cache = array(); + + public function __construct($greeting = "Hi") + { + $this->greeting = $greeting; + } + + public function greet($name) + { + if (!empty($this->cache[$name])) { + return "From cache: " . $this->cache[$name]; + } + $this->cache[$name] = sprintf("%s, %s!", $this->greeting, $name); + return $this->cache[$name]; + } +} From dd966b43c9789964ab80ad81d24ea8ce9c314673 Mon Sep 17 00:00:00 2001 From: "Kirill \"Nemoden\" K" Date: Tue, 7 Jun 2016 11:49:22 +1000 Subject: [PATCH 2/2] sharedConstructor option added --- tests/ShareInstancesTest.php | 2 +- tests/SharedConstructorTest.php | 78 +++++++++++++++++++++++- tests/TestData/SharedConstructor.php | 91 ++++++++++++++++++++++++++-- 3 files changed, 163 insertions(+), 8 deletions(-) diff --git a/tests/ShareInstancesTest.php b/tests/ShareInstancesTest.php index 0260246..92cc07d 100644 --- a/tests/ShareInstancesTest.php +++ b/tests/ShareInstancesTest.php @@ -86,4 +86,4 @@ public function testShareInstancesMultiple() { $this->assertNotEquals($shareTest->share1->shared->uniq, $shareTest2->share2->shared->uniq); } -} \ No newline at end of file +} diff --git a/tests/SharedConstructorTest.php b/tests/SharedConstructorTest.php index e831bcb..df64d34 100644 --- a/tests/SharedConstructorTest.php +++ b/tests/SharedConstructorTest.php @@ -5,9 +5,9 @@ public function testSharedConstructor() { $rule = []; $rule['sharedConstructor'] = ['true']; - $this->dice->addRule('Greeter', $rule); + $this->dice->addRule('Test\SharedConstructor\Greeter', $rule); - $greeter = $this->dice->create('Greeter'); + $greeter = $this->dice->create('Test\SharedConstructor\Greeter'); $actual = $greeter->greet("John Doe"); $expected = "Hi, John Doe!"; $this->assertEquals($expected, $actual); @@ -15,10 +15,82 @@ public function testSharedConstructor() $expected = "From cache: Hi, John Doe!"; $this->assertEquals($expected, $actual); - $greeter2 = $this->dice->create('Greeter'); + $greeter2 = $this->dice->create('Test\SharedConstructor\Greeter'); $actual = $greeter2->greet("John Doe"); $expected = "From cache: Hi, John Doe!"; $this->assertEquals($expected, $actual); $this->assertSame($greeter, $greeter2); } + + public function testSharedConstructorComplex() + { + $sharedConstructor = ['sharedConstructor' => true]; + $singleton = ['shared' => true]; + $this->dice->addRule('Test\SharedConstructor\BarBazShared', $sharedConstructor); + $this->dice->addRule('Test\SharedConstructor\BarBaz', $sharedConstructor); + $this->dice->addRule('Test\SharedConstructor\Baz', $singleton); + + $bar = $this->dice->create('Test\SharedConstructor\Bar', ['I am bar']); + $baz = $this->dice->create('Test\SharedConstructor\Baz', ['I am baz']); + $barBaz = $this->dice->create('Test\SharedConstructor\BarBaz', [$bar, $baz]); + $strongWrapper = function ($str) { + return "$str"; + }; + $bWrapper = function ($str) { + return "$str"; + }; + $barBazShared = $this->dice->create( + 'Test\SharedConstructor\BarBazShared', + [$barBaz, $strongWrapper] + ); + $barBazShared2 = $this->dice->create( + 'Test\SharedConstructor\BarBazShared', + [$barBaz, $strongWrapper] + ); + + $this->assertEquals($barBazShared->id, $barBazShared2->id); + + # Bar is not shared, so Dice will create new instance + # Despite that the argument to Bar is the same + $anotherBar = $this->dice->create( + 'Test\SharedConstructor\Bar', + ['I am bar'] + ); + $anotherBarBaz = $this->dice->create( + 'Test\SharedConstructor\BarBaz', + [$anotherBar, $baz] + ); + $barBazShared3 = $this->dice->create( + 'Test\SharedConstructor\BarBazShared', + [$anotherBarBaz, $strongWrapper] + ); + $this->assertFalse($barBazShared3->id == $barBazShared->id); + $this->assertFalse($barBazShared3->id == $barBazShared2->id); + + $this->assertEquals($barBazShared3->getBarBazWrapped(), $barBazShared->getBarBazWrapped()); + $this->assertEquals($barBazShared3->getBarBazWrapped(), $barBazShared2->getBarBazWrapped()); + + # Baz is the same always even if we pass different param to the constructor + $sameBaz = $this->dice->create( + 'Test\SharedConstructor\Baz', + ['I am another baz'] + ); + $sameBarBaz = $this->dice->create( + 'Test\SharedConstructor\BarBaz', + [$bar, $sameBaz] + ); + $barBazShared4 = $this->dice->create( + 'Test\SharedConstructor\BarBazShared', + [$sameBarBaz, $strongWrapper] + ); + $this->assertEquals($barBazShared4->id, $barBazShared->id); + + # test change closure + $barBazShared5 = $this->dice->create( + 'Test\SharedConstructor\BarBazShared', + [$sameBarBaz, $bWrapper] + ); + $this->assertFalse($barBazShared5->id == $barBazShared->id); + $this->assertFalse($barBazShared5->id == $barBazShared4->id); + } } diff --git a/tests/TestData/SharedConstructor.php b/tests/TestData/SharedConstructor.php index 1502ea5..4cc2a64 100644 --- a/tests/TestData/SharedConstructor.php +++ b/tests/TestData/SharedConstructor.php @@ -1,8 +1,8 @@ - * $ Date: Tue 07 Jun 2016 11:31:59 AM VLAT $ - */ +# First test-case. We are interested in getting data from cache if we created an object twice +# with same constructor params using Dice + +namespace Test\SharedConstructor; class Greeter { @@ -24,3 +24,86 @@ public function greet($name) return $this->cache[$name]; } } + +# Second test case. We are interested in more complex sharedConstructor usage. +# Namely, we want constructor to receive objects and closures so we can test +# arguments are hashed correctly +class BarBazShared +{ + private $barBaz; + private $clo; + public $id; + + public function __construct(BarBaz $barBaz, \Closure $clo) + { + $this->barBaz = $barBaz; + $this->clo = $clo; + $this->id = microtime(); // should stay the same if instance is the same + } + + public function getUid() + { + } + + public function getBarBazWrapped() + { + $clo = $this->clo; + return $clo(sprintf( + "Bar: %s, baz: %s", + $this->barBaz->getBar()->value(), + $this->barBaz->getBaz()->value() + )); + } +} + +class BarBaz +{ + private $bar; + private $baz; + + public function __construct(Bar $bar, Baz $baz) + { + $this->bar = $bar; + $this->baz = $baz; + } + + public function getBar() + { + return $this->bar; + } + + public function getBaz() + { + return $this->baz; + } +} + +class Bar +{ + private $bar; + + public function __construct($bar) + { + $this->bar = $bar; + } + + public function value() + { + return $this->bar; + } +} + +class Baz +{ + private $baz; + + public function __construct($baz) + { + $this->baz = $baz; + } + + public function value() + { + return $this->baz; + } +}