From 9ffa5786adc501c74c81063116d0729785f2e031 Mon Sep 17 00:00:00 2001 From: Anton Ukhanev Date: Sat, 21 Mar 2026 20:38:49 +0100 Subject: [PATCH 1/6] Add missing test for advertised feature --- tests/unit/Factories/ServiceListTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/unit/Factories/ServiceListTest.php b/tests/unit/Factories/ServiceListTest.php index f73a971..efa49cf 100644 --- a/tests/unit/Factories/ServiceListTest.php +++ b/tests/unit/Factories/ServiceListTest.php @@ -53,6 +53,27 @@ public function testInvoke() static::assertEquals($values, $result); } + public function testInvokeAssoc() + { + $services = [ + 'foo' => 'hello', + 'bar' => 'world', + ]; + + $map = array_combine([ + 'alpha', + 'beta', + ], array_keys($services)); + $values = array_values($services); + + $container = MockContainer::with($this, $services); + + $subject = new ServiceList($map); + $result = $subject($container); + + static::assertEquals(array_combine($map, $values), $result); + } + /** * @since [*next-version*] */ From d9c95e81f491c24b73ccced774541565568ff404 Mon Sep 17 00:00:00 2001 From: Anton Ukhanev Date: Sat, 21 Mar 2026 21:06:11 +0100 Subject: [PATCH 2/6] Fix test --- tests/unit/Factories/ServiceListTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/Factories/ServiceListTest.php b/tests/unit/Factories/ServiceListTest.php index efa49cf..9172f01 100644 --- a/tests/unit/Factories/ServiceListTest.php +++ b/tests/unit/Factories/ServiceListTest.php @@ -60,18 +60,18 @@ public function testInvokeAssoc() 'bar' => 'world', ]; + $values = array_values($services); $map = array_combine([ 'alpha', 'beta', ], array_keys($services)); - $values = array_values($services); $container = MockContainer::with($this, $services); $subject = new ServiceList($map); $result = $subject($container); - static::assertEquals(array_combine($map, $values), $result); + static::assertEquals(array_combine(array_keys($map), $values), $result); } /** From c727d087d42d046eb9f3607eea62399f470aa36b Mon Sep 17 00:00:00 2001 From: Anton Ukhanev Date: Sat, 21 Mar 2026 21:34:48 +0100 Subject: [PATCH 3/6] Helper now throws with useful message --- tests/helpers/MockContainer.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/helpers/MockContainer.php b/tests/helpers/MockContainer.php index 36262c6..b3c7e74 100644 --- a/tests/helpers/MockContainer.php +++ b/tests/helpers/MockContainer.php @@ -50,7 +50,8 @@ public static function with(TestCase $tCase, array $services) throw new ((string) (new ClassBuilder()) ->withExtends(Exception::class) ->withImplements([NotFoundExceptionInterface::class]) - )(); + + )("Key '{$key}' does not exist"); } return $services[$key]; From 3cde77ba50a299a11c2715819031a1f385f66199 Mon Sep 17 00:00:00 2001 From: Anton Ukhanev Date: Sat, 21 Mar 2026 21:35:41 +0100 Subject: [PATCH 4/6] Resolution now preserves keys (#6) --- src/Extension.php | 2 +- src/Factories/Constructor.php | 2 +- src/Factories/ServiceList.php | 19 +----- src/Factories/ServiceMap.php | 49 ++++++++++++++ src/Factory.php | 2 +- src/ResolveKeysCapableTrait.php | 9 +-- tests/unit/Extensions/ArrayExtensionTest.php | 3 +- tests/unit/Factories/ServiceListTest.php | 21 ------ tests/unit/Factories/ServiceMapTest.php | 68 ++++++++++++++++++++ tests/unit/ResolveKeysCapableTraitTest.php | 4 +- 10 files changed, 130 insertions(+), 49 deletions(-) create mode 100644 src/Factories/ServiceMap.php create mode 100644 tests/unit/Factories/ServiceMapTest.php diff --git a/src/Extension.php b/src/Extension.php index 807db14..f6fc6fa 100644 --- a/src/Extension.php +++ b/src/Extension.php @@ -57,7 +57,7 @@ public function __construct(array $dependencies, callable $definition) #[\Override] public function __invoke(ContainerInterface $c, mixed $prev = null): mixed { - $deps = $this->resolveDeps($c, $this->dependencies); + $deps = array_values($this->resolveDeps($c, $this->dependencies)); array_unshift($deps, $prev); return ($this->definition)(...$deps); diff --git a/src/Factories/Constructor.php b/src/Factories/Constructor.php index 4bf7e82..4105b10 100644 --- a/src/Factories/Constructor.php +++ b/src/Factories/Constructor.php @@ -60,7 +60,7 @@ public function __construct(string $className, array $dependencies = []) #[\Override] public function __invoke(ContainerInterface $c): object { - $deps = $this->resolveDeps($c, $this->dependencies); + $deps = array_values($this->resolveDeps($c, $this->dependencies)); $className = $this->className; /** @psalm-suppress MixedMethodCall Cannot guarantee any particular class, just that it's a class */ diff --git a/src/Factories/ServiceList.php b/src/Factories/ServiceList.php index da0226d..f4a14dc 100644 --- a/src/Factories/ServiceList.php +++ b/src/Factories/ServiceList.php @@ -32,22 +32,7 @@ * $list = $c->get('list'); // [5, "hello", 1.61803] * ``` * - * The array of service keys may also be associative. The array keys will be preserved in the result. - * - * ``` - * [ - * 'foo' => Value(5), - * 'bar' => Value("hello"), - * - * 'config' => new ServiceList([ - * 'num' => 'foo', - * 'msg' => 'bar' - * ]), - * ] - * - * $list = $c->get('list'); // ['num' => 5, 'msg' => "hello"] - * ``` - * + * @deprecated Use {@see ServiceMap} with {@see array_values()} instead. */ class ServiceList extends Service { @@ -61,6 +46,6 @@ class ServiceList extends Service #[\Override] public function __invoke(ContainerInterface $c) { - return $this->resolveDeps($c, $this->dependencies); + return array_values($this->resolveDeps($c, $this->dependencies)); } } diff --git a/src/Factories/ServiceMap.php b/src/Factories/ServiceMap.php new file mode 100644 index 0000000..064be6d --- /dev/null +++ b/src/Factories/ServiceMap.php @@ -0,0 +1,49 @@ + Value(5), + * 'bar' => Value("hello"), + * + * 'map' => new ServiceMap([ + * 'num' => 'foo', + * 'msg' => 'bar' + * ]), + * ] + * + * $map = $c->get('map'); // ['num' => 5, 'msg' => "hello"] + * ``` + * + */ +class ServiceMap extends Service +{ + use ResolveKeysCapableTrait; + + /** + * @inheritDoc + * + * @throws ContainerExceptionInterface If problem resolving from container. + */ + #[\Override] + public function __invoke(ContainerInterface $c) + { + return $this->resolveDeps($c, $this->dependencies); + } +} diff --git a/src/Factory.php b/src/Factory.php index 6c18351..bbe903e 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -54,7 +54,7 @@ public function __construct(array $dependencies, callable $definition) #[\Override] public function __invoke(ContainerInterface $c) { - $deps = $this->resolveDeps($c, $this->dependencies); + $deps = array_values($this->resolveDeps($c, $this->dependencies)); return ($this->definition)(...$deps); } diff --git a/src/ResolveKeysCapableTrait.php b/src/ResolveKeysCapableTrait.php index 86b16e2..495a6d0 100644 --- a/src/ResolveKeysCapableTrait.php +++ b/src/ResolveKeysCapableTrait.php @@ -21,7 +21,7 @@ trait ResolveKeysCapableTrait * @param array $keys The services keys to resolve. * @psalm-param array $keys * - * @return array A list containing the resolved service values, in the same order as in $keys. + * @return array A list containing the resolved service values, same order and keys as in $keys. * * @throws ContainerExceptionInterface If problem resolving from container. */ @@ -37,16 +37,17 @@ protected function resolveKeys(ContainerInterface $c, array $keys): array * @param array $deps The list of dependencies, where each is either a callable definitions or key. * @psalm-param ServiceRef[] $deps * - * @return array A list containing the resolved dependencies, in the same order as given in $keys. + * @return array A list containing the resolved service values, same order and keys as in $keys. + * If a dep is scalar and the key isn't a srin * * @throws ContainerExceptionInterface If problem resolving from container. */ protected function resolveDeps(ContainerInterface $c, array $deps): array { $result = []; - foreach ($deps as $dep) { + foreach ($deps as $key => $dep) { /** @psalm-suppress MixedAssignment We can't know the type that will be resolved */ - $result[] = $this->resolveSingleDep($c, $dep); + $result[is_scalar($dep) && !is_string($key) ? strval($dep) : $key] = $this->resolveSingleDep($c, $dep); } return $result; diff --git a/tests/unit/Extensions/ArrayExtensionTest.php b/tests/unit/Extensions/ArrayExtensionTest.php index 2be7a54..721909f 100644 --- a/tests/unit/Extensions/ArrayExtensionTest.php +++ b/tests/unit/Extensions/ArrayExtensionTest.php @@ -49,14 +49,13 @@ public function testInvoke() ]; $keys = array_keys($services); - $values = array_values($services); $container = MockContainer::with($this, $services); $subject = new ArrayExtension($keys); $result = $subject($container, $prev); - static::assertEquals(array_merge($prev, $values), $result); + static::assertEquals(array_merge($prev, $services), $result); } /** diff --git a/tests/unit/Factories/ServiceListTest.php b/tests/unit/Factories/ServiceListTest.php index 9172f01..f73a971 100644 --- a/tests/unit/Factories/ServiceListTest.php +++ b/tests/unit/Factories/ServiceListTest.php @@ -53,27 +53,6 @@ public function testInvoke() static::assertEquals($values, $result); } - public function testInvokeAssoc() - { - $services = [ - 'foo' => 'hello', - 'bar' => 'world', - ]; - - $values = array_values($services); - $map = array_combine([ - 'alpha', - 'beta', - ], array_keys($services)); - - $container = MockContainer::with($this, $services); - - $subject = new ServiceList($map); - $result = $subject($container); - - static::assertEquals(array_combine(array_keys($map), $values), $result); - } - /** * @since [*next-version*] */ diff --git a/tests/unit/Factories/ServiceMapTest.php b/tests/unit/Factories/ServiceMapTest.php new file mode 100644 index 0000000..35ab858 --- /dev/null +++ b/tests/unit/Factories/ServiceMapTest.php @@ -0,0 +1,68 @@ +getDependencies()); + } + + public function testInvoke() + { + $services = [ + 'foo' => 'hello', + 'bar' => 'world', + ]; + + $values = array_values($services); + $map = array_combine([ + 'alpha', + 'beta', + ], array_keys($services)); + + $container = MockContainer::with($this, $services); + + $subject = new ServiceMap($map); + $result = $subject($container); + + static::assertEquals(array_combine(array_keys($map), $values), $result); + } + + /** + * @since [*next-version*] + */ + public function testInvokeNoDeps() + { + $container = MockContainer::create($this); + + $subject = new ServiceMap([]); + $result = $subject($container); + + static::assertEmpty($result); + } +} diff --git a/tests/unit/ResolveKeysCapableTraitTest.php b/tests/unit/ResolveKeysCapableTraitTest.php index 20848f2..5216e2a 100644 --- a/tests/unit/ResolveKeysCapableTraitTest.php +++ b/tests/unit/ResolveKeysCapableTraitTest.php @@ -76,9 +76,9 @@ public function testResolveDeps() $method = AccessibleMethod::create($subject, 'resolveDeps'); // The test - $deps = ['foo', $depService, $depCallable]; + $deps = ['a' => 'foo', 'b' => $depService, 'c' => $depCallable]; $result = $method($container, $deps); - $expected = [$values[0], $depValue1, $depValue2]; + $expected = ['a' => $values[0], 'b' => $depValue1, 'c' => $depValue2]; static::assertEquals($expected, $result); } From cc100651c46dd0db4270e64308db4aa37c92f5ff Mon Sep 17 00:00:00 2001 From: Anton Ukhanev Date: Sat, 21 Mar 2026 21:36:47 +0100 Subject: [PATCH 5/6] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f573d00..c6687ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [[*next-version*]] - YYYY-MM-DD ### Changed - Drop PHP 7 and PHP 8.0 support, now requires PHP 8.1+ (#23). +- Service resolution now preserves keys, unless expected otherwise (#24). + +### Deprecated +- `ServiceList` deprecated in favour of new `ServiceMap` (#24). ## [0.1.1-alpha3] - 2023-02-01 ### Added From 7661fc254bbbd3dad3be6ffa2d788fc2311755a0 Mon Sep 17 00:00:00 2001 From: Anton Ukhanev Date: Sat, 21 Mar 2026 21:41:47 +0100 Subject: [PATCH 6/6] Fix missing detail --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6687ef..c314c21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [[*next-version*]] - YYYY-MM-DD +### Added +- Support for `psr/container:^2.0` (#22). + ### Changed - Drop PHP 7 and PHP 8.0 support, now requires PHP 8.1+ (#23). - Service resolution now preserves keys, unless expected otherwise (#24).