diff --git a/src/DI/Container.php b/src/DI/Container.php index 8ffa427dc..04cdb52d0 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -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) @@ -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; } @@ -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; diff --git a/tests/DI/Container.dynamic.phpt b/tests/DI/Container.dynamic.phpt index add6fb271..17a397344 100644 --- a/tests/DI/Container.dynamic.phpt +++ b/tests/DI/Container.dynamic.phpt @@ -15,10 +15,6 @@ require __DIR__ . '/../bootstrap.php'; class Service { - public static function create() - { - return new static; - } } @@ -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."); diff --git a/tests/DI/Container.static-dynamic.phpt b/tests/DI/Container.static-dynamic.phpt index 5e1df783e..e5d63e6ec 100644 --- a/tests/DI/Container.static-dynamic.phpt +++ b/tests/DI/Container.static-dynamic.phpt @@ -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.");