bhittani / container
PSR-11 dependency injection container implementation with automatic resolution, service providers, facades and macros.
Requires
- php: ^7.2
- psr/container: ^1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^2.18
- phpunit/phpunit: ^8.5
Provides
- psr/container-implementation: 1.0.0
README
PSR-11 dependency injection container implementation with automatic resolution, service providers, facades and macros. This package does not require any external dependencies.
Install
You may install this package using composer.
$ composer require bhittani/container --prefer-dist
Usage
PSR-11 Implementation
This package implements the PSR-11 container interface, hence, you can easily swap any existing implementation with the container provided in this package.
Container
In its simplest form, the container stores key value pairs so that it can be accessed later during your application life cycle.
<?php require_once __DIR__ . '/vendor/autload.php'; $container = new \Bhittani\Container\Container; $container->add('foo', 'bar'); echo $container->get('foo'); // 'bar'
Binding resolution
Practically, a dependency injection container is more useful by storing class factories/instances so that they are automatically resolved.
<?php require_once __DIR__ . '/vendor/autload.php'; class FooDatabase { // ... } $container = new Bhittani\Container\Container; $container->add(FooDatabase::class, new FooDatabase); $db = $container->get(FooDatabase::class);
The key being used FooDatabase
is significant. This key will act as a look-up against any class typehints during resolution attempts of binding parameters (in class constructors, methods, closures, callables, ..., etc).
Still confused? Lets take a closer look at a practical example.
<?php require_once __DIR__ . '/vendor/autload.php'; class FooDatabase { // ... } class Query { protected $db; public function __construct(FooDatabase $db) { $this->db = $db; } } $container = new Bhittani\Container\Container; $container->add(FooDatabase::class, new FooDatabase); $query = $container->get(Query::class); // Query
Here, $db
is automatically resolved by the container as it is type hinted with the 'FooDatabase' class which the container is aware of.
Automatic dependency resolution
Binding resolution is all handy and dandy but we can do much better and improve on our first iteration.
If we take a closer look at the previous code example, we see that we bind the FooDatabase
class explicitly into the container but the Query
class is implicitly resolved without any explicit binding.
This means, we should also be able to resolve the FooDatabase
class implicitly.
Let's apply our first refactor.
<?php require_once __DIR__ . '/vendor/autload.php'; class FooDatabase { // ... } class Query { protected $db; public function __construct(FooDatabase $db) { $this->db = $db; } } $container = new Bhittani\Container\Container; $query = $container->get(Query::class); // Query
In case you didn't notice, the code line $container->add(FooDatabase::class, new FooDatabase);
is completely removed as no binding is required.
How does this work? Lets go behind the scenes for a moment to see what actually happens.
When you call the get
method on the container,
- The container identifies the key as a class that exists.
- It takes a peek into the constructor parameters and notices a parameter type-hinted as the class
FooDatabase
. - In order to resolve this parameter, it repeats step 1 and 2 using the type-hint as the key.
- It doesn't see any constructor for the
FooDatabase
class so it instantiates it and uses that instance to instantiate theQuery
class.
A binding will take precedence over a new instantiation.
Interface resolution
Wouldn't it be nice if we could implement to an interface so that we could easily swap the underlying implementation?
<?php require_once __DIR__ . '/vendor/autload.php'; interface DatabaseInterface { // ... } class FooDatabase implements DatabaseInterface { // ... } class BarDatabase implements DatabaseInterface { // ... } class Query { public $db; public function __construct(DatabaseInterface $db) { $this->db = $db; } } $container = new Bhittani\Container\Container; $container->add(DatabaseInterface::class, new FooDatabase); $query = $container->get(Query::class); echo $query->db instanceof FooDatabase; // true $container->add(DatabaseInterface::class, new BarDatabase); $query = $container->get(Query::class); echo $query->db instanceof BarDatabase; // true
We have easily swapped the underlying database implementation from FooDatabase
to BarDatabase
.
Callable resolution
To resolve a callable/closure, we can invoke it directly.
$container = new Bhittani\Container\Container; class Acme { // ... } $container->call(function (Acme $acme) { echo $acme instanceof Acme; // true });
Custom parameter resolution
We can resolve entities that require custom arguments in two ways.
- Binding the custom argument into the container.
- Passing explicit arguments.
class Acme { public $foo; public function __construct($foo) { $this->foo = $foo; } } $container = new Bhittani\Container\Container; $container->add('foo', 'bar'); $acme = $container->get(Acme::class); echo $acme->foo; // bar $acme = $container->get(Acme::class, ['foo' => 'baz']); echo $acme->foo; // baz
Explicit arguments will take precedence over bindings.
Factory bindings
Factory bindings allow lazy loading of your instances. Which means that resolution will only occur when it is needed.
<?php require_once __DIR__ . '/vendor/autload.php'; interface DatabaseInterface { // ... } class FooDatabase implements DatabaseInterface { // ... } class Query { public function __construct(DatabaseInterface $db) { // ... } } $container = new Bhittani\Container\Container; $container->add(DatabaseInterface::class, function () { return new FooDatabase; }); $query = $container->get(Query::class); // will trigger the factory closure above.
This way, you can add as many bindings as you want in your container but only trigger/resolve them when needed. Hence, lazy loaded.
Shared bindings
Shared bindings allow the same instance to be resolved when accessed instead of a new instance every time it is needed.
$container = new Bhittani\Container\Container; // This will be shared as we are not using a factory. $container->add(DatabaseInterface::class, new FooDatabase); // This will be shared as we are using the method 'share' explicitly. $container->share(DatabaseInterface::class, function () { return new FooDatabase; }); // This will NOT be shared as we are using a factory. $container->add(DatabaseInterface::class, function () { return new FooDatabase; });
Delegates
Delegated containers serve as fallback containers that are looked-up for binding resolutions when it can not be found in the container.
use Bhittani\Container\Container; $container = new Container; $container->has('foo'); // false $delegateContainer = new Container; // Or any PSR-11 container. $delegateContainer->add('foo', 'bar'); $container->delegate($delegateContainer); $container->has('foo'); // true
Service providers
This package also ships with a service provider container which allows registering of service providers (Think of laravel service providers) in order to have a smooth and easy application development process.
In order to make use of service providers, we will work with a ServiceContainer
instead of a simple Container
.
use Bhittani\Container\ServiceContainer; use Bhittani\Container\ServiceProvider; class DatabaseServiceProvider extends ServiceProvider { public function boot($container) { // This method will be called when all service providers are registered. } public function register($container) { $container->share(DatabaseInterface::class, function () { return new SqliteDatabase; }); } } $container = new ServiceContainer; $container->addServiceProvider(DatabaseServiceProvider::class); $container->bootstrap(); $db = $container->get(DatabaseInterface::class); echo $db instanceof SqliteDatabase; // true
Facades
Facades extend the container by assigning custom properties onto the service container. When accessed, it will be resolved out of the service container.
use Bhittani\Container\ServiceContainer; $container = new ServiceContainer; $container->share(DatabaseInterface::class, function () { return new SqliteDatabase; }); $container->db = DatabaseInterface::class; echo $container->db instanceof SqliteDatabase; // true
Macros
Macros extend the container by assigning custom methods onto the service container.
use Bhittani\Container\ServiceContainer; $container = new ServiceContainer; $container->share(DatabaseInterface::class, function () { return new SqliteDatabase; }); $container->macro('query', function ($sql) { // $this will be set to the underlying ServiceContainer. return $this->get(DatabaseInterface::class)->query($sql); }); echo $container->query('SELECT * FROM users'); // Invokes the 'query' macro.
Deferred Service Providers
Service providers can be deferred so that services can be lazy loaded.
use Bhittani\Container\ServiceContainer; use Bhittani\Container\ServiceProvider; class DatabaseServiceProvider extends ServiceProvider { // A non empty $provides array will defer this service provider. protected $provides = [ DatabaseInterface::class, // If setting a facade, use the facade key as the index. // 'db' => DatabaseInterface::class, ]; // A non empty $macros array will defer this service provider as well. protected $macros = [ 'query', ]; public function register($container) { $container->share(DatabaseInterface::class, function () { return new SqliteDatabase; }); $container->macro('query', function ($sql) { return $this->get(DatabaseInterface::class)->query($sql); }); } } $container = new ServiceContainer; $container->addServiceProvider(DatabaseServiceProvider::class); $container->bootstrap(); // This will register and boot the service provider. $container->query('SELECT * FROM users');
Using deferred service providers is an efficient way to build up your application as these services will be lazy loaded and act as plug and play while having a minimimum impact on your application performance.
Changelog
Please see CHANGELOG for more information on what has changed.
Testing
git clone https://github.com/kamalkhan/container cd container composer install composer test
Contributing
Please see CONTRIBUTING and CONDUCT for details.
Security
If you discover any security related issues, please email shout@bhittani.com
instead of using the issue tracker.
Inspiration
Credits
License
The MIT License (MIT). Please see the License File for more information.