From 097304675fa3c7a0c70fd1ec7f569ea68140a724 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 02:33:19 +0300 Subject: [PATCH 01/31] Add benchmarks --- composer.json | 4 + phpbench.json | 12 ++ tests/Benchmark/DefinitionStorageBench.php | 133 +++++++++++++++++++ tests/Benchmark/NormalizerBench.php | 147 +++++++++++++++++++++ tests/Benchmark/TypicalUsageBench.php | 122 +++++++++++++++++ 5 files changed, 418 insertions(+) create mode 100644 phpbench.json create mode 100644 tests/Benchmark/DefinitionStorageBench.php create mode 100644 tests/Benchmark/NormalizerBench.php create mode 100644 tests/Benchmark/TypicalUsageBench.php diff --git a/composer.json b/composer.json index da795a7..f0fdd2c 100644 --- a/composer.json +++ b/composer.json @@ -33,11 +33,15 @@ "bamarni/composer-bin-plugin": "^1.8.3", "friendsofphp/php-cs-fixer": "^3.69", "maglnet/composer-require-checker": "^4.7.1", + "phpbench/phpbench": "^1.4.1", "phpunit/phpunit": "^10.5.45", "rector/rector": "^2.0.9", "spatie/phpunit-watcher": "^1.24", "yiisoft/test-support": "^3.0.1" }, + "suggest": { + "phpbench/phpbench": "To run benchmarks." + }, "autoload": { "psr-4": { "Yiisoft\\Definitions\\": "src" diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 0000000..5254f28 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,12 @@ +{ + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "tests/Benchmark", + "runner.retry_threshold": 3, + "report.outputs": { + "csv_file": { + "extends": "delimited", + "delimiter": ",", + "file": "benchmarks.csv" + } + } +} diff --git a/tests/Benchmark/DefinitionStorageBench.php b/tests/Benchmark/DefinitionStorageBench.php new file mode 100644 index 0000000..3f1c7cf --- /dev/null +++ b/tests/Benchmark/DefinitionStorageBench.php @@ -0,0 +1,133 @@ + + */ + private array $definitions = []; + + public function before(): void + { + $this->indexes = []; + $this->definitions = [ + EngineInterface::class => EngineMarkOne::class, + ColorInterface::class => ColorPink::class, + ]; + + for ($i = 0; $i < self::SERVICE_COUNT; $i++) { + $this->indexes[] = $i; + $this->definitions["service$i"] = Car::class; + } + } + + /** + * Measures construction with a typical services map. + * + * @Groups({"construct", "storage"}) + */ + public function benchConstruct(): void + { + new DefinitionStorage($this->definitions); + } + + /** + * Measures positive lookups of explicitly configured service IDs. + * + * @Groups({"lookup", "storage"}) + */ + public function benchSequentialExplicitLookups(): void + { + $storage = new DefinitionStorage($this->definitions); + + foreach ($this->indexes as $index) { + $storage->has("service$index"); + } + } + + /** + * Measures positive autowiring checks for an object graph. + * + * @Groups({"lookup", "storage", "autowire", "typical"}) + * @Revs(100) + */ + public function benchResolvableObjectGraph(): void + { + $storage = new DefinitionStorage([ + EngineInterface::class => EngineMarkOne::class, + ]); + + $storage->has(Car::class); + } + + /** + * Measures negative autowiring checks for a missing dependency. + * + * @Groups({"lookup", "storage", "autowire", "typical"}) + * @Revs(100) + */ + public function benchUnresolvableObjectGraph(): void + { + $storage = new DefinitionStorage(); + + $storage->has(Car::class); + } + + /** + * Measures fallback through a delegate container when a dependency is not explicitly defined. + * + * @Groups({"lookup", "storage", "delegate", "typical"}) + * @Revs(100) + */ + public function benchResolvableObjectGraphWithDelegate(): void + { + $storage = new DefinitionStorage(); + $storage->setDelegateContainer(new SimpleContainer([ + EngineInterface::class => new EngineMarkOne(), + ])); + + $storage->has(Car::class); + } + + /** + * Measures strict mode misses where class names are not implicitly resolvable. + * + * @Groups({"lookup", "storage", "strict"}) + */ + public function benchStrictModeClassMisses(): void + { + $storage = new DefinitionStorage([], true); + + foreach ($this->indexes as $_index) { + $storage->has(EngineMarkOne::class); + } + } +} diff --git a/tests/Benchmark/NormalizerBench.php b/tests/Benchmark/NormalizerBench.php new file mode 100644 index 0000000..4b8961e --- /dev/null +++ b/tests/Benchmark/NormalizerBench.php @@ -0,0 +1,147 @@ +classDefinitions = []; + $this->referenceDefinitions = []; + $this->arrayDefinitions = []; + $this->callableDefinitions = []; + $this->objectDefinitions = []; + + for ($i = 0; $i < self::DEFINITION_COUNT; $i++) { + $this->classDefinitions[] = ColorPink::class; + $this->referenceDefinitions[] = 'engine'; + $this->arrayDefinitions[] = [ + 'class' => Car::class, + '__construct()' => [Reference::to(EngineInterface::class)], + 'setColor()' => [Reference::to(ColorInterface::class)], + ]; + $this->callableDefinitions[] = [CarFactory::class, 'create']; + $this->objectDefinitions[] = new stdClass(); + } + } + + /** + * @Groups({"class"}) + */ + public function benchClassDefinitions(): void + { + foreach ($this->classDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"reference"}) + */ + public function benchReferenceDefinitions(): void + { + foreach ($this->referenceDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"definition", "typical"}) + */ + public function benchArrayDefinitions(): void + { + foreach ($this->arrayDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"factory", "typical"}) + */ + public function benchCallableDefinitions(): void + { + foreach ($this->callableDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"value"}) + */ + public function benchObjectDefinitions(): void + { + foreach ($this->objectDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"definition", "typical"}) + */ + public function benchArrayDefinitionsWithInferredClass(): void + { + foreach ($this->arrayDefinitions as $definition) { + unset($definition['class']); + Normalizer::normalize($definition, Car::class); + } + } + + /** + * @Groups({"class"}) + */ + public function benchSameClassDefinitions(): void + { + foreach ($this->classDefinitions as $_definition) { + Normalizer::normalize(EngineMarkOne::class, EngineMarkOne::class); + } + } +} diff --git a/tests/Benchmark/TypicalUsageBench.php b/tests/Benchmark/TypicalUsageBench.php new file mode 100644 index 0000000..539fddb --- /dev/null +++ b/tests/Benchmark/TypicalUsageBench.php @@ -0,0 +1,122 @@ +container = new SimpleContainer([ + EngineInterface::class => new EngineMarkOne(), + ColorInterface::class => new ColorPink(), + CarFactory::class => new CarFactory(), + ]); + + $this->objectGraphDefinition = ArrayDefinition::fromConfig([ + 'class' => Car::class, + '__construct()' => [Reference::to(EngineInterface::class)], + 'setColor()' => [Reference::to(ColorInterface::class)], + ]); + + $this->methodsAndPropertiesDefinition = ArrayDefinition::fromConfig([ + 'class' => Phone::class, + '__construct()' => [ + 'name' => 'Yii Phone', + 'version' => '3.0', + 'colors' => ['black', 'white'], + ], + '$dev' => true, + '$codeName' => 'Radar', + 'addApp()' => ['Browser', '7'], + 'setId()' => ['42'], + ]); + + $this->staticFactoryDefinition = new CallableDefinition([CarFactory::class, 'create']); + $this->serviceFactoryDefinition = new CallableDefinition([CarFactory::class, 'createWithColor']); + $this->reference = Reference::to(EngineInterface::class); + } + + /** + * Measures explicit array definitions with constructor, setter, and reference resolution. + * + * @Groups({"definition", "lookup", "typical"}) + * @Revs(100) + */ + public function benchArrayDefinitionObjectGraph(): void + { + $this->objectGraphDefinition->resolve($this->container); + } + + /** + * Measures constructor arguments, public property assignment, and method calls. + * + * @Groups({"definition", "lookup", "typical"}) + * @Revs(100) + */ + public function benchArrayDefinitionMethodsAndProperties(): void + { + $this->methodsAndPropertiesDefinition->resolve($this->container); + } + + /** + * Measures a static callable factory with autowired arguments. + * + * @Groups({"factory", "lookup", "typical"}) + * @Revs(100) + */ + public function benchStaticFactoryDefinition(): void + { + $this->staticFactoryDefinition->resolve($this->container); + } + + /** + * Measures a callable factory resolved as a service before invocation. + * + * @Groups({"factory", "lookup", "typical"}) + * @Revs(100) + */ + public function benchServiceFactoryDefinition(): void + { + $this->serviceFactoryDefinition->resolve($this->container); + } + + /** + * Measures direct reference resolution against a PSR container. + * + * @Groups({"reference", "lookup", "typical"}) + */ + public function benchReferenceResolution(): void + { + $this->reference->resolve($this->container); + } +} From dc72ee8b6d4bf2b0a97f66a453b0fa46c8340f24 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:02:21 +0300 Subject: [PATCH 02/31] Optimize string definition normalization --- src/Helpers/Normalizer.php | 32 +++++++++++++++++++++++---- tests/Unit/Helpers/NormalizerTest.php | 18 +++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 841b757..3603cea 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -17,6 +17,7 @@ use function is_callable; use function is_object; use function is_string; +use function str_contains; /** * Normalizer definition from configuration to an instance of {@see DefinitionInterface}. @@ -25,6 +26,11 @@ */ final class Normalizer { + /** + * @var array + */ + private static array $classNames = []; + /** * Normalize definition to an instance of {@see DefinitionInterface}. * Definition may be defined multiple ways: @@ -55,14 +61,32 @@ public static function normalize(mixed $definition, ?string $class = null): Defi if (is_string($definition)) { // Current class - if ( - $class === $definition - || ($class === null && class_exists($definition)) - ) { + if ($class === $definition) { + /** @psalm-var class-string $definition */ + return ArrayDefinition::fromPreparedData($definition); + } + + if ($class === null && isset(self::$classNames[$definition])) { /** @psalm-var class-string $definition */ return ArrayDefinition::fromPreparedData($definition); } + if ($class === null && $definition !== '') { + $firstCharacter = $definition[0]; + if ( + ( + ($firstCharacter >= 'A' && $firstCharacter <= 'Z') + || str_contains($definition, '\\') + ) + ? class_exists($definition) + : class_exists($definition, false) + ) { + self::$classNames[$definition] = true; + /** @psalm-var class-string $definition */ + return ArrayDefinition::fromPreparedData($definition); + } + } + // Reference to another class or alias return Reference::to($definition); } diff --git a/tests/Unit/Helpers/NormalizerTest.php b/tests/Unit/Helpers/NormalizerTest.php index 05b2e24..8177587 100644 --- a/tests/Unit/Helpers/NormalizerTest.php +++ b/tests/Unit/Helpers/NormalizerTest.php @@ -35,6 +35,24 @@ public function testClass(): void $this->assertSame([], $definition->getMethodsAndProperties()); } + public function testReferenceDoesNotTriggerAutoload(): void + { + $autoloadedClasses = []; + $autoload = static function (string $class) use (&$autoloadedClasses): void { + $autoloadedClasses[] = $class; + }; + + spl_autoload_register($autoload); + try { + $definition = Normalizer::normalize('engine'); + } finally { + spl_autoload_unregister($autoload); + } + + $this->assertInstanceOf(Reference::class, $definition); + $this->assertSame([], $autoloadedClasses); + } + public function testArray(): void { /** @var ArrayDefinition $definition */ From ec30e1ddf88a2ea221d20f57c6ae7414c42c0824 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:04:22 +0300 Subject: [PATCH 03/31] Optimize array definition config parsing --- src/ArrayDefinition.php | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index 1cd6994..2164e20 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -16,11 +16,12 @@ use function array_key_exists; use function call_user_func_array; -use function count; use function gettype; use function is_array; use function is_string; use function sprintf; +use function strpos; +use function substr; /** * Builds an object by array config. @@ -184,19 +185,19 @@ private static function getMethodsAndPropertiesFromConfig(array $config): array $methodsAndProperties = []; foreach ($config as $key => $value) { - if ($key === self::CONSTRUCTOR) { + if ($key === self::CLASS_NAME || $key === self::CONSTRUCTOR) { continue; } - /** - * @infection-ignore-all Explode limit does not affect the result. - * - * @see \Yiisoft\Definitions\Tests\Unit\Helpers\DefinitionValidatorTest::testIncorrectMethodName() - */ - if (count($methodArray = explode('()', $key, 2)) === 2) { - $methodsAndProperties[$key] = [self::TYPE_METHOD, $methodArray[0], $value]; - } elseif (count($propertyArray = explode('$', $key)) === 2) { - $methodsAndProperties[$key] = [self::TYPE_PROPERTY, $propertyArray[1], $value]; + $position = strpos($key, '()'); + if ($position !== false) { + $methodsAndProperties[$key] = [self::TYPE_METHOD, substr($key, 0, $position), $value]; + continue; + } + + $position = strpos($key, '$'); + if ($position !== false && strpos($key, '$', $position + 1) === false) { + $methodsAndProperties[$key] = [self::TYPE_PROPERTY, substr($key, $position + 1), $value]; } } From c3473218ef0bb2044b628e3fe24c2893ef119d54 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:07:11 +0300 Subject: [PATCH 04/31] Optimize explicit definition checks --- src/DefinitionStorage.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index ceff210..98c53e6 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -53,6 +53,11 @@ public function setDelegateContainer(ContainerInterface $delegateContainer): voi public function has(string $id): bool { $this->buildStack = []; + + if (isset($this->definitions[$id])) { + return true; + } + return $this->isResolvable($id, []); } From 35905e679808d95f54c3005614757984ab1a28cf Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:15:11 +0300 Subject: [PATCH 05/31] Cache class-only array definitions --- src/ArrayDefinition.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index 2164e20..ad59ec0 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -37,6 +37,11 @@ final class ArrayDefinition implements DefinitionInterface public const TYPE_PROPERTY = 'property'; public const TYPE_METHOD = 'method'; + /** + * @var array + */ + private static array $preparedDataCache = []; + /** * Container used to resolve references. */ @@ -82,6 +87,10 @@ public static function fromConfig(array $config): self */ public static function fromPreparedData(string $class, array $constructorArguments = [], array $methodsAndProperties = []): self { + if ($constructorArguments === [] && $methodsAndProperties === []) { + return self::$preparedDataCache[$class] ??= new self($class, [], []); + } + return new self($class, $constructorArguments, $methodsAndProperties); } From 9fabe3f8ca55528c71748d57722ebd8c169dc7c7 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:16:11 +0300 Subject: [PATCH 06/31] Cache normalized references --- src/Helpers/Normalizer.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 3603cea..599aaab 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -31,6 +31,11 @@ final class Normalizer */ private static array $classNames = []; + /** + * @var array + */ + private static array $references = []; + /** * Normalize definition to an instance of {@see DefinitionInterface}. * Definition may be defined multiple ways: @@ -88,7 +93,7 @@ public static function normalize(mixed $definition, ?string $class = null): Defi } // Reference to another class or alias - return Reference::to($definition); + return self::$references[$definition] ??= Reference::to($definition); } // Callable definition From c307ee1931e29fed03c6304f41afb7012927efa4 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:18:29 +0300 Subject: [PATCH 07/31] Cache callable definition dependencies --- src/CallableDefinition.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/CallableDefinition.php b/src/CallableDefinition.php index ec330a2..5a8b56b 100644 --- a/src/CallableDefinition.php +++ b/src/CallableDefinition.php @@ -29,6 +29,11 @@ final class CallableDefinition implements DefinitionInterface */ private $callable; + /** + * @var array|null + */ + private ?array $dependencies = null; + /** * @param array|callable $callable Callable to be used for building * an object. Dependencies are determined and passed based @@ -44,19 +49,17 @@ public function __construct(array|callable $callable) public function resolve(ContainerInterface $container): mixed { try { - $reflection = new ReflectionFunction( - $this->prepareClosure($this->callable, $container), - ); + $closure = $this->prepareClosure($this->callable, $container); + $this->dependencies ??= DefinitionExtractor::fromFunction(new ReflectionFunction($closure)); } catch (ReflectionException) { throw new NotInstantiableException( 'Can not instantiate callable definition. Got ' . var_export($this->callable, true), ); } - $dependencies = DefinitionExtractor::fromFunction($reflection); - $arguments = DefinitionResolver::resolveArray($container, null, $dependencies); + $arguments = DefinitionResolver::resolveArray($container, null, $this->dependencies); - return $reflection->invokeArgs($arguments); + return $closure(...$arguments); } /** From 00a08b07959e63a45261a3d0bef84b9d5b8120f0 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:20:00 +0300 Subject: [PATCH 08/31] Cache parsed array definition keys --- src/ArrayDefinition.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index ad59ec0..07868e0 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -42,6 +42,11 @@ final class ArrayDefinition implements DefinitionInterface */ private static array $preparedDataCache = []; + /** + * @var array + */ + private static array $configKeyCache = []; + /** * Container used to resolve references. */ @@ -198,16 +203,29 @@ private static function getMethodsAndPropertiesFromConfig(array $config): array continue; } + if (array_key_exists($key, self::$configKeyCache)) { + $item = self::$configKeyCache[$key]; + if ($item !== null) { + $methodsAndProperties[$key] = [$item[0], $item[1], $value]; + } + continue; + } + $position = strpos($key, '()'); if ($position !== false) { $methodsAndProperties[$key] = [self::TYPE_METHOD, substr($key, 0, $position), $value]; + self::$configKeyCache[$key] = [self::TYPE_METHOD, $methodsAndProperties[$key][1]]; continue; } $position = strpos($key, '$'); if ($position !== false && strpos($key, '$', $position + 1) === false) { $methodsAndProperties[$key] = [self::TYPE_PROPERTY, substr($key, $position + 1), $value]; + self::$configKeyCache[$key] = [self::TYPE_PROPERTY, $methodsAndProperties[$key][1]]; + continue; } + + self::$configKeyCache[$key] = null; } return $methodsAndProperties; From 7f09ce4483b4e7d57d0073ba8b3b555379cf7438 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:25:54 +0300 Subject: [PATCH 09/31] Avoid build stack reset for explicit definitions --- src/DefinitionStorage.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index 98c53e6..c601c0d 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -52,12 +52,15 @@ public function setDelegateContainer(ContainerInterface $delegateContainer): voi */ public function has(string $id): bool { - $this->buildStack = []; - if (isset($this->definitions[$id])) { + if ($this->buildStack !== []) { + $this->buildStack = []; + } return true; } + $this->buildStack = []; + return $this->isResolvable($id, []); } From a85649908005e088b350aad7ec615208a0438a98 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:27:51 +0300 Subject: [PATCH 10/31] Fast-path array definition class checks --- src/Helpers/Normalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 599aaab..ed7407d 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -104,7 +104,7 @@ public static function normalize(mixed $definition, ?string $class = null): Defi // Array definition if (is_array($definition)) { $config = $definition; - if (!array_key_exists(ArrayDefinition::CLASS_NAME, $config)) { + if (!isset($config[ArrayDefinition::CLASS_NAME]) && !array_key_exists(ArrayDefinition::CLASS_NAME, $config)) { if ($class === null) { throw new InvalidConfigException( 'Array definition should contain the key "class": ' . var_export($definition, true), From bb55c4ef8a3a432a5625926c9f4726644506e9be Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:29:08 +0300 Subject: [PATCH 11/31] Fast-path cached plain references --- src/Helpers/Normalizer.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index ed7407d..1bfbe1d 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -78,11 +78,15 @@ public static function normalize(mixed $definition, ?string $class = null): Defi if ($class === null && $definition !== '') { $firstCharacter = $definition[0]; + $isClassLike = ($firstCharacter >= 'A' && $firstCharacter <= 'Z') + || str_contains($definition, '\\'); + + if (!$isClassLike && isset(self::$references[$definition])) { + return self::$references[$definition]; + } + if ( - ( - ($firstCharacter >= 'A' && $firstCharacter <= 'Z') - || str_contains($definition, '\\') - ) + $isClassLike ? class_exists($definition) : class_exists($definition, false) ) { From ace9d0bf6623d372f138d4ddf43f7d8578161620 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:32:36 +0300 Subject: [PATCH 12/31] Fast-path array definition normalization --- src/Helpers/Normalizer.php | 41 ++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 1bfbe1d..f8050d0 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -13,6 +13,7 @@ use Yiisoft\Definitions\ValueDefinition; use function array_key_exists; +use function count; use function is_array; use function is_callable; use function is_object; @@ -100,24 +101,42 @@ public static function normalize(mixed $definition, ?string $class = null): Defi return self::$references[$definition] ??= Reference::to($definition); } - // Callable definition - if (is_callable($definition, true)) { + // Callable array definition + if ( + is_array($definition) + && isset($definition[0], $definition[1]) + && count($definition) === 2 + && is_string($definition[1]) + && (is_string($definition[0]) || is_object($definition[0])) + ) { return new CallableDefinition($definition); } // Array definition if (is_array($definition)) { - $config = $definition; - if (!isset($config[ArrayDefinition::CLASS_NAME]) && !array_key_exists(ArrayDefinition::CLASS_NAME, $config)) { - if ($class === null) { - throw new InvalidConfigException( - 'Array definition should contain the key "class": ' . var_export($definition, true), - ); + if ( + isset($definition[ArrayDefinition::CLASS_NAME]) + || $class !== null + || array_key_exists(ArrayDefinition::CLASS_NAME, $definition) + ) { + $config = $definition; + if (!isset($config[ArrayDefinition::CLASS_NAME]) && !array_key_exists(ArrayDefinition::CLASS_NAME, $config)) { + $config[ArrayDefinition::CLASS_NAME] = $class; } - $config[ArrayDefinition::CLASS_NAME] = $class; + /** @psalm-var ArrayDefinitionConfig $config */ + return ArrayDefinition::fromConfig($config); } - /** @psalm-var ArrayDefinitionConfig $config */ - return ArrayDefinition::fromConfig($config); + } + + // Callable definition + if (is_callable($definition, true)) { + return new CallableDefinition($definition); + } + + if (is_array($definition)) { + throw new InvalidConfigException( + 'Array definition should contain the key "class": ' . var_export($definition, true), + ); } // Ready object From ad619f1c9fe9449b3b139f5a068afa51178877ae Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:34:43 +0300 Subject: [PATCH 13/31] Fast-path strict mode misses --- src/DefinitionStorage.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index c601c0d..58f3ddc 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -59,6 +59,11 @@ public function has(string $id): bool return true; } + if ($this->useStrictMode) { + $this->buildStack = [$id => 1]; + return false; + } + $this->buildStack = []; return $this->isResolvable($id, []); From b47843e03b22c0d4681ad453dbe923a2720cfdf8 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:37:19 +0300 Subject: [PATCH 14/31] Cache normalized static callables --- src/Helpers/Normalizer.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index f8050d0..12e71e2 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -37,6 +37,11 @@ final class Normalizer */ private static array $references = []; + /** + * @var array + */ + private static array $callables = []; + /** * Normalize definition to an instance of {@see DefinitionInterface}. * Definition may be defined multiple ways: @@ -109,6 +114,11 @@ public static function normalize(mixed $definition, ?string $class = null): Defi && is_string($definition[1]) && (is_string($definition[0]) || is_object($definition[0])) ) { + if (is_string($definition[0])) { + return self::$callables[$definition[0] . "\0" . $definition[1]] + ??= new CallableDefinition($definition); + } + return new CallableDefinition($definition); } From 3f3af1d8a9579bd8b6ca38db3fcf140cfa62eadd Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:37:56 +0300 Subject: [PATCH 15/31] Avoid internal ArrayDefinition getter calls --- src/ArrayDefinition.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index 07868e0..501f721 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -127,13 +127,13 @@ public function resolve(ContainerInterface $container): object $resolvedConstructorArguments = $this->resolveFunctionArguments( $container, DefinitionExtractor::fromClassName($class), - $this->getConstructorArguments(), + $this->constructorArguments, ); /** @psalm-suppress MixedMethodCall */ $object = new $class(...$resolvedConstructorArguments); - foreach ($this->getMethodsAndProperties() as $item) { + foreach ($this->methodsAndProperties as $item) { [$type, $name, $value] = $item; if ($type === self::TYPE_METHOD) { /** @var array $value */ From a7260e15b33c2487091c19c9c155ee512237abda Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:39:28 +0300 Subject: [PATCH 16/31] Cache normalized object values --- src/Helpers/Normalizer.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 12e71e2..eed3a24 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -4,6 +4,7 @@ namespace Yiisoft\Definitions\Helpers; +use WeakMap; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\CallableDefinition; use Yiisoft\Definitions\Contract\DefinitionInterface; @@ -42,6 +43,11 @@ final class Normalizer */ private static array $callables = []; + /** + * @var WeakMap|null + */ + private static ?WeakMap $values = null; + /** * Normalize definition to an instance of {@see DefinitionInterface}. * Definition may be defined multiple ways: @@ -151,7 +157,8 @@ public static function normalize(mixed $definition, ?string $class = null): Defi // Ready object if (is_object($definition) && !($definition instanceof DefinitionInterface)) { - return new ValueDefinition($definition); + $values = self::$values ??= new WeakMap(); + return $values[$definition] ??= new ValueDefinition($definition); } throw new InvalidConfigException('Invalid definition: ' . var_export($definition, true)); From cbe0a4c0fb1d1f3803a19a626c1f6db6be076841 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:40:25 +0300 Subject: [PATCH 17/31] Cache normalized class definitions --- src/Helpers/Normalizer.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index eed3a24..0080fe9 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -33,6 +33,11 @@ final class Normalizer */ private static array $classNames = []; + /** + * @var array + */ + private static array $classDefinitions = []; + /** * @var array */ @@ -80,12 +85,12 @@ public static function normalize(mixed $definition, ?string $class = null): Defi // Current class if ($class === $definition) { /** @psalm-var class-string $definition */ - return ArrayDefinition::fromPreparedData($definition); + return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); } if ($class === null && isset(self::$classNames[$definition])) { /** @psalm-var class-string $definition */ - return ArrayDefinition::fromPreparedData($definition); + return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); } if ($class === null && $definition !== '') { @@ -104,7 +109,7 @@ public static function normalize(mixed $definition, ?string $class = null): Defi ) { self::$classNames[$definition] = true; /** @psalm-var class-string $definition */ - return ArrayDefinition::fromPreparedData($definition); + return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); } } From cc2999aa42ab00ef9be950e15d710759cee19970 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:41:27 +0300 Subject: [PATCH 18/31] Fast-path cached plain reference strings --- src/Helpers/Normalizer.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 0080fe9..8c9dd3c 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -43,6 +43,11 @@ final class Normalizer */ private static array $references = []; + /** + * @var array + */ + private static array $plainReferences = []; + /** * @var array */ @@ -93,6 +98,10 @@ public static function normalize(mixed $definition, ?string $class = null): Defi return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); } + if ($class === null && isset(self::$plainReferences[$definition])) { + return self::$references[$definition]; + } + if ($class === null && $definition !== '') { $firstCharacter = $definition[0]; $isClassLike = ($firstCharacter >= 'A' && $firstCharacter <= 'Z') @@ -111,6 +120,10 @@ public static function normalize(mixed $definition, ?string $class = null): Defi /** @psalm-var class-string $definition */ return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); } + + if (!$isClassLike) { + self::$plainReferences[$definition] = true; + } } // Reference to another class or alias From 08107ba92665f7c0879e904b3763581c6c073e12 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:47:58 +0300 Subject: [PATCH 19/31] Ensure psalm and full coverage pass --- src/Helpers/DefinitionValidator.php | 2 + src/Helpers/Normalizer.php | 18 +++++- tests/Unit/ArrayDefinitionTest.php | 19 ++++++ tests/Unit/DefinitionStorageTest.php | 11 ++++ .../Unit/Helpers/DefinitionValidatorTest.php | 7 +++ tests/Unit/Helpers/NormalizerTest.php | 58 +++++++++++++++++++ 6 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/Helpers/DefinitionValidator.php b/src/Helpers/DefinitionValidator.php index ea13313..68fd81e 100644 --- a/src/Helpers/DefinitionValidator.php +++ b/src/Helpers/DefinitionValidator.php @@ -346,11 +346,13 @@ private static function isPublicWritableProperty(ReflectionProperty $property): return true; } + // @codeCoverageIgnoreStart $modifiers = $property->getModifiers(); /** * @psalm-suppress UndefinedConstant, MixedOperand Needs for PHP 8.3 or lower */ return ($modifiers & (ReflectionProperty::IS_PRIVATE_SET | ReflectionProperty::IS_PROTECTED_SET)) === 0; + // @codeCoverageIgnoreEnd } } diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 8c9dd3c..598fe32 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -175,8 +175,22 @@ public static function normalize(mixed $definition, ?string $class = null): Defi // Ready object if (is_object($definition) && !($definition instanceof DefinitionInterface)) { - $values = self::$values ??= new WeakMap(); - return $values[$definition] ??= new ValueDefinition($definition); + if (self::$values === null) { + /** @var WeakMap $values */ + $values = new WeakMap(); + self::$values = $values; + } else { + $values = self::$values; + } + + $value = $values[$definition] ?? null; + if ($value instanceof ValueDefinition) { + return $value; + } + + $value = new ValueDefinition($definition); + $values[$definition] = $value; + return $value; } throw new InvalidConfigException('Invalid definition: ' . var_export($definition, true)); diff --git a/tests/Unit/ArrayDefinitionTest.php b/tests/Unit/ArrayDefinitionTest.php index 539f811..bb67adf 100644 --- a/tests/Unit/ArrayDefinitionTest.php +++ b/tests/Unit/ArrayDefinitionTest.php @@ -36,6 +36,25 @@ public function testClass(): void self::assertInstanceOf(Phone::class, $definition->resolve($container)); } + public function testPreparedDataWithArguments(): void + { + $definition = ArrayDefinition::fromPreparedData(Phone::class, ['Yii Phone']); + + self::assertSame(Phone::class, $definition->getClass()); + self::assertSame(['Yii Phone'], $definition->getConstructorArguments()); + self::assertSame([], $definition->getMethodsAndProperties()); + } + + public function testConfigWithIgnoredKey(): void + { + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Phone::class, + 'ignored' => true, + ]); + + self::assertSame([], $definition->getMethodsAndProperties()); + } + public static function dataConstructor(): array { return [ diff --git a/tests/Unit/DefinitionStorageTest.php b/tests/Unit/DefinitionStorageTest.php index e9a7dc1..11ad504 100644 --- a/tests/Unit/DefinitionStorageTest.php +++ b/tests/Unit/DefinitionStorageTest.php @@ -79,6 +79,17 @@ public function testExplicitDefinitionIsNotChecked(): void $this->assertSame([], $storage->getBuildStack()); } + public function testExplicitDefinitionClearsPreviousBuildStack(): void + { + $storage = new DefinitionStorage(['existing' => 'anything']); + + $this->assertFalse($storage->has(NonExisitng::class)); + $this->assertNotSame([], $storage->getBuildStack()); + + $this->assertTrue($storage->has('existing')); + $this->assertSame([], $storage->getBuildStack()); + } + public function testNonExistingService(): void { $storage = new DefinitionStorage([]); diff --git a/tests/Unit/Helpers/DefinitionValidatorTest.php b/tests/Unit/Helpers/DefinitionValidatorTest.php index bae7e3e..14fbcc3 100644 --- a/tests/Unit/Helpers/DefinitionValidatorTest.php +++ b/tests/Unit/Helpers/DefinitionValidatorTest.php @@ -38,6 +38,13 @@ public function testIntegerKeyOfArray(): void ]); } + public function testCallable(): void + { + DefinitionValidator::validate([CarFactory::class, 'create']); + + $this->expectNotToPerformAssertions(); + } + public static function dataInvalidClass(): array { return [ diff --git a/tests/Unit/Helpers/NormalizerTest.php b/tests/Unit/Helpers/NormalizerTest.php index 8177587..391a5aa 100644 --- a/tests/Unit/Helpers/NormalizerTest.php +++ b/tests/Unit/Helpers/NormalizerTest.php @@ -7,9 +7,11 @@ use PHPUnit\Framework\TestCase; use stdClass; use Yiisoft\Definitions\ArrayDefinition; +use Yiisoft\Definitions\CallableDefinition; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Definitions\Helpers\Normalizer; use Yiisoft\Definitions\Reference; +use Yiisoft\Definitions\Tests\Support\CarFactory; use Yiisoft\Definitions\Tests\Support\ColorPink; use Yiisoft\Definitions\Tests\Support\GearBox; use Yiisoft\Definitions\ValueDefinition; @@ -35,6 +37,26 @@ public function testClass(): void $this->assertSame([], $definition->getMethodsAndProperties()); } + public function testSameClass(): void + { + /** @var ArrayDefinition $definition */ + $definition = Normalizer::normalize(ColorPink::class, ColorPink::class); + + $this->assertInstanceOf(ArrayDefinition::class, $definition); + $this->assertSame(ColorPink::class, $definition->getClass()); + } + + public function testCachedClass(): void + { + Normalizer::normalize(GearBox::class); + + /** @var ArrayDefinition $definition */ + $definition = Normalizer::normalize(GearBox::class); + + $this->assertInstanceOf(ArrayDefinition::class, $definition); + $this->assertSame(GearBox::class, $definition->getClass()); + } + public function testReferenceDoesNotTriggerAutoload(): void { $autoloadedClasses = []; @@ -53,6 +75,20 @@ public function testReferenceDoesNotTriggerAutoload(): void $this->assertSame([], $autoloadedClasses); } + public function testCachedReference(): void + { + Normalizer::normalize('engine'); + + $this->assertInstanceOf(Reference::class, Normalizer::normalize('engine')); + } + + public function testCachedReferenceWithoutPlainReferenceFastPath(): void + { + Normalizer::normalize('engine-with-class', GearBox::class); + + $this->assertInstanceOf(Reference::class, Normalizer::normalize('engine-with-class')); + } + public function testArray(): void { /** @var ArrayDefinition $definition */ @@ -69,6 +105,20 @@ public function testArray(): void $this->assertSame([], $definition->getMethodsAndProperties()); } + public function testStaticCallableArray(): void + { + $definition = Normalizer::normalize([CarFactory::class, 'create']); + + $this->assertInstanceOf(CallableDefinition::class, $definition); + } + + public function testObjectCallableArray(): void + { + $definition = Normalizer::normalize([new CarFactory(), 'createWithColor']); + + $this->assertInstanceOf(CallableDefinition::class, $definition); + } + public function testReadyObject(): void { $container = new SimpleContainer(); @@ -82,6 +132,14 @@ public function testReadyObject(): void $this->assertSame($object, $definition->resolve($container)); } + public function testCachedReadyObject(): void + { + $object = new stdClass(); + $definition = Normalizer::normalize($object); + + $this->assertSame($definition, Normalizer::normalize($object)); + } + public function testInteger(): void { $this->expectException(InvalidConfigException::class); From 01f60ab22729b5acc47883326ed9a7af17f844ca Mon Sep 17 00:00:00 2001 From: vjik <525501+vjik@users.noreply.github.com> Date: Fri, 24 Apr 2026 05:32:58 +0000 Subject: [PATCH 20/31] Apply PHP CS Fixer and Rector changes (CI) --- src/ArrayDefinition.php | 2 +- tests/Benchmark/NormalizerBench.php | 2 +- tests/Benchmark/TypicalUsageBench.php | 2 +- tests/Unit/Helpers/DefinitionValidatorTest.php | 2 +- tests/Unit/Helpers/NormalizerTest.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index 501f721..8615562 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -219,7 +219,7 @@ private static function getMethodsAndPropertiesFromConfig(array $config): array } $position = strpos($key, '$'); - if ($position !== false && strpos($key, '$', $position + 1) === false) { + if ($position !== false && !str_contains(substr($key, $position + 1), '$')) { $methodsAndProperties[$key] = [self::TYPE_PROPERTY, substr($key, $position + 1), $value]; self::$configKeyCache[$key] = [self::TYPE_PROPERTY, $methodsAndProperties[$key][1]]; continue; diff --git a/tests/Benchmark/NormalizerBench.php b/tests/Benchmark/NormalizerBench.php index 4b8961e..b1ef1c2 100644 --- a/tests/Benchmark/NormalizerBench.php +++ b/tests/Benchmark/NormalizerBench.php @@ -69,7 +69,7 @@ public function before(): void '__construct()' => [Reference::to(EngineInterface::class)], 'setColor()' => [Reference::to(ColorInterface::class)], ]; - $this->callableDefinitions[] = [CarFactory::class, 'create']; + $this->callableDefinitions[] = CarFactory::create(...); $this->objectDefinitions[] = new stdClass(); } } diff --git a/tests/Benchmark/TypicalUsageBench.php b/tests/Benchmark/TypicalUsageBench.php index 539fddb..3eeeed6 100644 --- a/tests/Benchmark/TypicalUsageBench.php +++ b/tests/Benchmark/TypicalUsageBench.php @@ -61,7 +61,7 @@ public function before(): void 'setId()' => ['42'], ]); - $this->staticFactoryDefinition = new CallableDefinition([CarFactory::class, 'create']); + $this->staticFactoryDefinition = new CallableDefinition(CarFactory::create(...)); $this->serviceFactoryDefinition = new CallableDefinition([CarFactory::class, 'createWithColor']); $this->reference = Reference::to(EngineInterface::class); } diff --git a/tests/Unit/Helpers/DefinitionValidatorTest.php b/tests/Unit/Helpers/DefinitionValidatorTest.php index 14fbcc3..0dd0135 100644 --- a/tests/Unit/Helpers/DefinitionValidatorTest.php +++ b/tests/Unit/Helpers/DefinitionValidatorTest.php @@ -40,7 +40,7 @@ public function testIntegerKeyOfArray(): void public function testCallable(): void { - DefinitionValidator::validate([CarFactory::class, 'create']); + DefinitionValidator::validate(CarFactory::create(...)); $this->expectNotToPerformAssertions(); } diff --git a/tests/Unit/Helpers/NormalizerTest.php b/tests/Unit/Helpers/NormalizerTest.php index 391a5aa..63d9300 100644 --- a/tests/Unit/Helpers/NormalizerTest.php +++ b/tests/Unit/Helpers/NormalizerTest.php @@ -107,7 +107,7 @@ public function testArray(): void public function testStaticCallableArray(): void { - $definition = Normalizer::normalize([CarFactory::class, 'create']); + $definition = Normalizer::normalize(CarFactory::create(...)); $this->assertInstanceOf(CallableDefinition::class, $definition); } From 5b20f85ab68202d789173f7d9369ebb38165b51a Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 21:16:52 +0300 Subject: [PATCH 21/31] Address comments --- src/ArrayDefinition.php | 29 +--------------- src/Helpers/DefinitionValidator.php | 2 -- src/Helpers/Normalizer.php | 49 ++++++++++----------------- tests/Unit/Helpers/NormalizerTest.php | 30 +++++++++++++++- 4 files changed, 47 insertions(+), 63 deletions(-) diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index 8615562..33d6ae6 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -37,16 +37,6 @@ final class ArrayDefinition implements DefinitionInterface public const TYPE_PROPERTY = 'property'; public const TYPE_METHOD = 'method'; - /** - * @var array - */ - private static array $preparedDataCache = []; - - /** - * @var array - */ - private static array $configKeyCache = []; - /** * Container used to resolve references. */ @@ -92,10 +82,6 @@ public static function fromConfig(array $config): self */ public static function fromPreparedData(string $class, array $constructorArguments = [], array $methodsAndProperties = []): self { - if ($constructorArguments === [] && $methodsAndProperties === []) { - return self::$preparedDataCache[$class] ??= new self($class, [], []); - } - return new self($class, $constructorArguments, $methodsAndProperties); } @@ -199,33 +185,20 @@ private static function getMethodsAndPropertiesFromConfig(array $config): array $methodsAndProperties = []; foreach ($config as $key => $value) { - if ($key === self::CLASS_NAME || $key === self::CONSTRUCTOR) { - continue; - } - - if (array_key_exists($key, self::$configKeyCache)) { - $item = self::$configKeyCache[$key]; - if ($item !== null) { - $methodsAndProperties[$key] = [$item[0], $item[1], $value]; - } + if ($key === self::CONSTRUCTOR || $key === self::CLASS_NAME) { continue; } $position = strpos($key, '()'); if ($position !== false) { $methodsAndProperties[$key] = [self::TYPE_METHOD, substr($key, 0, $position), $value]; - self::$configKeyCache[$key] = [self::TYPE_METHOD, $methodsAndProperties[$key][1]]; continue; } $position = strpos($key, '$'); if ($position !== false && !str_contains(substr($key, $position + 1), '$')) { $methodsAndProperties[$key] = [self::TYPE_PROPERTY, substr($key, $position + 1), $value]; - self::$configKeyCache[$key] = [self::TYPE_PROPERTY, $methodsAndProperties[$key][1]]; - continue; } - - self::$configKeyCache[$key] = null; } return $methodsAndProperties; diff --git a/src/Helpers/DefinitionValidator.php b/src/Helpers/DefinitionValidator.php index 68fd81e..ea13313 100644 --- a/src/Helpers/DefinitionValidator.php +++ b/src/Helpers/DefinitionValidator.php @@ -346,13 +346,11 @@ private static function isPublicWritableProperty(ReflectionProperty $property): return true; } - // @codeCoverageIgnoreStart $modifiers = $property->getModifiers(); /** * @psalm-suppress UndefinedConstant, MixedOperand Needs for PHP 8.3 or lower */ return ($modifiers & (ReflectionProperty::IS_PRIVATE_SET | ReflectionProperty::IS_PROTECTED_SET)) === 0; - // @codeCoverageIgnoreEnd } } diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 598fe32..33f92a4 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -19,7 +19,6 @@ use function is_callable; use function is_object; use function is_string; -use function str_contains; /** * Normalizer definition from configuration to an instance of {@see DefinitionInterface}. @@ -28,11 +27,6 @@ */ final class Normalizer { - /** - * @var array - */ - private static array $classNames = []; - /** * @var array */ @@ -43,11 +37,6 @@ final class Normalizer */ private static array $references = []; - /** - * @var array - */ - private static array $plainReferences = []; - /** * @var array */ @@ -58,6 +47,19 @@ final class Normalizer */ private static ?WeakMap $values = null; + /** + * Clear internal normalization caches. + * + * This is useful for long-running processes that normalize dynamically generated service IDs. + */ + public static function clearCache(): void + { + self::$classDefinitions = []; + self::$references = []; + self::$callables = []; + self::$values = null; + } + /** * Normalize definition to an instance of {@see DefinitionInterface}. * Definition may be defined multiple ways: @@ -93,37 +95,20 @@ public static function normalize(mixed $definition, ?string $class = null): Defi return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); } - if ($class === null && isset(self::$classNames[$definition])) { + if ($class === null && isset(self::$classDefinitions[$definition])) { /** @psalm-var class-string $definition */ - return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); + return self::$classDefinitions[$definition]; } - if ($class === null && isset(self::$plainReferences[$definition])) { + if ($class === null && isset(self::$references[$definition])) { return self::$references[$definition]; } if ($class === null && $definition !== '') { - $firstCharacter = $definition[0]; - $isClassLike = ($firstCharacter >= 'A' && $firstCharacter <= 'Z') - || str_contains($definition, '\\'); - - if (!$isClassLike && isset(self::$references[$definition])) { - return self::$references[$definition]; - } - - if ( - $isClassLike - ? class_exists($definition) - : class_exists($definition, false) - ) { - self::$classNames[$definition] = true; + if (class_exists($definition)) { /** @psalm-var class-string $definition */ return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); } - - if (!$isClassLike) { - self::$plainReferences[$definition] = true; - } } // Reference to another class or alias diff --git a/tests/Unit/Helpers/NormalizerTest.php b/tests/Unit/Helpers/NormalizerTest.php index 63d9300..7073323 100644 --- a/tests/Unit/Helpers/NormalizerTest.php +++ b/tests/Unit/Helpers/NormalizerTest.php @@ -19,6 +19,11 @@ final class NormalizerTest extends TestCase { + protected function setUp(): void + { + Normalizer::clearCache(); + } + public function testReference(): void { $reference = Reference::to('test'); @@ -57,8 +62,31 @@ public function testCachedClass(): void $this->assertSame(GearBox::class, $definition->getClass()); } - public function testReferenceDoesNotTriggerAutoload(): void + public function testLowercaseClass(): void { + $class = 'lowercaseautoloadeddefinitiontest' . str_replace('.', '', uniqid('', true)); + $autoload = static function (string $autoloadedClass) use ($class): void { + if ($autoloadedClass === $class) { + eval('class ' . $class . ' {}'); + } + }; + + spl_autoload_register($autoload); + try { + /** @var ArrayDefinition $definition */ + $definition = Normalizer::normalize($class); + } finally { + spl_autoload_unregister($autoload); + } + + $this->assertInstanceOf(ArrayDefinition::class, $definition); + $this->assertSame($class, $definition->getClass()); + } + + public function testCachedReferenceDoesNotTriggerAutoload(): void + { + Normalizer::normalize('engine'); + $autoloadedClasses = []; $autoload = static function (string $class) use (&$autoloadedClasses): void { $autoloadedClasses[] = $class; From c0508a6aa2408dec7b7dddc25843cc415ed14352 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 23:09:17 +0300 Subject: [PATCH 22/31] Fix performance regression --- src/ArrayDefinition.php | 10 +++++++++- src/Helpers/Normalizer.php | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index 33d6ae6..5d9ee8c 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -20,6 +20,7 @@ use function is_array; use function is_string; use function sprintf; +use function str_contains; use function strpos; use function substr; @@ -42,6 +43,11 @@ final class ArrayDefinition implements DefinitionInterface */ private ?ContainerInterface $referenceContainer = null; + /** + * @var array> + */ + private array $methodDependencies = []; + /** * @psalm-param class-string $class * @psalm-param array $methodsAndProperties @@ -126,7 +132,8 @@ public function resolve(ContainerInterface $container): object if (method_exists($object, $name)) { $resolvedMethodArguments = $this->resolveFunctionArguments( $container, - DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)), + $this->methodDependencies[$name] + ??= DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)), $value, ); } else { @@ -171,6 +178,7 @@ public function merge(self $other): self } } $new->methodsAndProperties = $methodsAndProperties; + $new->methodDependencies = []; return $new; } diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 33f92a4..78bcabc 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -4,6 +4,7 @@ namespace Yiisoft\Definitions\Helpers; +use Closure; use WeakMap; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\CallableDefinition; @@ -115,6 +116,10 @@ public static function normalize(mixed $definition, ?string $class = null): Defi return self::$references[$definition] ??= Reference::to($definition); } + if ($definition instanceof Closure) { + return new CallableDefinition($definition); + } + // Callable array definition if ( is_array($definition) From c827cd6ef68951aa298a9fa3b4676f15df8ea498 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 23:21:12 +0300 Subject: [PATCH 23/31] Fix test name --- tests/Unit/Helpers/NormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Helpers/NormalizerTest.php b/tests/Unit/Helpers/NormalizerTest.php index 7073323..ca2684f 100644 --- a/tests/Unit/Helpers/NormalizerTest.php +++ b/tests/Unit/Helpers/NormalizerTest.php @@ -133,7 +133,7 @@ public function testArray(): void $this->assertSame([], $definition->getMethodsAndProperties()); } - public function testStaticCallableArray(): void + public function testFirstClassCallable(): void { $definition = Normalizer::normalize(CarFactory::create(...)); From a8c1f46494b5ae8dc8a5c7a616e3b9627fd2f2df Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 23:23:48 +0300 Subject: [PATCH 24/31] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5422afe..9d1a7f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Enh #112: Explicitly import constants in "use" section (@mspirkov) - Enh #113: Remove unnecessary files from Composer package (@mspirkov) +- Enh #114: Performance optimizations (@samdark) ## 3.4.1 December 02, 2025 From 43113813339d5275bbcbd3c80acfed22dfc6edcd Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Mon, 25 May 2026 23:27:13 +0300 Subject: [PATCH 25/31] Fixes --- src/ArrayDefinition.php | 6 ++---- src/DefinitionStorage.php | 4 +--- src/Helpers/Normalizer.php | 11 ++++++++--- tests/Unit/ArrayDefinitionTest.php | 10 ++++++++++ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index 5d9ee8c..a6341e3 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -20,7 +20,6 @@ use function is_array; use function is_string; use function sprintf; -use function str_contains; use function strpos; use function substr; @@ -203,9 +202,8 @@ private static function getMethodsAndPropertiesFromConfig(array $config): array continue; } - $position = strpos($key, '$'); - if ($position !== false && !str_contains(substr($key, $position + 1), '$')) { - $methodsAndProperties[$key] = [self::TYPE_PROPERTY, substr($key, $position + 1), $value]; + if (($key[0] ?? '') === '$') { + $methodsAndProperties[$key] = [self::TYPE_PROPERTY, substr($key, 1), $value]; } } diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index 58f3ddc..caa7799 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -53,9 +53,7 @@ public function setDelegateContainer(ContainerInterface $delegateContainer): voi public function has(string $id): bool { if (isset($this->definitions[$id])) { - if ($this->buildStack !== []) { - $this->buildStack = []; - } + $this->buildStack = []; return true; } diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 78bcabc..04115da 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -96,16 +96,21 @@ public static function normalize(mixed $definition, ?string $class = null): Defi return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); } - if ($class === null && isset(self::$classDefinitions[$definition])) { + if ($class !== null) { + // Reference to another class or alias. + return self::$references[$definition] ??= Reference::to($definition); + } + + if (isset(self::$classDefinitions[$definition])) { /** @psalm-var class-string $definition */ return self::$classDefinitions[$definition]; } - if ($class === null && isset(self::$references[$definition])) { + if (isset(self::$references[$definition])) { return self::$references[$definition]; } - if ($class === null && $definition !== '') { + if ($definition !== '') { if (class_exists($definition)) { /** @psalm-var class-string $definition */ return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); diff --git a/tests/Unit/ArrayDefinitionTest.php b/tests/Unit/ArrayDefinitionTest.php index bb67adf..ce797b8 100644 --- a/tests/Unit/ArrayDefinitionTest.php +++ b/tests/Unit/ArrayDefinitionTest.php @@ -55,6 +55,16 @@ public function testConfigWithIgnoredKey(): void self::assertSame([], $definition->getMethodsAndProperties()); } + public function testConfigWithDollarInMiddleIsIgnored(): void + { + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Phone::class, + 'ignored$dev' => true, + ]); + + self::assertSame([], $definition->getMethodsAndProperties()); + } + public static function dataConstructor(): array { return [ From 95f18eb6a88407fc16298843525f0bf589ad18e2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 26 May 2026 02:13:07 +0300 Subject: [PATCH 26/31] Fixes --- src/ArrayDefinition.php | 2 +- src/CallableDefinition.php | 58 ++++++++++++++++++++++----- tests/Unit/ArrayDefinitionTest.php | 19 +++++++++ tests/Unit/CallableDefinitionTest.php | 43 ++++++++++++++++++++ 4 files changed, 112 insertions(+), 10 deletions(-) diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index a6341e3..6ae80e3 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -197,7 +197,7 @@ private static function getMethodsAndPropertiesFromConfig(array $config): array } $position = strpos($key, '()'); - if ($position !== false) { + if ($position !== false && $position !== 0 && strpos($key, '()', $position + 2) === false) { $methodsAndProperties[$key] = [self::TYPE_METHOD, substr($key, 0, $position), $value]; continue; } diff --git a/src/CallableDefinition.php b/src/CallableDefinition.php index 5a8b56b..862e4d8 100644 --- a/src/CallableDefinition.php +++ b/src/CallableDefinition.php @@ -8,12 +8,14 @@ use Psr\Container\ContainerInterface; use ReflectionException; use ReflectionFunction; +use ReflectionFunctionAbstract; use ReflectionMethod; use Yiisoft\Definitions\Contract\DefinitionInterface; use Yiisoft\Definitions\Exception\NotInstantiableException; use Yiisoft\Definitions\Helpers\DefinitionExtractor; use Yiisoft\Definitions\Helpers\DefinitionResolver; +use function get_class; use function is_array; use function is_object; @@ -29,10 +31,15 @@ final class CallableDefinition implements DefinitionInterface */ private $callable; + /** + * @var array> + */ + private array $dependencies = []; + /** * @var array|null */ - private ?array $dependencies = null; + private ?array $callableDependencies = null; /** * @param array|callable $callable Callable to be used for building @@ -49,31 +56,64 @@ public function __construct(array|callable $callable) public function resolve(ContainerInterface $container): mixed { try { - $closure = $this->prepareClosure($this->callable, $container); - $this->dependencies ??= DefinitionExtractor::fromFunction(new ReflectionFunction($closure)); + [$closure, $dependencies] = $this->prepare($this->callable, $container); } catch (ReflectionException) { throw new NotInstantiableException( 'Can not instantiate callable definition. Got ' . var_export($this->callable, true), ); } - $arguments = DefinitionResolver::resolveArray($container, null, $this->dependencies); + $arguments = DefinitionResolver::resolveArray($container, null, $dependencies); return $closure(...$arguments); } /** * @psalm-param callable|array{0:class-string,1:string} $callable + * + * @psalm-return array{Closure,array} */ - private function prepareClosure(array|callable $callable, ContainerInterface $container): Closure + private function prepare(array|callable $callable, ContainerInterface $container): array { - if (is_array($callable) && !is_object($callable[0])) { + if (is_array($callable)) { $reflection = new ReflectionMethod($callable[0], $callable[1]); - if (!$reflection->isStatic()) { - $callable[0] = $container->get($callable[0]); + if (!is_object($callable[0]) && !$reflection->isStatic()) { + $class = $callable[0]; + /** @var object $object */ + $object = $container->get($callable[0]); + $callable[0] = $object; + if (get_class($object) !== $class) { + $reflection = new ReflectionMethod($object, $callable[1]); + } } + + return [ + Closure::fromCallable($callable), + $this->getDependencies($this->getArrayCallableDependenciesKey($callable), $reflection), + ]; } - return Closure::fromCallable($callable); + $closure = Closure::fromCallable($callable); + + return [ + $closure, + $this->callableDependencies ??= DefinitionExtractor::fromFunction(new ReflectionFunction($closure)), + ]; + } + + /** + * @param array{0:class-string|object,1:string} $callable + */ + private function getArrayCallableDependenciesKey(array $callable): string + { + return (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]) . "\0" . $callable[1]; + } + + /** + * @return array + */ + private function getDependencies(string $key, ReflectionFunctionAbstract $reflection): array + { + return $this->dependencies[$key] ??= DefinitionExtractor::fromFunction($reflection); } } diff --git a/tests/Unit/ArrayDefinitionTest.php b/tests/Unit/ArrayDefinitionTest.php index ce797b8..b109009 100644 --- a/tests/Unit/ArrayDefinitionTest.php +++ b/tests/Unit/ArrayDefinitionTest.php @@ -65,6 +65,25 @@ public function testConfigWithDollarInMiddleIsIgnored(): void self::assertSame([], $definition->getMethodsAndProperties()); } + public static function dataConfigWithInvalidMethodKey(): array + { + return [ + ['addApp()hm()'], + ['()addApp'], + ]; + } + + #[DataProvider('dataConfigWithInvalidMethodKey')] + public function testConfigWithInvalidMethodKeyIsIgnored(string $key): void + { + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Phone::class, + $key => ['Browser'], + ]); + + self::assertSame([], $definition->getMethodsAndProperties()); + } + public static function dataConstructor(): array { return [ diff --git a/tests/Unit/CallableDefinitionTest.php b/tests/Unit/CallableDefinitionTest.php index 42fc65d..49e4b8c 100644 --- a/tests/Unit/CallableDefinitionTest.php +++ b/tests/Unit/CallableDefinitionTest.php @@ -11,6 +11,9 @@ use Yiisoft\Definitions\Tests\Support\CarFactory; use Yiisoft\Definitions\Tests\Support\ColorInterface; use Yiisoft\Definitions\Tests\Support\ColorPink; +use Yiisoft\Definitions\Tests\Support\EngineInterface; +use Yiisoft\Definitions\Tests\Support\EngineMarkOne; +use Yiisoft\Definitions\Tests\Support\EngineMarkTwo; use Yiisoft\Test\Support\Container\SimpleContainer; final class CallableDefinitionTest extends TestCase @@ -33,6 +36,30 @@ public function testDynamicCallable(): void $this->assertInstanceOf(ColorPink::class, $car->getColor()); } + public function testDynamicCallableDependenciesAreCachedPerResolvedObject(): void + { + $definition = new CallableDefinition([EngineAwareCallableFactory::class, 'create']); + + /** @var Car $car */ + $car = $definition->resolve( + new SimpleContainer([ + EngineAwareCallableFactory::class => new EngineAwareCallableFactory(), + EngineInterface::class => new EngineMarkOne(), + ]), + ); + + $this->assertInstanceOf(EngineMarkOne::class, $car->getEngine()); + + /** @var Car $car */ + $car = $definition->resolve( + new SimpleContainer([ + EngineAwareCallableFactory::class => new EngineLessCallableFactory(), + ]), + ); + + $this->assertInstanceOf(EngineMarkTwo::class, $car->getEngine()); + } + public function testNonExistsClass(): void { $definition = new CallableDefinition(['NonExistsClass', 'run']); @@ -42,3 +69,19 @@ public function testNonExistsClass(): void $definition->resolve(new SimpleContainer()); } } + +final class EngineAwareCallableFactory +{ + public function create(EngineInterface $engine): Car + { + return new Car($engine); + } +} + +final class EngineLessCallableFactory +{ + public function create(): Car + { + return new Car(new EngineMarkTwo()); + } +} From 1583704845d80fa82cea8306016546f9bccf3212 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 26 May 2026 02:29:09 +0300 Subject: [PATCH 27/31] Fix edge case --- src/Helpers/Normalizer.php | 4 ++-- tests/Unit/Helpers/NormalizerTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 04115da..3a67699 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -98,7 +98,7 @@ public static function normalize(mixed $definition, ?string $class = null): Defi if ($class !== null) { // Reference to another class or alias. - return self::$references[$definition] ??= Reference::to($definition); + return Reference::to($definition); } if (isset(self::$classDefinitions[$definition])) { @@ -118,7 +118,7 @@ public static function normalize(mixed $definition, ?string $class = null): Defi } // Reference to another class or alias - return self::$references[$definition] ??= Reference::to($definition); + return self::$references[$definition] = Reference::to($definition); } if ($definition instanceof Closure) { diff --git a/tests/Unit/Helpers/NormalizerTest.php b/tests/Unit/Helpers/NormalizerTest.php index ca2684f..d736ee1 100644 --- a/tests/Unit/Helpers/NormalizerTest.php +++ b/tests/Unit/Helpers/NormalizerTest.php @@ -117,6 +117,30 @@ public function testCachedReferenceWithoutPlainReferenceFastPath(): void $this->assertInstanceOf(Reference::class, Normalizer::normalize('engine-with-class')); } + public function testReferenceWithClassDoesNotPreventLaterClassDetection(): void + { + $class = 'contextreferencethenautoloadeddefinitiontest' . str_replace('.', '', uniqid('', true)); + + $this->assertInstanceOf(Reference::class, Normalizer::normalize($class, GearBox::class)); + + $autoload = static function (string $autoloadedClass) use ($class): void { + if ($autoloadedClass === $class) { + eval('class ' . $class . ' {}'); + } + }; + + spl_autoload_register($autoload); + try { + /** @var ArrayDefinition $definition */ + $definition = Normalizer::normalize($class); + } finally { + spl_autoload_unregister($autoload); + } + + $this->assertInstanceOf(ArrayDefinition::class, $definition); + $this->assertSame($class, $definition->getClass()); + } + public function testArray(): void { /** @var ArrayDefinition $definition */ From 4f9758aad14feeeb4d6f5e69c7449a41bab2fe75 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 26 May 2026 02:47:34 +0300 Subject: [PATCH 28/31] Adjust benchmarks to be closer to reality. Remove unneeded optimizations --- src/Helpers/Normalizer.php | 133 +++------------- tests/Benchmark/DefinitionStorageBench.php | 76 ++++++++- tests/Benchmark/NormalizerBench.php | 176 ++++++++++++++++++--- tests/Benchmark/TypicalUsageBench.php | 49 ++++++ tests/Unit/Helpers/NormalizerTest.php | 39 +---- 5 files changed, 296 insertions(+), 177 deletions(-) diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 3a67699..705b958 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -4,8 +4,6 @@ namespace Yiisoft\Definitions\Helpers; -use Closure; -use WeakMap; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\CallableDefinition; use Yiisoft\Definitions\Contract\DefinitionInterface; @@ -15,7 +13,6 @@ use Yiisoft\Definitions\ValueDefinition; use function array_key_exists; -use function count; use function is_array; use function is_callable; use function is_object; @@ -28,39 +25,6 @@ */ final class Normalizer { - /** - * @var array - */ - private static array $classDefinitions = []; - - /** - * @var array - */ - private static array $references = []; - - /** - * @var array - */ - private static array $callables = []; - - /** - * @var WeakMap|null - */ - private static ?WeakMap $values = null; - - /** - * Clear internal normalization caches. - * - * This is useful for long-running processes that normalize dynamically generated service IDs. - */ - public static function clearCache(): void - { - self::$classDefinitions = []; - self::$references = []; - self::$callables = []; - self::$values = null; - } - /** * Normalize definition to an instance of {@see DefinitionInterface}. * Definition may be defined multiple ways: @@ -91,70 +55,16 @@ public static function normalize(mixed $definition, ?string $class = null): Defi if (is_string($definition)) { // Current class - if ($class === $definition) { - /** @psalm-var class-string $definition */ - return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); - } - - if ($class !== null) { - // Reference to another class or alias. - return Reference::to($definition); - } - - if (isset(self::$classDefinitions[$definition])) { + if ( + $class === $definition + || ($class === null && class_exists($definition)) + ) { /** @psalm-var class-string $definition */ - return self::$classDefinitions[$definition]; - } - - if (isset(self::$references[$definition])) { - return self::$references[$definition]; - } - - if ($definition !== '') { - if (class_exists($definition)) { - /** @psalm-var class-string $definition */ - return self::$classDefinitions[$definition] ??= ArrayDefinition::fromPreparedData($definition); - } + return ArrayDefinition::fromPreparedData($definition); } // Reference to another class or alias - return self::$references[$definition] = Reference::to($definition); - } - - if ($definition instanceof Closure) { - return new CallableDefinition($definition); - } - - // Callable array definition - if ( - is_array($definition) - && isset($definition[0], $definition[1]) - && count($definition) === 2 - && is_string($definition[1]) - && (is_string($definition[0]) || is_object($definition[0])) - ) { - if (is_string($definition[0])) { - return self::$callables[$definition[0] . "\0" . $definition[1]] - ??= new CallableDefinition($definition); - } - - return new CallableDefinition($definition); - } - - // Array definition - if (is_array($definition)) { - if ( - isset($definition[ArrayDefinition::CLASS_NAME]) - || $class !== null - || array_key_exists(ArrayDefinition::CLASS_NAME, $definition) - ) { - $config = $definition; - if (!isset($config[ArrayDefinition::CLASS_NAME]) && !array_key_exists(ArrayDefinition::CLASS_NAME, $config)) { - $config[ArrayDefinition::CLASS_NAME] = $class; - } - /** @psalm-var ArrayDefinitionConfig $config */ - return ArrayDefinition::fromConfig($config); - } + return Reference::to($definition); } // Callable definition @@ -163,29 +73,22 @@ public static function normalize(mixed $definition, ?string $class = null): Defi } if (is_array($definition)) { - throw new InvalidConfigException( - 'Array definition should contain the key "class": ' . var_export($definition, true), - ); + $config = $definition; + if (!array_key_exists(ArrayDefinition::CLASS_NAME, $config)) { + if ($class === null) { + throw new InvalidConfigException( + 'Array definition should contain the key "class": ' . var_export($definition, true), + ); + } + $config[ArrayDefinition::CLASS_NAME] = $class; + } + /** @psalm-var ArrayDefinitionConfig $config */ + return ArrayDefinition::fromConfig($config); } // Ready object if (is_object($definition) && !($definition instanceof DefinitionInterface)) { - if (self::$values === null) { - /** @var WeakMap $values */ - $values = new WeakMap(); - self::$values = $values; - } else { - $values = self::$values; - } - - $value = $values[$definition] ?? null; - if ($value instanceof ValueDefinition) { - return $value; - } - - $value = new ValueDefinition($definition); - $values[$definition] = $value; - return $value; + return new ValueDefinition($definition); } throw new InvalidConfigException('Invalid definition: ' . var_export($definition, true)); diff --git a/tests/Benchmark/DefinitionStorageBench.php b/tests/Benchmark/DefinitionStorageBench.php index 3f1c7cf..4dc5ab9 100644 --- a/tests/Benchmark/DefinitionStorageBench.php +++ b/tests/Benchmark/DefinitionStorageBench.php @@ -35,6 +35,12 @@ final class DefinitionStorageBench */ private array $definitions = []; + private DefinitionStorage $explicitStorage; + private DefinitionStorage $resolvableStorage; + private DefinitionStorage $unresolvableStorage; + private DefinitionStorage $delegateStorage; + private DefinitionStorage $strictStorage; + public function before(): void { $this->indexes = []; @@ -47,6 +53,17 @@ public function before(): void $this->indexes[] = $i; $this->definitions["service$i"] = Car::class; } + + $this->explicitStorage = new DefinitionStorage($this->definitions); + $this->resolvableStorage = new DefinitionStorage([ + EngineInterface::class => EngineMarkOne::class, + ]); + $this->unresolvableStorage = new DefinitionStorage(); + $this->delegateStorage = new DefinitionStorage(); + $this->delegateStorage->setDelegateContainer(new SimpleContainer([ + EngineInterface::class => new EngineMarkOne(), + ])); + $this->strictStorage = new DefinitionStorage([], true); } /** @@ -62,9 +79,9 @@ public function benchConstruct(): void /** * Measures positive lookups of explicitly configured service IDs. * - * @Groups({"lookup", "storage"}) + * @Groups({"lookup", "storage", "cold"}) */ - public function benchSequentialExplicitLookups(): void + public function benchSequentialExplicitLookupsColdStorage(): void { $storage = new DefinitionStorage($this->definitions); @@ -73,13 +90,25 @@ public function benchSequentialExplicitLookups(): void } } + /** + * Measures positive lookups against a reused definitions storage. + * + * @Groups({"lookup", "storage", "warm", "typical"}) + */ + public function benchSequentialExplicitLookupsWarmStorage(): void + { + foreach ($this->indexes as $index) { + $this->explicitStorage->has("service$index"); + } + } + /** * Measures positive autowiring checks for an object graph. * * @Groups({"lookup", "storage", "autowire", "typical"}) * @Revs(100) */ - public function benchResolvableObjectGraph(): void + public function benchResolvableObjectGraphColdStorage(): void { $storage = new DefinitionStorage([ EngineInterface::class => EngineMarkOne::class, @@ -88,26 +117,48 @@ public function benchResolvableObjectGraph(): void $storage->has(Car::class); } + /** + * Measures positive autowiring checks for an object graph against reused storage. + * + * @Groups({"lookup", "storage", "autowire", "warm", "typical"}) + * @Revs(100) + */ + public function benchResolvableObjectGraphWarmStorage(): void + { + $this->resolvableStorage->has(Car::class); + } + /** * Measures negative autowiring checks for a missing dependency. * * @Groups({"lookup", "storage", "autowire", "typical"}) * @Revs(100) */ - public function benchUnresolvableObjectGraph(): void + public function benchUnresolvableObjectGraphColdStorage(): void { $storage = new DefinitionStorage(); $storage->has(Car::class); } + /** + * Measures negative autowiring checks for a missing dependency against reused storage. + * + * @Groups({"lookup", "storage", "autowire", "warm", "typical"}) + * @Revs(100) + */ + public function benchUnresolvableObjectGraphWarmStorage(): void + { + $this->unresolvableStorage->has(Car::class); + } + /** * Measures fallback through a delegate container when a dependency is not explicitly defined. * * @Groups({"lookup", "storage", "delegate", "typical"}) * @Revs(100) */ - public function benchResolvableObjectGraphWithDelegate(): void + public function benchResolvableObjectGraphWithDelegateColdStorage(): void { $storage = new DefinitionStorage(); $storage->setDelegateContainer(new SimpleContainer([ @@ -117,6 +168,17 @@ public function benchResolvableObjectGraphWithDelegate(): void $storage->has(Car::class); } + /** + * Measures fallback through a delegate container against reused storage. + * + * @Groups({"lookup", "storage", "delegate", "warm", "typical"}) + * @Revs(100) + */ + public function benchResolvableObjectGraphWithDelegateWarmStorage(): void + { + $this->delegateStorage->has(Car::class); + } + /** * Measures strict mode misses where class names are not implicitly resolvable. * @@ -124,10 +186,8 @@ public function benchResolvableObjectGraphWithDelegate(): void */ public function benchStrictModeClassMisses(): void { - $storage = new DefinitionStorage([], true); - foreach ($this->indexes as $_index) { - $storage->has(EngineMarkOne::class); + $this->strictStorage->has(EngineMarkOne::class); } } } diff --git a/tests/Benchmark/NormalizerBench.php b/tests/Benchmark/NormalizerBench.php index b1ef1c2..608c497 100644 --- a/tests/Benchmark/NormalizerBench.php +++ b/tests/Benchmark/NormalizerBench.php @@ -11,12 +11,24 @@ use stdClass; use Yiisoft\Definitions\Helpers\Normalizer; use Yiisoft\Definitions\Reference; +use Yiisoft\Definitions\Tests\Support\Bike; use Yiisoft\Definitions\Tests\Support\Car; use Yiisoft\Definitions\Tests\Support\CarFactory; +use Yiisoft\Definitions\Tests\Support\Chair; use Yiisoft\Definitions\Tests\Support\ColorInterface; use Yiisoft\Definitions\Tests\Support\ColorPink; use Yiisoft\Definitions\Tests\Support\EngineInterface; use Yiisoft\Definitions\Tests\Support\EngineMarkOne; +use Yiisoft\Definitions\Tests\Support\EngineMarkTwo; +use Yiisoft\Definitions\Tests\Support\GearBox; +use Yiisoft\Definitions\Tests\Support\Mechanism; +use Yiisoft\Definitions\Tests\Support\Mouse; +use Yiisoft\Definitions\Tests\Support\Notebook; +use Yiisoft\Definitions\Tests\Support\Phone; +use Yiisoft\Definitions\Tests\Support\Recorder; +use Yiisoft\Definitions\Tests\Support\RedChair; +use Yiisoft\Definitions\Tests\Support\Table; +use Yiisoft\Definitions\Tests\Support\Tree; /** * @Iterations(5) @@ -26,7 +38,28 @@ */ final class NormalizerBench { - private const DEFINITION_COUNT = 200; + private const DEFINITION_COUNT = 120; + + /** + * @var class-string[] + */ + private const CLASS_DEFINITIONS = [ + Bike::class, + Car::class, + Chair::class, + ColorPink::class, + EngineMarkOne::class, + EngineMarkTwo::class, + GearBox::class, + Mechanism::class, + Mouse::class, + Notebook::class, + Phone::class, + Recorder::class, + RedChair::class, + Table::class, + Tree::class, + ]; /** * @var class-string[] @@ -53,6 +86,11 @@ final class NormalizerBench */ private array $objectDefinitions = []; + /** + * @var list + */ + private array $mixedDefinitions = []; + public function before(): void { $this->classDefinitions = []; @@ -60,34 +98,75 @@ public function before(): void $this->arrayDefinitions = []; $this->callableDefinitions = []; $this->objectDefinitions = []; + $this->mixedDefinitions = []; for ($i = 0; $i < self::DEFINITION_COUNT; $i++) { - $this->classDefinitions[] = ColorPink::class; - $this->referenceDefinitions[] = 'engine'; - $this->arrayDefinitions[] = [ + $this->classDefinitions[] = self::CLASS_DEFINITIONS[$i % count(self::CLASS_DEFINITIONS)]; + $this->referenceDefinitions[] = "service.$i"; + + $arrayDefinition = [ 'class' => Car::class, '__construct()' => [Reference::to(EngineInterface::class)], 'setColor()' => [Reference::to(ColorInterface::class)], ]; + $this->arrayDefinitions[] = $arrayDefinition; $this->callableDefinitions[] = CarFactory::create(...); - $this->objectDefinitions[] = new stdClass(); + $objectDefinition = new stdClass(); + $this->objectDefinitions[] = $objectDefinition; + + $type = $i % 6; + if ($type === 0) { + $this->mixedDefinitions[] = [$this->classDefinitions[$i], null]; + } elseif ($type === 1) { + $this->mixedDefinitions[] = [$this->referenceDefinitions[$i], null]; + } elseif ($type === 2) { + $this->mixedDefinitions[] = [$arrayDefinition, null]; + } elseif ($type === 3) { + $this->mixedDefinitions[] = [[ + '__construct()' => ['Yii Phone', '3.0'], + '$dev' => true, + 'addApp()' => ['Browser', '7'], + ], Phone::class]; + } elseif ($type === 4) { + $this->mixedDefinitions[] = [CarFactory::create(...), null]; + } else { + $this->mixedDefinitions[] = [$objectDefinition, null]; + } + } + + $this->clearNormalizerCache(); + $this->warmUpNormalizer($this->classDefinitions); + $this->warmUpNormalizer($this->referenceDefinitions); + $this->warmUpNormalizer($this->objectDefinitions); + $this->normalizeMixedDefinitions(); + } + + /** + * @Groups({"class", "warm"}) + */ + public function benchWarmClassDefinitions(): void + { + foreach ($this->classDefinitions as $definition) { + Normalizer::normalize($definition); } } /** - * @Groups({"class"}) + * @Groups({"class", "cold"}) */ - public function benchClassDefinitions(): void + public function benchColdClassDefinitions(): void { + $this->clearNormalizerCache(); + foreach ($this->classDefinitions as $definition) { Normalizer::normalize($definition); } } /** - * @Groups({"reference"}) + * @Groups({"reference", "warm"}) */ - public function benchReferenceDefinitions(): void + public function benchWarmReferenceDefinitions(): void { foreach ($this->referenceDefinitions as $definition) { Normalizer::normalize($definition); @@ -95,29 +174,45 @@ public function benchReferenceDefinitions(): void } /** - * @Groups({"definition", "typical"}) + * @Groups({"reference", "cold"}) + */ + public function benchColdReferenceDefinitions(): void + { + $this->clearNormalizerCache(); + + foreach ($this->referenceDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"definition", "cold", "typical"}) */ public function benchArrayDefinitions(): void { + $this->clearNormalizerCache(); + foreach ($this->arrayDefinitions as $definition) { Normalizer::normalize($definition); } } /** - * @Groups({"factory", "typical"}) + * @Groups({"factory", "cold", "typical"}) */ public function benchCallableDefinitions(): void { + $this->clearNormalizerCache(); + foreach ($this->callableDefinitions as $definition) { Normalizer::normalize($definition); } } /** - * @Groups({"value"}) + * @Groups({"value", "warm"}) */ - public function benchObjectDefinitions(): void + public function benchWarmObjectDefinitions(): void { foreach ($this->objectDefinitions as $definition) { Normalizer::normalize($definition); @@ -125,10 +220,24 @@ public function benchObjectDefinitions(): void } /** - * @Groups({"definition", "typical"}) + * @Groups({"value", "cold"}) + */ + public function benchColdObjectDefinitions(): void + { + $this->clearNormalizerCache(); + + foreach ($this->objectDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"definition", "cold", "typical"}) */ public function benchArrayDefinitionsWithInferredClass(): void { + $this->clearNormalizerCache(); + foreach ($this->arrayDefinitions as $definition) { unset($definition['class']); Normalizer::normalize($definition, Car::class); @@ -136,12 +245,43 @@ public function benchArrayDefinitionsWithInferredClass(): void } /** - * @Groups({"class"}) + * @Groups({"mixed", "cold", "typical"}) + */ + public function benchMixedApplicationDefinitionsCold(): void + { + $this->clearNormalizerCache(); + $this->normalizeMixedDefinitions(); + } + + /** + * @Groups({"mixed", "warm", "typical"}) */ - public function benchSameClassDefinitions(): void + public function benchMixedApplicationDefinitionsWarm(): void + { + $this->normalizeMixedDefinitions(); + } + + /** + * @param iterable $definitions + */ + private function warmUpNormalizer(iterable $definitions): void + { + foreach ($definitions as $definition) { + Normalizer::normalize($definition); + } + } + + private function normalizeMixedDefinitions(): void + { + foreach ($this->mixedDefinitions as [$definition, $class]) { + Normalizer::normalize($definition, $class); + } + } + + private function clearNormalizerCache(): void { - foreach ($this->classDefinitions as $_definition) { - Normalizer::normalize(EngineMarkOne::class, EngineMarkOne::class); + if (method_exists(Normalizer::class, 'clearCache')) { + Normalizer::clearCache(); } } } diff --git a/tests/Benchmark/TypicalUsageBench.php b/tests/Benchmark/TypicalUsageBench.php index 3eeeed6..0a2e1ce 100644 --- a/tests/Benchmark/TypicalUsageBench.php +++ b/tests/Benchmark/TypicalUsageBench.php @@ -27,6 +27,8 @@ */ final class TypicalUsageBench { + private const MIXED_DEFINITION_COUNT = 120; + private SimpleContainer $container; private ArrayDefinition $objectGraphDefinition; private ArrayDefinition $methodsAndPropertiesDefinition; @@ -34,6 +36,11 @@ final class TypicalUsageBench private CallableDefinition $serviceFactoryDefinition; private Reference $reference; + /** + * @var list + */ + private array $mixedDefinitions = []; + public function before(): void { $this->container = new SimpleContainer([ @@ -64,6 +71,35 @@ public function before(): void $this->staticFactoryDefinition = new CallableDefinition(CarFactory::create(...)); $this->serviceFactoryDefinition = new CallableDefinition([CarFactory::class, 'createWithColor']); $this->reference = Reference::to(EngineInterface::class); + $this->mixedDefinitions = []; + + for ($i = 0; $i < self::MIXED_DEFINITION_COUNT; $i++) { + $type = $i % 5; + if ($type === 0) { + $this->mixedDefinitions[] = ArrayDefinition::fromConfig([ + 'class' => Car::class, + '__construct()' => [Reference::to(EngineInterface::class)], + 'setColor()' => [Reference::to(ColorInterface::class)], + ]); + } elseif ($type === 1) { + $this->mixedDefinitions[] = ArrayDefinition::fromConfig([ + 'class' => Phone::class, + '__construct()' => [ + 'name' => 'Yii Phone', + 'version' => '3.0', + 'colors' => ['black', 'white'], + ], + '$dev' => true, + 'addApp()' => ['Browser', '7'], + ]); + } elseif ($type === 2) { + $this->mixedDefinitions[] = new CallableDefinition(CarFactory::create(...)); + } elseif ($type === 3) { + $this->mixedDefinitions[] = new CallableDefinition([CarFactory::class, 'createWithColor']); + } else { + $this->mixedDefinitions[] = Reference::to(EngineInterface::class); + } + } } /** @@ -119,4 +155,17 @@ public function benchReferenceResolution(): void { $this->reference->resolve($this->container); } + + /** + * Measures a mixed batch of array, callable, and reference definitions against one reused container. + * + * @Groups({"mixed", "lookup", "typical"}) + * @Revs(100) + */ + public function benchMixedDefinitionResolution(): void + { + foreach ($this->mixedDefinitions as $definition) { + $definition->resolve($this->container); + } + } } diff --git a/tests/Unit/Helpers/NormalizerTest.php b/tests/Unit/Helpers/NormalizerTest.php index d736ee1..102ed6e 100644 --- a/tests/Unit/Helpers/NormalizerTest.php +++ b/tests/Unit/Helpers/NormalizerTest.php @@ -19,11 +19,6 @@ final class NormalizerTest extends TestCase { - protected function setUp(): void - { - Normalizer::clearCache(); - } - public function testReference(): void { $reference = Reference::to('test'); @@ -51,7 +46,7 @@ public function testSameClass(): void $this->assertSame(ColorPink::class, $definition->getClass()); } - public function testCachedClass(): void + public function testClassRepeatedly(): void { Normalizer::normalize(GearBox::class); @@ -83,34 +78,14 @@ public function testLowercaseClass(): void $this->assertSame($class, $definition->getClass()); } - public function testCachedReferenceDoesNotTriggerAutoload(): void - { - Normalizer::normalize('engine'); - - $autoloadedClasses = []; - $autoload = static function (string $class) use (&$autoloadedClasses): void { - $autoloadedClasses[] = $class; - }; - - spl_autoload_register($autoload); - try { - $definition = Normalizer::normalize('engine'); - } finally { - spl_autoload_unregister($autoload); - } - - $this->assertInstanceOf(Reference::class, $definition); - $this->assertSame([], $autoloadedClasses); - } - - public function testCachedReference(): void + public function testReferenceRepeatedly(): void { Normalizer::normalize('engine'); $this->assertInstanceOf(Reference::class, Normalizer::normalize('engine')); } - public function testCachedReferenceWithoutPlainReferenceFastPath(): void + public function testReferenceWithClassRepeatedly(): void { Normalizer::normalize('engine-with-class', GearBox::class); @@ -184,14 +159,6 @@ public function testReadyObject(): void $this->assertSame($object, $definition->resolve($container)); } - public function testCachedReadyObject(): void - { - $object = new stdClass(); - $definition = Normalizer::normalize($object); - - $this->assertSame($definition, Normalizer::normalize($object)); - } - public function testInteger(): void { $this->expectException(InvalidConfigException::class); From 3dd134f5c2cc391d30d958d4644644268ea11092 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 26 May 2026 03:00:02 +0300 Subject: [PATCH 29/31] Refine performance changes after benchmarks --- CHANGELOG.md | 2 +- src/ArrayDefinition.php | 13 ++--- src/CallableDefinition.php | 63 ++++------------------ src/DefinitionStorage.php | 11 ---- src/Helpers/Normalizer.php | 1 + tests/Benchmark/DefinitionStorageBench.php | 3 ++ tests/Unit/CallableDefinitionTest.php | 2 +- 7 files changed, 19 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d1a7f7..c683ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Enh #112: Explicitly import constants in "use" section (@mspirkov) - Enh #113: Remove unnecessary files from Composer package (@mspirkov) -- Enh #114: Performance optimizations (@samdark) +- Enh #114: Improve array definition config parsing and add benchmarks (@samdark) ## 3.4.1 December 02, 2025 diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index 6ae80e3..1e65b86 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -42,11 +42,6 @@ final class ArrayDefinition implements DefinitionInterface */ private ?ContainerInterface $referenceContainer = null; - /** - * @var array> - */ - private array $methodDependencies = []; - /** * @psalm-param class-string $class * @psalm-param array $methodsAndProperties @@ -118,21 +113,20 @@ public function resolve(ContainerInterface $container): object $resolvedConstructorArguments = $this->resolveFunctionArguments( $container, DefinitionExtractor::fromClassName($class), - $this->constructorArguments, + $this->getConstructorArguments(), ); /** @psalm-suppress MixedMethodCall */ $object = new $class(...$resolvedConstructorArguments); - foreach ($this->methodsAndProperties as $item) { + foreach ($this->getMethodsAndProperties() as $item) { [$type, $name, $value] = $item; if ($type === self::TYPE_METHOD) { /** @var array $value */ if (method_exists($object, $name)) { $resolvedMethodArguments = $this->resolveFunctionArguments( $container, - $this->methodDependencies[$name] - ??= DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)), + DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)), $value, ); } else { @@ -177,7 +171,6 @@ public function merge(self $other): self } } $new->methodsAndProperties = $methodsAndProperties; - $new->methodDependencies = []; return $new; } diff --git a/src/CallableDefinition.php b/src/CallableDefinition.php index 862e4d8..ec330a2 100644 --- a/src/CallableDefinition.php +++ b/src/CallableDefinition.php @@ -8,14 +8,12 @@ use Psr\Container\ContainerInterface; use ReflectionException; use ReflectionFunction; -use ReflectionFunctionAbstract; use ReflectionMethod; use Yiisoft\Definitions\Contract\DefinitionInterface; use Yiisoft\Definitions\Exception\NotInstantiableException; use Yiisoft\Definitions\Helpers\DefinitionExtractor; use Yiisoft\Definitions\Helpers\DefinitionResolver; -use function get_class; use function is_array; use function is_object; @@ -31,16 +29,6 @@ final class CallableDefinition implements DefinitionInterface */ private $callable; - /** - * @var array> - */ - private array $dependencies = []; - - /** - * @var array|null - */ - private ?array $callableDependencies = null; - /** * @param array|callable $callable Callable to be used for building * an object. Dependencies are determined and passed based @@ -56,64 +44,33 @@ public function __construct(array|callable $callable) public function resolve(ContainerInterface $container): mixed { try { - [$closure, $dependencies] = $this->prepare($this->callable, $container); + $reflection = new ReflectionFunction( + $this->prepareClosure($this->callable, $container), + ); } catch (ReflectionException) { throw new NotInstantiableException( 'Can not instantiate callable definition. Got ' . var_export($this->callable, true), ); } + $dependencies = DefinitionExtractor::fromFunction($reflection); $arguments = DefinitionResolver::resolveArray($container, null, $dependencies); - return $closure(...$arguments); + return $reflection->invokeArgs($arguments); } /** * @psalm-param callable|array{0:class-string,1:string} $callable - * - * @psalm-return array{Closure,array} */ - private function prepare(array|callable $callable, ContainerInterface $container): array + private function prepareClosure(array|callable $callable, ContainerInterface $container): Closure { - if (is_array($callable)) { + if (is_array($callable) && !is_object($callable[0])) { $reflection = new ReflectionMethod($callable[0], $callable[1]); - if (!is_object($callable[0]) && !$reflection->isStatic()) { - $class = $callable[0]; - /** @var object $object */ - $object = $container->get($callable[0]); - $callable[0] = $object; - if (get_class($object) !== $class) { - $reflection = new ReflectionMethod($object, $callable[1]); - } + if (!$reflection->isStatic()) { + $callable[0] = $container->get($callable[0]); } - - return [ - Closure::fromCallable($callable), - $this->getDependencies($this->getArrayCallableDependenciesKey($callable), $reflection), - ]; } - $closure = Closure::fromCallable($callable); - - return [ - $closure, - $this->callableDependencies ??= DefinitionExtractor::fromFunction(new ReflectionFunction($closure)), - ]; - } - - /** - * @param array{0:class-string|object,1:string} $callable - */ - private function getArrayCallableDependenciesKey(array $callable): string - { - return (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]) . "\0" . $callable[1]; - } - - /** - * @return array - */ - private function getDependencies(string $key, ReflectionFunctionAbstract $reflection): array - { - return $this->dependencies[$key] ??= DefinitionExtractor::fromFunction($reflection); + return Closure::fromCallable($callable); } } diff --git a/src/DefinitionStorage.php b/src/DefinitionStorage.php index caa7799..ceff210 100644 --- a/src/DefinitionStorage.php +++ b/src/DefinitionStorage.php @@ -52,18 +52,7 @@ public function setDelegateContainer(ContainerInterface $delegateContainer): voi */ public function has(string $id): bool { - if (isset($this->definitions[$id])) { - $this->buildStack = []; - return true; - } - - if ($this->useStrictMode) { - $this->buildStack = [$id => 1]; - return false; - } - $this->buildStack = []; - return $this->isResolvable($id, []); } diff --git a/src/Helpers/Normalizer.php b/src/Helpers/Normalizer.php index 705b958..841b757 100644 --- a/src/Helpers/Normalizer.php +++ b/src/Helpers/Normalizer.php @@ -72,6 +72,7 @@ public static function normalize(mixed $definition, ?string $class = null): Defi return new CallableDefinition($definition); } + // Array definition if (is_array($definition)) { $config = $definition; if (!array_key_exists(ArrayDefinition::CLASS_NAME, $config)) { diff --git a/tests/Benchmark/DefinitionStorageBench.php b/tests/Benchmark/DefinitionStorageBench.php index 4dc5ab9..5a6e3d1 100644 --- a/tests/Benchmark/DefinitionStorageBench.php +++ b/tests/Benchmark/DefinitionStorageBench.php @@ -64,6 +64,9 @@ public function before(): void EngineInterface::class => new EngineMarkOne(), ])); $this->strictStorage = new DefinitionStorage([], true); + + $this->resolvableStorage->has(Car::class); + $this->delegateStorage->has(Car::class); } /** diff --git a/tests/Unit/CallableDefinitionTest.php b/tests/Unit/CallableDefinitionTest.php index 49e4b8c..2fd6b5a 100644 --- a/tests/Unit/CallableDefinitionTest.php +++ b/tests/Unit/CallableDefinitionTest.php @@ -36,7 +36,7 @@ public function testDynamicCallable(): void $this->assertInstanceOf(ColorPink::class, $car->getColor()); } - public function testDynamicCallableDependenciesAreCachedPerResolvedObject(): void + public function testDynamicCallableUsesResolvedObjectSignature(): void { $definition = new CallableDefinition([EngineAwareCallableFactory::class, 'create']); From a00b622e68264fba5cb47ed11d50f3134732262b Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 26 May 2026 03:08:35 +0300 Subject: [PATCH 30/31] Stabilize benchmark sampling --- tests/Benchmark/DefinitionStorageBench.php | 14 +++++++------- tests/Benchmark/NormalizerBench.php | 2 +- tests/Benchmark/TypicalUsageBench.php | 13 +++++++------ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/Benchmark/DefinitionStorageBench.php b/tests/Benchmark/DefinitionStorageBench.php index 5a6e3d1..c0dd3de 100644 --- a/tests/Benchmark/DefinitionStorageBench.php +++ b/tests/Benchmark/DefinitionStorageBench.php @@ -17,7 +17,7 @@ use Yiisoft\Test\Support\Container\SimpleContainer; /** - * @Iterations(5) + * @Iterations(15) * @Revs(1000) * @BeforeMethods({"before"}) */ @@ -109,7 +109,7 @@ public function benchSequentialExplicitLookupsWarmStorage(): void * Measures positive autowiring checks for an object graph. * * @Groups({"lookup", "storage", "autowire", "typical"}) - * @Revs(100) + * @Revs(1000) */ public function benchResolvableObjectGraphColdStorage(): void { @@ -124,7 +124,7 @@ public function benchResolvableObjectGraphColdStorage(): void * Measures positive autowiring checks for an object graph against reused storage. * * @Groups({"lookup", "storage", "autowire", "warm", "typical"}) - * @Revs(100) + * @Revs(10000) */ public function benchResolvableObjectGraphWarmStorage(): void { @@ -135,7 +135,7 @@ public function benchResolvableObjectGraphWarmStorage(): void * Measures negative autowiring checks for a missing dependency. * * @Groups({"lookup", "storage", "autowire", "typical"}) - * @Revs(100) + * @Revs(1000) */ public function benchUnresolvableObjectGraphColdStorage(): void { @@ -148,7 +148,7 @@ public function benchUnresolvableObjectGraphColdStorage(): void * Measures negative autowiring checks for a missing dependency against reused storage. * * @Groups({"lookup", "storage", "autowire", "warm", "typical"}) - * @Revs(100) + * @Revs(1000) */ public function benchUnresolvableObjectGraphWarmStorage(): void { @@ -159,7 +159,7 @@ public function benchUnresolvableObjectGraphWarmStorage(): void * Measures fallback through a delegate container when a dependency is not explicitly defined. * * @Groups({"lookup", "storage", "delegate", "typical"}) - * @Revs(100) + * @Revs(1000) */ public function benchResolvableObjectGraphWithDelegateColdStorage(): void { @@ -175,7 +175,7 @@ public function benchResolvableObjectGraphWithDelegateColdStorage(): void * Measures fallback through a delegate container against reused storage. * * @Groups({"lookup", "storage", "delegate", "warm", "typical"}) - * @Revs(100) + * @Revs(10000) */ public function benchResolvableObjectGraphWithDelegateWarmStorage(): void { diff --git a/tests/Benchmark/NormalizerBench.php b/tests/Benchmark/NormalizerBench.php index 608c497..6e65550 100644 --- a/tests/Benchmark/NormalizerBench.php +++ b/tests/Benchmark/NormalizerBench.php @@ -31,7 +31,7 @@ use Yiisoft\Definitions\Tests\Support\Tree; /** - * @Iterations(5) + * @Iterations(15) * @Revs(1000) * @Groups({"normalizer"}) * @BeforeMethods({"before"}) diff --git a/tests/Benchmark/TypicalUsageBench.php b/tests/Benchmark/TypicalUsageBench.php index 0a2e1ce..fa8e4d5 100644 --- a/tests/Benchmark/TypicalUsageBench.php +++ b/tests/Benchmark/TypicalUsageBench.php @@ -21,7 +21,7 @@ use Yiisoft\Test\Support\Container\SimpleContainer; /** - * @Iterations(5) + * @Iterations(15) * @Revs(1000) * @BeforeMethods({"before"}) */ @@ -106,7 +106,7 @@ public function before(): void * Measures explicit array definitions with constructor, setter, and reference resolution. * * @Groups({"definition", "lookup", "typical"}) - * @Revs(100) + * @Revs(1000) */ public function benchArrayDefinitionObjectGraph(): void { @@ -117,7 +117,7 @@ public function benchArrayDefinitionObjectGraph(): void * Measures constructor arguments, public property assignment, and method calls. * * @Groups({"definition", "lookup", "typical"}) - * @Revs(100) + * @Revs(1000) */ public function benchArrayDefinitionMethodsAndProperties(): void { @@ -128,7 +128,7 @@ public function benchArrayDefinitionMethodsAndProperties(): void * Measures a static callable factory with autowired arguments. * * @Groups({"factory", "lookup", "typical"}) - * @Revs(100) + * @Revs(1000) */ public function benchStaticFactoryDefinition(): void { @@ -139,7 +139,7 @@ public function benchStaticFactoryDefinition(): void * Measures a callable factory resolved as a service before invocation. * * @Groups({"factory", "lookup", "typical"}) - * @Revs(100) + * @Revs(1000) */ public function benchServiceFactoryDefinition(): void { @@ -150,6 +150,7 @@ public function benchServiceFactoryDefinition(): void * Measures direct reference resolution against a PSR container. * * @Groups({"reference", "lookup", "typical"}) + * @Revs(10000) */ public function benchReferenceResolution(): void { @@ -160,7 +161,7 @@ public function benchReferenceResolution(): void * Measures a mixed batch of array, callable, and reference definitions against one reused container. * * @Groups({"mixed", "lookup", "typical"}) - * @Revs(100) + * @Revs(1000) */ public function benchMixedDefinitionResolution(): void { From ff21c2a3c6acfc9dfe6fd5df576407152d8e2138 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 26 May 2026 09:53:49 +0300 Subject: [PATCH 31/31] Fix typo --- tests/Unit/DefinitionStorageTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Unit/DefinitionStorageTest.php b/tests/Unit/DefinitionStorageTest.php index 9393d50..b43ca61 100644 --- a/tests/Unit/DefinitionStorageTest.php +++ b/tests/Unit/DefinitionStorageTest.php @@ -83,7 +83,7 @@ public function testExplicitDefinitionClearsPreviousBuildStack(): void { $storage = new DefinitionStorage(['existing' => 'anything']); - $this->assertFalse($storage->has(NonExisitng::class)); + $this->assertFalse($storage->has(NonExisting::class)); $this->assertNotSame([], $storage->getBuildStack()); $this->assertTrue($storage->has('existing')); @@ -93,8 +93,8 @@ public function testExplicitDefinitionClearsPreviousBuildStack(): void public function testNonExistingService(): void { $storage = new DefinitionStorage([]); - $this->assertFalse($storage->has(NonExisitng::class)); - $this->assertSame([NonExisitng::class], $storage->getBuildStack()); + $this->assertFalse($storage->has(NonExisting::class)); + $this->assertSame([NonExisting::class], $storage->getBuildStack()); } public function testServiceWithNonExistingDependency(): void @@ -170,8 +170,8 @@ public function testEmptyDelegateContainer(): void $container = new SimpleContainer([]); $storage = new DefinitionStorage([]); $storage->setDelegateContainer($container); - $this->assertFalse($storage->has(\NonExisitng::class)); - $this->assertSame([\NonExisitng::class], $storage->getBuildStack()); + $this->assertFalse($storage->has(\NonExisting::class)); + $this->assertSame([\NonExisting::class], $storage->getBuildStack()); } public function testServiceWithNonExistingUnionTypes(): void