intraworlds / service-container
Lightweight yet powerful implementation of dependancy injection container with autowiring
Requires
- php: ^8.3
- psr/container: ^1.0||^2.0
Requires (Dev)
- infection/infection: ^0.33
- laravel/pint: ^1.29
- phpbench/phpbench: ^1.2
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^12.0
README
Lightweight yet powerful implementation of dependency injection container with autowiring.
Installation
Use Composer to install the package:
composer require intraworlds/service-container
Config
The container is implementing standard PSR-11 interface. You can use it with autowiring out-of-the-box. However, sometimes you want to configure things which needs parameters (eg. DB connection).
// zero configuration required $container = new ServiceContainer; $container->get(ClassWithManyDependencies::class); // bind factory of a value object #[Bind(self::load(...))] final class Config { function __construct(private array $values) static function load(Filesystem $fs) : self } // by method $container->bind(Config::class, Config::load(...)); // delay actual factory execution (eg. connection to DB) with lazy objects // inline factories in attributes are possible since PHP 8.5 #[Lazy, Bind(fn(Config $config) => new Aws\S3\S3Client(['region' => $config->get('S3_REGION')]))] final class S3 { public function __construct(private Aws\S3\S3Client $client) } // by methods $container->bind(S3::class, fn(Config $config) => new Aws\S3\S3Client(['region' => $config->get('S3_REGION')])); $container->lazy(S3::class); // use alias to bind without factory #[Alias(MemoryCache::class)] interface Cache // by method $container->alias(Cache::class, MemoryCache::class);
Composite types
A parameter typed as a union or intersection (eg. Cache|NullCache, Countable&Traversable)
is not autowired by trying each member. The container resolves it under the type's string
key, so you must register it explicitly — otherwise a CannotAutowireCompositType is thrown.
// a constructor param typed `Cache|NullCache` must be bound under that exact string $container->bind('Cache|NullCache', fn() => new NullCache);
Usage
It can be useful to manipulate with Service Container directly.
// get singleton using configured factories or auto-wiring $container->get(S3::class) === $container->get(S3::class); // make fresh instances $container->make(Uri::class) !== $container->get(Uri::class); // service is always available if it's possible to create it $container->has(Config::class) === true; // return service only if it was already initialized before, NULL otherwise $container->instance(Transaction::class); // same as `get()` but fails gracefully (to NULL) $container->try('NonExisting') === null; // register an already existing instance as a singleton $container->set(Clock::class, new FrozenClock($now)); // remove a singleton and return it (NULL if it wasn't set) $container->unset(Clock::class); // obtain (and memoize) the resolved factory for an ID $container->factory(S3::class); // instance of IW\ServiceContainer\ServiceFactory
Examples
Auto wiring arbitrary callable
Internal CallableFactory is useful for resolving any dependencies of a callable. Especially it's useful for init template method. See following example.
abstract class Parent { function __construct(private Dependency $dependency, ServiceContainer $container) { if (method_exists($this, 'init')) { (new CallableFactory($this->init(...)))($container); } } } class Child extends Parent { function init(AnotherDependency $another) { // ... } }
Manual Wiring
Sometimes you want to configure container manually. Let's consider following example on Command Pattern
interface OrderCommand { function execute(); } class OrderInvoker { function __construct(private OrderCommand ...$commands) {} function execute() : void { array_walk($this->commands, fn($command) => $command->execute()); } }
With IW\ServiceContainer you have several options how to resolve OrderInvoker's dependencies.
// an alias but that's no good for multiple commands $container->alias('OrderInvoker', 'ReserveItems'); // external factory $container->bind('OrderInvoker', function (IW\ServiceContainer $container) { return new OrderInvoker($container->get('ReserveItems'), $container->get('SendInvoice')); }); // internal factory $container->bind('OrderInvoker', OrderInvoker::create(...)); class OrderInvoker { static function create(ReserveItems $reserveItems, SendInvoice $sendInvoice) : OrderInvoker { return new OrderInvoker($reserveItems, $sendInvoice); } } // wiring factory $container->wire('OrderInvoker', 'ReserveItems', 'SendInvoice'); // ...same but with attribute #[Wire('OrderInvoker', 'ReserveItems', 'SendInvoice')] class OrderInvoker { }
Arguably all approaches have their advantages. internal factory approach is good for static analysis. wiring factory is useful for common application pattern and when dependencies may vary.
Errors
All exceptions extend IW\ServiceContainer\Exception, which implements PSR-11's
ContainerExceptionInterface; ServiceNotFound additionally implements
NotFoundExceptionInterface. When a failure is wrapped (eg. a broken dependency deep in a
resolution chain), Exception::getOrigin() returns the first original exception raised
outside the container, which is handy for pinpointing the root cause.
License
All contents of this package are licensed under the MIT license.