From 9a2aec881402ad2c7d90bd72ebfbb07948191dcb Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 01:05:35 +0300 Subject: [PATCH 01/18] Add benchmark with typical usage pattern --- tests/Benchmark/TypicalUsageBench.php | 167 ++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tests/Benchmark/TypicalUsageBench.php diff --git a/tests/Benchmark/TypicalUsageBench.php b/tests/Benchmark/TypicalUsageBench.php new file mode 100644 index 00000000..f0156e96 --- /dev/null +++ b/tests/Benchmark/TypicalUsageBench.php @@ -0,0 +1,167 @@ +cachedServiceContainer = new Container( + ContainerConfig::create() + ->withDefinitions([ + EngineInterface::class => EngineMarkOne::class, + Car::class => Car::class, + ]), + ); + $this->cachedServiceContainer->get(Car::class); + } + + /** + * Measures the hot path after the shared service was already built. + * + * @Groups({"lookup", "typical"}) + */ + public function benchCachedSharedService(): void + { + $this->cachedServiceContainer->get(Car::class); + } + + /** + * Measures first resolution of an autowired object graph by class name. + * + * @Groups({"lookup", "autowire", "typical"}) + * @Revs(100) + */ + public function benchAutowireObjectGraph(): void + { + $container = new Container( + ContainerConfig::create() + ->withDefinitions([ + EngineInterface::class => EngineMarkOne::class, + ]), + ); + + $container->get(Car::class); + } + + /** + * Measures explicit array definitions with constructor, setter, and reference resolution. + * + * @Groups({"lookup", "definition", "typical"}) + * @Revs(100) + */ + public function benchArrayDefinitionObjectGraph(): void + { + $container = new Container( + ContainerConfig::create() + ->withDefinitions([ + EngineInterface::class => EngineMarkOne::class, + ColorInterface::class => ColorRed::class, + 'car' => [ + 'class' => Car::class, + '__construct()' => [Reference::to(EngineInterface::class)], + 'setColor()' => [Reference::to(ColorInterface::class)], + ], + ]), + ); + + $container->get('car'); + } + + /** + * Measures first resolution of a callable factory definition. + * + * @Groups({"lookup", "factory", "typical"}) + * @Revs(100) + */ + public function benchFactoryDefinition(): void + { + $container = new Container( + ContainerConfig::create() + ->withDefinitions([ + ColorInterface::class => ColorRed::class, + 'car' => [CarFactory::class, 'createWithColor'], + ]), + ); + + $container->get('car'); + } + + /** + * Measures collecting all services registered under a tag. + * + * @Groups({"lookup", "tag", "typical"}) + * @Revs(100) + */ + public function benchTaggedServices(): void + { + $container = new Container( + ContainerConfig::create() + ->withDefinitions([ + EngineMarkOne::class => [ + 'class' => EngineMarkOne::class, + 'tags' => ['engine'], + ], + EngineMarkTwo::class => [ + 'class' => EngineMarkTwo::class, + 'tags' => ['engine'], + ], + 'engine-reference' => [ + 'definition' => Reference::to(EngineMarkOne::class), + 'tags' => ['engine'], + ], + ]), + ); + + $container->get(TagReference::id('engine')); + } + + /** + * Measures fallback through delegates configured on the container. + * + * @Groups({"lookup", "delegate", "typical"}) + * @Revs(100) + */ + public function benchDelegateFallback(): void + { + $container = new Container( + ContainerConfig::create() + ->withDelegates([ + static fn(ContainerInterface $container): ContainerInterface => new Container( + ContainerConfig::create() + ->withDefinitions([ + EngineInterface::class => EngineMarkOne::class, + ]), + ), + ]), + ); + + $container->get(EngineInterface::class); + } +} From 694978e6482d5833ea74282503e3d432c06cbebf Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 01:06:02 +0300 Subject: [PATCH 02/18] Introduce composite container lookup cache --- src/CompositeContainer.php | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/CompositeContainer.php b/src/CompositeContainer.php index 7450d4fe..a64518be 100644 --- a/src/CompositeContainer.php +++ b/src/CompositeContainer.php @@ -25,6 +25,13 @@ final class CompositeContainer implements ContainerInterface */ private array $containers = []; + /** + * Index of a container where a service ID was previously found. + * + * @psalm-var array + */ + private array $lookupCache = []; + /** * @psalm-template T * @psalm-param string|class-string $id @@ -71,8 +78,14 @@ public function get($id) return array_merge(...$tags); } - foreach ($this->containers as $container) { + if (isset($this->lookupCache[$id], $this->containers[$this->lookupCache[$id]])) { + /** @psalm-suppress MixedReturnStatement */ + return $this->containers[$this->lookupCache[$id]]->get($id); + } + + foreach ($this->containers as $index => $container) { if ($container->has($id)) { + $this->lookupCache[$id] = (int) $index; /** @psalm-suppress MixedReturnStatement */ return $container->get($id); } @@ -130,8 +143,13 @@ public function has($id): bool return false; } - foreach ($this->containers as $container) { + if (isset($this->lookupCache[$id], $this->containers[$this->lookupCache[$id]])) { + return true; + } + + foreach ($this->containers as $index => $container) { if ($container->has($id)) { + $this->lookupCache[$id] = (int) $index; return true; } } @@ -155,6 +173,7 @@ public function detach(ContainerInterface $container): void foreach ($this->containers as $i => $c) { if ($container === $c) { unset($this->containers[$i]); + $this->lookupCache = []; } } } From f9b88a6cedbc39ddb9debac93121a73f600c6956 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 01:06:37 +0300 Subject: [PATCH 03/18] Introduce cache for `has()` --- src/Container.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Container.php b/src/Container.php index ef225056..282c2480 100644 --- a/src/Container.php +++ b/src/Container.php @@ -62,6 +62,11 @@ final class Container implements ContainerInterface private CompositeContainer $delegates; + /** + * @psalm-var array + */ + private array $hasCache = []; + /** * @var array Tagged service IDs. The structure is `['tagID' => ['service1', 'service2']]`. * @psalm-var array> @@ -109,20 +114,24 @@ public function __construct(?ContainerConfigInterface $config = null) */ public function has(string $id): bool { + if (array_key_exists($id, $this->hasCache)) { + return $this->hasCache[$id]; + } + try { if ($this->definitions->has($id)) { - return true; + return $this->hasCache[$id] = true; } } catch (CircularReferenceException) { - return true; + return $this->hasCache[$id] = true; } if (TagReference::isTagAlias($id)) { $tag = TagReference::extractTagFromAlias($id); - return isset($this->tags[$tag]); + return $this->hasCache[$id] = isset($this->tags[$tag]); } - return false; + return $this->hasCache[$id] = false; } /** @@ -269,6 +278,7 @@ private function addDefinition(string $id, mixed $definition): void } unset($this->instances[$id]); + $this->hasCache = []; $this->addDefinitionToStorage($id, $definition); } From f1e65683f05ab5857ca442800b0b04fcf89dcc57 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 01:29:59 +0300 Subject: [PATCH 04/18] Fast path for empty definition validation --- src/Container.php | 8 ++++ tests/Benchmark/ContainerBench.php | 69 ++++++++++++++++++++++++++++++ tests/Unit/ContainerTest.php | 13 ++++++ 3 files changed, 90 insertions(+) diff --git a/src/Container.php b/src/Container.php index 282c2480..6ea24439 100644 --- a/src/Container.php +++ b/src/Container.php @@ -28,6 +28,7 @@ use function is_object; use function is_string; use function sprintf; +use function trim; /** * Container implements a [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) container. @@ -354,6 +355,13 @@ private function validateDefinition(mixed $definition, ?string $id = null): void return; } + if (is_string($definition)) { + if (trim($definition) === '') { + throw new InvalidConfigException('Invalid definition: class name must be a non-empty string.'); + } + return; + } + if (is_array($definition)) { if (isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) { $class = $definition['class']; diff --git a/tests/Benchmark/ContainerBench.php b/tests/Benchmark/ContainerBench.php index 5e2a228d..01f361b7 100644 --- a/tests/Benchmark/ContainerBench.php +++ b/tests/Benchmark/ContainerBench.php @@ -147,6 +147,25 @@ public function benchConstruct(): void ); } + /** + * @Groups({"construct", "no-validation"}) + * + * @throws InvalidConfigException + * @throws NotInstantiableException + */ + public function benchConstructWithoutValidation(): void + { + $definitions = []; + for ($i = 0; $i < self::SERVICE_COUNT; $i++) { + $definitions["service$i"] = PropertyTestClass::class; + } + $container = new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions($definitions), + ); + } + /** * @Groups({"lookup"}) * @ParamProviders({"provideDefinitions"}) @@ -171,6 +190,31 @@ public function benchSequentialLookups($params): void } } + /** + * @Groups({"lookup", "no-validation"}) + * @ParamProviders({"provideDefinitions"}) + */ + public function benchSequentialLookupsWithoutValidation($params): void + { + $definitions = []; + for ($i = 0; $i < self::SERVICE_COUNT; $i++) { + $definitions["service$i"] = $params['serviceClass']; + } + if (isset($params['otherDefinitions'])) { + $definitions = array_merge($definitions, $params['otherDefinitions']); + } + $container = new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions($definitions), + ); + for ($i = 0; $i < self::SERVICE_COUNT / 2; $i++) { + // Do array lookup. + $index = $this->indexes[$i]; + $container->get("service$index"); + } + } + /** * @Groups({"lookup"}) * @ParamProviders({"provideDefinitions"}) @@ -195,6 +239,31 @@ public function benchRandomLookups($params): void } } + /** + * @Groups({"lookup", "no-validation"}) + * @ParamProviders({"provideDefinitions"}) + */ + public function benchRandomLookupsWithoutValidation($params): void + { + $definitions = []; + for ($i = 0; $i < self::SERVICE_COUNT; $i++) { + $definitions["service$i"] = $params['serviceClass']; + } + if (isset($params['otherDefinitions'])) { + $definitions = array_merge($definitions, $params['otherDefinitions']); + } + $container = new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions($definitions), + ); + for ($i = 0; $i < self::SERVICE_COUNT / 2; $i++) { + // Do array lookup. + $index = $this->randomIndexes[$i]; + $container->get("service$index"); + } + } + /** * @Groups({"lookup"}) * @ParamProviders({"provideDefinitions"}) diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index d11b7684..670780d8 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -83,6 +83,19 @@ public function testSettingScalars(): void $container->get('scalar'); } + public function testEmptyStringDefinition(): void + { + $this->expectException(InvalidConfigException::class); + $this->expectExceptionMessage('Invalid definition: class name must be a non-empty string.'); + + new Container( + ContainerConfig::create() + ->withDefinitions([ + 'empty' => '', + ]), + ); + } + public function testIntegerKeys(): void { $this->expectException(InvalidConfigException::class); From 209ce3a8d321d70b52141b46b53b195c2bea9df2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 01:41:36 +0300 Subject: [PATCH 05/18] More benchmarks --- tests/Benchmark/TypicalUsageBench.php | 119 ++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/Benchmark/TypicalUsageBench.php b/tests/Benchmark/TypicalUsageBench.php index f0156e96..63d61f15 100644 --- a/tests/Benchmark/TypicalUsageBench.php +++ b/tests/Benchmark/TypicalUsageBench.php @@ -70,6 +70,25 @@ public function benchAutowireObjectGraph(): void $container->get(Car::class); } + /** + * Measures first resolution of an autowired object graph by class name without eager definition validation. + * + * @Groups({"lookup", "autowire", "typical", "no-validation"}) + * @Revs(100) + */ + public function benchAutowireObjectGraphWithoutValidation(): void + { + $container = new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions([ + EngineInterface::class => EngineMarkOne::class, + ]), + ); + + $container->get(Car::class); + } + /** * Measures explicit array definitions with constructor, setter, and reference resolution. * @@ -94,6 +113,31 @@ public function benchArrayDefinitionObjectGraph(): void $container->get('car'); } + /** + * Measures explicit array definitions with constructor, setter, and reference resolution without eager validation. + * + * @Groups({"lookup", "definition", "typical", "no-validation"}) + * @Revs(100) + */ + public function benchArrayDefinitionObjectGraphWithoutValidation(): void + { + $container = new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions([ + EngineInterface::class => EngineMarkOne::class, + ColorInterface::class => ColorRed::class, + 'car' => [ + 'class' => Car::class, + '__construct()' => [Reference::to(EngineInterface::class)], + 'setColor()' => [Reference::to(ColorInterface::class)], + ], + ]), + ); + + $container->get('car'); + } + /** * Measures first resolution of a callable factory definition. * @@ -113,6 +157,26 @@ public function benchFactoryDefinition(): void $container->get('car'); } + /** + * Measures first resolution of a callable factory definition without eager validation. + * + * @Groups({"lookup", "factory", "typical", "no-validation"}) + * @Revs(100) + */ + public function benchFactoryDefinitionWithoutValidation(): void + { + $container = new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions([ + ColorInterface::class => ColorRed::class, + 'car' => [CarFactory::class, 'createWithColor'], + ]), + ); + + $container->get('car'); + } + /** * Measures collecting all services registered under a tag. * @@ -142,6 +206,36 @@ public function benchTaggedServices(): void $container->get(TagReference::id('engine')); } + /** + * Measures collecting all services registered under a tag without eager validation. + * + * @Groups({"lookup", "tag", "typical", "no-validation"}) + * @Revs(100) + */ + public function benchTaggedServicesWithoutValidation(): void + { + $container = new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions([ + EngineMarkOne::class => [ + 'class' => EngineMarkOne::class, + 'tags' => ['engine'], + ], + EngineMarkTwo::class => [ + 'class' => EngineMarkTwo::class, + 'tags' => ['engine'], + ], + 'engine-reference' => [ + 'definition' => Reference::to(EngineMarkOne::class), + 'tags' => ['engine'], + ], + ]), + ); + + $container->get(TagReference::id('engine')); + } + /** * Measures fallback through delegates configured on the container. * @@ -164,4 +258,29 @@ public function benchDelegateFallback(): void $container->get(EngineInterface::class); } + + /** + * Measures fallback through delegates configured on the container without eager validation. + * + * @Groups({"lookup", "delegate", "typical", "no-validation"}) + * @Revs(100) + */ + public function benchDelegateFallbackWithoutValidation(): void + { + $container = new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDelegates([ + static fn(ContainerInterface $container): ContainerInterface => new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions([ + EngineInterface::class => EngineMarkOne::class, + ]), + ), + ]), + ); + + $container->get(EngineInterface::class); + } } From 63e5e2c9fde14069344f0c5f7504c7a7934bfaa5 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 01:42:30 +0300 Subject: [PATCH 06/18] Use try-catch instead of has() and then get() --- src/Container.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Container.php b/src/Container.php index 6ea24439..ea5802d9 100644 --- a/src/Container.php +++ b/src/Container.php @@ -8,6 +8,7 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; +use RuntimeException; use Throwable; use Yiisoft\Definitions\ArrayDefinition; use Yiisoft\Definitions\DefinitionStorage; @@ -561,14 +562,15 @@ private function build(string $id): mixed return $this->getTaggedServices($id); } - // Check if the definition exists. - if (!$this->definitions->has($id)) { + try { + $definition = $this->definitions->get($id); + } catch (RuntimeException) { throw new NotFoundException($id, $this->definitions->getBuildStack()); } $this->building[$id] = 1; try { - $normalizedDefinition = DefinitionNormalizer::normalize($this->definitions->get($id), $id); + $normalizedDefinition = DefinitionNormalizer::normalize($definition, $id); $object = $normalizedDefinition->resolve($this->get(ContainerInterface::class)); } finally { unset($this->building[$id]); From a9818c385ac426fd115058cb515835013228dab2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 01:49:08 +0300 Subject: [PATCH 07/18] Call `DefinitionParser::parse` only if definition is an array --- src/Container.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Container.php b/src/Container.php index ea5802d9..6f0f7e27 100644 --- a/src/Container.php +++ b/src/Container.php @@ -259,7 +259,12 @@ private function prepareStateResetter(): StateResetter */ private function addDefinition(string $id, mixed $definition): void { - [$definition, $meta] = DefinitionParser::parse($definition); + if (is_array($definition)) { + [$definition, $meta] = DefinitionParser::parse($definition); + } else { + $meta = []; + } + if ($this->validate) { $this->validateDefinition($definition, $id); // Only validate meta if it's not empty. From 10ec99d98aac7a16a649308c7977a338a145f062 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 01:54:06 +0300 Subject: [PATCH 08/18] Inline addDefinition() --- src/Container.php | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Container.php b/src/Container.php index 6f0f7e27..ecf20a26 100644 --- a/src/Container.php +++ b/src/Container.php @@ -310,7 +310,39 @@ private function addDefinitions(array $config): void } /** @var string $id */ - $this->addDefinition($id, $definition); + if (is_array($definition)) { + [$definition, $meta] = DefinitionParser::parse($definition); + } else { + $meta = []; + } + + if ($this->validate) { + $this->validateDefinition($definition, $id); + // Only validate meta if it's not empty. + if ($meta !== []) { + $this->validateMeta($meta); + } + } + /** + * @psalm-var array{reset?:Closure,tags?:string[]} $meta + */ + + // Process meta only if it has tags or reset callback. + if (isset($meta[self::META_TAGS])) { + $this->setDefinitionTags($id, $meta[self::META_TAGS]); + } + if (isset($meta[self::META_RESET])) { + $this->setDefinitionResetter($id, $meta[self::META_RESET]); + } + + unset($this->instances[$id]); + $this->hasCache = []; + + $this->definitions->set($id, $definition); + + if ($id === StateResetter::class) { + $this->useResettersFromMeta = false; + } } } From c6dcb5c53f3e4e650426a4f8535e821451fe38d6 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 01:57:46 +0300 Subject: [PATCH 09/18] Batch-prepare definitions --- src/Container.php | 60 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/src/Container.php b/src/Container.php index ecf20a26..074956a1 100644 --- a/src/Container.php +++ b/src/Container.php @@ -91,16 +91,18 @@ public function __construct(?ContainerConfigInterface $config = null) { $config ??= ContainerConfig::create(); - $this->definitions = new DefinitionStorage( + $this->validate = $config->shouldValidate(); + $this->setTags($config->getTags()); + + $definitions = $this->prepareDefinitions( + $config->getDefinitions(), [ ContainerInterface::class => $this, StateResetter::class => StateResetter::class, ], - $config->useStrictMode(), ); - $this->validate = $config->shouldValidate(); - $this->setTags($config->getTags()); - $this->addDefinitions($config->getDefinitions()); + $this->definitions = new DefinitionStorage($definitions, $config->useStrictMode()); + $this->addProviders($config->getProviders()); $this->setDelegates($config->getDelegates()); } @@ -346,6 +348,54 @@ private function addDefinitions(array $config): void } } + private function prepareDefinitions(array $config, array $definitions): array + { + foreach ($config as $id => $definition) { + if ($this->validate && !is_string($id)) { + throw new InvalidConfigException( + sprintf( + 'Key must be a string. %s given.', + get_debug_type($id), + ), + ); + } + /** @var string $id */ + + if (is_array($definition)) { + [$definition, $meta] = DefinitionParser::parse($definition); + } else { + $meta = []; + } + + if ($this->validate) { + $this->validateDefinition($definition, $id); + // Only validate meta if it's not empty. + if ($meta !== []) { + $this->validateMeta($meta); + } + } + /** + * @psalm-var array{reset?:Closure,tags?:string[]} $meta + */ + + // Process meta only if it has tags or reset callback. + if (isset($meta[self::META_TAGS])) { + $this->setDefinitionTags($id, $meta[self::META_TAGS]); + } + if (isset($meta[self::META_RESET])) { + $this->setDefinitionResetter($id, $meta[self::META_RESET]); + } + + if ($id === StateResetter::class) { + $this->useResettersFromMeta = false; + } + + $definitions[$id] = $definition; + } + + return $definitions; + } + /** * Set container delegates. * From a77213df05d3b20c68cc5e9c571efe275306ab50 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 02:01:36 +0300 Subject: [PATCH 10/18] Add fast path for empty delegates --- src/Container.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Container.php b/src/Container.php index 074956a1..2b360bc7 100644 --- a/src/Container.php +++ b/src/Container.php @@ -408,6 +408,10 @@ private function setDelegates(array $delegates): void { $this->delegates = new CompositeContainer(); + if ($delegates === []) { + return; + } + $container = $this->get(ContainerInterface::class); foreach ($delegates as $delegate) { From 423766a3a1f22c2df8c47684b37c149152c31d63 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 02:07:56 +0300 Subject: [PATCH 11/18] Use separate code branches for preps with/without validation --- src/Container.php | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/Container.php b/src/Container.php index 2b360bc7..755dbc0a 100644 --- a/src/Container.php +++ b/src/Container.php @@ -350,8 +350,12 @@ private function addDefinitions(array $config): void private function prepareDefinitions(array $config, array $definitions): array { + if (!$this->validate) { + return $this->prepareDefinitionsWithoutValidation($config, $definitions); + } + foreach ($config as $id => $definition) { - if ($this->validate && !is_string($id)) { + if (!is_string($id)) { throw new InvalidConfigException( sprintf( 'Key must be a string. %s given.', @@ -367,12 +371,10 @@ private function prepareDefinitions(array $config, array $definitions): array $meta = []; } - if ($this->validate) { - $this->validateDefinition($definition, $id); - // Only validate meta if it's not empty. - if ($meta !== []) { - $this->validateMeta($meta); - } + $this->validateDefinition($definition, $id); + // Only validate meta if it's not empty. + if ($meta !== []) { + $this->validateMeta($meta); } /** * @psalm-var array{reset?:Closure,tags?:string[]} $meta @@ -396,6 +398,36 @@ private function prepareDefinitions(array $config, array $definitions): array return $definitions; } + private function prepareDefinitionsWithoutValidation(array $config, array $definitions): array + { + foreach ($config as $id => $definition) { + /** @var string $id */ + if (is_array($definition)) { + [$definition, $meta] = DefinitionParser::parse($definition); + + /** + * @psalm-var array{reset?:Closure,tags?:string[]} $meta + */ + + // Process meta only if it has tags or reset callback. + if (isset($meta[self::META_TAGS])) { + $this->setDefinitionTags($id, $meta[self::META_TAGS]); + } + if (isset($meta[self::META_RESET])) { + $this->setDefinitionResetter($id, $meta[self::META_RESET]); + } + } + + if ($id === StateResetter::class) { + $this->useResettersFromMeta = false; + } + + $definitions[$id] = $definition; + } + + return $definitions; + } + /** * Set container delegates. * From 28dbc9688cbc97254aa8ef0f05d2541e0857ade4 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 02:41:38 +0300 Subject: [PATCH 12/18] Fix edge cases --- src/CompositeContainer.php | 19 +++++++-- src/Container.php | 13 ++++++- tests/Unit/CompositeContainerTest.php | 55 +++++++++++++++++++++++++++ tests/Unit/ContainerTest.php | 46 ++++++++++++++++++++++ 4 files changed, 128 insertions(+), 5 deletions(-) diff --git a/src/CompositeContainer.php b/src/CompositeContainer.php index a64518be..9176326a 100644 --- a/src/CompositeContainer.php +++ b/src/CompositeContainer.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use RuntimeException; use Throwable; use Yiisoft\Di\Reference\TagReference; @@ -79,8 +80,16 @@ public function get($id) } if (isset($this->lookupCache[$id], $this->containers[$this->lookupCache[$id]])) { - /** @psalm-suppress MixedReturnStatement */ - return $this->containers[$this->lookupCache[$id]]->get($id); + $index = $this->lookupCache[$id]; + $container = $this->containers[$index]; + if ($container->has($id)) { + try { + /** @psalm-suppress MixedReturnStatement */ + return $container->get($id); + } catch (NotFoundExceptionInterface) { + } + } + unset($this->lookupCache[$id]); } foreach ($this->containers as $index => $container) { @@ -144,7 +153,11 @@ public function has($id): bool } if (isset($this->lookupCache[$id], $this->containers[$this->lookupCache[$id]])) { - return true; + $index = $this->lookupCache[$id]; + if ($this->containers[$index]->has($id)) { + return true; + } + unset($this->lookupCache[$id]); } foreach ($this->containers as $index => $container) { diff --git a/src/Container.php b/src/Container.php index 755dbc0a..03e9cf1c 100644 --- a/src/Container.php +++ b/src/Container.php @@ -301,8 +301,10 @@ private function addDefinition(string $id, mixed $definition): void */ private function addDefinitions(array $config): void { + $this->hasCache = []; + foreach ($config as $id => $definition) { - if ($this->validate && !is_string($id)) { + if (!is_string($id)) { throw new InvalidConfigException( sprintf( 'Key must be a string. %s given.', @@ -338,7 +340,6 @@ private function addDefinitions(array $config): void } unset($this->instances[$id]); - $this->hasCache = []; $this->definitions->set($id, $definition); @@ -401,6 +402,14 @@ private function prepareDefinitions(array $config, array $definitions): array private function prepareDefinitionsWithoutValidation(array $config, array $definitions): array { foreach ($config as $id => $definition) { + if (!is_string($id)) { + throw new InvalidConfigException( + sprintf( + 'Key must be a string. %s given.', + get_debug_type($id), + ), + ); + } /** @var string $id */ if (is_array($definition)) { [$definition, $meta] = DefinitionParser::parse($definition); diff --git a/tests/Unit/CompositeContainerTest.php b/tests/Unit/CompositeContainerTest.php index 96628127..1c637369 100644 --- a/tests/Unit/CompositeContainerTest.php +++ b/tests/Unit/CompositeContainerTest.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use Yiisoft\Di\CompositeContainer; use Yiisoft\Di\CompositeNotFoundException; use Yiisoft\Di\Container; @@ -14,8 +15,10 @@ use Yiisoft\Di\Tests\Support\EngineMarkOne; use Yiisoft\Di\Tests\Support\EngineMarkTwo; use Yiisoft\Di\Tests\Support\NonPsrContainer; +use Yiisoft\Test\Support\Container\Exception\NotFoundException; use Yiisoft\Test\Support\Container\SimpleContainer; +use function array_key_exists; use function PHPUnit\Framework\assertFalse; use function PHPUnit\Framework\assertSame; @@ -110,4 +113,56 @@ public function testHasTagWithoutYiiContainer(): void assertFalse($container->has('tag@engine')); } + + public function testHasRechecksCachedContainer(): void + { + $container = new CompositeContainer(); + $delegate = new MutableContainer([ + 'engine' => new EngineMarkOne(), + ]); + + $container->attach($delegate); + + $this->assertTrue($container->has('engine')); + unset($delegate->definitions['engine']); + + assertFalse($container->has('engine')); + } + + public function testGetFallsBackWhenCachedContainerNoLongerHasDefinition(): void + { + $container = new CompositeContainer(); + $firstDelegate = new MutableContainer(['engine' => new EngineMarkOne()]); + $secondDelegate = new MutableContainer(['engine' => new EngineMarkTwo()]); + + $container->attach($firstDelegate); + $container->attach($secondDelegate); + + $this->assertInstanceOf(EngineMarkOne::class, $container->get('engine')); + unset($firstDelegate->definitions['engine']); + + $this->assertInstanceOf(EngineMarkTwo::class, $container->get('engine')); + } +} + +final class MutableContainer implements ContainerInterface +{ + public function __construct( + public array $definitions, + ) { + } + + public function get($id) + { + if (!$this->has($id)) { + throw new NotFoundException($id); + } + + return $this->definitions[$id]; + } + + public function has($id): bool + { + return array_key_exists($id, $this->definitions); + } } diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index 670780d8..840d6468 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -110,6 +110,52 @@ public function testIntegerKeys(): void $container->get(Car::class); } + public function testIntegerKeysWithoutValidation(): void + { + $this->expectException(InvalidConfigException::class); + $this->expectExceptionMessage('Key must be a string. int given.'); + + new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions([ + [ + 'class' => EngineMarkOne::class, + 'tags' => ['engine'], + ], + ]), + ); + } + + public function testIntegerKeysInProviderWithoutValidation(): void + { + $this->expectException(InvalidConfigException::class); + $this->expectExceptionMessage('Key must be a string. int given.'); + + $provider = new class implements ServiceProviderInterface { + public function getDefinitions(): array + { + return [ + [ + 'class' => EngineMarkOne::class, + 'tags' => ['engine'], + ], + ]; + } + + public function getExtensions(): array + { + return []; + } + }; + + new Container( + ContainerConfig::create() + ->withValidate(false) + ->withProviders([$provider]), + ); + } + public function testNullableClassDependency(): void { $container = new Container(); From ae231b52ccc819e0c26e0c56c2ba8e26debd1f7f Mon Sep 17 00:00:00 2001 From: samdark <47294+samdark@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:42:16 +0000 Subject: [PATCH 13/18] Apply PHP CS Fixer and Rector changes (CI) --- tests/Unit/CompositeContainerTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Unit/CompositeContainerTest.php b/tests/Unit/CompositeContainerTest.php index 1c637369..120c5676 100644 --- a/tests/Unit/CompositeContainerTest.php +++ b/tests/Unit/CompositeContainerTest.php @@ -149,8 +149,7 @@ final class MutableContainer implements ContainerInterface { public function __construct( public array $definitions, - ) { - } + ) {} public function get($id) { From 821c585d9aeba8396498a83b6b337730335e9051 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 02:49:04 +0300 Subject: [PATCH 14/18] Raise code coverage --- tests/Unit/ContainerTest.php | 88 +++++++++++++++++++++ tests/Unit/Helpers/DefinitionParserTest.php | 8 ++ 2 files changed, 96 insertions(+) diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index 840d6468..a51558a0 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -156,6 +156,67 @@ public function getExtensions(): array ); } + public function testMetadataWithoutValidation(): void + { + $container = new Container( + ContainerConfig::create() + ->withValidate(false) + ->withDefinitions([ + EngineMarkOne::class => [ + 'class' => EngineMarkOne::class, + 'tags' => ['engine'], + 'reset' => function (): void { + $this->number = 7; + }, + ], + StateResetter::class => StateResetter::class, + ]), + ); + + $engines = $container->get('tag@engine'); + + $this->assertIsArray($engines); + $this->assertSame(EngineMarkOne::class, $engines[0]::class); + } + + public function testMetadataInProviderDefinitions(): void + { + $provider = new class implements ServiceProviderInterface { + public function getDefinitions(): array + { + return [ + EngineMarkOne::class => [ + 'class' => EngineMarkOne::class, + 'tags' => ['engine'], + 'reset' => function (): void { + $this->number = 7; + }, + ], + ]; + } + + public function getExtensions(): array + { + return []; + } + }; + + $container = new Container( + ContainerConfig::create() + ->withProviders([$provider]), + ); + + $engine = $container->get(EngineMarkOne::class); + $engine->setNumber(42); + $container->get(StateResetter::class)->reset(); + + $engines = $container->get('tag@engine'); + + $this->assertSame(7, $engine->getNumber()); + $this->assertIsArray($engines); + $this->assertSame($engine, $engines[0]); + } + public function testNullableClassDependency(): void { $container = new Container(); @@ -731,6 +792,33 @@ public function testFalsePositiveCircularReferenceWithStringID(): void } } + public function testAddDefinitionWithMetadata(): void + { + $container = new Container(); + + (fn(string $id, $definition) => $this->addDefinition($id, $definition))->call( + $container, + EngineMarkOne::class, + [ + 'class' => EngineMarkOne::class, + 'tags' => ['engine'], + 'reset' => function (): void { + $this->number = 7; + }, + ], + ); + + $engine = $container->get(EngineMarkOne::class); + $engine->setNumber(42); + $container->get(StateResetter::class)->reset(); + + $engines = $container->get('tag@engine'); + + $this->assertSame(7, $engine->getNumber()); + $this->assertIsArray($engines); + $this->assertSame($engine, $engines[0]); + } + public function testCallable(): void { $config = ContainerConfig::create() diff --git a/tests/Unit/Helpers/DefinitionParserTest.php b/tests/Unit/Helpers/DefinitionParserTest.php index e5f9485a..5beaf406 100644 --- a/tests/Unit/Helpers/DefinitionParserTest.php +++ b/tests/Unit/Helpers/DefinitionParserTest.php @@ -11,6 +11,14 @@ final class DefinitionParserTest extends TestCase { + public function testParseNonArrayDefinition(): void + { + [$definition, $meta] = DefinitionParser::parse(EngineMarkOne::class); + + $this->assertSame(EngineMarkOne::class, $definition); + $this->assertSame([], $meta); + } + public function testParseCallableDefinition(): void { $fn = static fn() => new EngineMarkOne(); From 3da230f9e77f45c92fa839de04377f6bf0f09174 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 02:53:11 +0300 Subject: [PATCH 15/18] Cap has() cache to avoid using too much memory --- src/Container.php | 14 ++++++++++++-- tests/Unit/ContainerTest.php | 13 +++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Container.php b/src/Container.php index 03e9cf1c..ff86e21b 100644 --- a/src/Container.php +++ b/src/Container.php @@ -39,6 +39,7 @@ final class Container implements ContainerInterface private const META_TAGS = 'tags'; private const META_RESET = 'reset'; private const ALLOWED_META = [self::META_TAGS, self::META_RESET]; + private const HAS_CACHE_LIMIT = 1024; /** * @var DefinitionStorage Storage of object definitions. @@ -132,10 +133,19 @@ public function has(string $id): bool if (TagReference::isTagAlias($id)) { $tag = TagReference::extractTagFromAlias($id); - return $this->hasCache[$id] = isset($this->tags[$tag]); + return $this->cacheHasResult($id, isset($this->tags[$tag])); } - return $this->hasCache[$id] = false; + return $this->cacheHasResult($id, false); + } + + private function cacheHasResult(string $id, bool $result): bool + { + if (count($this->hasCache) >= self::HAS_CACHE_LIMIT) { + $this->hasCache = []; + } + + return $this->hasCache[$id] = $result; } /** diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index a51558a0..6b0f91d9 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -287,6 +287,19 @@ public function testHas(bool $expected, $id): void $this->assertSame($expected, $container->has($id)); } + public function testHasCacheIsBounded(): void + { + $container = new Container(); + + for ($i = 0; $i < 1_100; $i++) { + $container->has('missing-service-' . $i); + } + + $cacheSize = (fn(): int => count($this->hasCache))->call($container); + + $this->assertLessThanOrEqual(1024, $cacheSize); + } + public static function dataUnionTypes(): array { return [ From ff3830df3878a6005b317b56a40bc0b00791efdd Mon Sep 17 00:00:00 2001 From: samdark <47294+samdark@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:55:51 +0000 Subject: [PATCH 16/18] Apply PHP CS Fixer and Rector changes (CI) --- src/Container.php | 19 ++++++++++--------- tests/Unit/ContainerTest.php | 1 + 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Container.php b/src/Container.php index ff86e21b..938dcccc 100644 --- a/src/Container.php +++ b/src/Container.php @@ -30,6 +30,7 @@ use function is_string; use function sprintf; use function trim; +use function count; /** * Container implements a [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) container. @@ -139,15 +140,6 @@ public function has(string $id): bool return $this->cacheHasResult($id, false); } - private function cacheHasResult(string $id, bool $result): bool - { - if (count($this->hasCache) >= self::HAS_CACHE_LIMIT) { - $this->hasCache = []; - } - - return $this->hasCache[$id] = $result; - } - /** * Returns an instance by either interface name or alias. * @@ -227,6 +219,15 @@ public function get(string $id) return $this->instances[$id]; } + private function cacheHasResult(string $id, bool $result): bool + { + if (count($this->hasCache) >= self::HAS_CACHE_LIMIT) { + $this->hasCache = []; + } + + return $this->hasCache[$id] = $result; + } + private function prepareStateResetter(): StateResetter { $delegatesResetter = null; diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index 6b0f91d9..e693fb2b 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -57,6 +57,7 @@ use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertSame; +use function count; /** * ContainerTest contains tests for \Yiisoft\Di\Container From 7ff3356dadb8ff308bc82c056de1158e6fdc99e4 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 24 Apr 2026 03:17:20 +0300 Subject: [PATCH 17/18] Make cache cap configurable, add README  Conflicts:  src/Container.php --- CHANGELOG.md | 1 + README.md | 14 +++++++++ src/Container.php | 31 +++++++++++-------- src/ContainerConfig.php | 24 +++++++++++++++ src/ContainerConfigInterface.php | 5 +++ tests/Unit/ContainerTest.php | 52 +++++++++++++++++++++++++++++++- 6 files changed, 113 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e461ee4..19e5b358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 1.4.2 under development +- Enh #400: Improve container performance and make `has()` cache limit configurable (@samdark) - Enh #397: Explicitly import functions in "use" section (@mspirkov) ## 1.4.1 December 01, 2025 diff --git a/README.md b/README.md index 5ec3c586..705a371b 100644 --- a/README.md +++ b/README.md @@ -617,6 +617,20 @@ $config = ContainerConfig::create() $container = new Container($config); ``` +The container caches `has()` results to speed up repeated lookups. The cache is limited to 1024 entries by default. +If your application checks many dynamic service IDs in a long-running process, you can adjust the limit or disable the +cache: + +```php +use Yiisoft\Di\Container; +use Yiisoft\Di\ContainerConfig; + +$config = ContainerConfig::create() + ->withHasCacheLimit(0); // Disable `has()` cache. + +$container = new Container($config); +``` + ## Strict mode Container may work in a strict mode, that's when you should define everything in the container explicitly. diff --git a/src/Container.php b/src/Container.php index 938dcccc..e99bb242 100644 --- a/src/Container.php +++ b/src/Container.php @@ -22,6 +22,7 @@ use function array_key_exists; use function array_keys; +use function count; use function implode; use function in_array; use function is_array; @@ -30,7 +31,6 @@ use function is_string; use function sprintf; use function trim; -use function count; /** * Container implements a [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) container. @@ -40,7 +40,6 @@ final class Container implements ContainerInterface private const META_TAGS = 'tags'; private const META_RESET = 'reset'; private const ALLOWED_META = [self::META_TAGS, self::META_RESET]; - private const HAS_CACHE_LIMIT = 1024; /** * @var DefinitionStorage Storage of object definitions. @@ -57,6 +56,7 @@ final class Container implements ContainerInterface * @var bool $validate If definitions should be validated. */ private readonly bool $validate; + private readonly int $hasCacheLimit; /** * @var array Cached instances. @@ -94,6 +94,7 @@ public function __construct(?ContainerConfigInterface $config = null) $config ??= ContainerConfig::create(); $this->validate = $config->shouldValidate(); + $this->hasCacheLimit = $config->getHasCacheLimit(); $this->setTags($config->getTags()); $definitions = $this->prepareDefinitions( @@ -126,10 +127,10 @@ public function has(string $id): bool try { if ($this->definitions->has($id)) { - return $this->hasCache[$id] = true; + return $this->cacheHasResult($id, true); } } catch (CircularReferenceException) { - return $this->hasCache[$id] = true; + return $this->cacheHasResult($id, true); } if (TagReference::isTagAlias($id)) { @@ -140,6 +141,19 @@ public function has(string $id): bool return $this->cacheHasResult($id, false); } + private function cacheHasResult(string $id, bool $result): bool + { + if ($this->hasCacheLimit === 0) { + return $result; + } + + if (count($this->hasCache) >= $this->hasCacheLimit) { + $this->hasCache = []; + } + + return $this->hasCache[$id] = $result; + } + /** * Returns an instance by either interface name or alias. * @@ -219,15 +233,6 @@ public function get(string $id) return $this->instances[$id]; } - private function cacheHasResult(string $id, bool $result): bool - { - if (count($this->hasCache) >= self::HAS_CACHE_LIMIT) { - $this->hasCache = []; - } - - return $this->hasCache[$id] = $result; - } - private function prepareStateResetter(): StateResetter { $delegatesResetter = null; diff --git a/src/ContainerConfig.php b/src/ContainerConfig.php index b324e9dc..882a2ec6 100644 --- a/src/ContainerConfig.php +++ b/src/ContainerConfig.php @@ -4,15 +4,20 @@ namespace Yiisoft\Di; +use InvalidArgumentException; + /** * Container configuration. */ final class ContainerConfig implements ContainerConfigInterface { + public const DEFAULT_HAS_CACHE_LIMIT = 1024; + private array $definitions = []; private array $providers = []; private array $tags = []; private bool $validate = true; + private int $hasCacheLimit = self::DEFAULT_HAS_CACHE_LIMIT; private array $delegates = []; private bool $useStrictMode = false; @@ -83,6 +88,25 @@ public function shouldValidate(): bool return $this->validate; } + /** + * @param int $limit Maximum number of cached `has()` results. `0` disables the cache. + */ + public function withHasCacheLimit(int $limit): self + { + if ($limit < 0) { + throw new InvalidArgumentException('Has cache limit must be greater than or equal to 0.'); + } + + $new = clone $this; + $new->hasCacheLimit = $limit; + return $new; + } + + public function getHasCacheLimit(): int + { + return $this->hasCacheLimit; + } + /** * @param array $delegates Container delegates. Each delegate is a callable in format * `function (ContainerInterface $container): ContainerInterface`. The container instance returned is used diff --git a/src/ContainerConfigInterface.php b/src/ContainerConfigInterface.php index 6be10e34..6f388fc9 100644 --- a/src/ContainerConfigInterface.php +++ b/src/ContainerConfigInterface.php @@ -29,6 +29,11 @@ public function getTags(): array; */ public function shouldValidate(): bool; + /** + * @return int Maximum number of cached `has()` results. `0` disables the cache. + */ + public function getHasCacheLimit(): int; + /** * @return array Container delegates. Each delegate is a callable in format * `function (ContainerInterface $container): ContainerInterface`. The container instance returned is used diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index e693fb2b..928702cc 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -290,10 +290,21 @@ public function testHas(bool $expected, $id): void public function testHasCacheIsBounded(): void { - $container = new Container(); + $definitions = []; + for ($i = 0; $i < 1_100; $i++) { + $definitions['existing-service-' . $i] = EngineMarkOne::class; + } + + $container = new Container( + ContainerConfig::create() + ->withDefinitions($definitions), + ); + + $this->assertSame(1024, ContainerConfig::create()->getHasCacheLimit()); for ($i = 0; $i < 1_100; $i++) { $container->has('missing-service-' . $i); + $container->has('existing-service-' . $i); } $cacheSize = (fn(): int => count($this->hasCache))->call($container); @@ -301,6 +312,45 @@ public function testHasCacheIsBounded(): void $this->assertLessThanOrEqual(1024, $cacheSize); } + public function testHasCacheLimitIsConfigurable(): void + { + $container = new Container( + ContainerConfig::create() + ->withHasCacheLimit(2), + ); + + $container->has('missing-service-1'); + $container->has('missing-service-2'); + $container->has('missing-service-3'); + + $cacheSize = (fn(): int => count($this->hasCache))->call($container); + + $this->assertSame(1, $cacheSize); + } + + public function testHasCacheCanBeDisabled(): void + { + $config = ContainerConfig::create() + ->withHasCacheLimit(0); + $container = new Container($config); + + $this->assertSame(0, $config->getHasCacheLimit()); + $this->assertFalse($container->has('missing-service')); + + $cacheSize = (fn(): int => count($this->hasCache))->call($container); + + $this->assertSame(0, $cacheSize); + } + + public function testHasCacheLimitCanNotBeNegative(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Has cache limit must be greater than or equal to 0.'); + + ContainerConfig::create() + ->withHasCacheLimit(-1); + } + public static function dataUnionTypes(): array { return [ From cc81a0af8455c17d647928263675a058b8f87907 Mon Sep 17 00:00:00 2001 From: samdark <47294+samdark@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:18:16 +0000 Subject: [PATCH 18/18] Apply PHP CS Fixer and Rector changes (CI) --- src/Container.php | 26 +++++++++++++------------- tests/Unit/ContainerTest.php | 3 ++- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Container.php b/src/Container.php index e99bb242..ad818cc7 100644 --- a/src/Container.php +++ b/src/Container.php @@ -141,19 +141,6 @@ public function has(string $id): bool return $this->cacheHasResult($id, false); } - private function cacheHasResult(string $id, bool $result): bool - { - if ($this->hasCacheLimit === 0) { - return $result; - } - - if (count($this->hasCache) >= $this->hasCacheLimit) { - $this->hasCache = []; - } - - return $this->hasCache[$id] = $result; - } - /** * Returns an instance by either interface name or alias. * @@ -233,6 +220,19 @@ public function get(string $id) return $this->instances[$id]; } + private function cacheHasResult(string $id, bool $result): bool + { + if ($this->hasCacheLimit === 0) { + return $result; + } + + if (count($this->hasCache) >= $this->hasCacheLimit) { + $this->hasCache = []; + } + + return $this->hasCache[$id] = $result; + } + private function prepareStateResetter(): StateResetter { $delegatesResetter = null; diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index 928702cc..2cd4c894 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -54,6 +54,7 @@ use Yiisoft\Definitions\Reference; use Yiisoft\Injector\Injector; use Yiisoft\Test\Support\Container\SimpleContainer; +use InvalidArgumentException; use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertSame; @@ -344,7 +345,7 @@ public function testHasCacheCanBeDisabled(): void public function testHasCacheLimitCanNotBeNegative(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Has cache limit must be greater than or equal to 0.'); ContainerConfig::create()