Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shared constructor #107

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions Dice.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion tests/ShareInstancesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ public function testShareInstancesMultiple() {
$this->assertNotEquals($shareTest->share1->shared->uniq, $shareTest2->share2->shared->uniq);

}
}
}
96 changes: 96 additions & 0 deletions tests/SharedConstructorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php
class SharedConstructorTest extends DiceTest
{
public function testSharedConstructor()
{
$rule = [];
$rule['sharedConstructor'] = ['true'];
$this->dice->addRule('Test\SharedConstructor\Greeter', $rule);

$greeter = $this->dice->create('Test\SharedConstructor\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('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 "<strong>$str</strong>";
};
$bWrapper = function ($str) {
return "<b>$str</b>";
};
$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);
}
}
109 changes: 109 additions & 0 deletions tests/TestData/SharedConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php
# 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
{
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];
}
}

# 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;
}
}