From c91e38a79fe39ce811586d60b130f5ee9fe4955b Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 24 Mar 2024 15:16:11 +0100 Subject: [PATCH 1/3] allow to use an enum case to register a service --- .gitattributes | 1 + composer.json | 3 ++- fixtures/Services.php | 9 +++++++++ src/Builder.php | 6 +++++- src/Container.php | 6 +++++- tests/ContainerTest.php | 11 +++++++++++ 6 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 fixtures/Services.php diff --git a/.gitattributes b/.gitattributes index 18e14aa..c25dee4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ /tests export-ignore +/fixtures export-ignore diff --git a/composer.json b/composer.json index ebc9cb0..e6c5a2f 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ }, "autoload-dev": { "psr-4": { - "Tests\\Innmind\\DI\\": "tests/" + "Tests\\Innmind\\DI\\": "tests/", + "Fixtures\\Innmind\\DI\\": "fixtures/" } }, "require-dev": { diff --git a/fixtures/Services.php b/fixtures/Services.php new file mode 100644 index 0000000..13e9c06 --- /dev/null +++ b/fixtures/Services.php @@ -0,0 +1,9 @@ +definitions; $definitions[$name] = $definition; diff --git a/src/Container.php b/src/Container.php index 47af15d..171c122 100644 --- a/src/Container.php +++ b/src/Container.php @@ -31,8 +31,12 @@ private function __construct(array $definitions) * @throws ServiceNotFound * @throws CircularDependency */ - public function __invoke(string $name): object + public function __invoke(string|\UnitEnum $name): object { + if ($name instanceof \UnitEnum) { + $name = \spl_object_hash($name); + } + if (!\array_key_exists($name, $this->definitions)) { throw new ServiceNotFound($name); } diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 8892eb5..b42a40e 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -14,6 +14,7 @@ PHPUnit\Framework\TestCase, Set, }; +use Fixtures\Innmind\DI\Services; class ContainerTest extends TestCase { @@ -99,4 +100,14 @@ public function testCircularDependenciesAreIntercepted() } }); } + + public function testEnumCaseCanBeUsedToReferenceAService() + { + $expected = new \stdClass; + $container = Builder::new() + ->add(Services::a, static fn() => $expected) + ->build(); + + $this->assertSame($expected, $container(Services::a)); + } } From 52e6999810d8110a99f7ac5ee012796f4de8b4b8 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 24 Mar 2024 15:42:44 +0100 Subject: [PATCH 2/3] add interface to specify the returned object type on an enum case --- CHANGELOG.md | 4 +++ README.md | 79 +++++++++++++++++++++++++++++++++++++++++++ fixtures/Services.php | 17 +++++++++- fixtures/psalm.php | 11 ++++++ psalm.xml | 1 + src/Builder.php | 4 +-- src/Container.php | 16 +++++++-- src/Service.php | 11 ++++++ 8 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 fixtures/psalm.php create mode 100644 src/Service.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b64f36..8a30b72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Ability to use enums to reference services and specify the returned object type + ### Removed - Support for PHP `8.1` diff --git a/README.md b/README.md index ec42d24..60f1dd7 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,82 @@ $connection instanceof ConnectionPool; // true ``` The `add` method accepts any `callable` that will return an `object`. This allows to use either anonymous functions for the ease of use (but have a memory impact) or callables of the form `[Service::class, 'factoryMethod']` that allows to only load the class file when the service is loaded. + +### Use enums instead of strings to reference services + +Using `string`s to name services when adding them via `Builder::add()` is simple but static analysis tools can't determine the type of the returned services. This results in _mixed argument_ errors that need to be suppressed. + +Instead you can use enums like so: +```php +use Innmind\DI\Service; + +/** + * @template S + * @implements Service + */ +enum Services implements Service +{ + case connection; + case connectionA; + case connectionB; + + /** + * @return self + */ + public static function connection(): self + { + /** @var self */ + return self::connection; + } + + /** + * @internal + * + * @return self<\PDO> + */ + public static function connectionA(): self + { + /** @var self<\PDO> */ + return self::connectionA; + } + + /** + * @internal + * + * @return self<\PDO> + */ + public static function connectionB(): self + { + /** @var self<\PDO> */ + return self::connectionB; + } +} +``` + +And to use it: +```php +use Innmind\DI\{ + Builder, + Container, +}; + +$container = Builder::new() + ->add(Services::connection, fn(Container $get) => new ConnectionPool( // imaginary class + $get(Services::connectionA), + $get(Services::connectionB), + )) + ->add(Services::connectionA, fn() => new \PDO('mysql://localhost')) + ->add(Services::connectionB, fn() => new \PDO('mysql://docker')) + ->build(); + +$connection = $container(Services::connection); +$connection instanceof ConnectionPool; // true +``` + +> [!TIP] +> By using enums you can easily reference all the defined services in one place. If you distribute your package, users can look at the enum to see what service they can use (since you can declare `@internal` services). +> +> On top of that no more typos in the services name and the services are automatically namespaced (no collision possible between packages). + +> [!NOTE] +> Named constructors are used on the enum in order to specify the class that is returned. Psalm dosn't allow to directly specify a template value on a `case`. diff --git a/fixtures/Services.php b/fixtures/Services.php index 13e9c06..d9e5c44 100644 --- a/fixtures/Services.php +++ b/fixtures/Services.php @@ -3,7 +3,22 @@ namespace Fixtures\Innmind\DI; -enum Services +use Innmind\DI\Service; + +/** + * @template S of object + * @implements Service + */ +enum Services implements Service { case a; + + /** + * @return self<\Exception> + */ + public static function a(): self + { + /** @var self<\Exception> */ + return self::a; + } } diff --git a/fixtures/psalm.php b/fixtures/psalm.php new file mode 100644 index 0000000..a042b21 --- /dev/null +++ b/fixtures/psalm.php @@ -0,0 +1,11 @@ +add(Services::a, static fn() => new \Exception('foo')) + ->build(); + +echo $container(Services::a())->getMessage(); diff --git a/psalm.xml b/psalm.xml index 510148d..90d05be 100644 --- a/psalm.xml +++ b/psalm.xml @@ -10,6 +10,7 @@ > + diff --git a/src/Builder.php b/src/Builder.php index d893c6d..06fa158 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -30,9 +30,9 @@ public static function new(): self /** * @param callable(Container): object $definition */ - public function add(string|\UnitEnum $name, callable $definition): self + public function add(string|Service $name, callable $definition): self { - if ($name instanceof \UnitEnum) { + if ($name instanceof Service) { $name = \spl_object_hash($name); } diff --git a/src/Container.php b/src/Container.php index 171c122..57f60b2 100644 --- a/src/Container.php +++ b/src/Container.php @@ -28,16 +28,25 @@ private function __construct(array $definitions) } /** + * @template T of object + * @template N of string|Service + * + * @param N $name + * * @throws ServiceNotFound * @throws CircularDependency + * + * @return (N is string ? object : T) */ - public function __invoke(string|\UnitEnum $name): object + public function __invoke(string|Service $name): object { - if ($name instanceof \UnitEnum) { + if ($name instanceof Service) { $name = \spl_object_hash($name); } + /** @psalm-suppress PossiblyInvalidArgument */ if (!\array_key_exists($name, $this->definitions)) { + /** @psalm-suppress PossiblyInvalidArgument */ throw new ServiceNotFound($name); } @@ -46,12 +55,15 @@ public function __invoke(string|\UnitEnum $name): object $path[] = $name; $this->building = []; + /** @psalm-suppress InvalidArgument */ throw new CircularDependency(\implode(' > ', $path)); } + /** @psalm-suppress InvalidPropertyAssignmentValue */ $this->building[] = $name; try { + /** @psalm-suppress InvalidPropertyAssignmentValue */ return $this->services[$name] ??= ($this->definitions[$name])($this); } finally { \array_pop($this->building); diff --git a/src/Service.php b/src/Service.php new file mode 100644 index 0000000..4516cc4 --- /dev/null +++ b/src/Service.php @@ -0,0 +1,11 @@ + Date: Sun, 24 Mar 2024 15:44:27 +0100 Subject: [PATCH 3/3] deprecate using strings as service name --- CHANGELOG.md | 4 ++++ src/Builder.php | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a30b72..cb83c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,7 @@ ### Removed - Support for PHP `8.1` + +### Deprecated + +- Using `string`s as a service name diff --git a/src/Builder.php b/src/Builder.php index 06fa158..d67705e 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -28,6 +28,7 @@ public static function new(): self } /** + * @param string|Service $name Using a string is deprecated * @param callable(Container): object $definition */ public function add(string|Service $name, callable $definition): self