From 007bc0fb18385211ad2307ea8218b5178e7a157c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 3 Feb 2025 03:31:24 +0100 Subject: [PATCH 01/22] github actions updated --- .github/workflows/coding-style.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 96d05c498..995988d49 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.3 coverage: none - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.3 coverage: none - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b908378f2..189614db5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,9 +22,9 @@ jobs: - run: composer install --no-progress --prefer-dist - run: vendor/bin/tester tests -s -C - if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: output + name: output-${{ matrix.php }} path: tests/**/output From b3776b1c0f7af4810ad8898f6ecff2ebaa351043 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 6 Jun 2025 00:56:10 +0200 Subject: [PATCH 02/22] composer: require stable packages outside of nette --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c3bec0059..78e4439e4 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "require-dev": { "nette/tester": "^2.5.2", "tracy/tracy": "^2.9", - "phpstan/phpstan": "^1.0" + "phpstan/phpstan-nette": "^2.0@stable" }, "autoload": { "classmap": ["src/"] From 9ab27c6decbc0db80afcbf5b6cd245567b501d4f Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 6 Jun 2025 01:20:03 +0200 Subject: [PATCH 03/22] composer: added psr-4 loader --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 78e4439e4..cbf4588ac 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,10 @@ "phpstan/phpstan-nette": "^2.0@stable" }, "autoload": { - "classmap": ["src/"] + "classmap": ["src/"], + "psr-4": { + "Nette\\": "src" + } }, "minimum-stability": "dev", "scripts": { From 908e6f5f1d5cb66d6a25a8010d005e441432a5df Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 3 Feb 2025 01:42:36 +0100 Subject: [PATCH 04/22] tests: improved descriptions --- tests/DI/Compiler.addExtension.phpt | 6 +- .../DI/ContainerBuilder.autowiring.types.phpt | 28 +++++----- tests/DI/ContainerBuilder.create.error.phpt | 56 +++++++++---------- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/tests/DI/Compiler.addExtension.phpt b/tests/DI/Compiler.addExtension.phpt index e27c1d1f7..071642abf 100644 --- a/tests/DI/Compiler.addExtension.phpt +++ b/tests/DI/Compiler.addExtension.phpt @@ -21,21 +21,21 @@ class FooExtension extends DI\CompilerExtension } -testException('', function () { +testException('adding extension during loadConfiguration triggers deprecation', function () { $compiler = new DI\Compiler; $compiler->addExtension('foo', new FooExtension); $container = createContainer($compiler); }, Nette\DeprecatedException::class, "Extensions 'bar' were added while container was being compiled."); -testException('', function () { +testException('duplicate extension name throws error', function () { $compiler = new DI\Compiler; $compiler->addExtension('foo', new FooExtension); $compiler->addExtension('foo', new FooExtension); }, Nette\InvalidArgumentException::class, "Name 'foo' is already used or reserved."); -testException('', function () { +testException('extension name conflict due to case-insensitivity', function () { $compiler = new DI\Compiler; $compiler->addExtension('foo', new FooExtension); $compiler->addExtension('Foo', new FooExtension); diff --git a/tests/DI/ContainerBuilder.autowiring.types.phpt b/tests/DI/ContainerBuilder.autowiring.types.phpt index f14f11531..66e3b1f3e 100644 --- a/tests/DI/ContainerBuilder.autowiring.types.phpt +++ b/tests/DI/ContainerBuilder.autowiring.types.phpt @@ -30,7 +30,7 @@ class Bar extends Foo implements IBar } -test('Autowiring limited to Bar class and its subclasses', function () { +test('autowire using self type only', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -43,7 +43,7 @@ test('Autowiring limited to Bar class and its subclasses', function () { }); -test('Autowiring limited to Bar class via self', function () { +test('autowire with "self" keyword works correctly', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -56,7 +56,7 @@ test('Autowiring limited to Bar class via self', function () { }); -test('Autowiring limited to IBar interface and its implementations', function () { +test('autowire via interface returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -69,7 +69,7 @@ test('Autowiring limited to IBar interface and its implementations', function () }); -test('Autowiring limited to Foo class and its subclasses', function () { +test('autowire via parent class returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -82,7 +82,7 @@ test('Autowiring limited to Foo class and its subclasses', function () { }); -test('Autowiring limited to IFoo interface and its implementations', function () { +test('autowire using implemented interface returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -95,7 +95,7 @@ test('Autowiring limited to IFoo interface and its implementations', function () }); -test('Autowiring limited to two interfaces', function () { +test('autowire with multiple types registers for all', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -108,7 +108,7 @@ test('Autowiring limited to two interfaces', function () { }); -test('Autowiring limited to two classes', function () { +test('autowire with redundant types excludes mismatches', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -121,7 +121,7 @@ test('Autowiring limited to two classes', function () { }); -test('Autowiring limited to class and interface', function () { +test('autowire with parent and interface returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -134,7 +134,7 @@ test('Autowiring limited to class and interface', function () { }); -test('Autowiring limited to class and interface', function () { +test('autowire with self and interface returns service', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -147,7 +147,7 @@ test('Autowiring limited to class and interface', function () { }); -test('Distribution between two services with parent-child relation', function () { +test('separate definitions for parent and self types', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -164,7 +164,7 @@ test('Distribution between two services with parent-child relation', function () }); -test('Distribution between two services of same type', function () { +test('prefer autowired service when multiple exist', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one') ->setType(stdClass::class); @@ -177,7 +177,7 @@ test('Distribution between two services of same type', function () { }); -test('', function () { +test('autowire with override of secondary definition', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('bar') ->setType(Bar::class) @@ -194,7 +194,7 @@ test('', function () { }); -test('', function () { +test('ambiguous autowiring throws exception for multiple services', function () { $builder = new DI\ContainerBuilder; $bar = $builder->addDefinition('bar') ->setType(Bar::class) @@ -221,7 +221,7 @@ test('', function () { }); -test('', function () { +test('incompatible autowired type triggers exception', function () { $builder = new DI\ContainerBuilder; $bar = $builder->addDefinition('bar') ->setType(Foo::class) diff --git a/tests/DI/ContainerBuilder.create.error.phpt b/tests/DI/ContainerBuilder.create.error.phpt index 0af01c12c..6d4ba9a5a 100644 --- a/tests/DI/ContainerBuilder.create.error.phpt +++ b/tests/DI/ContainerBuilder.create.error.phpt @@ -14,20 +14,20 @@ use Nette\DI\Definitions\Statement; require __DIR__ . '/../bootstrap.php'; -testException('', function () { +testException('non-existent class in type causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setType('X')->setCreator('Unknown'); }, Nette\InvalidArgumentException::class, "Service 'one': Class or interface 'X' not found."); -testException('', function () { +testException('missing class in creator triggers service creation error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition(null)->setCreator('Unknown'); $builder->complete(); }, Nette\DI\ServiceCreationException::class, "Service (Unknown::__construct()): Class 'Unknown' not found."); -testException('', function () { +testException('undefined class in dependency throws error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('@two'); $builder->addDefinition('two')->setCreator('Unknown'); @@ -35,7 +35,7 @@ testException('', function () { }, Nette\InvalidStateException::class, "Service 'two': Class 'Unknown' not found."); -testException('', function () { +testException('reference to undefined class in dependency causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator(new Reference('two')); $builder->addDefinition('two')->setCreator('Unknown'); @@ -43,21 +43,21 @@ testException('', function () { }, Nette\InvalidStateException::class, "Service 'two': Class 'Unknown' not found."); -testException('', function () { +testException('non-callable method in creator causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('stdClass::foo'); $builder->complete(); }, Nette\InvalidStateException::class, "Service 'one': Method stdClass::foo() is not callable."); -testException('', function () { +testException('uncallable magic method in creator triggers error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('Nette\DI\Container::foo'); // has __magic $builder->complete(); }, Nette\InvalidStateException::class, "Service 'one': Method Nette\\DI\\Container::foo() is not callable."); -testException('', function () { +testException('non-existent interface in factory definition causes error', function () { $builder = new DI\ContainerBuilder; $builder->addFactoryDefinition('one') ->setImplement('Unknown'); @@ -70,7 +70,7 @@ interface Bad4 public function create(); } -testException('', function () { +testException('undeclared return type in factory interface triggers error', function () { $builder = new DI\ContainerBuilder; $builder->addFactoryDefinition('one') ->setImplement(Bad4::class); @@ -82,7 +82,7 @@ interface Bad5 public function get($arg); } -testException('', function () { +testException('method with parameters in accessor interface causes error', function () { $builder = new DI\ContainerBuilder; $builder->addAccessorDefinition('one') ->setImplement(Bad5::class); @@ -97,7 +97,7 @@ class Bad6 } } -testException('', function () { +testException('non-callable factory method due to protection level', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('Bad6::create'); $builder->complete(); @@ -111,7 +111,7 @@ class Bad7 } } -testException('', function () { +testException('factory method without return type causes unknown service type error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('Bad7::create'); $builder->complete(); @@ -125,7 +125,7 @@ class Bad8 } } -testException('', function () { +testException('private constructor in service type causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setType(Bad8::class); $builder->complete(); @@ -139,13 +139,13 @@ class Good } } -testException('fail in argument', function () { +testException('unknown class in constructor argument triggers error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator(Good::class, [new Statement('Unknown')]); $builder->complete(); }, Nette\InvalidStateException::class, "Service 'one' (type of Good): Class 'Unknown' not found. (used in Good::__construct())"); -testException('fail in argument', function () { +testException('private constructor in argument service causes error', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator(Good::class, [new Statement(Bad8::class)]); $builder->complete(); @@ -173,7 +173,7 @@ trait Bad10 } } -testException('trait cannot be instantiated', function () { +testException('trait method is not callable as service creator', function () { $builder = new DI\ContainerBuilder; $builder->addDefinition('one')->setCreator('Bad10::method'); $builder->complete(); @@ -194,7 +194,7 @@ class MethodParam } } -testException('autowiring fail', function () { +testException('ambiguous constructor dependency triggers multiple services error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -204,7 +204,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of ConstructorParam): Multiple services of type stdClass found: a, b (required by \$x in ConstructorParam::__construct())"); -testException('forced autowiring fail', function () { +testException('ambiguous constructor dependency via argument reference', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -214,7 +214,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of ConstructorParam): Multiple services of type stdClass found: a, b (used in ConstructorParam::__construct())"); -testException('autowiring fail in chain', function () { +testException('ambiguous method parameter dependency triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -224,7 +224,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of MethodParam): Multiple services of type stdClass found: a, b (required by \$x in MethodParam::foo())"); -testException('forced autowiring fail in chain', function () { +testException('ambiguous dependency in method call triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -234,7 +234,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of MethodParam): Multiple services of type stdClass found: a, b (used in foo())"); -testException('autowiring fail in argument', function () { +testException('multiple services in constructor dependency cause ambiguity', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -244,7 +244,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (required by \$x in ConstructorParam::__construct()) (used in Good::__construct())"); -testException('forced autowiring fail in argument', function () { +testException('ambiguous dependency in constructor argument triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -254,7 +254,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in ConstructorParam::__construct())"); -testException('autowiring fail in chain in argument', function () { +testException('ambiguous dependency in method parameter causes error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -264,7 +264,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (required by \$x in MethodParam::foo()) (used in Good::__construct())"); -testException('forced autowiring fail in chain in argument', function () { +testException('ambiguous dependency in method call triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -274,7 +274,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in foo())"); -testException('forced autowiring fail in property passing', function () { +testException('ambiguous dependency in property setup triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -287,7 +287,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in @bad::\$a)"); -testException('autowiring fail in rich property passing', function () { +testException('ambiguous dependency in method setup triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -300,7 +300,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in foo())"); -testException('autowiring fail in method calling', function () { +testException('ambiguous dependency in method call during setup triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -313,7 +313,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of MethodParam): Multiple services of type stdClass found: a, b (required by \$x in MethodParam::foo())"); -testException('forced autowiring fail in method calling', function () { +testException('ambiguous dependency in method call on service triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass @@ -326,7 +326,7 @@ services: }, Nette\DI\ServiceCreationException::class, "Service 'bad' (type of Good): Multiple services of type stdClass found: a, b (used in @bad::bar())"); -testException('autowiring fail in rich method calling', function () { +testException('ambiguous dependency in method call setup triggers error', function () { createContainer(new DI\Compiler, ' services: a: stdClass From ca55ed7ecffdeaee0122765722e9ad9f8254f9a5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 18 Jun 2025 21:28:51 +0200 Subject: [PATCH 05/22] tests: merges some tests --- tests/DI/Compiler.referenceBug.phpt | 41 ------------------- ...Extension.getInjectProperties().php74.phpt | 39 ------------------ ...Extension.getInjectProperties().php80.phpt | 38 ----------------- ...InjectExtension.getInjectProperties().phpt | 25 ++++++++--- 4 files changed, 19 insertions(+), 124 deletions(-) delete mode 100644 tests/DI/Compiler.referenceBug.phpt delete mode 100644 tests/DI/InjectExtension.getInjectProperties().php74.phpt delete mode 100644 tests/DI/InjectExtension.getInjectProperties().php80.phpt diff --git a/tests/DI/Compiler.referenceBug.phpt b/tests/DI/Compiler.referenceBug.phpt deleted file mode 100644 index 4351067ae..000000000 --- a/tests/DI/Compiler.referenceBug.phpt +++ /dev/null @@ -1,41 +0,0 @@ -args = func_get_args(); - } -} - - -$container = createContainer(new DI\Compiler, ' -services: - - stdClass - a: Lorem(x: true) - b: Lorem(x: Lorem(x: true)) - c: Lorem("@test") -'); - - -Assert::same(['@foo', '@@foo', '@\stdClass', true], $container->getService('a')->args); -Assert::equal(['@foo', '@@foo', '@\stdClass', new Lorem('@foo', '@@foo', '@\stdClass', true)], $container->getService('b')->args); -Assert::same(['@test'], $container->getService('c')->args); diff --git a/tests/DI/InjectExtension.getInjectProperties().php74.phpt b/tests/DI/InjectExtension.getInjectProperties().php74.phpt deleted file mode 100644 index 33a5cb95e..000000000 --- a/tests/DI/InjectExtension.getInjectProperties().php74.phpt +++ /dev/null @@ -1,39 +0,0 @@ - A\AInjected::class, - 'varC' => A\AInjected::class, - ], InjectExtension::getInjectProperties(A\AClass::class)); -} diff --git a/tests/DI/InjectExtension.getInjectProperties().php80.phpt b/tests/DI/InjectExtension.getInjectProperties().php80.phpt deleted file mode 100644 index e24155f65..000000000 --- a/tests/DI/InjectExtension.getInjectProperties().php80.phpt +++ /dev/null @@ -1,38 +0,0 @@ - InjectExtension::getInjectProperties(AClass::class), - Nette\InvalidStateException::class, - "Type of property AClass::\$var is expected to not be nullable/built-in/complex, 'AClass|stdClass' given.", -); - -Assert::same([ - 'varA' => 'stdClass', -], InjectExtension::getInjectProperties(EClass::class)); diff --git a/tests/DI/InjectExtension.getInjectProperties().phpt b/tests/DI/InjectExtension.getInjectProperties().phpt index 79c00b86e..80b3793e7 100644 --- a/tests/DI/InjectExtension.getInjectProperties().phpt +++ b/tests/DI/InjectExtension.getInjectProperties().phpt @@ -10,14 +10,14 @@ namespace A { class AClass { - /** @var AInjected @inject */ - public $varA; + /** @var Different @inject */ + public AInjected $varA; /** @var B\BInjected @inject */ public $varB; - /** @var AInjected @inject */ - public $varC; + /** @inject */ + public AInjected $varC; /** @var AInjected */ public $varD; @@ -26,16 +26,23 @@ namespace A class AInjected { } + + class BadClass + { + /** @inject */ + public AClass|\stdClass $var; + } } namespace A\B { use A; + use Nette\DI\Attributes\Inject; class BClass extends A\AClass { - /** @var BInjected @inject */ - public $varF; + #[Inject] + public BInjected $varF; } class BInjected @@ -95,4 +102,10 @@ namespace { 'var3' => C\CInjected::class, 'var4' => C\CInjected::class, ], InjectExtension::getInjectProperties(C\CClass::class)); + + Assert::exception( + fn() => InjectExtension::getInjectProperties(A\BadClass::class), + Nette\InvalidStateException::class, + "Type of property A\\BadClass::\$var is expected to not be nullable/built-in/complex, 'A\\AClass|stdClass' given.", + ); } From 5c1ce07762b1ecebad2b10ac43221e77731929b3 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 12 Jan 2025 08:31:19 +0100 Subject: [PATCH 06/22] ConnectionPanel: convert templates to Latte-like syntax --- src/Bridges/DITracy/ContainerPanel.php | 4 +- src/Bridges/DITracy/dist/panel.phtml | 87 +++++++++++++++++++ src/Bridges/DITracy/dist/tab.phtml | 10 +++ src/Bridges/DITracy/panel.latte | 84 ++++++++++++++++++ src/Bridges/DITracy/tab.latte | 6 ++ .../templates/ContainerPanel.panel.phtml | 81 ----------------- .../templates/ContainerPanel.tab.phtml | 11 --- 7 files changed, 189 insertions(+), 94 deletions(-) create mode 100644 src/Bridges/DITracy/dist/panel.phtml create mode 100644 src/Bridges/DITracy/dist/tab.phtml create mode 100644 src/Bridges/DITracy/panel.latte create mode 100644 src/Bridges/DITracy/tab.latte delete mode 100644 src/Bridges/DITracy/templates/ContainerPanel.panel.phtml delete mode 100644 src/Bridges/DITracy/templates/ContainerPanel.tab.phtml diff --git a/src/Bridges/DITracy/ContainerPanel.php b/src/Bridges/DITracy/ContainerPanel.php index 82051f931..f0519c3d6 100644 --- a/src/Bridges/DITracy/ContainerPanel.php +++ b/src/Bridges/DITracy/ContainerPanel.php @@ -40,7 +40,7 @@ public function getTab(): string { return Nette\Utils\Helpers::capture(function () { $elapsedTime = $this->elapsedTime; - require __DIR__ . '/templates/ContainerPanel.tab.phtml'; + require __DIR__ . '/dist/tab.phtml'; }); } @@ -76,7 +76,7 @@ public function getPanel(): string $parameters = $rc->getMethod('getStaticParameters')->getDeclaringClass()->getName() === Container::class ? null : $container->getParameters(); - require __DIR__ . '/templates/ContainerPanel.panel.phtml'; + require __DIR__ . '/dist/panel.phtml'; }); } } diff --git a/src/Bridges/DITracy/dist/panel.phtml b/src/Bridges/DITracy/dist/panel.phtml new file mode 100644 index 000000000..8525b5794 --- /dev/null +++ b/src/Bridges/DITracy/dist/panel.phtml @@ -0,0 +1,87 @@ + + + + +

Nette DI Container

+ +
+
+

Source: +

+ + + + + + + + + + + + $type): ?> + + + + + + +
NameAutowiredServiceTags
+ + + + + + + + + + + true, Dumper::LIVE => true, Dumper::DEPTH => 5]) ?> + + + + + + + + =  true]) ?> + + true]) ?> + +
+ +

Parameters

+ +
+ disabled via 'di › export › parameters' + + +
+
+
diff --git a/src/Bridges/DITracy/dist/tab.phtml b/src/Bridges/DITracy/dist/tab.phtml new file mode 100644 index 000000000..f9cb72857 --- /dev/null +++ b/src/Bridges/DITracy/dist/tab.phtml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/Bridges/DITracy/panel.latte b/src/Bridges/DITracy/panel.latte new file mode 100644 index 000000000..87d51026e --- /dev/null +++ b/src/Bridges/DITracy/panel.latte @@ -0,0 +1,84 @@ +{use Tracy\Dumper} + + + +

Nette DI Container

+ +
+
+

Source: {Tracy\Helpers::editorLink($file)}

+ + + + + + + + + + + + {foreach $services as $name => $type} + {do $name = (string) $name} + {do $autowired = in_array($name, array_merge($wiring[$type][0] ?? [], $wiring[$type][1] ?? []), strict: true)} + + + + + + + {/foreach} + +
NameAutowiredServiceTags
+ {if is_numeric($name)}{$name}{else}{$name}{/if} + + {$autowired ? yes : (isset($wiring[$type]) ? no : '?')} + + {if isset($instances[$name]) && !$instances[$name] instanceof Nette\DI\Container} + {Dumper::toHtml($instances[$name], [Dumper::COLLAPSE => true, Dumper::LIVE => true, Dumper::DEPTH => 5])} + {elseif isset($instances[$name])} + {get_class($instances[$name])} + {elseif is_string($type)} + {$type} + {/if} + + {if !isset($tags[$name])} + {elseif count($tags[$name]) === 1} + {key($tags[$name])} = {Dumper::toHtml(current($tags[$name]), [Dumper::COLLAPSE => true])} + {else} + {Dumper::toHtml($tags[$name], [Dumper::COLLAPSE => true])} + {/if} +
+ +

Parameters

+ +
+ {if $parameters === null} + disabled via 'di › export › parameters' + {else} + {Dumper::toHtml($parameters)} + {/if} +
+
+
diff --git a/src/Bridges/DITracy/tab.latte b/src/Bridges/DITracy/tab.latte new file mode 100644 index 000000000..42322c5ec --- /dev/null +++ b/src/Bridges/DITracy/tab.latte @@ -0,0 +1,6 @@ + + + + {$elapsedTime ? sprintf('%0.1f ms', $elapsedTime * 1000) : ''} + diff --git a/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml b/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml deleted file mode 100644 index b84022914..000000000 --- a/src/Bridges/DITracy/templates/ContainerPanel.panel.phtml +++ /dev/null @@ -1,81 +0,0 @@ - - - -

Nette DI Container

- -
-
-

Source:

- - - - - - - - - - - - $type): ?> - - - - - - - - - - -
NameAutowiredServiceTags
$name" : Helpers::escapeHtml($name) ?> - - true, Dumper::LIVE => true, Dumper::DEPTH => 5]); ?> - - - - - - true]) - : Dumper::toHtml($tags[$name], [Dumper::COLLAPSE => true]); - } ?>
- -

Parameters

- -
- disabled via 'di › export › parameters'" : Dumper::toHtml($parameters) ?> -
-
-
diff --git a/src/Bridges/DITracy/templates/ContainerPanel.tab.phtml b/src/Bridges/DITracy/templates/ContainerPanel.tab.phtml deleted file mode 100644 index 640319dde..000000000 --- a/src/Bridges/DITracy/templates/ContainerPanel.tab.phtml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - From 6db42d622b6e8bdee8ffc63c8c8353c908c218cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Dobe=C5=A1?= Date: Wed, 18 Jun 2025 20:21:55 +0200 Subject: [PATCH 07/22] used generics for Container::createInstance() (#323) --- src/DI/Container.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/DI/Container.php b/src/DI/Container.php index d72639a63..da8557cc9 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -320,6 +320,9 @@ private function preventDeadLock(string $key, \Closure $callback): mixed /** * Creates an instance of the class and passes dependencies to the constructor using autowiring. + * @template T of object + * @param class-string $class + * @return T */ public function createInstance(string $class, array $args = []): object { From 01b29b04e8e4f0552787fb0f3c12e101bde3b99e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 12 Dec 2024 06:06:50 +0100 Subject: [PATCH 08/22] exception: use natural explanatory style --- src/DI/exceptions.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DI/exceptions.php b/src/DI/exceptions.php index 83fcb526d..9f13a87d3 100644 --- a/src/DI/exceptions.php +++ b/src/DI/exceptions.php @@ -13,7 +13,7 @@ /** - * Service not found exception. + * The requested service was not found in the container. */ class MissingServiceException extends Nette\InvalidStateException { @@ -21,7 +21,7 @@ class MissingServiceException extends Nette\InvalidStateException /** - * Service creation exception. + * Failed to create the service instance. */ class ServiceCreationException extends Nette\InvalidStateException { @@ -34,7 +34,7 @@ public function setMessage(string $message): static /** - * Not allowed when container is resolving. + * Operation is not allowed while container is resolving dependencies. */ class NotAllowedDuringResolvingException extends Nette\InvalidStateException { @@ -42,7 +42,7 @@ class NotAllowedDuringResolvingException extends Nette\InvalidStateException /** - * Error in configuration. + * The DI container configuration is invalid. */ class InvalidConfigurationException extends Nette\InvalidStateException { From 39accabdbae35d615d85ceaaafa4f4d75d742f67 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 19 Jun 2025 19:10:58 +0200 Subject: [PATCH 09/22] optimized global function calls --- src/Bridges/DITracy/ContainerPanel.php | 1 + src/DI/Autowiring.php | 2 ++ src/DI/Compiler.php | 1 + src/DI/CompilerExtension.php | 1 + src/DI/Config/Adapters/NeonAdapter.php | 1 + src/DI/Config/Helpers.php | 1 + src/DI/Config/Loader.php | 2 ++ src/DI/Container.php | 1 + src/DI/ContainerBuilder.php | 1 + src/DI/ContainerLoader.php | 2 ++ src/DI/Definitions/AccessorDefinition.php | 1 + src/DI/Definitions/Definition.php | 1 + src/DI/Definitions/FactoryDefinition.php | 1 + src/DI/Definitions/LocatorDefinition.php | 1 + src/DI/Definitions/ServiceDefinition.php | 1 + src/DI/Definitions/Statement.php | 1 + src/DI/DependencyChecker.php | 2 ++ src/DI/Extensions/DIExtension.php | 2 ++ src/DI/Extensions/DecoratorExtension.php | 1 + src/DI/Extensions/DefinitionSchema.php | 1 + src/DI/Extensions/ExtensionsExtension.php | 1 + src/DI/Extensions/InjectExtension.php | 1 + src/DI/Extensions/ParametersExtension.php | 1 + src/DI/Extensions/SearchExtension.php | 1 + src/DI/Extensions/ServicesExtension.php | 1 + src/DI/Helpers.php | 2 ++ src/DI/PhpGenerator.php | 1 + src/DI/Resolver.php | 1 + 28 files changed, 34 insertions(+) diff --git a/src/Bridges/DITracy/ContainerPanel.php b/src/Bridges/DITracy/ContainerPanel.php index f0519c3d6..57f4431e9 100644 --- a/src/Bridges/DITracy/ContainerPanel.php +++ b/src/Bridges/DITracy/ContainerPanel.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\Container; use Tracy; +use const SORT_NATURAL; /** diff --git a/src/DI/Autowiring.php b/src/DI/Autowiring.php index 601f59ed7..3be57cb3a 100644 --- a/src/DI/Autowiring.php +++ b/src/DI/Autowiring.php @@ -9,6 +9,8 @@ namespace Nette\DI; +use function array_merge, class_exists, class_implements, class_parents, count, implode, interface_exists, is_a, is_array, natsort, sprintf, str_contains; + /** * Autowiring. diff --git a/src/DI/Compiler.php b/src/DI/Compiler.php index 80e9d0fb0..6f675174d 100644 --- a/src/DI/Compiler.php +++ b/src/DI/Compiler.php @@ -11,6 +11,7 @@ use Nette; use Nette\Schema; +use function array_diff_key, array_filter, array_keys, array_merge, assert, count, implode, key, sprintf, strtolower; /** diff --git a/src/DI/CompilerExtension.php b/src/DI/CompilerExtension.php index 8d623b891..ed6b17200 100644 --- a/src/DI/CompilerExtension.php +++ b/src/DI/CompilerExtension.php @@ -10,6 +10,7 @@ namespace Nette\DI; use Nette; +use function array_diff_key, array_keys, func_num_args, implode, is_object, is_string, key, sprintf, str_replace, str_starts_with, substr_replace; /** diff --git a/src/DI/Config/Adapters/NeonAdapter.php b/src/DI/Config/Adapters/NeonAdapter.php index fcc2d5459..5b26583b9 100644 --- a/src/DI/Config/Adapters/NeonAdapter.php +++ b/src/DI/Config/Adapters/NeonAdapter.php @@ -15,6 +15,7 @@ use Nette\DI\Definitions\Statement; use Nette\Neon; use Nette\Neon\Node; +use function array_walk_recursive, constant, count, defined, implode, is_array, is_string, ltrim, preg_match, preg_replace, sprintf, str_contains, str_ends_with, str_starts_with, substr; /** diff --git a/src/DI/Config/Helpers.php b/src/DI/Config/Helpers.php index 9124a5236..3f47edbd9 100644 --- a/src/DI/Config/Helpers.php +++ b/src/DI/Config/Helpers.php @@ -10,6 +10,7 @@ namespace Nette\DI\Config; use Nette; +use function is_array; /** diff --git a/src/DI/Config/Loader.php b/src/DI/Config/Loader.php index cd845f019..5bfb0d185 100644 --- a/src/DI/Config/Loader.php +++ b/src/DI/Config/Loader.php @@ -11,6 +11,8 @@ use Nette; use Nette\Utils\Validators; +use function array_unique, dirname, is_file, is_object, is_readable, pathinfo, preg_match, sprintf, strtolower; +use const PATHINFO_EXTENSION; /** diff --git a/src/DI/Container.php b/src/DI/Container.php index da8557cc9..db03ae246 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -10,6 +10,7 @@ namespace Nette\DI; use Nette; +use function array_flip, array_key_exists, array_keys, array_map, array_merge, array_values, class_exists, count, get_class_methods, implode, interface_exists, is_a, is_object, natsort, sprintf, str_replace, ucfirst; /** diff --git a/src/DI/ContainerBuilder.php b/src/DI/ContainerBuilder.php index f2844d508..617ec0dbf 100644 --- a/src/DI/ContainerBuilder.php +++ b/src/DI/ContainerBuilder.php @@ -11,6 +11,7 @@ use Nette; use Nette\DI\Definitions\Definition; +use function array_diff, array_filter, array_walk_recursive, class_implements, class_parents, is_a, is_int, key, ksort, preg_match, sprintf, strtolower; /** diff --git a/src/DI/ContainerLoader.php b/src/DI/ContainerLoader.php index 8162013d7..3b9acf4f9 100644 --- a/src/DI/ContainerLoader.php +++ b/src/DI/ContainerLoader.php @@ -10,6 +10,8 @@ namespace Nette\DI; use Nette; +use function class_exists, file_get_contents, file_put_contents, flock, fopen, function_exists, hash, is_file, rename, serialize, sprintf, strlen, substr, unlink, unserialize; +use const LOCK_EX, LOCK_UN; /** diff --git a/src/DI/Definitions/AccessorDefinition.php b/src/DI/Definitions/AccessorDefinition.php index bd6c1a1a5..7900de605 100644 --- a/src/DI/Definitions/AccessorDefinition.php +++ b/src/DI/Definitions/AccessorDefinition.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\Helpers; use Nette\Utils\Type; +use function count, interface_exists, sprintf, str_starts_with, substr; /** diff --git a/src/DI/Definitions/Definition.php b/src/DI/Definitions/Definition.php index ca96659b8..6183f0a6f 100644 --- a/src/DI/Definitions/Definition.php +++ b/src/DI/Definitions/Definition.php @@ -10,6 +10,7 @@ namespace Nette\DI\Definitions; use Nette; +use function class_exists, interface_exists, is_array, is_string, sprintf; /** diff --git a/src/DI/Definitions/FactoryDefinition.php b/src/DI/Definitions/FactoryDefinition.php index 7c2e69caa..9518ab91b 100644 --- a/src/DI/Definitions/FactoryDefinition.php +++ b/src/DI/Definitions/FactoryDefinition.php @@ -14,6 +14,7 @@ use Nette\DI\ServiceCreationException; use Nette\PhpGenerator as Php; use Nette\Utils\Type; +use function array_keys, array_map, count, implode, interface_exists, is_string, serialize, sprintf, str_replace, unserialize; /** diff --git a/src/DI/Definitions/LocatorDefinition.php b/src/DI/Definitions/LocatorDefinition.php index a9e3f735a..a26296d7b 100644 --- a/src/DI/Definitions/LocatorDefinition.php +++ b/src/DI/Definitions/LocatorDefinition.php @@ -10,6 +10,7 @@ namespace Nette\DI\Definitions; use Nette; +use function array_map, interface_exists, lcfirst, preg_match, sprintf, str_starts_with, substr; /** diff --git a/src/DI/Definitions/ServiceDefinition.php b/src/DI/Definitions/ServiceDefinition.php index ff934c26a..b41fe8574 100644 --- a/src/DI/Definitions/ServiceDefinition.php +++ b/src/DI/Definitions/ServiceDefinition.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\ServiceCreationException; use Nette\Utils\Strings; +use function array_pop, class_exists, class_parents, count, implode, is_string, preg_grep, serialize, strpbrk, unserialize; /** diff --git a/src/DI/Definitions/Statement.php b/src/DI/Definitions/Statement.php index cd80f6d44..27aebe3da 100644 --- a/src/DI/Definitions/Statement.php +++ b/src/DI/Definitions/Statement.php @@ -10,6 +10,7 @@ namespace Nette\DI\Definitions; use Nette; +use function array_keys, class_exists, explode, is_array, is_string, str_contains, str_starts_with, substr; /** diff --git a/src/DI/DependencyChecker.php b/src/DI/DependencyChecker.php index 94eb9fdc8..b58cb52e4 100644 --- a/src/DI/DependencyChecker.php +++ b/src/DI/DependencyChecker.php @@ -13,6 +13,8 @@ use Nette\Utils\Reflection; use ReflectionClass; use ReflectionMethod; +use function array_combine, array_flip, array_keys, array_map, array_merge, array_unique, class_implements, class_parents, class_uses, count, get_debug_type, get_parent_class, hash, is_object, is_string, rtrim, serialize, sprintf, str_contains; +use const PHP_VERSION_ID, SORT_REGULAR; /** diff --git a/src/DI/Extensions/DIExtension.php b/src/DI/Extensions/DIExtension.php index 99e9de0f1..0bcf76948 100644 --- a/src/DI/Extensions/DIExtension.php +++ b/src/DI/Extensions/DIExtension.php @@ -12,6 +12,8 @@ use Nette; use Nette\DI\Definitions\ServiceDefinition; use Tracy; +use function array_flip, array_intersect_key, is_array, microtime; +use const PHP_VERSION_ID; /** diff --git a/src/DI/Extensions/DecoratorExtension.php b/src/DI/Extensions/DecoratorExtension.php index 0725d8086..00c2596bc 100644 --- a/src/DI/Extensions/DecoratorExtension.php +++ b/src/DI/Extensions/DecoratorExtension.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\Definitions; use Nette\Schema\Expect; +use function array_filter, array_values, class_exists, interface_exists, is_a, is_array, key, sprintf; /** diff --git a/src/DI/Extensions/DefinitionSchema.php b/src/DI/Extensions/DefinitionSchema.php index 7935324e8..97a16166a 100644 --- a/src/DI/Extensions/DefinitionSchema.php +++ b/src/DI/Extensions/DefinitionSchema.php @@ -16,6 +16,7 @@ use Nette\Schema\Context; use Nette\Schema\Expect; use Nette\Schema\Schema; +use function array_keys, end, get_class, interface_exists, is_array, is_string, method_exists, preg_match, substr; /** diff --git a/src/DI/Extensions/ExtensionsExtension.php b/src/DI/Extensions/ExtensionsExtension.php index 591ad4fda..026680138 100644 --- a/src/DI/Extensions/ExtensionsExtension.php +++ b/src/DI/Extensions/ExtensionsExtension.php @@ -10,6 +10,7 @@ namespace Nette\DI\Extensions; use Nette; +use function is_a, is_int, sprintf; /** diff --git a/src/DI/Extensions/InjectExtension.php b/src/DI/Extensions/InjectExtension.php index 4c40b30c0..278f1019a 100644 --- a/src/DI/Extensions/InjectExtension.php +++ b/src/DI/Extensions/InjectExtension.php @@ -13,6 +13,7 @@ use Nette\DI; use Nette\DI\Definitions; use Nette\Utils\Reflection; +use function array_keys, array_reverse, array_search, array_unshift, get_class_methods, is_a, is_subclass_of, ksort, sprintf, str_starts_with, uksort; /** diff --git a/src/DI/Extensions/ParametersExtension.php b/src/DI/Extensions/ParametersExtension.php index 144554d5f..1d2f3365d 100644 --- a/src/DI/Extensions/ParametersExtension.php +++ b/src/DI/Extensions/ParametersExtension.php @@ -12,6 +12,7 @@ use Nette; use Nette\DI\DynamicParameter; use Nette\DI\Helpers; +use function array_diff_key, array_fill_keys, array_keys, array_walk_recursive, implode, var_export; /** diff --git a/src/DI/Extensions/SearchExtension.php b/src/DI/Extensions/SearchExtension.php index 0be9a8b25..4739b3777 100644 --- a/src/DI/Extensions/SearchExtension.php +++ b/src/DI/Extensions/SearchExtension.php @@ -13,6 +13,7 @@ use Nette\Loaders\RobotLoader; use Nette\Schema\Expect; use Nette\Utils\Arrays; +use function array_filter, array_keys, array_merge, array_unique, class_exists, count, implode, in_array, interface_exists, is_dir, is_string, method_exists, preg_match, preg_quote, sprintf, str_contains, str_replace, trait_exists; /** diff --git a/src/DI/Extensions/ServicesExtension.php b/src/DI/Extensions/ServicesExtension.php index 38d7d47af..dc69ec4a7 100644 --- a/src/DI/Extensions/ServicesExtension.php +++ b/src/DI/Extensions/ServicesExtension.php @@ -13,6 +13,7 @@ use Nette\DI\Definitions; use Nette\DI\Definitions\Statement; use Nette\DI\Helpers; +use function array_replace, array_values, is_array, is_int, is_string, key, preg_match, substr; /** diff --git a/src/DI/Helpers.php b/src/DI/Helpers.php index 48e421fbf..51bcda464 100644 --- a/src/DI/Helpers.php +++ b/src/DI/Helpers.php @@ -14,6 +14,8 @@ use Nette\DI\Definitions\Statement; use Nette\Utils\Reflection; use Nette\Utils\Type; +use function array_key_exists, array_keys, array_shift, class_exists, explode, get_debug_type, implode, interface_exists, is_array, is_scalar, is_string, preg_match, preg_quote, preg_replace, preg_split, settype, sprintf, str_replace, strlen, strncmp, substr, trim, ucfirst, var_export; +use const PREG_SPLIT_DELIM_CAPTURE; /** diff --git a/src/DI/PhpGenerator.php b/src/DI/PhpGenerator.php index b5176c40d..8c02cb236 100644 --- a/src/DI/PhpGenerator.php +++ b/src/DI/PhpGenerator.php @@ -13,6 +13,7 @@ use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; use Nette\PhpGenerator as Php; +use function array_walk_recursive, is_array, is_object, is_string, ksort, sprintf, str_contains, str_ends_with, str_starts_with, substr; /** diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index ceab3d135..9dd4339f1 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -18,6 +18,7 @@ use Nette\Utils\Callback; use Nette\Utils\Reflection; use Nette\Utils\Validators; +use function array_filter, array_key_exists, array_map, array_merge, array_values, array_walk_recursive, assert, class_exists, count, ctype_digit, explode, function_exists, gettype, implode, in_array, interface_exists, is_a, is_array, is_int, is_scalar, is_string, iterator_to_array, ltrim, preg_match, preg_replace, sprintf, str_contains, str_ends_with, str_replace, str_starts_with, strlen, substr; /** From 7cdef7115c70406e732126eb5650ef9bb1d96372 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 18 Jun 2025 20:28:44 +0200 Subject: [PATCH 10/22] opened 3.3-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cbf4588ac..73d99d8dc 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } } } From 8990bb852efc1d9915857cf27ce921a68e29342f Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 18 Jun 2025 21:20:11 +0200 Subject: [PATCH 11/22] uses nette/schema 1.3 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 73d99d8dc..2a9b6241f 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "nette/neon": "^3.3", "nette/php-generator": "^4.1.6", "nette/robot-loader": "^4.0", - "nette/schema": "^1.2.5", + "nette/schema": "^1.3", "nette/utils": "^4.0" }, "require-dev": { From 499e74477ed72f1aa72139a25359d0bdac6dff9d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 18 Jun 2025 21:20:24 +0200 Subject: [PATCH 12/22] uses nette/neon 3.4 --- composer.json | 2 +- src/DI/Config/Adapters/NeonAdapter.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2a9b6241f..f0a143343 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "php": "8.1 - 8.4", "ext-tokenizer": "*", "ext-ctype": "*", - "nette/neon": "^3.3", + "nette/neon": "^3.4", "nette/php-generator": "^4.1.6", "nette/robot-loader": "^4.0", "nette/schema": "^1.3", diff --git a/src/DI/Config/Adapters/NeonAdapter.php b/src/DI/Config/Adapters/NeonAdapter.php index 5b26583b9..9bc2b63b4 100644 --- a/src/DI/Config/Adapters/NeonAdapter.php +++ b/src/DI/Config/Adapters/NeonAdapter.php @@ -112,7 +112,7 @@ function (&$val): void { } }, ); - return "# generated by Nette\n\n" . Neon\Neon::encode($data, Neon\Neon::BLOCK); + return "# generated by Nette\n\n" . Neon\Neon::encode($data, blockMode: true); } From 4563395ce04b21251cbe06e354324d49abd8f72c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 29 Nov 2024 19:25:33 +0100 Subject: [PATCH 13/22] used attribute Deprecated --- src/DI/ContainerBuilder.php | 4 ++-- src/DI/Definitions/Reference.php | 2 +- src/DI/DependencyChecker.php | 2 +- src/DI/Extensions/InjectExtension.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/DI/ContainerBuilder.php b/src/DI/ContainerBuilder.php index 617ec0dbf..2895f4f4c 100644 --- a/src/DI/ContainerBuilder.php +++ b/src/DI/ContainerBuilder.php @@ -23,10 +23,10 @@ class ContainerBuilder ThisService = 'self', ThisContainer = 'container'; - /** @deprecated use ContainerBuilder::ThisService */ + #[\Deprecated('use ContainerBuilder::ThisService')] public const THIS_SERVICE = self::ThisService; - /** @deprecated use ContainerBuilder::ThisContainer */ + #[\Deprecated('use ContainerBuilder::ThisContainer')] public const THIS_CONTAINER = self::ThisContainer; public array $parameters = []; diff --git a/src/DI/Definitions/Reference.php b/src/DI/Definitions/Reference.php index 25625dec7..a23552003 100644 --- a/src/DI/Definitions/Reference.php +++ b/src/DI/Definitions/Reference.php @@ -17,7 +17,7 @@ final class Reference { public const Self = 'self'; - /** @deprecated use Reference::Self */ + #[\Deprecated('use Reference::Self')] public const SELF = self::Self; private string $value; diff --git a/src/DI/DependencyChecker.php b/src/DI/DependencyChecker.php index b58cb52e4..0fefd3e0d 100644 --- a/src/DI/DependencyChecker.php +++ b/src/DI/DependencyChecker.php @@ -24,7 +24,7 @@ class DependencyChecker { public const Version = 1; - /** @deprecated use DependencyChecker::Version */ + #[\Deprecated('use DependencyChecker::Version')] public const VERSION = self::Version; /** @var array */ diff --git a/src/DI/Extensions/InjectExtension.php b/src/DI/Extensions/InjectExtension.php index 278f1019a..50f7982a3 100644 --- a/src/DI/Extensions/InjectExtension.php +++ b/src/DI/Extensions/InjectExtension.php @@ -23,7 +23,7 @@ final class InjectExtension extends DI\CompilerExtension { public const TagInject = 'nette.inject'; - /** @deprecated use InjectExtension::TagInject */ + #[\Deprecated('use InjectExtension::TagInject')] public const TAG_INJECT = self::TagInject; From 6f686611a9b2766ae1e798ec7d5047523b918661 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 2 Dec 2024 00:10:08 +0100 Subject: [PATCH 14/22] Definition::generateMethod() replaced with generateCode() --- src/DI/Definitions/AccessorDefinition.php | 4 ++-- src/DI/Definitions/Definition.php | 9 ++++++++- src/DI/Definitions/FactoryDefinition.php | 4 ++-- src/DI/Definitions/ImportedDefinition.php | 5 ++--- src/DI/Definitions/LocatorDefinition.php | 4 ++-- src/DI/Definitions/ServiceDefinition.php | 10 +++++----- 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/DI/Definitions/AccessorDefinition.php b/src/DI/Definitions/AccessorDefinition.php index 7900de605..ba49ab49c 100644 --- a/src/DI/Definitions/AccessorDefinition.php +++ b/src/DI/Definitions/AccessorDefinition.php @@ -108,7 +108,7 @@ public function complete(Nette\DI\Resolver $resolver): void } - public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { $class = (new Nette\PhpGenerator\ClassType) ->addImplement($this->getType()); @@ -124,6 +124,6 @@ public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGe ->setBody('return $this->container->getService(?);', [$this->reference->getValue()]) ->setReturnType((string) Type::fromReflection($rm)); - $method->setBody('return new class ($this) ' . $class . ';'); + return 'return new class ($this) ' . $class . ';'; } } diff --git a/src/DI/Definitions/Definition.php b/src/DI/Definitions/Definition.php index 6183f0a6f..da5568960 100644 --- a/src/DI/Definitions/Definition.php +++ b/src/DI/Definitions/Definition.php @@ -148,7 +148,7 @@ abstract public function resolveType(Nette\DI\Resolver $resolver): void; abstract public function complete(Nette\DI\Resolver $resolver): void; - abstract public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void; + //abstract public function generateCode(Nette\DI\PhpGenerator $generator): string; final public function setNotifier(?\Closure $notifier): void @@ -160,6 +160,13 @@ final public function setNotifier(?\Closure $notifier): void /********************* deprecated stuff from former ServiceDefinition ****************d*g**/ + /** @deprecated */ + public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void + { + $method->setBody($this->generateCode($generator)); + } + + /** @deprecated Use setType() */ public function setClass(?string $type) { diff --git a/src/DI/Definitions/FactoryDefinition.php b/src/DI/Definitions/FactoryDefinition.php index 9518ab91b..f97e9ee7b 100644 --- a/src/DI/Definitions/FactoryDefinition.php +++ b/src/DI/Definitions/FactoryDefinition.php @@ -197,7 +197,7 @@ public function convertArguments(array &$args): void } - public function generateMethod(Php\Method $method, Nette\DI\PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { $class = (new Php\ClassType) ->addImplement($this->getType()); @@ -219,7 +219,7 @@ public function generateMethod(Php\Method $method, Nette\DI\PhpGenerator $genera ->setReturnType((string) Type::fromReflection($rm)) ->setBody($body); - $method->setBody('return new class ($this) ' . $class . ';'); + return 'return new class ($this) ' . $class . ';'; } diff --git a/src/DI/Definitions/ImportedDefinition.php b/src/DI/Definitions/ImportedDefinition.php index e9116653b..cc8caf70e 100644 --- a/src/DI/Definitions/ImportedDefinition.php +++ b/src/DI/Definitions/ImportedDefinition.php @@ -10,7 +10,6 @@ namespace Nette\DI\Definitions; use Nette; -use Nette\DI\PhpGenerator; /** @@ -34,9 +33,9 @@ public function complete(Nette\DI\Resolver $resolver): void } - public function generateMethod(Nette\PhpGenerator\Method $method, PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { - $method->setBody( + return $generator->formatPhp( 'throw new Nette\DI\ServiceCreationException(?);', ["Unable to create imported service '{$this->getName()}', it must be added using addService()"], ); diff --git a/src/DI/Definitions/LocatorDefinition.php b/src/DI/Definitions/LocatorDefinition.php index a26296d7b..c3601d858 100644 --- a/src/DI/Definitions/LocatorDefinition.php +++ b/src/DI/Definitions/LocatorDefinition.php @@ -128,7 +128,7 @@ public function complete(Nette\DI\Resolver $resolver): void } - public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { $class = (new Nette\PhpGenerator\ClassType) ->addImplement($this->getType()); @@ -172,6 +172,6 @@ public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGe } } - $method->setBody('return new class ($this) ' . $class . ';'); + return 'return new class ($this) ' . $class . ';'; } } diff --git a/src/DI/Definitions/ServiceDefinition.php b/src/DI/Definitions/ServiceDefinition.php index b41fe8574..d936ff04f 100644 --- a/src/DI/Definitions/ServiceDefinition.php +++ b/src/DI/Definitions/ServiceDefinition.php @@ -182,7 +182,7 @@ private function prependSelf(Statement $setup): Statement } - public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGenerator $generator): void + public function generateCode(Nette\DI\PhpGenerator $generator): string { $lines = []; foreach ([$this->creator, ...$this->setup] as $stmt) { @@ -194,15 +194,15 @@ public function generateMethod(Nette\PhpGenerator\Method $method, Nette\DI\PhpGe $lines[0] = (new \ReflectionClass($class))->hasMethod('__construct') ? $generator->formatPhp("\$service->__construct(...?:);\n", [$this->creator->arguments]) : ''; - $method->setBody("return new ReflectionClass($class::class)->newLazyGhost(function (\$service) {\n" + return "return new ReflectionClass($class::class)->newLazyGhost(function (\$service) {\n" . Strings::indent(implode('', $lines)) - . '});'); + . '});'; } elseif (count($lines) === 1) { - $method->setBody('return ' . $lines[0]); + return 'return ' . $lines[0]; } else { - $method->setBody('$service = ' . implode('', $lines) . 'return $service;'); + return '$service = ' . implode('', $lines) . 'return $service;'; } } From a8eb347ff35c23bdcd4fd63c15ad4e8446370f7c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 1 Dec 2024 23:49:52 +0100 Subject: [PATCH 15/22] Resolver: used withCurrentServiceAvailable() to control $currentServiceAllowed --- src/DI/Definitions/ServiceDefinition.php | 2 +- src/DI/Resolver.php | 42 ++++++++++++++---------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/DI/Definitions/ServiceDefinition.php b/src/DI/Definitions/ServiceDefinition.php index d936ff04f..bf16685e2 100644 --- a/src/DI/Definitions/ServiceDefinition.php +++ b/src/DI/Definitions/ServiceDefinition.php @@ -169,7 +169,7 @@ public function complete(Nette\DI\Resolver $resolver): void $this->creator = $resolver->completeStatement($this->creator); foreach ($this->setup as &$setup) { - $setup = $resolver->completeStatement($setup, true); + $setup = $resolver->withCurrentServiceAvailable()->completeStatement($setup); } } diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index 9dd4339f1..93740bdd4 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -43,6 +43,26 @@ public function __construct(ContainerBuilder $builder) } + private function withCurrentService(Definition $definition): self + { + $dolly = clone $this; + $dolly->currentService = in_array($definition, $this->builder->getDefinitions(), strict: true) + ? $definition + : null; + $dolly->currentServiceType = $definition->getType(); + $dolly->currentServiceAllowed = false; + return $dolly; + } + + + public function withCurrentServiceAvailable(): self + { + $dolly = clone $this; + $dolly->currentServiceAllowed = true; + return $dolly; + } + + public function getContainerBuilder(): ContainerBuilder { return $this->builder; @@ -60,7 +80,6 @@ public function resolveDefinition(Definition $def): void $this->recursive->attach($def); $def->resolveType($this); - if (!$def->getType()) { throw new ServiceCreationException('Type of service is unknown.'); } @@ -160,29 +179,18 @@ interface_exists($entity) public function completeDefinition(Definition $def): void { - $this->currentService = in_array($def, $this->builder->getDefinitions(), strict: true) - ? $def - : null; - $this->currentServiceType = $def->getType(); - $this->currentServiceAllowed = false; - try { - $def->complete($this); - + $def->complete($this->withCurrentService($def)); $this->addDependency(new \ReflectionClass($def->getType())); } catch (\Throwable $e) { throw $this->completeException($e, $def); - - } finally { - $this->currentService = $this->currentServiceType = null; } } - public function completeStatement(Statement $statement, bool $currentServiceAllowed = false): Statement + public function completeStatement(Statement $statement): Statement { - $this->currentServiceAllowed = $currentServiceAllowed; $entity = $this->normalizeEntity($statement); $arguments = $this->convertReferences($statement->arguments); $getter = fn(string $type, bool $single) => $single @@ -195,7 +203,7 @@ public function completeStatement(Statement $statement, bool $currentServiceAllo throw new ServiceCreationException(sprintf('Cannot create closure for %s(...)', $entity)); } if ($entity[0] instanceof Statement) { - $entity[0] = $this->completeStatement($entity[0], $this->currentServiceAllowed); + $entity[0] = $this->completeStatement($entity[0]); } break; @@ -269,7 +277,7 @@ public function completeStatement(Statement $statement, bool $currentServiceAllo break; case $entity[0] instanceof Statement: - $entity[0] = $this->completeStatement($entity[0], $this->currentServiceAllowed); + $entity[0] = $this->completeStatement($entity[0]); // break omitted case is_string($entity[0]): // static method call @@ -330,7 +338,7 @@ public function completeArguments(array $arguments): array $val = $this->completeArguments($services); } else { - $val = $this->completeStatement($val, $this->currentServiceAllowed); + $val = $this->completeStatement($val); } } elseif ($val instanceof Definition || $val instanceof Reference) { $val = $this->normalizeEntity(new Statement($val)); From eb6f8df8a0adf6674ab4cf541b196eca36be3a30 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 2 Dec 2024 06:11:21 +0100 Subject: [PATCH 16/22] added Definitions\Expression --- src/DI/Definitions/Expression.php | 17 +++++++++++++++++ src/DI/Definitions/Reference.php | 3 ++- src/DI/Definitions/Statement.php | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 src/DI/Definitions/Expression.php diff --git a/src/DI/Definitions/Expression.php b/src/DI/Definitions/Expression.php new file mode 100644 index 000000000..f4f0ae4d2 --- /dev/null +++ b/src/DI/Definitions/Expression.php @@ -0,0 +1,17 @@ + Date: Mon, 2 Dec 2024 00:12:29 +0100 Subject: [PATCH 17/22] PhpGenerator::formatStatement() moved to Statement & Reference --- src/DI/Definitions/Expression.php | 1 + src/DI/Definitions/Reference.php | 12 ++++ src/DI/Definitions/ServiceDefinition.php | 2 +- src/DI/Definitions/Statement.php | 57 +++++++++++++++++++ src/DI/PhpGenerator.php | 72 ++---------------------- 5 files changed, 77 insertions(+), 67 deletions(-) diff --git a/src/DI/Definitions/Expression.php b/src/DI/Definitions/Expression.php index f4f0ae4d2..de3a3a246 100644 --- a/src/DI/Definitions/Expression.php +++ b/src/DI/Definitions/Expression.php @@ -14,4 +14,5 @@ abstract class Expression { + abstract public function generateCode(Nette\DI\PhpGenerator $generator): string; } diff --git a/src/DI/Definitions/Reference.php b/src/DI/Definitions/Reference.php index d3ba5650c..086e210e4 100644 --- a/src/DI/Definitions/Reference.php +++ b/src/DI/Definitions/Reference.php @@ -9,6 +9,8 @@ namespace Nette\DI\Definitions; +use Nette\DI; + /** @@ -62,4 +64,14 @@ public function isSelf(): bool { return $this->value === self::Self; } + + + public function generateCode(DI\PhpGenerator $generator): string + { + return match (true) { + $this->isSelf() => '$service', + $this->value === DI\ContainerBuilder::ThisContainer => '$this', + default => $generator->formatPhp('$this->getService(?)', [$this->value]), + }; + } } diff --git a/src/DI/Definitions/ServiceDefinition.php b/src/DI/Definitions/ServiceDefinition.php index bf16685e2..bdc20b7dc 100644 --- a/src/DI/Definitions/ServiceDefinition.php +++ b/src/DI/Definitions/ServiceDefinition.php @@ -186,7 +186,7 @@ public function generateCode(Nette\DI\PhpGenerator $generator): string { $lines = []; foreach ([$this->creator, ...$this->setup] as $stmt) { - $lines[] = $generator->formatStatement($stmt) . ";\n"; + $lines[] = $stmt->generateCode($generator) . ";\n"; } if ($this->canBeLazy() && !preg_grep('#(?:func_get_arg|func_num_args)#i', $lines)) { // latteFactory workaround diff --git a/src/DI/Definitions/Statement.php b/src/DI/Definitions/Statement.php index e91579092..23018621d 100644 --- a/src/DI/Definitions/Statement.php +++ b/src/DI/Definitions/Statement.php @@ -10,6 +10,8 @@ namespace Nette\DI\Definitions; use Nette; +use Nette\DI; +use Nette\PhpGenerator as Php; use function array_keys, class_exists, explode, is_array, is_string, str_contains, str_starts_with, substr; @@ -63,6 +65,61 @@ public function getEntity(): string|array|Definition|Reference|null { return $this->entity; } + + + /** + * Formats PHP code for class instantiating, function calling or property setting in PHP. + */ + public function generateCode(DI\PhpGenerator $generator): string + { + $entity = $this->entity; + $arguments = $this->arguments; + + switch (true) { + case is_string($entity) && str_contains($entity, '?'): // PHP literal + return $generator->formatPhp($entity, $arguments); + + case is_string($entity): // create class + return $arguments + ? $generator->formatPhp("new $entity(...?:)", [$arguments]) + : $generator->formatPhp("new $entity", []); + + case is_array($entity): + switch (true) { + case $entity[1][0] === '$': // property getter, setter or appender + $name = substr($entity[1], 1); + if ($append = (str_ends_with($name, '[]'))) { + $name = substr($name, 0, -2); + } + + $prop = $entity[0] instanceof Reference + ? $generator->formatPhp('?->?', [$entity[0], $name]) + : $generator->formatPhp('?::$?', [$entity[0], $name]); + return $arguments + ? $generator->formatPhp(($append ? '?[]' : '?') . ' = ?', [new Php\Literal($prop), $arguments[0]]) + : $prop; + + case $entity[0] instanceof self: + $inner = $generator->formatPhp('?', [$entity[0]]); + if (str_starts_with($inner, 'new ')) { + $inner = "($inner)"; + } + + return $generator->formatPhp('?->?(...?:)', [new Php\Literal($inner), $entity[1], $arguments]); + + case $entity[0] instanceof Reference: + return $generator->formatPhp('?->?(...?:)', [$entity[0], $entity[1], $arguments]); + + case $entity[0] === '': // function call + return $generator->formatPhp('?(...?:)', [new Php\Literal($entity[1]), $arguments]); + + case is_string($entity[0]): // static method call + return $generator->formatPhp('?::?(...?:)', [new Php\Literal($entity[0]), $entity[1], $arguments]); + } + } + + throw new Nette\InvalidStateException; + } } diff --git a/src/DI/PhpGenerator.php b/src/DI/PhpGenerator.php index 8c02cb236..35788b294 100644 --- a/src/DI/PhpGenerator.php +++ b/src/DI/PhpGenerator.php @@ -9,9 +9,7 @@ namespace Nette\DI; -use Nette; -use Nette\DI\Definitions\Reference; -use Nette\DI\Definitions\Statement; +use Nette\DI\Definitions\Expression; use Nette\PhpGenerator as Php; use function array_walk_recursive, is_array, is_object, is_string, ksort, sprintf, str_contains, str_ends_with, str_starts_with, substr; @@ -104,58 +102,10 @@ public function generateMethod(Definitions\Definition $def): Php\Method } - /** - * Formats PHP code for class instantiating, function calling or property setting in PHP. - */ - public function formatStatement(Statement $statement): string + /** @deprecated */ + public function formatStatement(Definitions\Statement $statement): string { - $entity = $statement->getEntity(); - $arguments = $statement->arguments; - - switch (true) { - case is_string($entity) && str_contains($entity, '?'): // PHP literal - return $this->formatPhp($entity, $arguments); - - case is_string($entity): // create class - return $arguments - ? $this->formatPhp("new $entity(...?:)", [$arguments]) - : $this->formatPhp("new $entity", []); - - case is_array($entity): - switch (true) { - case $entity[1][0] === '$': // property getter, setter or appender - $name = substr($entity[1], 1); - if ($append = (str_ends_with($name, '[]'))) { - $name = substr($name, 0, -2); - } - - $prop = $entity[0] instanceof Reference - ? $this->formatPhp('?->?', [$entity[0], $name]) - : $this->formatPhp('?::$?', [$entity[0], $name]); - return $arguments - ? $this->formatPhp(($append ? '?[]' : '?') . ' = ?', [new Php\Literal($prop), $arguments[0]]) - : $prop; - - case $entity[0] instanceof Statement: - $inner = $this->formatPhp('?', [$entity[0]]); - if (str_starts_with($inner, 'new ')) { - $inner = "($inner)"; - } - - return $this->formatPhp('?->?(...?:)', [new Php\Literal($inner), $entity[1], $arguments]); - - case $entity[0] instanceof Reference: - return $this->formatPhp('?->?(...?:)', [$entity[0], $entity[1], $arguments]); - - case $entity[0] === '': // function call - return $this->formatPhp('?(...?:)', [new Php\Literal($entity[1]), $arguments]); - - case is_string($entity[0]): // static method call - return $this->formatPhp('?::?(...?:)', [new Php\Literal($entity[0]), $entity[1], $arguments]); - } - } - - throw new Nette\InvalidStateException; + return $statement->generateCode($this); } @@ -172,18 +122,8 @@ public function formatPhp(string $statement, array $args): string public function convertArguments(array $args): array { array_walk_recursive($args, function (&$val): void { - if ($val instanceof Statement) { - $val = new Php\Literal($this->formatStatement($val)); - - } elseif ($val instanceof Reference) { - $name = $val->getValue(); - if ($val->isSelf()) { - $val = new Php\Literal('$service'); - } elseif ($name === ContainerBuilder::ThisContainer) { - $val = new Php\Literal('$this'); - } else { - $val = ContainerBuilder::literal('$this->getService(?)', [$name]); - } + if ($val instanceof Expression) { + $val = new Php\Literal($val->generateCode($this)); } elseif ( is_object($val) && !$val instanceof Php\Literal && !$val instanceof \DateTimeInterface From 06a2a7093b12aea4fabcfebe6cba7477d918f805 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 1 Dec 2024 19:46:30 +0100 Subject: [PATCH 18/22] Resolver::resolve*Type() moved to Statement & Reference --- src/DI/Definitions/Expression.php | 3 + src/DI/Definitions/FactoryDefinition.php | 2 +- src/DI/Definitions/Reference.php | 18 +++ src/DI/Definitions/ServiceDefinition.php | 2 +- src/DI/Definitions/Statement.php | 71 ++++++++++++ src/DI/Extensions/InjectExtension.php | 2 +- src/DI/Resolver.php | 134 ++++++----------------- 7 files changed, 130 insertions(+), 102 deletions(-) diff --git a/src/DI/Definitions/Expression.php b/src/DI/Definitions/Expression.php index de3a3a246..df8eb0ee5 100644 --- a/src/DI/Definitions/Expression.php +++ b/src/DI/Definitions/Expression.php @@ -14,5 +14,8 @@ abstract class Expression { + abstract public function resolveType(Nette\DI\Resolver $resolver): ?string; + + abstract public function generateCode(Nette\DI\PhpGenerator $generator): string; } diff --git a/src/DI/Definitions/FactoryDefinition.php b/src/DI/Definitions/FactoryDefinition.php index f97e9ee7b..4abae2c11 100644 --- a/src/DI/Definitions/FactoryDefinition.php +++ b/src/DI/Definitions/FactoryDefinition.php @@ -144,7 +144,7 @@ private function completeParameters(Nette\DI\Resolver $resolver): void $ctorParams = []; if ( - ($class = $resolver->resolveEntityType($this->resultDefinition->getCreator())) + ($class = $this->resultDefinition->getCreator()->resolveType($resolver)) && ($ctor = (new \ReflectionClass($class))->getConstructor()) ) { foreach ($ctor->getParameters() as $param) { diff --git a/src/DI/Definitions/Reference.php b/src/DI/Definitions/Reference.php index 086e210e4..af54fc18b 100644 --- a/src/DI/Definitions/Reference.php +++ b/src/DI/Definitions/Reference.php @@ -66,6 +66,24 @@ public function isSelf(): bool } + public function resolveType(DI\Resolver $resolver): ?string + { + if ($this->isSelf()) { + return $resolver->getCurrentService(type: true); + + } elseif ($this->isType()) { + return ltrim($this->value, '\\'); + } + + $def = $resolver->getContainerBuilder()->getDefinition($this->value); + if (!$def->getType()) { + $resolver->resolveDefinition($def); + } + + return $def->getType(); + } + + public function generateCode(DI\PhpGenerator $generator): string { return match (true) { diff --git a/src/DI/Definitions/ServiceDefinition.php b/src/DI/Definitions/ServiceDefinition.php index bdc20b7dc..8ddcd1180 100644 --- a/src/DI/Definitions/ServiceDefinition.php +++ b/src/DI/Definitions/ServiceDefinition.php @@ -142,7 +142,7 @@ public function resolveType(Nette\DI\Resolver $resolver): void $this->setCreator($this->getType(), $this->creator->arguments ?? []); } elseif (!$this->getType()) { - $type = $resolver->resolveEntityType($this->creator); + $type = $this->creator->resolveType($resolver); if (!$type) { throw new ServiceCreationException('Unknown service type, specify it or declare return type of factory method.'); } diff --git a/src/DI/Definitions/Statement.php b/src/DI/Definitions/Statement.php index 23018621d..95ec858be 100644 --- a/src/DI/Definitions/Statement.php +++ b/src/DI/Definitions/Statement.php @@ -11,7 +11,10 @@ use Nette; use Nette\DI; +use Nette\DI\Resolver; +use Nette\DI\ServiceCreationException; use Nette\PhpGenerator as Php; +use Nette\Utils\Callback; use function array_keys, class_exists, explode, is_array, is_string, str_contains, str_starts_with, substr; @@ -67,6 +70,74 @@ public function getEntity(): string|array|Definition|Reference|null } + public function resolveType(Resolver $resolver): ?string + { + $entity = $resolver->normalizeEntity($this); + + if ($this->arguments === Resolver::getFirstClassCallable()) { + return \Closure::class; + + } elseif (is_array($entity)) { + if ($entity[0] instanceof Expression) { + $entity[0] = $entity[0]->resolveType($resolver); + if (!$entity[0]) { + return null; + } + } + + try { + $reflection = Callback::toReflection($entity[0] === '' ? $entity[1] : $entity); + assert($reflection instanceof \ReflectionMethod || $reflection instanceof \ReflectionFunction); + $refClass = $reflection instanceof \ReflectionMethod + ? $reflection->getDeclaringClass() + : null; + } catch (\ReflectionException $e) { + $refClass = $reflection = null; + } + + if (isset($e) || ($refClass && (!$reflection->isPublic() + || ($refClass->isTrait() && !$reflection->isStatic()) + ))) { + throw new ServiceCreationException(sprintf('Method %s() is not callable.', Callback::toString($entity)), 0, $e ?? null); + } + + $resolver->addDependency($reflection); + + $type = Nette\Utils\Type::fromReflection($reflection) ?? ($annotation = DI\Helpers::getReturnTypeAnnotation($reflection)); + if ($type && !in_array($type->getSingleName(), ['object', 'mixed'], strict: true)) { + if (isset($annotation)) { + trigger_error('Annotation @return should be replaced with native return type at ' . Callback::toString($entity), E_USER_DEPRECATED); + } + + return DI\Helpers::ensureClassType( + $type, + sprintf('return type of %s()', Callback::toString($entity)), + allowNullable: true, + ); + } + + return null; + + } elseif ($entity instanceof Expression) { + return $entity->resolveType($resolver); + + } elseif (is_string($entity)) { // class + if (!class_exists($entity)) { + throw new ServiceCreationException(sprintf( + interface_exists($entity) + ? "Interface %s can not be used as 'create' or 'factory', did you mean 'implement'?" + : "Class '%s' not found.", + $entity, + )); + } + + return $entity; + } + + return null; + } + + /** * Formats PHP code for class instantiating, function calling or property setting in PHP. */ diff --git a/src/DI/Extensions/InjectExtension.php b/src/DI/Extensions/InjectExtension.php index 50f7982a3..3b3461a65 100644 --- a/src/DI/Extensions/InjectExtension.php +++ b/src/DI/Extensions/InjectExtension.php @@ -50,7 +50,7 @@ public function beforeCompile(): void private function updateDefinition(Definitions\ServiceDefinition $def): void { - $resolvedType = (new DI\Resolver($this->getContainerBuilder()))->resolveEntityType($def->getCreator()); + $resolvedType = $def->getCreator()->resolveType(new DI\Resolver($this->getContainerBuilder())); $class = is_subclass_of($resolvedType, $def->getType()) ? $resolvedType : $def->getType(); diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index 93740bdd4..9870a7f41 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -11,6 +11,7 @@ use Nette; use Nette\DI\Definitions\Definition; +use Nette\DI\Definitions\Expression; use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; use Nette\PhpGenerator\Helpers as PhpHelpers; @@ -63,6 +64,12 @@ public function withCurrentServiceAvailable(): self } + public function getCurrentService(bool $type = false): Definition|string|null + { + return $type ? $this->currentServiceType : $this->currentService; + } + + public function getContainerBuilder(): ContainerBuilder { return $this->builder; @@ -92,91 +99,6 @@ public function resolveDefinition(Definition $def): void } - public function resolveReferenceType(Reference $ref): ?string - { - if ($ref->isSelf()) { - return $this->currentServiceType; - } elseif ($ref->isType()) { - return ltrim($ref->getValue(), '\\'); - } - - $def = $this->resolveReference($ref); - if (!$def->getType()) { - $this->resolveDefinition($def); - } - - return $def->getType(); - } - - - public function resolveEntityType(Statement $statement): ?string - { - $entity = $this->normalizeEntity($statement); - - if ($statement->arguments === self::getFirstClassCallable()) { - return \Closure::class; - - } elseif (is_array($entity)) { - if ($entity[0] instanceof Reference || $entity[0] instanceof Statement) { - $entity[0] = $this->resolveEntityType($entity[0] instanceof Statement ? $entity[0] : new Statement($entity[0])); - if (!$entity[0]) { - return null; - } - } - - try { - $reflection = Callback::toReflection($entity[0] === '' ? $entity[1] : $entity); - assert($reflection instanceof \ReflectionMethod || $reflection instanceof \ReflectionFunction); - $refClass = $reflection instanceof \ReflectionMethod - ? $reflection->getDeclaringClass() - : null; - } catch (\ReflectionException $e) { - $refClass = $reflection = null; - } - - if (isset($e) || ($refClass && (!$reflection->isPublic() - || ($refClass->isTrait() && !$reflection->isStatic()) - ))) { - throw new ServiceCreationException(sprintf('Method %s() is not callable.', Callback::toString($entity)), 0, $e ?? null); - } - - $this->addDependency($reflection); - - $type = Nette\Utils\Type::fromReflection($reflection) ?? ($annotation = Helpers::getReturnTypeAnnotation($reflection)); - if ($type && !in_array($type->getSingleName(), ['object', 'mixed'], strict: true)) { - if (isset($annotation)) { - trigger_error('Annotation @return should be replaced with native return type at ' . Callback::toString($entity), E_USER_DEPRECATED); - } - - return Helpers::ensureClassType( - $type, - sprintf('return type of %s()', Callback::toString($entity)), - allowNullable: true, - ); - } - - return null; - - } elseif ($entity instanceof Reference) { // alias or factory - return $this->resolveReferenceType($entity); - - } elseif (is_string($entity)) { // class - if (!class_exists($entity)) { - throw new ServiceCreationException(sprintf( - interface_exists($entity) - ? "Interface %s can not be used as 'create' or 'factory', did you mean 'implement'?" - : "Class '%s' not found.", - $entity, - )); - } - - return $entity; - } - - return null; - } - - public function completeDefinition(Definition $def): void { try { @@ -288,9 +210,7 @@ public function completeStatement(Statement $statement): Statement throw new ServiceCreationException(sprintf('Missing argument for %s.', $entity[1])); } } elseif ( - $type = $entity[0] instanceof Reference - ? $this->resolveReferenceType($entity[0]) - : $this->resolveEntityType($entity[0] instanceof Statement ? $entity[0] : new Statement($entity[0])) + $type = ($entity[0] instanceof Expression ? $entity[0] : new Statement($entity[0]))->resolveType($this) ) { $rc = new \ReflectionClass($type); if ($rc->hasMethod($entity[1])) { @@ -349,7 +269,7 @@ public function completeArguments(array $arguments): array /** Returns literal, Class, Reference, [Class, member], [, globalFunc], [Reference, member], [Statement, member] */ - private function normalizeEntity(Statement $statement): string|array|Reference|null + public function normalizeEntity(Statement $statement): string|array|Reference|null { $entity = $statement->getEntity(); if (is_array($entity)) { @@ -400,14 +320,6 @@ public function normalizeReference(Reference $ref): Reference } - public function resolveReference(Reference $ref): Definition - { - return $ref->isSelf() - ? $this->currentService - : $this->builder->getDefinition($ref->getValue()); - } - - /** * Returns named reference to service resolved by type (or 'self' reference for local-autowiring). * @throws ServiceCreationException when multiple found @@ -445,7 +357,8 @@ public function addDependency(\ReflectionClass|\ReflectionFunctionAbstract|strin } - private function completeException(\Throwable $e, Definition $def): ServiceCreationException + /** @internal */ + public function completeException(\Throwable $e, Definition $def): ServiceCreationException { if ($e instanceof ServiceCreationException && str_starts_with($e->getMessage(), "Service '")) { return $e; @@ -508,7 +421,7 @@ private function convertReferences(array $arguments): array if (!isset($pair[1])) { // @service $val = new Reference($pair[0]); } elseif (preg_match('#^[A-Z][a-zA-Z0-9_]*$#D', $pair[1])) { // @service::CONSTANT - $val = ContainerBuilder::literal($this->resolveReferenceType(new Reference($pair[0])) . '::' . $pair[1]); + $val = ContainerBuilder::literal((new Reference($pair[0]))->resolveType($this) . '::' . $pair[1]); } else { // @service::property $val = new Statement([new Reference($pair[0]), '$' . $pair[1]]); } @@ -669,4 +582,27 @@ public static function getFirstClassCallable(): array static $x = [new Nette\PhpGenerator\Literal('...')]; return $x; } + + + /** @deprecated */ + public function resolveReferenceType(Reference $ref): ?string + { + return $ref->resolveType($this); + } + + + /** @deprecated */ + public function resolveEntityType(Statement $statement): ?string + { + return $statement->resolveType($this); + } + + + /** @deprecated */ + public function resolveReference(Reference $ref): Definition + { + return $ref->isSelf() + ? $this->currentService + : $this->builder->getDefinition($ref->getValue()); + } } From decab76ec91b5aaada6ae1e0c785a5b270745d0b Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 1 Dec 2024 21:06:01 +0100 Subject: [PATCH 19/22] Resolver::completeStatement() moved to Statement & Reference --- src/DI/ContainerBuilder.php | 4 +- src/DI/Definitions/AccessorDefinition.php | 2 +- src/DI/Definitions/Expression.php | 3 + src/DI/Definitions/LocatorDefinition.php | 4 +- src/DI/Definitions/Reference.php | 26 +++ src/DI/Definitions/ServiceDefinition.php | 10 +- src/DI/Definitions/Statement.php | 199 +++++++++++++++- src/DI/Resolver.php | 264 +++------------------- 8 files changed, 270 insertions(+), 242 deletions(-) diff --git a/src/DI/ContainerBuilder.php b/src/DI/ContainerBuilder.php index 2895f4f4c..46f8c9e79 100644 --- a/src/DI/ContainerBuilder.php +++ b/src/DI/ContainerBuilder.php @@ -395,8 +395,8 @@ public static function literal(string $code, ?array $args = null): Nette\PhpGene public function formatPhp(string $statement, array $args): string { array_walk_recursive($args, function (&$val): void { - if ($val instanceof Nette\DI\Definitions\Statement) { - $val = (new Resolver($this))->completeStatement($val); + if ($val instanceof Nette\DI\Definitions\Expression) { + $val->complete(new Resolver($this)); } elseif ($val instanceof Definition) { $val = new Definitions\Reference($val->getName()); diff --git a/src/DI/Definitions/AccessorDefinition.php b/src/DI/Definitions/AccessorDefinition.php index ba49ab49c..51a55ef91 100644 --- a/src/DI/Definitions/AccessorDefinition.php +++ b/src/DI/Definitions/AccessorDefinition.php @@ -104,7 +104,7 @@ public function complete(Nette\DI\Resolver $resolver): void $this->setReference(Type::fromReflection($method)->getSingleName()); } - $this->reference = $resolver->normalizeReference($this->reference); + $this->reference->complete($resolver); } diff --git a/src/DI/Definitions/Expression.php b/src/DI/Definitions/Expression.php index df8eb0ee5..da7b4a24f 100644 --- a/src/DI/Definitions/Expression.php +++ b/src/DI/Definitions/Expression.php @@ -17,5 +17,8 @@ abstract class Expression abstract public function resolveType(Nette\DI\Resolver $resolver): ?string; + abstract public function complete(Nette\DI\Resolver $resolver): void; + + abstract public function generateCode(Nette\DI\PhpGenerator $generator): string; } diff --git a/src/DI/Definitions/LocatorDefinition.php b/src/DI/Definitions/LocatorDefinition.php index c3601d858..9b77c7945 100644 --- a/src/DI/Definitions/LocatorDefinition.php +++ b/src/DI/Definitions/LocatorDefinition.php @@ -122,8 +122,8 @@ public function complete(Nette\DI\Resolver $resolver): void } } - foreach ($this->references as $name => $ref) { - $this->references[$name] = $resolver->normalizeReference($ref); + foreach ($this->references as $ref) { + $ref->complete($resolver); } } diff --git a/src/DI/Definitions/Reference.php b/src/DI/Definitions/Reference.php index af54fc18b..b10dd6745 100644 --- a/src/DI/Definitions/Reference.php +++ b/src/DI/Definitions/Reference.php @@ -84,6 +84,32 @@ public function resolveType(DI\Resolver $resolver): ?string } + /** + * Normalizes reference to 'self' or named reference (or leaves it typed if it is not possible during resolving) and checks existence of service. + */ + public function complete(DI\Resolver $resolver): void + { + if ($this->isSelf()) { + return; + + } elseif ($this->isType()) { + try { + $this->value = $resolver->getByType($this->value)->value; + } catch (DI\NotAllowedDuringResolvingException) { + } + return; + } + + if (!$resolver->getContainerBuilder()->hasDefinition($this->value)) { + throw new DI\ServiceCreationException(sprintf("Reference to missing service '%s'.", $this->value)); + } + + if ($this->value === $resolver->getCurrentService()?->getName()) { + $this->value = self::Self; + } + } + + public function generateCode(DI\PhpGenerator $generator): string { return match (true) { diff --git a/src/DI/Definitions/ServiceDefinition.php b/src/DI/Definitions/ServiceDefinition.php index 8ddcd1180..eb5b46b57 100644 --- a/src/DI/Definitions/ServiceDefinition.php +++ b/src/DI/Definitions/ServiceDefinition.php @@ -162,14 +162,14 @@ public function complete(Nette\DI\Resolver $resolver): void { $entity = $this->creator->getEntity(); if ($entity instanceof Reference && !$this->creator->arguments && !$this->setup) { - $ref = $resolver->normalizeReference($entity); - $this->setCreator([new Reference(Nette\DI\ContainerBuilder::ThisContainer), 'getService'], [$ref->getValue()]); + $entity->complete($resolver); + $this->setCreator([new Reference(Nette\DI\ContainerBuilder::ThisContainer), 'getService'], [$entity->getValue()]); } - $this->creator = $resolver->completeStatement($this->creator); + $this->creator->complete($resolver); - foreach ($this->setup as &$setup) { - $setup = $resolver->withCurrentServiceAvailable()->completeStatement($setup); + foreach ($this->setup as $setup) { + $setup->complete($resolver->withCurrentServiceAvailable()); } } diff --git a/src/DI/Definitions/Statement.php b/src/DI/Definitions/Statement.php index 95ec858be..efee81d42 100644 --- a/src/DI/Definitions/Statement.php +++ b/src/DI/Definitions/Statement.php @@ -15,6 +15,7 @@ use Nette\DI\ServiceCreationException; use Nette\PhpGenerator as Php; use Nette\Utils\Callback; +use Nette\Utils\Validators; use function array_keys, class_exists, explode, is_array, is_string, str_contains, str_starts_with, substr; @@ -72,7 +73,7 @@ public function getEntity(): string|array|Definition|Reference|null public function resolveType(Resolver $resolver): ?string { - $entity = $resolver->normalizeEntity($this); + $entity = $this->normalizeEntity($resolver); if ($this->arguments === Resolver::getFirstClassCallable()) { return \Closure::class; @@ -138,6 +139,202 @@ interface_exists($entity) } + public function complete(Resolver $resolver): void + { + $entity = $this->normalizeEntity($resolver); + $this->convertReferences($resolver); + $arguments = $this->arguments; + + switch (true) { + case $this->arguments === Resolver::getFirstClassCallable(): + if (!is_array($entity) || !Php\Helpers::isIdentifier($entity[1])) { + throw new ServiceCreationException(sprintf('Cannot create closure for %s(...)', $entity)); + } + if ($entity[0] instanceof self) { + $entity[0]->complete($resolver); + } + break; + + case is_string($entity) && str_contains($entity, '?'): // PHP literal + break; + + case $entity === 'not': + if (count($arguments) !== 1) { + throw new ServiceCreationException(sprintf('Function %s() expects 1 parameter, %s given.', $entity, count($arguments))); + } + + $this->entity = ['', '!']; + break; + + case $entity === 'bool': + case $entity === 'int': + case $entity === 'float': + case $entity === 'string': + if (count($arguments) !== 1) { + throw new ServiceCreationException(sprintf('Function %s() expects 1 parameter, %s given.', $entity, count($arguments))); + } + + $arguments = [$arguments[0], $entity]; + $this->entity = [DI\Helpers::class, 'convertType']; + break; + + case is_string($entity): // create class + if (!class_exists($entity)) { + throw new ServiceCreationException(sprintf("Class '%s' not found.", $entity)); + } elseif ((new \ReflectionClass($entity))->isAbstract()) { + throw new ServiceCreationException(sprintf('Class %s is abstract.', $entity)); + } elseif (($rm = (new \ReflectionClass($entity))->getConstructor()) !== null && !$rm->isPublic()) { + throw new ServiceCreationException(sprintf('Class %s has %s constructor.', $entity, $rm->isProtected() ? 'protected' : 'private')); + } elseif ($constructor = (new \ReflectionClass($entity))->getConstructor()) { + $arguments = $resolver->autowireServices($constructor, $arguments); + $resolver->addDependency($constructor); + } elseif ($arguments) { + throw new ServiceCreationException(sprintf( + 'Unable to pass arguments, class %s has no constructor.', + $entity, + )); + } + + break; + + case $entity instanceof Reference: + if ($arguments) { + $e = $resolver->completeException(new ServiceCreationException(sprintf('Parameters were passed to reference @%s, although references cannot have any parameters.', $entity->getValue())), $resolver->getCurrentService()); + trigger_error($e->getMessage(), E_USER_DEPRECATED); + } + $this->entity = [new Reference(DI\ContainerBuilder::ThisContainer), DI\Container::getMethodName($entity->getValue())]; + break; + + case is_array($entity): + if (!preg_match('#^\$?(\\\?' . Php\Helpers::ReIdentifier . ')+(\[\])?$#D', $entity[1])) { + throw new ServiceCreationException(sprintf( + "Expected function, method or property name, '%s' given.", + $entity[1], + )); + } + + switch (true) { + case $entity[0] === '': // function call + if (!function_exists($entity[1])) { + throw new ServiceCreationException(sprintf("Function %s doesn't exist.", $entity[1])); + } + + $rf = new \ReflectionFunction($entity[1]); + $arguments = $resolver->autowireServices($rf, $arguments); + $resolver->addDependency($rf); + break; + + case $entity[0] instanceof self: + $entity[0]->complete($resolver); + // break omitted + + case is_string($entity[0]): // static method call + case $entity[0] instanceof Reference: + if ($entity[1][0] === '$') { // property getter, setter or appender + Validators::assert($arguments, 'list:0..1', "setup arguments for '" . Callback::toString($entity) . "'"); + if (!$arguments && str_ends_with($entity[1], '[]')) { + throw new ServiceCreationException(sprintf('Missing argument for %s.', $entity[1])); + } + } elseif ( + $type = ($entity[0] instanceof Expression ? $entity[0] : new self($entity[0]))->resolveType($resolver) + ) { + $rc = new \ReflectionClass($type); + if ($rc->hasMethod($entity[1])) { + $rm = $rc->getMethod($entity[1]); + if (!$rm->isPublic()) { + throw new ServiceCreationException(sprintf('%s::%s() is not callable.', $type, $entity[1])); + } + + $arguments = $resolver->autowireServices($rm, $arguments); + $resolver->addDependency($rm); + } + } + } + } + + try { + $this->arguments = $this->completeArguments($resolver, $arguments); + } catch (ServiceCreationException $e) { + if (!str_contains($e->getMessage(), ' (used in')) { + $e->setMessage($e->getMessage() . " (used in {$resolver->entityToString($entity)})"); + } + + throw $e; + } + } + + + public function completeArguments(Resolver $resolver, array $arguments): array + { + array_walk_recursive($arguments, function (&$val) use ($resolver): void { + if ($val instanceof self) { + if ($val->entity === 'typed' || $val->entity === 'tagged') { + $services = []; + $current = $resolver->getCurrentService()?->getName(); + foreach ($val->arguments as $argument) { + foreach ($val->entity === 'tagged' ? $resolver->getContainerBuilder()->findByTag($argument) : $resolver->getContainerBuilder()->findAutowired($argument) as $name => $foo) { + if ($name !== $current) { + $services[] = new Reference($name); + } + } + } + + $val = $this->completeArguments($resolver, $services); + } else { + $val->complete($resolver); + } + } elseif ($val instanceof Definition || $val instanceof Reference) { + $val = (new self($val))->normalizeEntity($resolver); + } + }); + return $arguments; + } + + + /** Returns literal, Class, Reference, [Class, member], [, globalFunc], [Reference, member], [Statement, member] */ + private function normalizeEntity(Resolver $resolver): string|array|Reference|null + { + if (is_array($this->entity)) { + $item = &$this->entity[0]; + } else { + $item = &$this->entity; + } + + if ($item instanceof Definition) { + if ($resolver->getContainerBuilder()->getDefinition($item->getName()) !== $item) { + throw new ServiceCreationException(sprintf("Service '%s' does not match the expected service.", $item->getName())); + + } + $item = new Reference($item->getName()); + } + + if ($item instanceof Reference) { + $item->complete($resolver); + } + + return $this->entity; + } + + + private function convertReferences(Resolver $resolver): void + { + array_walk_recursive($this->arguments, function (&$val) use ($resolver): void { + if (is_string($val) && strlen($val) > 1 && $val[0] === '@' && $val[1] !== '@') { + $pair = explode('::', substr($val, 1), 2); + if (!isset($pair[1])) { // @service + $val = new Reference($pair[0]); + } elseif (preg_match('#^[A-Z][a-zA-Z0-9_]*$#D', $pair[1])) { // @service::CONSTANT + $val = DI\ContainerBuilder::literal((new Reference($pair[0]))->resolveType($resolver) . '::' . $pair[1]); + } else { // @service::property + $val = new self([new Reference($pair[0]), '$' . $pair[1]]); + } + } elseif (is_string($val) && str_starts_with($val, '@@')) { // escaped text @@ + $val = substr($val, 1); + } + }); + } + + /** * Formats PHP code for class instantiating, function calling or property setting in PHP. */ diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index 9870a7f41..d1c446089 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -11,14 +11,10 @@ use Nette; use Nette\DI\Definitions\Definition; -use Nette\DI\Definitions\Expression; use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; -use Nette\PhpGenerator\Helpers as PhpHelpers; use Nette\Utils\Arrays; -use Nette\Utils\Callback; use Nette\Utils\Reflection; -use Nette\Utils\Validators; use function array_filter, array_key_exists, array_map, array_merge, array_values, array_walk_recursive, assert, class_exists, count, ctype_digit, explode, function_exists, gettype, implode, in_array, interface_exists, is_a, is_array, is_int, is_scalar, is_string, iterator_to_array, ltrim, preg_match, preg_replace, sprintf, str_contains, str_ends_with, str_replace, str_starts_with, strlen, substr; @@ -44,7 +40,7 @@ public function __construct(ContainerBuilder $builder) } - private function withCurrentService(Definition $definition): self + public function withCurrentService(Definition $definition): self { $dolly = clone $this; $dolly->currentService = in_array($definition, $this->builder->getDefinitions(), strict: true) @@ -111,215 +107,6 @@ public function completeDefinition(Definition $def): void } - public function completeStatement(Statement $statement): Statement - { - $entity = $this->normalizeEntity($statement); - $arguments = $this->convertReferences($statement->arguments); - $getter = fn(string $type, bool $single) => $single - ? $this->getByType($type) - : array_values(array_filter($this->builder->findAutowired($type), fn($obj) => $obj !== $this->currentService)); - - switch (true) { - case $statement->arguments === self::getFirstClassCallable(): - if (!is_array($entity) || !PhpHelpers::isIdentifier($entity[1])) { - throw new ServiceCreationException(sprintf('Cannot create closure for %s(...)', $entity)); - } - if ($entity[0] instanceof Statement) { - $entity[0] = $this->completeStatement($entity[0]); - } - break; - - case is_string($entity) && str_contains($entity, '?'): // PHP literal - break; - - case $entity === 'not': - if (count($arguments) !== 1) { - throw new ServiceCreationException(sprintf('Function %s() expects 1 parameter, %s given.', $entity, count($arguments))); - } - - $entity = ['', '!']; - break; - - case $entity === 'bool': - case $entity === 'int': - case $entity === 'float': - case $entity === 'string': - if (count($arguments) !== 1) { - throw new ServiceCreationException(sprintf('Function %s() expects 1 parameter, %s given.', $entity, count($arguments))); - } - - $arguments = [$arguments[0], $entity]; - $entity = [Helpers::class, 'convertType']; - break; - - case is_string($entity): // create class - if (!class_exists($entity)) { - throw new ServiceCreationException(sprintf("Class '%s' not found.", $entity)); - } elseif ((new \ReflectionClass($entity))->isAbstract()) { - throw new ServiceCreationException(sprintf('Class %s is abstract.', $entity)); - } elseif (($rm = (new \ReflectionClass($entity))->getConstructor()) !== null && !$rm->isPublic()) { - throw new ServiceCreationException(sprintf('Class %s has %s constructor.', $entity, $rm->isProtected() ? 'protected' : 'private')); - } elseif ($constructor = (new \ReflectionClass($entity))->getConstructor()) { - $arguments = self::autowireArguments($constructor, $arguments, $getter); - $this->addDependency($constructor); - } elseif ($arguments) { - throw new ServiceCreationException(sprintf( - 'Unable to pass arguments, class %s has no constructor.', - $entity, - )); - } - - break; - - case $entity instanceof Reference: - if ($arguments) { - $e = $this->completeException(new ServiceCreationException(sprintf('Parameters were passed to reference @%s, although references cannot have any parameters.', $entity->getValue())), $this->currentService); - trigger_error($e->getMessage(), E_USER_DEPRECATED); - } - $entity = [new Reference(ContainerBuilder::ThisContainer), Container::getMethodName($entity->getValue())]; - break; - - case is_array($entity): - if (!preg_match('#^\$?(\\\?' . PhpHelpers::ReIdentifier . ')+(\[\])?$#D', $entity[1])) { - throw new ServiceCreationException(sprintf( - "Expected function, method or property name, '%s' given.", - $entity[1], - )); - } - - switch (true) { - case $entity[0] === '': // function call - if (!function_exists($entity[1])) { - throw new ServiceCreationException(sprintf("Function %s doesn't exist.", $entity[1])); - } - - $rf = new \ReflectionFunction($entity[1]); - $arguments = self::autowireArguments($rf, $arguments, $getter); - $this->addDependency($rf); - break; - - case $entity[0] instanceof Statement: - $entity[0] = $this->completeStatement($entity[0]); - // break omitted - - case is_string($entity[0]): // static method call - case $entity[0] instanceof Reference: - if ($entity[1][0] === '$') { // property getter, setter or appender - Validators::assert($arguments, 'list:0..1', "setup arguments for '" . Callback::toString($entity) . "'"); - if (!$arguments && str_ends_with($entity[1], '[]')) { - throw new ServiceCreationException(sprintf('Missing argument for %s.', $entity[1])); - } - } elseif ( - $type = ($entity[0] instanceof Expression ? $entity[0] : new Statement($entity[0]))->resolveType($this) - ) { - $rc = new \ReflectionClass($type); - if ($rc->hasMethod($entity[1])) { - $rm = $rc->getMethod($entity[1]); - if (!$rm->isPublic()) { - throw new ServiceCreationException(sprintf('%s::%s() is not callable.', $type, $entity[1])); - } - - $arguments = self::autowireArguments($rm, $arguments, $getter); - $this->addDependency($rm); - } - } - } - } - - try { - $arguments = $this->completeArguments($arguments); - } catch (ServiceCreationException $e) { - if (!str_contains($e->getMessage(), ' (used in')) { - $e->setMessage($e->getMessage() . " (used in {$this->entityToString($entity)})"); - } - - throw $e; - } - - return new Statement($entity, $arguments); - } - - - public function completeArguments(array $arguments): array - { - array_walk_recursive($arguments, function (&$val): void { - if ($val instanceof Statement) { - $entity = $val->getEntity(); - if ($entity === 'typed' || $entity === 'tagged') { - $services = []; - $current = $this->currentService?->getName(); - foreach ($val->arguments as $argument) { - foreach ($entity === 'tagged' ? $this->builder->findByTag($argument) : $this->builder->findAutowired($argument) as $name => $foo) { - if ($name !== $current) { - $services[] = new Reference($name); - } - } - } - - $val = $this->completeArguments($services); - } else { - $val = $this->completeStatement($val); - } - } elseif ($val instanceof Definition || $val instanceof Reference) { - $val = $this->normalizeEntity(new Statement($val)); - } - }); - return $arguments; - } - - - /** Returns literal, Class, Reference, [Class, member], [, globalFunc], [Reference, member], [Statement, member] */ - public function normalizeEntity(Statement $statement): string|array|Reference|null - { - $entity = $statement->getEntity(); - if (is_array($entity)) { - $item = &$entity[0]; - } else { - $item = &$entity; - } - - if ($item instanceof Definition) { - if ($this->builder->getDefinition($item->getName()) !== $item) { - throw new ServiceCreationException(sprintf("Service '%s' does not match the expected service.", $item->getName())); - - } - $item = new Reference($item->getName()); - } - - if ($item instanceof Reference) { - $item = $this->normalizeReference($item); - } - - return $entity; - } - - - /** - * Normalizes reference to 'self' or named reference (or leaves it typed if it is not possible during resolving) and checks existence of service. - */ - public function normalizeReference(Reference $ref): Reference - { - $service = $ref->getValue(); - if ($ref->isSelf()) { - return $ref; - } elseif ($ref->isName()) { - if (!$this->builder->hasDefinition($service)) { - throw new ServiceCreationException(sprintf("Reference to missing service '%s'.", $service)); - } - - return $this->currentService && $service === $this->currentService->getName() - ? new Reference(Reference::Self) - : $ref; - } - - try { - return $this->getByType($service); - } catch (NotAllowedDuringResolvingException) { - return new Reference($service); - } - } - - /** * Returns named reference to service resolved by type (or 'self' reference for local-autowiring). * @throws ServiceCreationException when multiple found @@ -386,7 +173,8 @@ public function completeException(\Throwable $e, Definition $def): ServiceCreati } - private function entityToString($entity): string + /** @internal */ + public function entityToString($entity): string { $referenceToText = fn(Reference $ref): string => $ref->isSelf() && $this->currentService ? '@' . $this->currentService->getName() @@ -413,23 +201,12 @@ private function entityToString($entity): string } - private function convertReferences(array $arguments): array + public function autowireServices(\ReflectionFunctionAbstract $method, array $arguments): array { - array_walk_recursive($arguments, function (&$val): void { - if (is_string($val) && strlen($val) > 1 && $val[0] === '@' && $val[1] !== '@') { - $pair = explode('::', substr($val, 1), 2); - if (!isset($pair[1])) { // @service - $val = new Reference($pair[0]); - } elseif (preg_match('#^[A-Z][a-zA-Z0-9_]*$#D', $pair[1])) { // @service::CONSTANT - $val = ContainerBuilder::literal((new Reference($pair[0]))->resolveType($this) . '::' . $pair[1]); - } else { // @service::property - $val = new Statement([new Reference($pair[0]), '$' . $pair[1]]); - } - } elseif (is_string($val) && str_starts_with($val, '@@')) { // escaped text @@ - $val = substr($val, 1); - } - }); - return $arguments; + $getter = fn(string $type, bool $single) => $single + ? $this->getByType($type) + : array_values(array_filter($this->builder->findAutowired($type), fn($obj) => $obj !== $this->currentService)); + return self::autowireArguments($method, $arguments, $getter); } @@ -605,4 +382,29 @@ public function resolveReference(Reference $ref): Definition ? $this->currentService : $this->builder->getDefinition($ref->getValue()); } + + + /** @deprecated */ + public function normalizeReference(Reference $ref): Reference + { + $ref->complete($this); + return $ref; + } + + + /** @deprecated */ + public function completeStatement(Statement $statement, bool $currentServiceAllowed = false): Statement + { + $resolver = $this->withCurrentService($this->currentService); + $resolver->currentServiceAllowed = $currentServiceAllowed; + $statement->complete($resolver); + return $statement; + } + + + /** @deprecated */ + public function completeArguments(array $arguments): array + { + return (new Statement(null, $arguments))->completeArguments($this, $arguments); + } } From f36eb1d1052fc128e3024718be4bf24a0e8842cb Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 2 Dec 2024 05:27:24 +0100 Subject: [PATCH 20/22] NeonAdapter: processing of 'prevent merging' and 'entity to statement' moved to visitors --- src/DI/Config/Adapters/NeonAdapter.php | 135 ++++++++++++++++--------- 1 file changed, 90 insertions(+), 45 deletions(-) diff --git a/src/DI/Config/Adapters/NeonAdapter.php b/src/DI/Config/Adapters/NeonAdapter.php index 9bc2b63b4..50578c7c6 100644 --- a/src/DI/Config/Adapters/NeonAdapter.php +++ b/src/DI/Config/Adapters/NeonAdapter.php @@ -25,6 +25,7 @@ final class NeonAdapter implements Nette\DI\Config\Adapter { private const PreventMergingSuffix = '!'; private string $file; + private \WeakMap $parents; /** @@ -41,61 +42,23 @@ public function load(string $file): array $decoder = new Neon\Decoder; $node = $decoder->parseToNode($input); $traverser = new Neon\Traverser; + $node = $traverser->traverse($node, $this->deprecatedQuestionMarkVisitor(...)); $node = $traverser->traverse($node, $this->firstClassCallableVisitor(...)); $node = $traverser->traverse($node, $this->removeUnderscoreVisitor(...)); $node = $traverser->traverse($node, $this->convertAtSignVisitor(...)); $node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...)); $node = $traverser->traverse($node, $this->resolveConstantsVisitor(...)); - return $this->process((array) $node->toValue()); + $node = $traverser->traverse($node, $this->preventMergingVisitor(...)); + $this->connectParentsVisitor($traverser, $node); + $node = $traverser->traverse($node, leave: $this->entityToExpressionVisitor(...)); + return (array) $node->toValue(); } - /** @throws Nette\InvalidStateException */ + /** @deprecated */ public function process(array $arr): array { - $res = []; - foreach ($arr as $key => $val) { - if (is_string($key) && str_ends_with($key, self::PreventMergingSuffix)) { - if (!is_array($val) && $val !== null) { - throw new Nette\DI\InvalidConfigurationException(sprintf( - "Replacing operator is available only for arrays, item '%s' is not array (used in '%s')", - $key, - $this->file, - )); - } - - $key = substr($key, 0, -1); - $val[DI\Config\Helpers::PREVENT_MERGING] = true; - } - - if (is_array($val)) { - $val = $this->process($val); - - } elseif ($val instanceof Neon\Entity) { - if ($val->value === Neon\Neon::Chain) { - $tmp = null; - foreach ($this->process($val->attributes) as $st) { - $tmp = new Statement( - $tmp === null ? $st->getEntity() : [$tmp, ltrim(implode('::', (array) $st->getEntity()), ':')], - $st->arguments, - ); - } - - $val = $tmp; - } else { - $tmp = $this->process([$val->value]); - if (is_string($tmp[0]) && str_contains($tmp[0], '?')) { - throw new Nette\DI\InvalidConfigurationException("Operator ? is deprecated in config file (used in '$this->file')"); - } - - $val = new Statement($tmp[0], $this->process($val->attributes)); - } - } - - $res[$key] = $val; - } - - return $res; + return $arr; } @@ -165,6 +128,71 @@ private function firstClassCallableVisitor(Node $node): void } + private function preventMergingVisitor(Node $node): void + { + if ($node instanceof Node\ArrayItemNode + && $node->key instanceof Node\LiteralNode + && is_string($node->key->value) + && str_ends_with($node->key->value, self::PreventMergingSuffix) + ) { + if ($node->value instanceof Node\LiteralNode && $node->value->value === null) { + $node->value = new Node\InlineArrayNode('['); + } elseif (!$node->value instanceof Node\ArrayNode) { + throw new Nette\DI\InvalidConfigurationException(sprintf( + "Replacing operator is available only for arrays, item '%s' is not array (used in '%s')", + $node->key->value, + $this->file, + )); + } + + $node->key->value = substr($node->key->value, 0, -1); + $node->value->items[] = $item = new Node\ArrayItemNode; + $item->key = new Node\LiteralNode(DI\Config\Helpers::PREVENT_MERGING); + $item->value = new Node\LiteralNode(true); + } + } + + + private function deprecatedQuestionMarkVisitor(Node $node): void + { + if ($node instanceof Node\EntityNode + && ($node->value instanceof Node\LiteralNode || $node->value instanceof Node\StringNode) + && is_string($node->value->value) + && str_contains($node->value->value, '?') + ) { + throw new Nette\DI\InvalidConfigurationException("Operator ? is deprecated in config file (used in '$this->file')"); + } + } + + + private function entityToExpressionVisitor(Node $node): Node + { + if ($node instanceof Node\EntityChainNode) { + return new Node\LiteralNode($this->buildExpression($node->chain)); + + } elseif ( + $node instanceof Node\EntityNode + && !$this->parents[$node] instanceof Node\EntityChainNode + ) { + return new Node\LiteralNode($this->buildExpression([$node])); + + } else { + return $node; + } + } + + + private function buildExpression(array $chain): Statement + { + $node = array_pop($chain); + $entity = $node->toValue(); + return new Statement( + $chain ? [$this->buildExpression($chain), ltrim($entity->value, ':')] : $entity->value, + $entity->attributes, + ); + } + + private function removeUnderscoreVisitor(Node $node): void { if (!$node instanceof Node\EntityNode) { @@ -241,4 +269,21 @@ private function resolveConstantsVisitor(Node $node): void } } } + + + private function connectParentsVisitor(Neon\Traverser $traverser, Node $node): void + { + $this->parents = new \WeakMap; + $stack = []; + $traverser->traverse( + $node, + enter: function (Node $node) use (&$stack) { + $this->parents[$node] = end($stack); + $stack[] = $node; + }, + leave: function () use (&$stack) { + array_pop($stack); + }, + ); + } } From 9c7619c39d404aa306d50276697c629adffe40ad Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 2 Dec 2024 02:17:53 +0100 Subject: [PATCH 21/22] added FunctionCallable & MethodCallable, expressions representing first-class callables --- src/DI/Config/Adapters/NeonAdapter.php | 48 ++++++++++++------- src/DI/Definitions/FunctionCallable.php | 44 +++++++++++++++++ src/DI/Definitions/MethodCallable.php | 53 +++++++++++++++++++++ src/DI/Definitions/Statement.php | 14 +----- src/DI/Resolver.php | 8 ---- tests/DI/Compiler.first-class-callable.phpt | 8 ++-- 6 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 src/DI/Definitions/FunctionCallable.php create mode 100644 src/DI/Definitions/MethodCallable.php diff --git a/src/DI/Config/Adapters/NeonAdapter.php b/src/DI/Config/Adapters/NeonAdapter.php index 50578c7c6..9363cf67b 100644 --- a/src/DI/Config/Adapters/NeonAdapter.php +++ b/src/DI/Config/Adapters/NeonAdapter.php @@ -11,6 +11,7 @@ use Nette; use Nette\DI; +use Nette\DI\Definitions; use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; use Nette\Neon; @@ -43,7 +44,6 @@ public function load(string $file): array $node = $decoder->parseToNode($input); $traverser = new Neon\Traverser; $node = $traverser->traverse($node, $this->deprecatedQuestionMarkVisitor(...)); - $node = $traverser->traverse($node, $this->firstClassCallableVisitor(...)); $node = $traverser->traverse($node, $this->removeUnderscoreVisitor(...)); $node = $traverser->traverse($node, $this->convertAtSignVisitor(...)); $node = $traverser->traverse($node, $this->deprecatedParametersVisitor(...)); @@ -115,19 +115,6 @@ function (&$val): void { } - private function firstClassCallableVisitor(Node $node): void - { - if ($node instanceof Node\EntityNode - && count($node->attributes) === 1 - && $node->attributes[0]->key === null - && $node->attributes[0]->value instanceof Node\LiteralNode - && $node->attributes[0]->value->value === '...' - ) { - $node->attributes[0]->value->value = Nette\DI\Resolver::getFirstClassCallable()[0]; - } - } - - private function preventMergingVisitor(Node $node): void { if ($node instanceof Node\ArrayItemNode @@ -182,14 +169,37 @@ private function entityToExpressionVisitor(Node $node): Node } - private function buildExpression(array $chain): Statement + private function buildExpression(array $chain): Definitions\Expression { $node = array_pop($chain); $entity = $node->toValue(); - return new Statement( + $stmt = new Statement( $chain ? [$this->buildExpression($chain), ltrim($entity->value, ':')] : $entity->value, $entity->attributes, ); + + if ($this->isFirstClassCallable($node)) { + $entity = $stmt->getEntity(); + if (is_array($entity)) { + if ($entity[0] === '') { + return new Definitions\FunctionCallable($entity[1]); + } + return new Definitions\MethodCallable(...$entity); + } else { + throw new Nette\DI\InvalidConfigurationException("Cannot create closure for '$entity' in config file (used in '$this->file')"); + } + } + + return $stmt; + } + + + private function isFirstClassCallable(Node\EntityNode $node): bool + { + return array_keys($node->attributes) === [0] + && $node->attributes[0]->key === null + && $node->attributes[0]->value instanceof Node\LiteralNode + && $node->attributes[0]->value->value === '...'; } @@ -211,7 +221,11 @@ private function removeUnderscoreVisitor(Node $node): void unset($node->attributes[$i]); $index = true; - } elseif ($attr->value instanceof Node\LiteralNode && $attr->value->value === '...') { + } elseif ( + $attr->value instanceof Node\LiteralNode + && $attr->value->value === '...' + && !$this->isFirstClassCallable($node) + ) { trigger_error("Replace ... with _ in configuration file '$this->file'.", E_USER_DEPRECATED); unset($node->attributes[$i]); $index = true; diff --git a/src/DI/Definitions/FunctionCallable.php b/src/DI/Definitions/FunctionCallable.php new file mode 100644 index 000000000..1de1a15ab --- /dev/null +++ b/src/DI/Definitions/FunctionCallable.php @@ -0,0 +1,44 @@ +function . '(...)'; + } +} diff --git a/src/DI/Definitions/MethodCallable.php b/src/DI/Definitions/MethodCallable.php new file mode 100644 index 000000000..116926c3d --- /dev/null +++ b/src/DI/Definitions/MethodCallable.php @@ -0,0 +1,53 @@ +objectOrClass instanceof Expression) { + $this->objectOrClass->complete($resolver); + } + } + + + public function generateCode(PhpGenerator $generator): string + { + return is_string($this->objectOrClass) + ? $generator->formatPhp('?::?(...)', [new Php\Literal($this->objectOrClass), $this->method]) + : $generator->formatPhp('?->?(...)', [new Php\Literal($this->objectOrClass->generateCode($generator)), $this->method]); + } +} diff --git a/src/DI/Definitions/Statement.php b/src/DI/Definitions/Statement.php index efee81d42..9c65b3953 100644 --- a/src/DI/Definitions/Statement.php +++ b/src/DI/Definitions/Statement.php @@ -75,10 +75,7 @@ public function resolveType(Resolver $resolver): ?string { $entity = $this->normalizeEntity($resolver); - if ($this->arguments === Resolver::getFirstClassCallable()) { - return \Closure::class; - - } elseif (is_array($entity)) { + if (is_array($entity)) { if ($entity[0] instanceof Expression) { $entity[0] = $entity[0]->resolveType($resolver); if (!$entity[0]) { @@ -146,15 +143,6 @@ public function complete(Resolver $resolver): void $arguments = $this->arguments; switch (true) { - case $this->arguments === Resolver::getFirstClassCallable(): - if (!is_array($entity) || !Php\Helpers::isIdentifier($entity[1])) { - throw new ServiceCreationException(sprintf('Cannot create closure for %s(...)', $entity)); - } - if ($entity[0] instanceof self) { - $entity[0]->complete($resolver); - } - break; - case is_string($entity) && str_contains($entity, '?'): // PHP literal break; diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index d1c446089..11a8851c0 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -353,14 +353,6 @@ private static function isArrayOf(\ReflectionParameter $parameter, ?Nette\Utils\ } - /** @internal */ - public static function getFirstClassCallable(): array - { - static $x = [new Nette\PhpGenerator\Literal('...')]; - return $x; - } - - /** @deprecated */ public function resolveReferenceType(Reference $ref): ?string { diff --git a/tests/DI/Compiler.first-class-callable.phpt b/tests/DI/Compiler.first-class-callable.phpt index c2601dff0..bc8ab2966 100644 --- a/tests/DI/Compiler.first-class-callable.phpt +++ b/tests/DI/Compiler.first-class-callable.phpt @@ -28,7 +28,7 @@ class Service test('Valid callables', function () { $config = ' services: - - Service( Service::foo(...), @a::foo(...), ::trim(...) ) + - Service( Service::foo(...), @a::b()::foo(...), ::trim(...) ) a: stdClass '; $loader = new DI\Config\Loader; @@ -36,7 +36,7 @@ test('Valid callables', function () { $compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon'))); $code = $compiler->compile(); - Assert::contains('new Service(Service::foo(...), $this->getService(\'a\')->foo(...), trim(...));', $code); + Assert::contains('new Service(Service::foo(...), $this->getService(\'a\')->b()->foo(...), trim(...));', $code); }); @@ -50,7 +50,7 @@ Assert::exception(function () { $compiler = new DI\Compiler; $compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon'))); $compiler->compile(); -}, Nette\DI\ServiceCreationException::class, 'Service of type Closure: Cannot create closure for Service(...)'); +}, Nette\DI\InvalidConfigurationException::class, "Cannot create closure for 'Service' in config file (used in %a%)"); // Invalid callable 2 @@ -63,4 +63,4 @@ Assert::exception(function () { $compiler = new DI\Compiler; $compiler->addConfig($loader->load(Tester\FileMock::create($config, 'neon'))); $compiler->compile(); -}, Nette\DI\ServiceCreationException::class, 'Service of type Service: Cannot create closure for Service(...) (used in Service::__construct())'); +}, Nette\DI\InvalidConfigurationException::class, "Cannot create closure for 'Service' in config file (used in %a%)"); From bca3335882a33392ab398b1fd9b9b6eb9282f087 Mon Sep 17 00:00:00 2001 From: Rixafy <45132928+Rixafy@users.noreply.github.com> Date: Sun, 25 May 2025 19:35:43 +0200 Subject: [PATCH 22/22] added support for injecting services by tags --- src/DI/Attributes/Inject.php | 6 +- src/DI/Autowiring.php | 65 +++-- src/DI/Container.php | 66 +++++- src/DI/ContainerBuilder.php | 13 +- src/DI/Definitions/Reference.php | 12 +- src/DI/Extensions/InjectExtension.php | 108 ++++++++- src/DI/Resolver.php | 7 +- tests/DI/InjectExtension.basic.phpt | 16 +- tests/DI/InjectExtension.errors.phpt | 49 ++++ ...InjectExtension.getInjectProperties().phpt | 55 ++++- ...xtension.getInjectProperties().traits.phpt | 17 +- tests/DI/InjectExtension.tags.phpt | 223 ++++++++++++++++++ 12 files changed, 579 insertions(+), 58 deletions(-) create mode 100644 tests/DI/InjectExtension.tags.phpt diff --git a/src/DI/Attributes/Inject.php b/src/DI/Attributes/Inject.php index 325e2b291..f7fd89bab 100644 --- a/src/DI/Attributes/Inject.php +++ b/src/DI/Attributes/Inject.php @@ -12,7 +12,11 @@ use Attribute; -#[Attribute(Attribute::TARGET_PROPERTY)] +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] class Inject { + public function __construct( + public readonly ?string $tag = null, + ) { + } } diff --git a/src/DI/Autowiring.php b/src/DI/Autowiring.php index 3be57cb3a..fea6f8cd7 100644 --- a/src/DI/Autowiring.php +++ b/src/DI/Autowiring.php @@ -42,35 +42,66 @@ public function __construct(ContainerBuilder $builder) * @throws ServiceCreationException when multiple found */ public function getByType(string $type, bool $throw = false): ?string + { + return $this->getByTypeAndTag($type, null, $throw); + } + + + /** + * Resolves service name by type and tag. + * @return ($throw is true ? string : ?string) + * @throws MissingServiceException when not found + * @throws ServiceCreationException when multiple found + */ + public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = false): ?string { $type = Helpers::normalizeClass($type); $types = $this->highPriority; - if (empty($types[$type])) { + $services = $types[$type] ?? []; + + if ($services === []) { if ($throw) { if (!class_exists($type) && !interface_exists($type)) { throw new MissingServiceException(sprintf("Service of type '%s' not found. Check the class name because it cannot be found.", $type)); } - throw new MissingServiceException(sprintf('Service of type %s not found. Did you add it to configuration file?', $type)); } - return null; + } + + if ($tag !== null) { + $services = array_filter($services, fn(string $name) => ($this->builder->getDefinition($name)->getTags()[$tag] ?? false) !== false); + if ($services === []) { + if ($throw) { + throw new MissingServiceException(sprintf('Service of type %s with tag "%s" not found.', $type, $tag)); + } + return null; + } + } - } elseif (count($types[$type]) === 1) { - return $types[$type][0]; - - } else { - $list = $types[$type]; - natsort($list); - $hint = count($list) === 2 && ($tmp = str_contains($list[0], '.') xor str_contains($list[1], '.')) - ? '. If you want to overwrite service ' . $list[$tmp ? 0 : 1] . ', give it proper name.' - : ''; - throw new ServiceCreationException(sprintf( - "Multiple services of type $type found: %s%s", - implode(', ', $list), - $hint, - )); + if (count($services) === 1) { + return reset($services); } + + if ($tag === null) { + $default = array_filter($services, fn(string $name) => ($this->builder->getDefinition($name)->getTags()['default'] ?? false) !== false); + if (count($default) === 1) { + return reset($default); + } + } + + natsort($services); + $hint = count($services) === 2 && ($tmp = str_contains($services[0], '.') xor str_contains($services[1], '.')) + ? '. If you want to overwrite service ' . $services[$tmp ? 0 : 1] . ', give it proper name.' + : ''; + + throw new ServiceCreationException(sprintf( + 'Multiple services of type %s%s found: %s%s', + $type, + $tag !== null ? " with tag '$tag'" : '', + implode(', ', $services), + $hint, + )); } diff --git a/src/DI/Container.php b/src/DI/Container.php index db03ae246..00caa858f 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -236,24 +236,76 @@ public function createService(string $name): object * Returns an instance of the autowired service of the given type. If it has not been created yet, it creates it. * @template T of object * @param class-string $type + * @param bool $throw throw exception if service doesn't exist? * @return ($throw is true ? T : ?T) * @throws MissingServiceException */ public function getByType(string $type, bool $throw = true): ?object + { + return $this->getByTypeAndTag($type, null, $throw); + } + + + /** + * Returns an instance of the autowired service of the given type and tag. If it has not been created yet, it creates it. + * @template T of object + * @param class-string $type + * @param bool $throw throw exception if service doesn't exist? + * @return ($throw is true ? T : ?T) + * @throws MissingServiceException + */ + public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = true): ?object { $type = Helpers::normalizeClass($type); if (!empty($this->wiring[$type][0])) { - if (count($names = $this->wiring[$type][0]) === 1) { - return $this->getService($names[0]); + $names = $this->wiring[$type][0]; + + // Filter by tag if specified + if ($tag !== null) { + $taggedNames = []; + foreach ($names as $name) { + $serviceTags = $this->findByTag($tag); + if (isset($serviceTags[$name])) { + $taggedNames[] = $name; + } + } + $names = $taggedNames; + } + + // Try to find service with tag default + if ($tag === null && count($names) > 1) { + $defaultTagNames = []; + foreach ($names as $name) { + if (isset($this->findByTag('default')[$name])) { + $defaultTagNames[] = $name; + } + } + + if ($defaultTagNames !== []) { + $names = $defaultTagNames; + } } - natsort($names); - throw new MissingServiceException(sprintf("Multiple services of type $type found: %s.", implode(', ', $names))); + if (count($names) === 1) { + return $this->getService($names[0]); + } elseif (count($names) > 1) { + natsort($names); + + throw new MissingServiceException(sprintf( + 'Multiple services of type %s%s found: %s.', + $type, + $tag !== null ? " with tag '$tag'" : '', + implode(', ', $names), + )); + } + } - } elseif ($throw) { + if ($throw) { if (!class_exists($type) && !interface_exists($type)) { throw new MissingServiceException(sprintf("Service of type '%s' not found. Check the class name because it cannot be found.", $type)); - } elseif ($this->findByType($type)) { + } elseif ($tag !== null) { + throw new MissingServiceException(sprintf("Service of type %s with tag '%s' not found.", $type, $tag)); + } elseif ($this->findByType($type) !== []) { throw new MissingServiceException(sprintf("Service of type %s is not autowired or is missing in di\u{a0}›\u{a0}export\u{a0}›\u{a0}types.", $type)); } else { throw new MissingServiceException(sprintf('Service of type %s not found. Did you add it to configuration file?', $type)); @@ -363,7 +415,7 @@ public function callMethod(callable $function, array $args = []): mixed private function autowireArguments(\ReflectionFunctionAbstract $function, array $args = []): array { return Resolver::autowireArguments($function, $args, fn(string $type, bool $single) => $single - ? $this->getByType($type) + ? $this->getByType($type, throw: true) : array_map($this->getService(...), $this->findAutowired($type))); } diff --git a/src/DI/ContainerBuilder.php b/src/DI/ContainerBuilder.php index 46f8c9e79..1a0ffa455 100644 --- a/src/DI/ContainerBuilder.php +++ b/src/DI/ContainerBuilder.php @@ -215,9 +215,20 @@ public function addExcludedClasses(array $types): static * @throws MissingServiceException */ public function getByType(string $type, bool $throw = false): ?string + { + return $this->getByTypeAndTag($type, null, $throw); + } + + + /** + * Resolves autowired service name by type and tag. + * @return ($throw is true ? string : ?string) + * @throws MissingServiceException + */ + public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = false): ?string { $this->needResolved(); - return $this->autowiring->getByType($type, $throw); + return $this->autowiring->getByTypeAndTag($type, $tag, $throw); } diff --git a/src/DI/Definitions/Reference.php b/src/DI/Definitions/Reference.php index b10dd6745..72fb52397 100644 --- a/src/DI/Definitions/Reference.php +++ b/src/DI/Definitions/Reference.php @@ -25,20 +25,23 @@ final class Reference extends Expression private string $value; + private ?string $tag; - public static function fromType(string $value): static + + public static function fromType(string $value, ?string $tag = null): static { if (!str_contains($value, '\\')) { $value = '\\' . $value; } - return new static($value); + return new static($value, $tag); } - public function __construct(string $value) + public function __construct(string $value, ?string $tag = null) { $this->value = $value; + $this->tag = $tag; } @@ -94,7 +97,8 @@ public function complete(DI\Resolver $resolver): void } elseif ($this->isType()) { try { - $this->value = $resolver->getByType($this->value)->value; + $reference = $resolver->getByType($this->value, $this->tag); + $this->value = $reference->value; } catch (DI\NotAllowedDuringResolvingException) { } return; diff --git a/src/DI/Extensions/InjectExtension.php b/src/DI/Extensions/InjectExtension.php index 3b3461a65..df8899bc9 100644 --- a/src/DI/Extensions/InjectExtension.php +++ b/src/DI/Extensions/InjectExtension.php @@ -56,9 +56,51 @@ private function updateDefinition(Definitions\ServiceDefinition $def): void : $def->getType(); $setups = $def->getSetup(); - foreach (self::getInjectProperties($class) as $property => $type) { + // Inject attributes in constructor parameters + $constructor = (new \ReflectionClass($class))->getConstructor(); + if ($constructor !== null) { + foreach ($constructor->getParameters() as $param) { + $attributes = $param->getAttributes(DI\Attributes\Inject::class); + if ($attributes !== []) { + $injectAttribute = $attributes[0]->newInstance(); + $tag = $injectAttribute->tag; + if ($tag === null) { + throw new Nette\InvalidStateException(sprintf( + 'Attribute #[Inject] on parameter $%s in %s is redundant.', + $param->getName(), + Reflection::toString($constructor), + )); + } + + $type = Nette\Utils\Type::fromReflection($param); + if ($type === null) { + throw new Nette\InvalidStateException(sprintf( + 'Parameter $%s in %s has no type hint.', + $param->getName(), + Reflection::toString($constructor), + )); + } + + // Update the creator arguments to use the tagged service + $creator = $def->getCreator(); + $arguments = $creator->arguments; + $arguments[$param->getName()] = Definitions\Reference::fromType((string) $type, $tag); + $def->setCreator($creator->getEntity(), $arguments); + } + } + } + + // Inject attributes in properties + foreach (self::getInjectProperties($class) as $property => $typeAndTag) { + $type = $typeAndTag['type']; + $tag = $typeAndTag['tag']; + $builder = $this->getContainerBuilder(); - $inject = new Definitions\Statement(['@self', '$' . $property], [Definitions\Reference::fromType((string) $type)]); + $inject = new Definitions\Statement( + ['@self', '$' . $property], + [Definitions\Reference::fromType($type, $tag)], + ); + foreach ($setups as $key => $setup) { if ($setup->getEntity() == $inject->getEntity()) { // intentionally == $inject = $setup; @@ -67,14 +109,46 @@ private function updateDefinition(Definitions\ServiceDefinition $def): void } } - if ($builder) { - self::checkType($class, $property, $type, $builder); + if ($builder !== null) { + self::checkType($class, $property, $type, $builder, $tag); } array_unshift($setups, $inject); } foreach (array_reverse(self::getInjectMethods($class)) as $method) { $inject = new Definitions\Statement(['@self', $method]); + $methodReflection = new \ReflectionMethod($class, $method); + $arguments = []; + + // Inject attributes in inject methods + foreach ($methodReflection->getParameters() as $param) { + $attributes = $param->getAttributes(DI\Attributes\Inject::class); + if ($attributes !== []) { + $injectAttribute = $attributes[0]->newInstance(); + $tag = $injectAttribute->tag; + if ($tag === null) { + throw new Nette\InvalidStateException(sprintf( + 'Parameter %s has #[Inject] attribute, but no tag specified.', + Reflection::toString($param), + )); + } + + $type = Nette\Utils\Type::fromReflection($param); + if ($type === null) { + throw new Nette\InvalidStateException(sprintf( + 'Parameter $%s in %s has no type hint.', + $param->getName(), + Reflection::toString($methodReflection), + )); + } + $arguments[$param->getName()] = Definitions\Reference::fromType((string) $type, $tag); + } + } + + if ($arguments !== []) { + $inject = new Definitions\Statement(['@self', $method], $arguments); + } + foreach ($setups as $key => $setup) { if ($setup->getEntity() == $inject->getEntity()) { // intentionally == $inject = $setup; @@ -113,11 +187,16 @@ public static function getInjectMethods(string $class): array /** * Generates list of properties with annotation @inject. * @internal + * @return array */ public static function getInjectProperties(string $class): array { $res = []; foreach ((new \ReflectionClass($class))->getProperties() as $rp) { + if ($rp->isPromoted()) { + continue; // Setup is in constructor + } + $hasAttr = $rp->getAttributes(DI\Attributes\Inject::class); if ($hasAttr || DI\Helpers::parseAnnotation($rp, 'inject') !== null) { if (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly()) { @@ -130,7 +209,12 @@ public static function getInjectProperties(string $class): array $type = Nette\Utils\Type::fromString($annotation); } - $res[$rp->getName()] = DI\Helpers::ensureClassType($type, 'type of property ' . Reflection::toString($rp)); + $tag = null; + if ($hasAttr !== []) { + $tag = $hasAttr[0]->newInstance()->tag; + } + + $res[$rp->getName()] = ['type' => DI\Helpers::ensureClassType($type, 'type of property ' . Reflection::toString($rp)), 'tag' => $tag]; } } @@ -148,9 +232,11 @@ public static function callInjects(DI\Container $container, object $service): vo $container->callMethod([$service, $method]); } - foreach (self::getInjectProperties($service::class) as $property => $type) { - self::checkType($service, $property, $type, $container); - $service->$property = $container->getByType($type); + foreach (self::getInjectProperties($service::class) as $property => $propertyInfo) { + $type = $propertyInfo['type']; + $tag = $propertyInfo['tag']; + self::checkType($service, $property, $type, $container, $tag); + $service->$property = $container->getByTypeAndTag($type, $tag, throw: true); } } @@ -160,12 +246,14 @@ private static function checkType( string $name, ?string $type, DI\Container|DI\ContainerBuilder $container, + ?string $tag = null, ): void { - if (!$container->getByType($type, throw: false)) { + if (!$container->getByTypeAndTag($type, $tag, throw: false)) { throw new Nette\DI\MissingServiceException(sprintf( - 'Service of type %s required by %s not found. Did you add it to configuration file?', + 'Service of type %s%s required by %s not found. Did you add it to configuration file?', $type, + $tag !== null ? " with tag '$tag'" : '', Reflection::toString(new \ReflectionProperty($class, $name)), )); } diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index 11a8851c0..71ee05c10 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -112,17 +112,18 @@ public function completeDefinition(Definition $def): void * @throws ServiceCreationException when multiple found * @throws MissingServiceException when not found */ - public function getByType(string $type): Reference + public function getByType(string $type, ?string $tag = null): Reference { if ( $this->currentService && $this->currentServiceAllowed && is_a($this->currentServiceType, $type, allow_string: true) + && $tag === null ) { return new Reference(Reference::Self); } - $name = $this->builder->getByType($type, throw: true); + $name = $this->builder->getByTypeAndTag($type, $tag, true); if ( !$this->currentServiceAllowed && $this->currentService === $this->builder->getDefinition($name) @@ -130,7 +131,7 @@ public function getByType(string $type): Reference throw new MissingServiceException; } - return new Reference($name); + return new Reference($name, $tag); } diff --git a/tests/DI/InjectExtension.basic.phpt b/tests/DI/InjectExtension.basic.phpt index 1b6d3fb52..939125010 100644 --- a/tests/DI/InjectExtension.basic.phpt +++ b/tests/DI/InjectExtension.basic.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\DI; +use Nette\DI\Attributes\Inject; use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; use Tester\Assert; @@ -34,6 +35,9 @@ class ParentClass /** @var stdClass @inject */ public $a; + #[Inject(tag: 'tag')] + public stdClass $aTag; + public function injectA() { @@ -91,7 +95,14 @@ extensions: ext: LastExtension services: - std: stdClass + std: + create: stdClass + tags: + - default + stdTag: + create: stdClass + tags: + - tag a: ConcreteDependencyA b: ConcreteDependencyB two: @@ -113,6 +124,7 @@ Assert::equal([ new Statement([new Reference('self'), 'injectD']), new Statement([new Reference('self'), '$e'], [new Reference('a')]), new Statement([new Reference('self'), '$c'], [new Reference('std')]), + new Statement([new Reference('self'), '$aTag'], [new Reference('stdTag', 'tag')]), new Statement([new Reference('self'), '$a'], [new Reference('std')]), ], $builder->getDefinition('last.one')->getSetup()); @@ -123,6 +135,7 @@ Assert::equal([ new Statement([new Reference('self'), 'injectD']), new Statement([new Reference('self'), '$e'], [new Reference('a')]), new Statement([new Reference('self'), '$c'], [new Reference('std')]), + new Statement([new Reference('self'), '$aTag'], [new Reference('stdTag', 'tag')]), new Statement([new Reference('self'), '$a'], [new Reference('std')]), ], $builder->getDefinition('ext.one')->getSetup()); @@ -133,5 +146,6 @@ Assert::equal([ new Statement([new Reference('self'), 'injectD']), new Statement([new Reference('self'), '$e'], [new Reference('b')]), new Statement([new Reference('self'), '$c'], [new Reference('std')]), + new Statement([new Reference('self'), '$aTag'], [new Reference('stdTag', 'tag')]), new Statement([new Reference('self'), '$a'], [new Reference('std')]), ], $builder->getDefinition('two')->getSetup()); diff --git a/tests/DI/InjectExtension.errors.phpt b/tests/DI/InjectExtension.errors.phpt index e2dc16c88..d38b0b8c0 100644 --- a/tests/DI/InjectExtension.errors.phpt +++ b/tests/DI/InjectExtension.errors.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\DI; +use Nette\DI\Attributes\Inject; use Nette\InvalidStateException; use Tester\Assert; @@ -14,6 +15,11 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; +class Known +{ +} + + class ServiceA { /** @var DateTimeImmutable @inject */ @@ -28,6 +34,23 @@ class ServiceB } +class ServiceB2 +{ + #[Inject(tag: 'test')] + public Known $a; +} + + +class ServiceB3 +{ + public function __construct( + #[Inject] + private Known $a, + ) { + } +} + + class ServiceC { /** @inject */ @@ -74,6 +97,32 @@ services: Check the type of property ServiceB::\$a."); +Assert::exception(function () { + $compiler = new DI\Compiler; + $compiler->addExtension('inject', new Nette\DI\Extensions\InjectExtension); + createContainer($compiler, ' +services: + known: Known + service: + create: ServiceB2 + inject: yes +'); +}, InvalidStateException::class, "Service of type Known with tag 'test' required by ServiceB2::\$a not found. Did you add it to configuration file?"); + + +Assert::exception(function () { + $compiler = new DI\Compiler; + $compiler->addExtension('inject', new Nette\DI\Extensions\InjectExtension); + createContainer($compiler, ' +services: + known: Known + service: + create: ServiceB3 + inject: yes +'); +}, InvalidStateException::class, 'Attribute #[Inject] on parameter $a in ServiceB3::__construct() is redundant.'); + + Assert::exception(function () { $compiler = new DI\Compiler; $compiler->addExtension('inject', new Nette\DI\Extensions\InjectExtension); diff --git a/tests/DI/InjectExtension.getInjectProperties().phpt b/tests/DI/InjectExtension.getInjectProperties().phpt index 80b3793e7..2be133e9e 100644 --- a/tests/DI/InjectExtension.getInjectProperties().phpt +++ b/tests/DI/InjectExtension.getInjectProperties().phpt @@ -84,23 +84,56 @@ namespace { Assert::same([ - 'varA' => A\AInjected::class, - 'varB' => A\B\BInjected::class, - 'varC' => A\AInjected::class, + 'varA' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'varB' => [ + 'type' => A\B\BInjected::class, + 'tag' => null, + ], + 'varC' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], ], InjectExtension::getInjectProperties(A\AClass::class)); Assert::same([ - 'varA' => A\AInjected::class, - 'varB' => A\B\BInjected::class, - 'varC' => A\AInjected::class, - 'varF' => A\B\BInjected::class, + 'varA' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'varB' => [ + 'type' => A\B\BInjected::class, + 'tag' => null, + ], + 'varC' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'varF' => [ + 'type' => A\B\BInjected::class, + 'tag' => null, + ], ], InjectExtension::getInjectProperties(A\B\BClass::class)); Assert::same([ - 'var1' => A\AInjected::class, - 'var2' => A\B\BInjected::class, - 'var3' => C\CInjected::class, - 'var4' => C\CInjected::class, + 'var1' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'var2' => [ + 'type' => A\B\BInjected::class, + 'tag' => null, + ], + 'var3' => [ + 'type' => C\CInjected::class, + 'tag' => null, + ], + 'var4' => [ + 'type' => C\CInjected::class, + 'tag' => null, + ], ], InjectExtension::getInjectProperties(C\CClass::class)); Assert::exception( diff --git a/tests/DI/InjectExtension.getInjectProperties().traits.phpt b/tests/DI/InjectExtension.getInjectProperties().traits.phpt index 1fc6c211e..5059354aa 100644 --- a/tests/DI/InjectExtension.getInjectProperties().traits.phpt +++ b/tests/DI/InjectExtension.getInjectProperties().traits.phpt @@ -16,11 +16,15 @@ namespace A namespace B { use A\AInjected; + use Nette\DI\Attributes\Inject; trait BTrait { - /** @var AInjected @inject */ - public $varA; + #[Inject] + public AInjected $varA; + + #[Inject(tag: 'tagB')] + public AInjected $varB; } } @@ -42,6 +46,13 @@ namespace { Assert::same([ - 'varA' => A\AInjected::class, + 'varA' => [ + 'type' => A\AInjected::class, + 'tag' => null, + ], + 'varB' => [ + 'type' => A\AInjected::class, + 'tag' => 'tagB', + ], ], InjectExtension::getInjectProperties(C\CClass::class)); } diff --git a/tests/DI/InjectExtension.tags.phpt b/tests/DI/InjectExtension.tags.phpt new file mode 100644 index 000000000..e3bb4c1fe --- /dev/null +++ b/tests/DI/InjectExtension.tags.phpt @@ -0,0 +1,223 @@ +injectedA = $injectedA; + $this->injectedB = $injectedB; + } + + + public function getInjectedA(): Dependency + { + return $this->injectedA; + } + + + public function getInjectedB(): Dependency + { + return $this->injectedB; + } + + + public function getPrivateA(): Dependency + { + return $this->privateA; + } + + + public function getPrivateB(): Dependency + { + return $this->privateB; + } +} + + +$compiler = new DI\Compiler; +$compiler->addExtension('inject', new Nette\DI\Extensions\InjectExtension); +$container = createContainer($compiler, ' +services: + a: + create: DependencyA + tags: + - default + b: + create: DependencyB + tags: + - alt + c: + create: DependencyA + tags: + - duplicate + service: + create: Service + inject: true +'); + + +$builder = $compiler->getContainerBuilder(); + +Assert::same( + $builder->getByType(Dependency::class), + 'a', +); + +Assert::same( + $builder->getByTypeAndTag(Dependency::class, tag: 'alt'), + 'b', +); + +Assert::same( + $builder->getByTypeAndTag(Dependency::class, tag: 'duplicate'), + 'c', +); + +Assert::same( + $builder->getByTypeAndTag(Dependency::class, tag: 'default'), + 'a', +); + + +Assert::equal( + $container->getByType(Service::class)->dependencyA, + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyADuplicate, + new DependencyA, +); + +Assert::notSame( + $container->getByType(Service::class)->dependencyA, + $container->getByType(Service::class)->dependencyADuplicate, +); + +Assert::same( + $container->getByType(Service::class)->dependencyA, + $container->getByType(Service::class)->dependencyC, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyB, + new DependencyB, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyC, + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyD, + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyE, + new DependencyB, +); + +Assert::equal( + $container->getByType(Service::class)->dependencyF, + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->getInjectedA(), + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->getInjectedB(), + new DependencyB, +); + +Assert::equal( + $container->getByType(Service::class)->getPrivateA(), + new DependencyA, +); + +Assert::equal( + $container->getByType(Service::class)->getPrivateB(), + new DependencyB, +); + + +Assert::equal( + $container->getByType(Dependency::class), + new DependencyA, +); + +Assert::equal( + $container->getByTypeAndTag(Dependency::class, tag: 'alt'), + new DependencyB, +); + +Assert::equal( + $container->getByTypeAndTag(Dependency::class, tag: 'default'), + new DependencyA, +); + +Assert::same( + $container->findByType(Dependency::class), + ['a', 'b', 'c'], +);