Skip to content

Commit

Permalink
Container::addService() accepts factory closures
Browse files Browse the repository at this point in the history
Revert "DI\Container: dynamically added services via addService() may be only objects, factories/classnames are deprecated"

This reverts commit cde2eb8.
  • Loading branch information
dg committed Apr 3, 2019
1 parent 36845b5 commit 19d8353
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 19 deletions.
30 changes: 21 additions & 9 deletions src/DI/Container.php
Expand Up @@ -59,7 +59,7 @@ public function getParameters(): array

/**
* Adds the service to the container.
* @param object $service
* @param object $service service or its factory
* @return static
*/
public function addService(string $name, $service)
Expand All @@ -70,16 +70,27 @@ public function addService(string $name, $service)

} elseif (!is_object($service)) {
throw new Nette\InvalidArgumentException(sprintf("Service '%s' must be a object, %s given.", $name, gettype($service)));
}

$type = $service instanceof \Closure
? (string) (new \ReflectionFunction($service))->getReturnType()
: get_class($service);

} elseif (!isset($this->methods[self::getMethodName($name)])) {
if (!isset($this->methods[self::getMethodName($name)])) {
trigger_error(__METHOD__ . "() service '$name' should be defined as 'imported'", E_USER_NOTICE);
$this->types[$name] = get_class($service);
$this->types[$name] = $type;

} elseif (($type = $this->getServiceType($name)) && !$service instanceof $type) {
throw new Nette\InvalidArgumentException(sprintf("Service '%s' must be instance of %s, %s given.", $name, $type, get_class($service)));
} elseif (($expectedType = $this->getServiceType($name)) && !is_a($type, $expectedType, true)) {
throw new Nette\InvalidArgumentException("Service '$name' must be instance of $expectedType, " . ($type ? "$type given." : 'add typehint to closure.'));
}

if ($service instanceof \Closure) {
$this->methods[self::getMethodName($name)] = $service;
$this->types[$name] = $type;
} else {
$this->instances[$name] = $service;
}

$this->instances[$name] = $service;
return $this;
}

Expand Down Expand Up @@ -165,23 +176,24 @@ public function createService(string $name, array $args = [])
{
$name = $this->aliases[$name] ?? $name;
$method = self::getMethodName($name);
$cb = $this->methods[$method] ?? null;
if (isset($this->creating[$name])) {
throw new Nette\InvalidStateException(sprintf('Circular reference detected for services: %s.', implode(', ', array_keys($this->creating))));

} elseif (!isset($this->methods[$method])) {
} elseif ($cb === null) {
throw new MissingServiceException("Service '$name' not found.");
}

try {
$this->creating[$name] = true;
$service = $this->$method(...$args);
$service = $cb instanceof \Closure ? $cb(...$args) : $this->$method(...$args);

} finally {
unset($this->creating[$name]);
}

if (!is_object($service)) {
throw new Nette\UnexpectedValueException("Unable to create service '$name', value returned by method $method() is not object.");
throw new Nette\UnexpectedValueException("Unable to create service '$name', value returned by " . ($cb instanceof \Closure ? 'closure' : "method $method()") . ' is not object.');
}

return $service;
Expand Down
37 changes: 33 additions & 4 deletions tests/DI/Container.dynamic.phpt
Expand Up @@ -15,10 +15,6 @@ require __DIR__ . '/../bootstrap.php';

class Service
{
public static function create()
{
return new static;
}
}


Expand All @@ -41,3 +37,36 @@ test(function () use ($container) {
Assert::same(Service::class, $container->getServiceType('one'));
Assert::same(Service::class, $container->getServiceType('two'));
});


// closure
test(function () use ($container) {
@$container->addService('four', function () { // @ triggers service should be defined as "imported"
return new Service;
});

Assert::true($container->hasService('four'));
Assert::false($container->isCreated('four'));
Assert::true($container->getService('four') instanceof Service);
Assert::true($container->isCreated('four'));
Assert::same($container->getService('four'), $container->getService('four')); // shared

Assert::same('', $container->getServiceType('four'));
});


// closure with typehint
test(function () use ($container) {
@$container->addService('five', function (): Service { // @ triggers service should be defined as "imported"
return new Service;
});

Assert::same(Service::class, $container->getServiceType('five'));
});


// bad closure
Assert::exception(function () use ($container) {
@$container->addService('six', function () {}); // @ triggers service should be defined as "imported"
$container->getService('six');
}, Nette\UnexpectedValueException::class, "Unable to create service 'six', value returned by closure is not object.");
71 changes: 65 additions & 6 deletions tests/DI/Container.static-dynamic.phpt
Expand Up @@ -19,16 +19,75 @@ class MyContainer extends Container
{
return new stdClass;
}


protected function createServiceTypehint(): stdClass
{
return new stdClass;
}
}


$container = new MyContainer;
test(function () {
$container = new MyContainer;

Assert::true($container->hasService('one'));

$container->addService('one', new stdClass);

Assert::true($container->hasService('one'));
Assert::same('', $container->getServiceType('one'));

Assert::type(stdClass::class, $container->getService('one'));
Assert::same($container->getService('one'), $container->getService('one')); // shared
});


test(function () { // closure
$container = new MyContainer;

$container->addService('one', function () { return new stdClass; });

Assert::true($container->hasService('one'));
Assert::same('', $container->getServiceType('one'));
Assert::type(stdClass::class, $container->getService('one'));
Assert::same($container->getService('one'), $container->getService('one')); // shared
});


test(function () { // closure & typehint
$container = new MyContainer;

$container->addService('one', function (): stdClass { return new stdClass; });

Assert::same(stdClass::class, $container->getServiceType('one'));
Assert::true($container->hasService('one'));
Assert::type(stdClass::class, $container->getService('one'));
});


test(function () { // closure & matching typehint
$container = new MyContainer;

class MyClass extends stdClass
{
}

$container->addService('typehint', function (): MyClass { return new MyClass; });

Assert::same(MyClass::class, $container->getServiceType('typehint'));
Assert::true($container->hasService('typehint'));
Assert::type(MyClass::class, $container->getService('typehint'));
});

Assert::true($container->hasService('one'));

$container->addService('one', new stdClass);
Assert::exception(function () { // closure & wrong typehint
$container = new MyContainer;
$container->addService('typehint', function () { return new DateTime; });
}, Nette\InvalidArgumentException::class, "Service 'typehint' must be instance of stdClass, add typehint to closure.");

Assert::true($container->hasService('one'));

Assert::type(stdClass::class, $container->getService('one'));
Assert::same($container->getService('one'), $container->getService('one')); // shared
Assert::exception(function () { // closure & wrong typehint
$container = new MyContainer;
$container->addService('typehint', function (): DateTime { return new DateTime; });
}, Nette\InvalidArgumentException::class, "Service 'typehint' must be instance of stdClass, DateTime given.");

0 comments on commit 19d8353

Please sign in to comment.