PSR-11 Container
, (*1)
PSR-11 dependency injection container implementation with automatic resolution, service providers, facades and macros. This package does not require any external dependencies., (*2)
Install
You may install this package using composer., (*3)
$ 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., (*4)
Container
In its simplest form, the container stores key value pairs so that it can be accessed later during your application life cycle., (*5)
<?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., (*6)
<?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)., (*7)
Still confused? Lets take a closer look at a practical example., (*8)
<?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., (*9)
Automatic dependency resolution
Binding resolution is all handy and dandy but we can do much better and improve on our first iteration., (*10)
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., (*11)
This means, we should also be able to resolve the FooDatabase
class implicitly., (*12)
Let's apply our first refactor., (*13)
<?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., (*14)
How does this work? Lets go behind the scenes for a moment to see what actually happens., (*15)
When you call the get
method on the container,, (*16)
- 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 the Query
class.
A binding will take precedence over a new instantiation., (*17)
Interface resolution
Wouldn't it be nice if we could implement to an interface so that we could easily swap the underlying implementation?, (*18)
<?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
., (*19)
Callable resolution
To resolve a callable/closure, we can invoke it directly., (*20)
$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., (*21)
- 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., (*22)
Factory bindings
Factory bindings allow lazy loading of your instances. Which means that resolution will only occur when it is needed., (*23)
<?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., (*24)
Shared bindings
Shared bindings allow the same instance to be resolved when accessed instead of a new instance every time it is needed., (*25)
$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., (*26)
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., (*27)
In order to make use of service providers, we will work with a ServiceContainer
instead of a simple Container
., (*28)
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., (*29)
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., (*30)
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., (*31)
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., (*32)
Changelog
Please see CHANGELOG for more information on what has changed., (*33)
Testing
git clone https://github.com/kamalkhan/container
cd container
composer install
composer test
Contributing
Please see CONTRIBUTING and CONDUCT for details., (*34)
Security
If you discover any security related issues, please email shout@bhittani.com
instead of using the issue tracker., (*35)
Inspiration
Credits
License
The MIT License (MIT). Please see the License File for more information., (*36)
, (*37)