dhii / services
A collection of useful DI service implementations.
Installs: 32 541
Dependents: 2
Suggesters: 0
Security: 0
Stars: 3
Watchers: 4
Forks: 2
Open Issues: 10
Requires
- php: ^7.1 | ^8.0
- psr/container: ^1.0
Requires (Dev)
- codeclimate/php-test-reporter: <=0.3.2
- phpunit/phpunit: ^7.0 | ^8.0 | ^9.0
- slevomat/coding-standard: ^6.0
- vimeo/psalm: ^4.8
This package is auto-updated.
Last update: 2024-10-18 08:30:59 UTC
README
This package provides a collection of service factory and extension definition implementations that can be used with PSR-11 containers, as well as the experimental service provider spec, to replace the anonymous functions that are typically used for definitions.
Requirements
- PHP >= 7.0 < PHP 8
Installation
With Composer:
composer require dhii/services
Without Composer:
- Go here.
- Install it.
- See "With Composer"
Classes
All implementations in this package inherit from Service
; an invocable object with a getDependencies
method that returns an array of keys.
Factory
A simple implementation that uses a callback to construct its service.
Unlike a normal anonymous function, the callback given to the Factory
does not get a ContainerInterface
argument, but rather the services that match the given dependency keys. This allows omitting a lot of trivial service retrieval code, and most importantly type-hinting the services:
new Factory(['dep1', 'dep2'], function(int $dep1, SomeInterface $dep2) { // ... });
Roughly equivalent to:
function (ContainerInterface $c) { $dep1 = $c->get('dep1'); if (!is_int($dep1)) { throw new TypeError(sprintf('Parameter $dep1 must be of type int; %1$s given', is_object($dep1) ? get_class($dep1) : gettype($dep1)); } $dep2 = $c->get('dep2'); if (!($dep2 instanceof SomeInterface)) { throw new TypeError(sprintf('Parameter $dep2 must be of type int; %1$s given', is_object($dep2) ? get_class($dep2) : gettype($dep2)); } // ... }
This is true for all implementations that use a similar mechanic of passing already resolved services to other definitions. Therefore, type-checking code will be henceforth omitted for brevity.
Extension
Very similar to Factory
, but the callback also receives the service instance from the original factory or previous extension as the first argument.
new Extension(['dep1', 'dep2'], function($prev, $dep1, $dep2) { // ... });
Equivalent to:
function (ContainerInterface $c, $prev) { $dep1 = $c->get('dep1'); $dep2 = $c->get('dep2'); // ... }
Constructor
A variant of Factory
that invokes a constructor, rather than a callback function. Very useful in cases where a class is only constructed using other services.
new Constructor(MyClass::class, ['dep1', 'dep2']);
Equivalent to:
function (ContainerInterface $c) { $dep1 = $c->get('dep1'); $dep2 = $c->get('dep2'); return new MyClass($dep1, $dep2); }
Consequently, it also works without any dependencies, which is useful when the constructor is parameterless:
new Constructor(MyClass::class);
Equivalent to:
function (ContainerInterface $c) { return new MyClass(); }
ServiceList
Creates an array that contains the services indicated by its dependencies. Very useful for managing registration of instances when coupled with ArrayExtension
.
new ServiceList(['service1', 'service2']);
Equivalent to:
function (ContainerInterface $c) { return [ $c->get('service1'), $c->get('service2'), ]; }
ArrayExtension
An extension implementation that adds its dependencies to the previous value. Very useful for registering new instances to a list.
new ArrayExtension(['dep1', 'dep2'])
Equivalent to:
function (ContainerInterface $c, array $prev) { return array_merge($prev, [ $c->get('dep1'), $c->get('dep2'), ]); }
FuncService
A variant of Factory
, but it returns the callback rather than invoking it. Invocation arguments will be passed before the injected dependencies. Very useful for declaring callback services.
new FuncService(['dep1', 'dep2'], function($arg1, $arg2, $dep1, $dep2) { // ... });
Equivalent to:
function (ContainerInterface $c) { $dep1 = $c->get('dep1'); $dep2 = $c->get('dep2'); return function ($arg1, $arg2) use ($dep1, $dep2) { // ... }; }
Others
StringService
- For services that return strings that are interpolated with other services.Value
- For services that always return a static value.Alias
- An alias for another service, with defaulting capabilities for when the original does not exist.GlobalVar
- For services that return global variables.
Mutation
The withDependencies()
method allows all service instances to be copied with different dependencies, while leaving the original instances unaffected.
$service = new Factory(['database'], function ($database) { // ... }); $service2 = $service->withDependencies(['db']);
This makes it possible to modify service dependencies at run-time, or even during a build process, which can be especially useful when dealing with 3rd party service providers that need to be rewired.
Multi-boxing
One of the benefits of being able to derive new services with different dependencies is the ability to use the same provider multiple times. Let's look at an example.
Consider a service provider for a logger that writes to a file.
class LoggerProvider implements ServiceProviderInterface { public function getFactories() { return [ 'logger' => new Constructor(FileLogger::class, ['file_path']), 'file_path' => new Value(sys_get_tmp_dir() . '/log.txt') ]; } public function getExtensions () { return []; } }
Our application needs to keep 2 different log files: one for errors and one for debugging.
Simply using the above service provider twice won't work; we'd be re-declaring the logger
and file_path
services.
Prefixing the factories would allow us to have two instances of the service provider, but that would break the dependencies. If we prefixed the factories from one logger such that they become debug_logger
and debug_file_path
, the debug_logger
factory would still be depending on file_path
, which would no longer exist after prefixing.
This where mutation of dependencies comes in. We can write a PrefixingProvider
decorator that not only prefixes all services in a provider, but also prefixes any dependencies.
(The below class is incomplete for the sake of brevity. Assume that a constructor exists and that it initializes its $prefix
and $provider
properties).
class PrefixingProvider implements ServiceProviderInterface { public function getFactories() { $factories = []; foreach ($this->provider->getFactories() as $key => $factory) { $deps = $factory->getDependencies(); $newDeps = array_map(fn($dep) => $this->prefix . $dep, $deps); $factories[$this->prefix . $key] = $factory->withDependencies($newDeps); } return $factories; } }
We can now create two different versions of the same service provider:
$debugLogProvider = new PrefixingProvider('debug_', new LoggerProvider); $errorLogProvider = new PrefixingProvider('error_', new LoggerProvider);
The first one will provide debug_logger
and debug_file_path
, while the second will provide error_logger
and error_file_path
.
Static Analysis
By having all services declare their dependencies, we open up the possibility to create an inspection tool that statically analyzes a list of services to build a dependency graph. This graph can help uncover various potential problems without needing to run the code. These insights can reveal:
- Circular dependency
- Dependencies that do not exist
- Factories that override each other, rather than using extensions
- Dependency chains that are too deep
- Unused services
No such tool exists at the time of writing, but I do plan on taking on this task.