diff --git a/composer.json b/composer.json index 62f537e44..f2fe3fcc4 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "nette/neon": "^3.0", "nette/php-generator": "^3.2", "nette/robot-loader": "^3.2", - "nette/utils": "^3.0" + "nette/utils": "^3.0.1" }, "require-dev": { "nette/tester": "^2.0", diff --git a/readme.md b/readme.md index f913a68bc..d3fb8f9d5 100644 --- a/readme.md +++ b/readme.md @@ -29,7 +29,7 @@ The recommended way to install is via Composer: composer require nette/di ``` -It requires PHP version 5.6 and supports PHP up to 7.2. The dev-master version requires PHP 7.1. +It requires PHP version 7.1 and supports PHP up to 7.3. Usage diff --git a/src/DI/CompilerExtension.php b/src/DI/CompilerExtension.php index de7bf8183..4073c5f8a 100644 --- a/src/DI/CompilerExtension.php +++ b/src/DI/CompilerExtension.php @@ -25,7 +25,7 @@ abstract class CompilerExtension /** @var string */ protected $name; - /** @var array */ + /** @var array|object */ protected $config = []; @@ -41,10 +41,14 @@ public function setCompiler(Compiler $compiler, string $name) /** + * @param array|object $config * @return static */ - public function setConfig(array $config) + public function setConfig($config) { + if (!is_array($config) && !is_object($config)) { + throw new Nette\InvalidArgumentException; + } $this->config = $config; return $this; } @@ -52,8 +56,9 @@ public function setConfig(array $config) /** * Returns extension configuration. + * @return array|object */ - public function getConfig(): array + public function getConfig() { return $this->config; } diff --git a/src/DI/Config/Loader.php b/src/DI/Config/Loader.php index af717268f..2f49b8355 100644 --- a/src/DI/Config/Loader.php +++ b/src/DI/Config/Loader.php @@ -31,6 +31,8 @@ class Loader private $loadedFiles = []; + private $parameters = []; + /** * Reads configuration from file. @@ -52,7 +54,8 @@ public function load(string $file, ?bool $merge = true): array $res = []; if (isset($data[self::INCLUDES_KEY])) { Validators::assert($data[self::INCLUDES_KEY], 'list', "section 'includes' in file '$file'"); - foreach ($data[self::INCLUDES_KEY] as $include) { + $includes = Nette\DI\Helpers::expand($data[self::INCLUDES_KEY], $this->parameters); + foreach ($includes as $include) { $include = $this->expandIncludedFile($include, $file); $res = Helpers::merge($this->load($include, $merge), $res); } @@ -119,4 +122,14 @@ private function getAdapter(string $file): Adapter } return is_object($this->adapters[$extension]) ? $this->adapters[$extension] : new $this->adapters[$extension]; } + + + /** + * @return static + */ + public function setParameters(array $params) + { + $this->parameters = $params; + return $this; + } } diff --git a/src/DI/Config/Processor.php b/src/DI/Config/Processor.php index 0c65c6754..07357c565 100644 --- a/src/DI/Config/Processor.php +++ b/src/DI/Config/Processor.php @@ -134,6 +134,9 @@ public function normalizeConfig($config): array return ['factory' => $config]; } elseif (is_array($config)) { + if (isset($config['class']) && !isset($config['factory'])) { + $config['factory'] = null; + } foreach (['class' => 'type', 'dynamic' => 'imported'] as $alias => $original) { if (array_key_exists($alias, $config)) { if (array_key_exists($original, $config)) { @@ -195,22 +198,18 @@ private function updateServiceDefinition(Definitions\ServiceDefinition $definiti { $config = self::processArguments($config); - if (array_key_exists('type', $config) || array_key_exists('factory', $config)) { + if (array_key_exists('factory', $config)) { + $definition->setFactory($config['factory']); $definition->setType(null); - $definition->setFactory(null); } if (array_key_exists('type', $config)) { if ($config['type'] instanceof Statement) { trigger_error("Service '$name': option 'type' or 'class' should be changed to 'factory'.", E_USER_DEPRECATED); + $definition->setFactory($config['type']); } else { $definition->setType($config['type']); } - $definition->setFactory($config['type']); - } - - if (array_key_exists('factory', $config)) { - $definition->setFactory($config['factory']); } if (array_key_exists('arguments', $config)) { diff --git a/src/DI/ContainerBuilder.php b/src/DI/ContainerBuilder.php index a89ae8aca..ea6884e11 100644 --- a/src/DI/ContainerBuilder.php +++ b/src/DI/ContainerBuilder.php @@ -137,6 +137,61 @@ public function getDefinition(string $name): Definition } + public function getServiceDefinition(string $name): Nette\DI\Definitions\ServiceDefinition + { + $service = $this->getDefinition($name); + if (!$service instanceof Nette\DI\Definitions\ServiceDefinition) { + throw new MissingServiceException("ServiceDefinition with name '$name' not found."); + } + + return $service; + } + + + public function getAccessorDefinition(string $name): Nette\DI\Definitions\AccessorDefinition + { + $service = $this->getDefinition($name); + if (!$service instanceof Nette\DI\Definitions\AccessorDefinition) { + throw new MissingServiceException("AccessorDefinition with name '$name' not found."); + } + + return $service; + } + + + public function getFactoryDefinition(string $name): Nette\DI\Definitions\FactoryDefinition + { + $service = $this->getDefinition($name); + if (!$service instanceof Nette\DI\Definitions\FactoryDefinition) { + throw new MissingServiceException("FactoryDefinition with name '$name' not found."); + } + + return $service; + } + + + public function getLocatorDefinition(string $name): Nette\DI\Definitions\LocatorDefinition + { + $service = $this->getDefinition($name); + if (!$service instanceof Nette\DI\Definitions\LocatorDefinition) { + throw new MissingServiceException("LocatorDefinition with name '$name' not found."); + } + + return $service; + } + + + public function getImportedDefinition(string $name): Nette\DI\Definitions\ImportedDefinition + { + $service = $this->getDefinition($name); + if (!$service instanceof Nette\DI\Definitions\ImportedDefinition) { + throw new MissingServiceException("ImportedDefinition with name '$name' not found."); + } + + return $service; + } + + /** * Gets all service definitions. * @return Definition[] diff --git a/src/DI/Extensions/InjectExtension.php b/src/DI/Extensions/InjectExtension.php index 12fe2af89..ef3d15a51 100644 --- a/src/DI/Extensions/InjectExtension.php +++ b/src/DI/Extensions/InjectExtension.php @@ -102,10 +102,7 @@ public static function getInjectProperties(string $class): array foreach (get_class_vars($class) as $name => $foo) { $rp = new \ReflectionProperty($class, $name); if (DI\Helpers::parseAnnotation($rp, 'inject') !== null) { - if ($type = DI\Helpers::parseAnnotation($rp, 'var')) { - $type = Reflection::expandClassName($type, Reflection::getPropertyDeclaringClass($rp)); - } - $res[$name] = $type; + $res[$name] = DI\Helpers::getPropertyType($rp); } } ksort($res); diff --git a/src/DI/Helpers.php b/src/DI/Helpers.php index 323190d15..664ea4e47 100644 --- a/src/DI/Helpers.php +++ b/src/DI/Helpers.php @@ -167,6 +167,20 @@ public static function getReturnType(\ReflectionFunctionAbstract $func): ?string } + public static function getPropertyType(\ReflectionProperty $prop): ?string + { + if ($type = Reflection::getPropertyType($prop)) { + return ($prop->getType()->allowsNull() ? '?' : '') . $type; + } elseif ($type = preg_replace('#\s.*#', '', (string) self::parseAnnotation($prop, 'var'))) { + $class = Reflection::getPropertyDeclaringClass($prop); + return preg_replace_callback('#[\w\\\\]+#', function ($m) use ($class) { + return Reflection::expandClassName($m[0], $class); + }, $type); + } + return null; + } + + public static function normalizeClass(string $type): string { return class_exists($type) || interface_exists($type) diff --git a/tests/DI/Compiler.extensionOverride.phpt b/tests/DI/Compiler.extensionOverride.phpt index 9a673ee83..0d85be395 100644 --- a/tests/DI/Compiler.extensionOverride.phpt +++ b/tests/DI/Compiler.extensionOverride.phpt @@ -73,6 +73,10 @@ class FooExtension extends Nette\DI\CompilerExtension $builder->addDefinition('one8') ->setFactory('Lorem', [1]) ->addSetup('__construct', [2]); + $builder->addDefinition('one9') + ->setFactory('Lorem', [1]); + $builder->addDefinition('one10') + ->setFactory('Lorem', [1]); $builder->addDefinition('two1') ->setType('Lorem') @@ -101,6 +105,12 @@ class FooExtension extends Nette\DI\CompilerExtension $builder->addDefinition('two9') ->setType('Lorem') ->setFactory('Factory::createLorem', [1, 2]); + $builder->addDefinition('two10') + ->setType('Lorem') + ->setFactory('Factory::createLorem', [1]); + $builder->addDefinition('two11') + ->setType('Lorem') + ->setFactory('Factory::createLorem', [1]); $builder->addDefinition('three1') ->setFactory('Factory::createLorem', [1]); @@ -116,6 +126,10 @@ class FooExtension extends Nette\DI\CompilerExtension ->setFactory('Factory::createLorem', [1]); $builder->addDefinition('three7') ->setFactory('Factory::createLorem', [1]); + $builder->addDefinition('three8') + ->setFactory('Factory::createLorem', [1]); + $builder->addDefinition('three9') + ->setFactory('Factory::createLorem', [1]); } } @@ -166,6 +180,16 @@ Assert::same([ 'Ipsum::__construct ', ], Notes::fetch()); +Assert::exception(function () use ($container) { + $container->getService('one9'); +}, TypeError::class, 'Return value of %a%::createServiceOne9() must be an instance of Ipsum, instance of Lorem returned'); +Notes::fetch(); + +Assert::type(Ipsum::class, $container->getService('one10')); +Assert::same([ + 'Ipsum::__construct ', +], Notes::fetch()); + Assert::type(Ipsum::class, $container->getService('two1')); Assert::same([ @@ -217,6 +241,16 @@ Assert::same([ 'Lorem::__construct 2 new', ], Notes::fetch()); +Assert::exception(function () use ($container) { + $container->getService('two11'); +}, TypeError::class, 'Return value of %a%::createServiceTwo11() must be an instance of Ipsum, instance of Lorem returned'); +Notes::fetch(); + +Assert::type(Ipsum::class, $container->getService('two12')); +Assert::same([ + 'Ipsum::__construct ', +], Notes::fetch()); + Assert::type(Ipsum::class, $container->getService('three1')); @@ -253,3 +287,13 @@ Assert::type(Ipsum::class, $container->getService('three7')); Assert::same([ 'Ipsum::__construct 2', ], Notes::fetch()); + +Assert::exception(function () use ($container) { + $container->getService('three8'); +}, TypeError::class, 'Return value of %a%::createServiceThree8() must be an instance of Ipsum, instance of Lorem returned'); +Notes::fetch(); + +Assert::type(Ipsum::class, $container->getService('three9')); +Assert::same([ + 'Ipsum::__construct ', +], Notes::fetch()); diff --git a/tests/DI/CompilerExtension.loadDefinitionsFromConfig.phpt b/tests/DI/CompilerExtension.loadDefinitionsFromConfig.phpt index 08cd2b8b0..6ef8d3dee 100644 --- a/tests/DI/CompilerExtension.loadDefinitionsFromConfig.phpt +++ b/tests/DI/CompilerExtension.loadDefinitionsFromConfig.phpt @@ -27,6 +27,6 @@ $compilerExtension = (new CompilerExtension)->setCompiler($compiler, 'blog'); $compilerExtension->loadDefinitionsFromConfig($config['services']); -Assert::same('@blog.articles', $builder->getDefinition('blog.comments')->getFactory()->arguments[1]); -Assert::equal(new Reference('blog.articles'), $builder->getDefinition('blog.articlesList')->getFactory()->arguments[0]); -Assert::equal(new Reference('blog.comments'), $builder->getDefinition('blog.commentsControl')->getFactory()->arguments[0]->getEntity()); +Assert::same('@blog.articles', $builder->getServiceDefinition('blog.comments')->getFactory()->arguments[1]); +Assert::equal(new Reference('blog.articles'), $builder->getServiceDefinition('blog.articlesList')->getFactory()->arguments[0]); +Assert::equal(new Reference('blog.comments'), $builder->getServiceDefinition('blog.commentsControl')->getFactory()->arguments[0]->getEntity()); diff --git a/tests/DI/Config.Processor.normalizeConfig.phpt b/tests/DI/Config.Processor.normalizeConfig.phpt index 2c1dbda40..7cb9eb750 100644 --- a/tests/DI/Config.Processor.normalizeConfig.phpt +++ b/tests/DI/Config.Processor.normalizeConfig.phpt @@ -43,7 +43,7 @@ Assert::same(['implement' => Iface::class, 'tagged' => 123], $processor->normali // aliases -Assert::same(['type' => 'val'], $processor->normalizeConfig(['class' => 'val'])); +Assert::same(['factory' => null, 'type' => 'val'], $processor->normalizeConfig(['class' => 'val'])); Assert::same(['imported' => 'val'], $processor->normalizeConfig(['dynamic' => 'val'])); Assert::exception(function () use ($processor) { diff --git a/tests/DI/ContainerBuilder.getAccessorDefinition.phpt b/tests/DI/ContainerBuilder.getAccessorDefinition.phpt new file mode 100644 index 000000000..c49dffb30 --- /dev/null +++ b/tests/DI/ContainerBuilder.getAccessorDefinition.phpt @@ -0,0 +1,38 @@ +addAccessorDefinition('one') + ->setImplement(AccessorDefinition::class) + ->setClass(\stdClass::class); + +$builder->addDefinition('two') + ->setType(stdClass::class); + + +$definition = $builder->getAccessorDefinition('one'); +Assert::same($definitionOne, $definition); + +Assert::exception(function () use ($builder) { + $builder->getAccessorDefinition('unknown'); +}, Nette\DI\MissingServiceException::class, "Service 'unknown' not found."); + +Assert::exception(function () use ($builder) { + $builder->getAccessorDefinition('two'); +}, Nette\DI\MissingServiceException::class, "AccessorDefinition with name 'two' not found."); diff --git a/tests/DI/ContainerBuilder.getFactoryDefinition.phpt b/tests/DI/ContainerBuilder.getFactoryDefinition.phpt new file mode 100644 index 000000000..564e3e0f1 --- /dev/null +++ b/tests/DI/ContainerBuilder.getFactoryDefinition.phpt @@ -0,0 +1,33 @@ +addFactoryDefinition('one'); +$definitionOne->getResultDefinition() + ->setFactory(SplFileInfo::class); + +$builder->addDefinition('two') + ->setType(stdClass::class); + + +$definition = $builder->getFactoryDefinition('one'); +Assert::same($definitionOne, $definition); + +Assert::exception(function () use ($builder) { + $builder->getFactoryDefinition('unknown'); +}, Nette\DI\MissingServiceException::class, "Service 'unknown' not found."); + +Assert::exception(function () use ($builder) { + $builder->getFactoryDefinition('two'); +}, Nette\DI\MissingServiceException::class, "FactoryDefinition with name 'two' not found."); diff --git a/tests/DI/ContainerBuilder.getImportedDefinition.phpt b/tests/DI/ContainerBuilder.getImportedDefinition.phpt new file mode 100644 index 000000000..8b5c3ced5 --- /dev/null +++ b/tests/DI/ContainerBuilder.getImportedDefinition.phpt @@ -0,0 +1,31 @@ +addImportedDefinition('one'); + +$builder->addDefinition('two') + ->setType(stdClass::class); + + +$definition = $builder->getImportedDefinition('one'); +Assert::same($definitionOne, $definition); + +Assert::exception(function () use ($builder) { + $builder->getImportedDefinition('unknown'); +}, Nette\DI\MissingServiceException::class, "Service 'unknown' not found."); + +Assert::exception(function () use ($builder) { + $builder->getImportedDefinition('two'); +}, Nette\DI\MissingServiceException::class, "ImportedDefinition with name 'two' not found."); diff --git a/tests/DI/ContainerBuilder.getLocatorDefinition.phpt b/tests/DI/ContainerBuilder.getLocatorDefinition.phpt new file mode 100644 index 000000000..34db44e02 --- /dev/null +++ b/tests/DI/ContainerBuilder.getLocatorDefinition.phpt @@ -0,0 +1,31 @@ +addLocatorDefinition('one'); + +$builder->addDefinition('two') + ->setType(stdClass::class); + + +$definition = $builder->getLocatorDefinition('one'); +Assert::same($definitionOne, $definition); + +Assert::exception(function () use ($builder) { + $builder->getLocatorDefinition('unknown'); +}, Nette\DI\MissingServiceException::class, "Service 'unknown' not found."); + +Assert::exception(function () use ($builder) { + $builder->getLocatorDefinition('two'); +}, Nette\DI\MissingServiceException::class, "LocatorDefinition with name 'two' not found."); diff --git a/tests/DI/ContainerBuilder.getServiceDefinition.phpt b/tests/DI/ContainerBuilder.getServiceDefinition.phpt new file mode 100644 index 000000000..3a9ae4ab0 --- /dev/null +++ b/tests/DI/ContainerBuilder.getServiceDefinition.phpt @@ -0,0 +1,33 @@ +addDefinition('one') + ->setType(stdClass::class); + +$builder->addFactoryDefinition('two') + ->getResultDefinition() + ->setFactory(SplFileInfo::class); + + +$definition = $builder->getServiceDefinition('one'); +Assert::same($definitionOne, $definition); + +Assert::exception(function () use ($builder) { + $builder->getServiceDefinition('unknown'); +}, Nette\DI\MissingServiceException::class, "Service 'unknown' not found."); + +Assert::exception(function () use ($builder) { + $builder->getServiceDefinition('two'); +}, Nette\DI\MissingServiceException::class, "ServiceDefinition with name 'two' not found."); diff --git a/tests/DI/DecoratorExtension.basic.phpt b/tests/DI/DecoratorExtension.basic.phpt index 5ad5e57ab..18bf7c257 100644 --- a/tests/DI/DecoratorExtension.basic.phpt +++ b/tests/DI/DecoratorExtension.basic.phpt @@ -79,4 +79,4 @@ Assert::equal([ new Statement([new Reference('self'), 'setup'], ['Iface']), new Statement([new Reference('self'), 'setup']), new Statement([new Reference('self'), '$a'], [10]), -], $builder->getDefinition('one')->getSetup()); +], $builder->getServiceDefinition('one')->getSetup()); diff --git a/tests/DI/Helpers.getPropertyType.php74.phptx b/tests/DI/Helpers.getPropertyType.php74.phptx new file mode 100644 index 000000000..cd062b742 --- /dev/null +++ b/tests/DI/Helpers.getPropertyType.php74.phptx @@ -0,0 +1,75 @@ +getDefinition('last.one')->getSetup()); +], $builder->getServiceDefinition('last.one')->getSetup()); Assert::equal([ new Statement([new Reference('self'), 'injectA']), @@ -129,7 +129,7 @@ Assert::equal([ new Statement([new Reference('self'), '$e'], [new Reference('a')]), new Statement([new Reference('self'), '$c'], [new Reference('std')]), new Statement([new Reference('self'), '$a'], [new Reference('std')]), -], $builder->getDefinition('ext.one')->getSetup()); +], $builder->getServiceDefinition('ext.one')->getSetup()); Assert::equal([ new Statement([new Reference('self'), 'injectA']), @@ -139,4 +139,4 @@ Assert::equal([ new Statement([new Reference('self'), '$e'], [new Reference('b')]), new Statement([new Reference('self'), '$c'], [new Reference('std')]), new Statement([new Reference('self'), '$a'], [new Reference('std')]), -], $builder->getDefinition('two')->getSetup()); +], $builder->getServiceDefinition('two')->getSetup()); diff --git a/tests/DI/InjectExtension.implement.phpt b/tests/DI/InjectExtension.implement.phpt index 1104640ac..212685720 100644 --- a/tests/DI/InjectExtension.implement.phpt +++ b/tests/DI/InjectExtension.implement.phpt @@ -47,4 +47,4 @@ $builder = $compiler->getContainerBuilder(); Assert::equal([ new Statement([new Reference('self'), 'injectFoo'], [new Reference('01')]), -], $builder->getDefinition('sf')->getResultDefinition()->getSetup()); +], $builder->getFactoryDefinition('sf')->getResultDefinition()->getSetup()); diff --git a/tests/DI/Loader.include.params.phpt b/tests/DI/Loader.include.params.phpt new file mode 100644 index 000000000..599985ccf --- /dev/null +++ b/tests/DI/Loader.include.params.phpt @@ -0,0 +1,38 @@ +load('files/loader.includes.params.neon'); +}, Nette\InvalidArgumentException::class, "Missing parameter 'name'."); + + +test(function () { + $config = new Config\Loader; + $config->setParameters(['name' => 'loader.includes.params.child']); + $data = $config->load('files/loader.includes.params.neon'); + + Assert::same([ + 'files/loader.includes.params.neon', + 'files/loader.includes.params.child.neon', + ], $config->getDependencies()); + + Assert::same([ + 'parameters' => [ + 'foo' => 'bar', + 'name' => 'ignored', + ], + ], $data); +}); diff --git a/tests/DI/files/compiler.extensionOverride.neon b/tests/DI/files/compiler.extensionOverride.neon index 305c1d6ff..0e00e5679 100644 --- a/tests/DI/files/compiler.extensionOverride.neon +++ b/tests/DI/files/compiler.extensionOverride.neon @@ -18,6 +18,10 @@ services: factory: IpsumFactory::create(2) one8!: factory: Ipsum + one9: + type: Ipsum + one10: + class: Ipsum two1: factory: Ipsum @@ -42,6 +46,10 @@ services: two10: factory: Factory::createLorem(2) arguments: [1: new] + two11: + type: Ipsum + two12: + class: Ipsum three1: factory: Ipsum @@ -59,3 +67,7 @@ services: arguments: [2] three7: factory: IpsumFactory::create(2) + three8: + type: Ipsum + three9: + class: Ipsum diff --git a/tests/DI/files/loader.includes.params.child.neon b/tests/DI/files/loader.includes.params.child.neon new file mode 100644 index 000000000..8c788f38d --- /dev/null +++ b/tests/DI/files/loader.includes.params.child.neon @@ -0,0 +1,2 @@ +parameters: + foo: bar diff --git a/tests/DI/files/loader.includes.params.neon b/tests/DI/files/loader.includes.params.neon new file mode 100644 index 000000000..5711e9af3 --- /dev/null +++ b/tests/DI/files/loader.includes.params.neon @@ -0,0 +1,5 @@ +parameters: + name: ignored + +includes: + - %name%.neon