Skip to content

Commit

Permalink
Merge pull request #2 from Innmind/enums
Browse files Browse the repository at this point in the history
Use Enums as a service name
  • Loading branch information
Baptouuuu committed Mar 24, 2024
2 parents bd7873e + 9b5a79c commit 4c766ea
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitattributes
@@ -1 +1,2 @@
/tests export-ignore
/fixtures export-ignore
8 changes: 8 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,14 @@

## [Unreleased]

### Added

- Ability to use enums to reference services and specify the returned object type

### Removed

- Support for PHP `8.1`

### Deprecated

- Using `string`s as a service name
79 changes: 79 additions & 0 deletions README.md
Expand Up @@ -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<S>
*/
enum Services implements Service
{
case connection;
case connectionA;
case connectionB;

/**
* @return self<ConnectionPool>
*/
public static function connection(): self
{
/** @var self<ConnectionPool> */
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`.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -24,7 +24,8 @@
},
"autoload-dev": {
"psr-4": {
"Tests\\Innmind\\DI\\": "tests/"
"Tests\\Innmind\\DI\\": "tests/",
"Fixtures\\Innmind\\DI\\": "fixtures/"
}
},
"require-dev": {
Expand Down
24 changes: 24 additions & 0 deletions fixtures/Services.php
@@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);

namespace Fixtures\Innmind\DI;

use Innmind\DI\Service;

/**
* @template S of object
* @implements Service<S>
*/
enum Services implements Service
{
case a;

/**
* @return self<\Exception>
*/
public static function a(): self
{
/** @var self<\Exception> */
return self::a;
}
}
11 changes: 11 additions & 0 deletions fixtures/psalm.php
@@ -0,0 +1,11 @@
<?php

namespace Fixtures\Innmind\DI;

use Innmind\DI\Builder;

$container = Builder::new()
->add(Services::a, static fn() => new \Exception('foo'))
->build();

echo $container(Services::a())->getMessage();
1 change: 1 addition & 0 deletions psalm.xml
Expand Up @@ -10,6 +10,7 @@
>
<projectFiles>
<directory name="src" />
<directory name="fixtures" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
Expand Down
7 changes: 6 additions & 1 deletion src/Builder.php
Expand Up @@ -28,10 +28,15 @@ public static function new(): self
}

/**
* @param string|Service $name Using a string is deprecated
* @param callable(Container): object $definition
*/
public function add(string $name, callable $definition): self
public function add(string|Service $name, callable $definition): self
{
if ($name instanceof Service) {
$name = \spl_object_hash($name);
}

$definitions = $this->definitions;
$definitions[$name] = $definition;

Expand Down
18 changes: 17 additions & 1 deletion src/Container.php
Expand Up @@ -28,12 +28,25 @@ private function __construct(array $definitions)
}

/**
* @template T of object
* @template N of string|Service<T>
*
* @param N $name
*
* @throws ServiceNotFound
* @throws CircularDependency
*
* @return (N is string ? object : T)
*/
public function __invoke(string $name): object
public function __invoke(string|Service $name): object
{
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);
}

Expand All @@ -42,12 +55,15 @@ public function __invoke(string $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);
Expand Down
11 changes: 11 additions & 0 deletions src/Service.php
@@ -0,0 +1,11 @@
<?php
declare(strict_types = 1);

namespace Innmind\DI;

/**
* @template T of object
*/
interface Service extends \UnitEnum
{
}
11 changes: 11 additions & 0 deletions tests/ContainerTest.php
Expand Up @@ -14,6 +14,7 @@
PHPUnit\Framework\TestCase,
Set,
};
use Fixtures\Innmind\DI\Services;

class ContainerTest extends TestCase
{
Expand Down Expand Up @@ -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));
}
}

0 comments on commit 4c766ea

Please sign in to comment.