From 5d29ef27af8bec06aebf801f880968a7d276cf3d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 2 Oct 2023 21:20:16 +0200 Subject: [PATCH] ParametersExtension, Container: redesigned way of handling dynamic parameters via getParameter() [Closes #291][Closes #288] - parameters with expressions are automatically treated as dynamic --- src/Bridges/DITracy/ContainerPanel.php | 3 + .../templates/ContainerPanel.panel.phtml | 2 +- src/DI/Container.php | 28 ++++++++- src/DI/Extensions/DIExtension.php | 17 ++--- src/DI/Extensions/ParametersExtension.php | 62 ++++++++++++------- src/DI/Helpers.php | 13 +++- src/DI/PhpGenerator.php | 8 ++- tests/DI/Compiler.dynamicParameters.phpt | 29 --------- .../Compiler.dynamicParameters.validator.phpt | 15 +++-- .../DI/Compiler.extension.schema.dynamic.phpt | 2 +- tests/DI/Compiler.extension.schema.phpt | 16 +++++ tests/DI/Compiler.parameters.phpt | 40 ++++++++++-- tests/DI/DIExtension.exportParameters.phpt | 8 ++- tests/DI/Helpers.expand().phpt | 19 +++++- 14 files changed, 180 insertions(+), 82 deletions(-) diff --git a/src/Bridges/DITracy/ContainerPanel.php b/src/Bridges/DITracy/ContainerPanel.php index fdc427322..a50c0d6de 100644 --- a/src/Bridges/DITracy/ContainerPanel.php +++ b/src/Bridges/DITracy/ContainerPanel.php @@ -79,6 +79,9 @@ public function getPanel(): string $file = $rc->getFileName(); $instances = (function () { return $this->instances; })->bindTo($this->container, Container::class)(); $wiring = (function () { return $this->wiring; })->bindTo($this->container, $this->container)(); + $parameters = $rc->getMethod('getStaticParameters')->getDeclaringClass()->getName() === Container::class + ? null + : $container->getParameters(); require __DIR__ . '/templates/ContainerPanel.panel.phtml'; }); } diff --git a/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml b/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml index 49cce06b3..a54f7b138 100644 --- a/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml +++ b/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml @@ -75,7 +75,7 @@ use Tracy\Helpers;

Parameters

- parameters ? Dumper::toHtml($container->parameters) : "disabled via 'di › export › parameters'" ?> + disabled via 'di › export › parameters'" : Dumper::toHtml($parameters) ?>
diff --git a/src/DI/Container.php b/src/DI/Container.php index 91d1f2486..dca9d43d6 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -19,7 +19,10 @@ class Container { use Nette\SmartObject; - /** @var array user parameters */ + /** + * @var mixed[] + * @deprecated use Container::getParameter() or getParameters() + */ public $parameters = []; /** @var string[] services name => type (complete list of available services) */ @@ -46,7 +49,7 @@ class Container public function __construct(array $params = []) { - $this->parameters = $params; + $this->parameters = $params + $this->getStaticParameters(); $this->methods = array_flip(array_filter( get_class_methods($this), function ($s) { return preg_match('#^createService.#', $s); } @@ -60,6 +63,27 @@ public function getParameters(): array } + public function getParameter($key) + { + if (!array_key_exists($key, $this->parameters)) { + $this->parameters[$key] = $this->getDynamicParameter($key); + } + return $this->parameters[$key]; + } + + + protected function getStaticParameters(): array + { + return []; + } + + + protected function getDynamicParameter($key) + { + throw new Nette\InvalidStateException(sprintf("Parameter '%s' not found. Check if 'di › export › parameters' is enabled.", $key)); + } + + /** * Adds the service to the container. * @param object $service service or its factory diff --git a/src/DI/Extensions/DIExtension.php b/src/DI/Extensions/DIExtension.php index 254d86006..e66564857 100644 --- a/src/DI/Extensions/DIExtension.php +++ b/src/DI/Extensions/DIExtension.php @@ -69,20 +69,13 @@ public function loadConfiguration() } - public function beforeCompile() - { - if (!$this->config->export->parameters) { - $this->getContainerBuilder()->parameters = []; - } - } - - public function afterCompile(Nette\PhpGenerator\ClassType $class) { if ($this->config->parentClass) { $class->setExtends($this->config->parentClass); } + $this->restrictParameters($class); $this->restrictTags($class); $this->restrictTypes($class); @@ -95,6 +88,14 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class) } + private function restrictParameters(Nette\PhpGenerator\ClassType $class): void + { + if (!$this->config->export->parameters) { + $class->removeMethod('getStaticParameters'); + } + } + + private function restrictTags(Nette\PhpGenerator\ClassType $class): void { $option = $this->config->export->tags; diff --git a/src/DI/Extensions/ParametersExtension.php b/src/DI/Extensions/ParametersExtension.php index b97dfa4ad..3278a0f24 100644 --- a/src/DI/Extensions/ParametersExtension.php +++ b/src/DI/Extensions/ParametersExtension.php @@ -10,7 +10,9 @@ namespace Nette\DI\Extensions; use Nette; +use Nette\DI\Container; use Nette\DI\DynamicParameter; +use Nette\PhpGenerator\Method; /** @@ -37,15 +39,7 @@ public function __construct(array &$compilerConfig) public function loadConfiguration() { $builder = $this->getContainerBuilder(); - $params = $this->config; - $resolver = new Nette\DI\Resolver($builder); - $generator = new Nette\DI\PhpGenerator($builder); - - foreach ($this->dynamicParams as $key) { - $params[$key] = array_key_exists($key, $params) - ? new DynamicParameter($generator->formatPhp('($this->parameters[?] \?\? ?)', $resolver->completeArguments(Nette\DI\Helpers::filterArguments([$key, $params[$key]])))) - : new DynamicParameter((new Nette\PhpGenerator\Dumper)->format('$this->parameters[?]', $key)); - } + $params = array_fill_keys($this->dynamicParams, new DynamicParameter('')) + $this->config; $builder->parameters = Nette\DI\Helpers::expand($params, $params, true); @@ -58,21 +52,47 @@ public function loadConfiguration() public function afterCompile(Nette\PhpGenerator\ClassType $class) { - $parameters = $this->getContainerBuilder()->parameters; - array_walk_recursive($parameters, function (&$val): void { - if ($val instanceof Nette\DI\Definitions\Statement || $val instanceof DynamicParameter) { - $val = null; - } - }); + $builder = $this->getContainerBuilder(); + $dynamicParams = $this->dynamicParams; + foreach ($builder->parameters as $key => $value) { + $value = [$value]; + array_walk_recursive($value, function ($val) use (&$dynamicParams, $key): void { + if ($val instanceof DynamicParameter || $val instanceof Nette\DI\Definitions\Statement) { + $dynamicParams[] = $key; + } + }); + } + $dynamicParams = array_values(array_unique($dynamicParams)); + + $method = Method::from([Container::class, 'getStaticParameters']) + ->addBody('return ?;', [array_diff_key($builder->parameters, array_flip($dynamicParams))]); + $class->addMember($method); + + if (!$dynamicParams) { + return; + } + + $resolver = new Nette\DI\Resolver($builder); + $generator = new Nette\DI\PhpGenerator($builder); + $method = Method::from([Container::class, 'getDynamicParameter']); + $class->addMember($method); + $method->addBody('switch (true) {'); + foreach ($dynamicParams as $key) { + $value = Nette\DI\Helpers::expand($this->config[$key] ?? null, $builder->parameters); + $value = $resolver->completeArguments(Nette\DI\Helpers::filterArguments([$value])); + $method->addBody("\tcase \$key === ?: return ?;", [$key, $generator->convertArguments($value)[0]]); + } + $method->addBody("\tdefault: return parent::getDynamicParameter(\$key);\n};"); + + $method = Method::from([Container::class, 'getParameters']); + $class->addMember($method); + $method->addBody('array_map(function ($key) { $this->getParameter($key); }, ?);', [$dynamicParams]); + $method->addBody('return parent::getParameters();'); - $cnstr = $class->getMethod('__construct'); - $cnstr->addBody('$this->parameters += ?;', [$parameters]); foreach ($this->dynamicValidators as [$param, $expected]) { - if ($param instanceof Nette\DI\Definitions\Statement) { - continue; + if (!$param instanceof Nette\DI\Definitions\Statement) { + $this->initialization->addBody('Nette\Utils\Validators::assert(?, ?, ?);', [$param, $expected, 'dynamic parameter']); } - - $cnstr->addBody('Nette\Utils\Validators::assert(?, ?, ?);', [$param, $expected, 'dynamic parameter']); } } } diff --git a/src/DI/Helpers.php b/src/DI/Helpers.php index 582214a91..34b01cecf 100644 --- a/src/DI/Helpers.php +++ b/src/DI/Helpers.php @@ -87,8 +87,15 @@ private static function expandString(string $var, array $params, $recursive = fa foreach (explode('.', $part) as $key) { if (is_array($val) && array_key_exists($key, $val)) { $val = $val[$key]; - } elseif ($val instanceof DynamicParameter) { - $val = new DynamicParameter($val . '[' . var_export($key, true) . ']'); + if ($val instanceof DynamicParameter || $val instanceof Statement) { + $val = '$this->getParameter'; + foreach (explode('.', $part) as $i => $key) { + $key = var_export($key, true); + $val .= $i ? "[$key]" : "($key)"; + } + $val = new DynamicParameter($val); + break; + } } else { throw new Nette\InvalidArgumentException(sprintf("Missing parameter '%s'.", $part)); } @@ -116,7 +123,7 @@ private static function expandString(string $var, array $params, $recursive = fa $res = array_filter($res, function ($val): bool { return $val !== ''; }); $res = array_map(function ($val): string { return $val instanceof DynamicParameter - ? "($val)" + ? (string) $val : var_export((string) $val, true); }, $res); return new DynamicParameter(implode(' . ', $res)); diff --git a/src/DI/PhpGenerator.php b/src/DI/PhpGenerator.php index 5ba0a0776..00ba2755d 100644 --- a/src/DI/PhpGenerator.php +++ b/src/DI/PhpGenerator.php @@ -170,6 +170,12 @@ public function formatStatement(Statement $statement): string * @internal */ public function formatPhp(string $statement, array $args): string + { + return (new Php\Dumper)->format($statement, ...$this->convertArguments($args)); + } + + + public function convertArguments(array $args): array { array_walk_recursive($args, function (&$val): void { if ($val instanceof Statement) { @@ -186,7 +192,7 @@ public function formatPhp(string $statement, array $args): string } } }); - return (new Php\Dumper)->format($statement, ...$args); + return $args; } diff --git a/tests/DI/Compiler.dynamicParameters.phpt b/tests/DI/Compiler.dynamicParameters.phpt index 3b1ad0e96..9e73ac393 100644 --- a/tests/DI/Compiler.dynamicParameters.phpt +++ b/tests/DI/Compiler.dynamicParameters.phpt @@ -101,20 +101,6 @@ test('Array item as dynamic parameter within string expansion', function () { test('Class constant as parameter', function () { - $compiler = new DI\Compiler; - $compiler->setDynamicParameterNames(['dynamic']); - $container = createContainer($compiler, ' - parameters: - dynamic: ::trim(" a ") - - services: - one: Service(%dynamic%) - '); - Assert::same('a', $container->getService('one')->arg); -}); - - -test('', function () { $compiler = new DI\Compiler; $compiler->setDynamicParameterNames(['dynamic']); $container = createContainer($compiler, ' @@ -126,18 +112,3 @@ test('', function () { '); Assert::same('hello', $container->getService('one')->arg); }); - - -test('', function () { - $compiler = new DI\Compiler; - $compiler->setDynamicParameterNames(['dynamic']); - Assert::exception(function () use ($compiler) { - createContainer($compiler, ' - parameters: - dynamic: @one - - services: - one: Service - '); - }, Nette\DI\ServiceCreationException::class, "Reference to missing service 'one'."); -}); diff --git a/tests/DI/Compiler.dynamicParameters.validator.phpt b/tests/DI/Compiler.dynamicParameters.validator.phpt index 1d5daa4fa..fd32d3b34 100644 --- a/tests/DI/Compiler.dynamicParameters.validator.phpt +++ b/tests/DI/Compiler.dynamicParameters.validator.phpt @@ -28,11 +28,12 @@ test("Dynamic parameter of type int given to 'string' configuration", function ( $compiler->addExtension('foo', new FooExtension); $compiler->setDynamicParameterNames(['dynamic']); Assert::exception(function () use ($compiler) { - createContainer($compiler, ' + $container = createContainer($compiler, ' foo: key: string: %dynamic% ', ['dynamic' => 123]); + $container->initialize(); }, Nette\Utils\AssertionException::class, 'The dynamic parameter expects to be string, int 123 given.'); }); @@ -42,11 +43,12 @@ test("Dynamic parameter of type null given to 'string' configuration", function $compiler->addExtension('foo', new FooExtension); $compiler->setDynamicParameterNames(['dynamic']); Assert::exception(function () use ($compiler) { - createContainer($compiler, ' + $container = createContainer($compiler, ' foo: key: string: %dynamic% ', ['dynamic' => null]); + $container->initialize(); }, Nette\Utils\AssertionException::class, 'The dynamic parameter expects to be string, null given.'); }); @@ -56,11 +58,12 @@ test("Dynamic sub-parameter of type int given to 'string' configuration", functi $compiler->addExtension('foo', new FooExtension); $compiler->setDynamicParameterNames(['dynamic']); Assert::exception(function () use ($compiler) { - createContainer($compiler, ' + $container = createContainer($compiler, ' foo: key: string: %dynamic.sub% ', ['dynamic' => ['sub' => 123]]); + $container->initialize(); }, Nette\Utils\AssertionException::class, 'The dynamic parameter expects to be string, int 123 given.'); }); @@ -70,11 +73,12 @@ test("Dynamic parameter of type int successfully given to 'int|null' configurati $compiler->addExtension('foo', new FooExtension); $compiler->setDynamicParameterNames(['dynamic']); Assert::noError(function () use ($compiler) { - createContainer($compiler, ' + $container = createContainer($compiler, ' foo: key: intnull: %dynamic% ', ['dynamic' => 123]); + $container->initialize(); }); }); @@ -84,10 +88,11 @@ test("Dynamic parameter of type null successfully given to 'int|null' configurat $compiler->addExtension('foo', new FooExtension); $compiler->setDynamicParameterNames(['dynamic']); Assert::noError(function () use ($compiler) { - createContainer($compiler, ' + $container = createContainer($compiler, ' foo: key: intnull: %dynamic% ', ['dynamic' => null]); + $container->initialize(); }); }); diff --git a/tests/DI/Compiler.extension.schema.dynamic.phpt b/tests/DI/Compiler.extension.schema.dynamic.phpt index 4b26b660c..6b5b1c2e4 100644 --- a/tests/DI/Compiler.extension.schema.dynamic.phpt +++ b/tests/DI/Compiler.extension.schema.dynamic.phpt @@ -66,5 +66,5 @@ test('Statement via parameter', function () { foo: key: %dynamic% '); - Assert::type(Nette\DI\Definitions\Statement::class, $foo->getConfig()->key); + Assert::type(Nette\DI\DynamicParameter::class, $foo->getConfig()->key); }); diff --git a/tests/DI/Compiler.extension.schema.phpt b/tests/DI/Compiler.extension.schema.phpt index 1dd71038b..f53234694 100644 --- a/tests/DI/Compiler.extension.schema.phpt +++ b/tests/DI/Compiler.extension.schema.phpt @@ -72,3 +72,19 @@ test('Extension without configuration', function () { '); Assert::equal((object) ['key' => null], $foo->getConfig()); }); + + +test('Extension with parameter expansion', function () { + $compiler = new Nette\DI\Compiler; + $compiler->addExtension('foo', $foo = new FooExtension); + createContainer($compiler, ' + parameters: + foo: + scalar: hello + dynamic: ::trim(x) + + foo: + key: %foo.scalar% + '); + Assert::equal((object) ['key' => 'hello'], $foo->getConfig()); +}); diff --git a/tests/DI/Compiler.parameters.phpt b/tests/DI/Compiler.parameters.phpt index 9ee73e1dc..238412425 100644 --- a/tests/DI/Compiler.parameters.phpt +++ b/tests/DI/Compiler.parameters.phpt @@ -39,12 +39,32 @@ test('Statement as parameter', function () { one: Service(%bar%) '); - Assert::null($container->parameters['bar']); + Assert::same(['bar' => 'a'], $container->getParameters()); + Assert::same('a', $container->getParameter('bar')); Assert::same('a', $container->getService('one')->arg); }); test('Statement within string expansion', function () { + $compiler = new DI\Compiler; + $container = createContainer($compiler, ' + parameters: + bar: ::trim(" a ") + expand: hello%bar% + + services: + one: Service(%expand%) + '); + + Assert::same( + ['bar' => 'a', 'expand' => 'helloa'], + $container->getParameters() + ); + Assert::same('helloa', $container->getService('one')->arg); +}); + + +test('NOT class constant as parameter', function () { $compiler = new DI\Compiler; $container = createContainer($compiler, ' parameters: @@ -54,7 +74,7 @@ test('Statement within string expansion', function () { one: Service(%bar%) '); - Assert::same('Service::Name', $container->parameters['bar']); // not resolved + Assert::same(['bar' => 'Service::Name'], $container->getParameters()); // not resolved Assert::same('hello', $container->getService('one')->arg); }); @@ -69,7 +89,7 @@ test('Class method and constant resolution', function () { one: Service(%bar%) '); - Assert::null($container->parameters['bar']); + Assert::same(['bar' => 'Service::method hello'], $container->getParameters()); Assert::same('Service::method hello', $container->getService('one')->arg); }); @@ -102,7 +122,10 @@ test('Parameter as an instantiated class', function () { two: Service(two) '); - Assert::null($container->parameters['bar']); + Assert::equal( + ['bar' => new Service($container->getService('two'))], + $container->getParameters() + ); Assert::same($container->getService('two'), $container->getService('one')->arg->arg); }); @@ -118,6 +141,11 @@ test('Detecting circular references', function () { two: Service(two) '); - Assert::null($container->parameters['bar']); - Assert::same([$container->getService('two')], $container->getService('one')->arg); + Assert::exception( + function () use ($container) { + $container->getParameter('bar'); + }, + Nette\InvalidStateException::class, + 'Circular reference detected for services: one.' + ); }); diff --git a/tests/DI/DIExtension.exportParameters.phpt b/tests/DI/DIExtension.exportParameters.phpt index f23b2bfde..4e024f08a 100644 --- a/tests/DI/DIExtension.exportParameters.phpt +++ b/tests/DI/DIExtension.exportParameters.phpt @@ -26,6 +26,7 @@ test('Parameters are exported when setting is true', function () { parameters: true '); + Assert::same(['key' => 'val'], $container->parameters); Assert::same(['key' => 'val'], $container->getParameters()); }); @@ -42,6 +43,7 @@ test('Parameters are not exported when setting is false', function () { parameters: false '); + Assert::same([], $container->parameters); Assert::same([], $container->getParameters()); }); @@ -59,7 +61,8 @@ test('Dynamic parameters are correctly exported when export setting is true', fu parameters: true ', ['dynamic' => 123]); - Assert::same(['dynamic' => 123, 'key' => null], $container->getParameters()); + Assert::same(['dynamic' => 123], $container->parameters); + Assert::same(['dynamic' => 123, 'key' => 123], $container->getParameters()); }); @@ -76,5 +79,6 @@ test('Dynamic parameters remain even when export setting is false', function () parameters: false ', ['dynamic' => 123]); - Assert::same(['dynamic' => 123], $container->getParameters()); + Assert::same(['dynamic' => 123], $container->parameters); + Assert::same(['dynamic' => 123, 'key' => 123], $container->getParameters()); }); diff --git a/tests/DI/Helpers.expand().phpt b/tests/DI/Helpers.expand().phpt index a515d5719..cda476341 100644 --- a/tests/DI/Helpers.expand().phpt +++ b/tests/DI/Helpers.expand().phpt @@ -35,9 +35,22 @@ Assert::same( Assert::equal(new PhpLiteral('func()'), Helpers::expand('%key%', ['key' => new PhpLiteral('func()')])); -Assert::equal(new DynamicParameter("'text' . (func())"), Helpers::expand('text%key%', ['key' => new DynamicParameter('func()')])); -Assert::equal(new DynamicParameter("(func()) . 'text'"), Helpers::expand('%key%text', ['key' => new DynamicParameter('func()')])); -Assert::equal(new DynamicParameter("'a' . (func()) . 'b' . '123' . (func()) . 'c'"), Helpers::expand('a%key1%b%key2%%key1%c', ['key1' => new DynamicParameter('func()'), 'key2' => 123])); +Assert::equal( + new DynamicParameter("\$this->getParameter('key')['foo']"), + Helpers::expand('%key.foo%', ['key' => new DynamicParameter('')]) +); +Assert::equal( + new DynamicParameter("'text' . \$this->getParameter('key')"), + Helpers::expand('text%key%', ['key' => new DynamicParameter('')]) +); +Assert::equal( + new DynamicParameter("\$this->getParameter('key') . 'text'"), + Helpers::expand('%key%text', ['key' => new DynamicParameter('')]) +); +Assert::equal( + new DynamicParameter("'a' . \$this->getParameter('key1') . 'b' . '123' . \$this->getParameter('key1') . 'c'"), + Helpers::expand('a%key1%b%key2%%key1%c', ['key1' => new DynamicParameter(''), 'key2' => 123]) +); Assert::exception(function () {