PSR-11 dependency injection container implementation with automatic resolution, service providers, facades and macros. This package does not require any external dependencies.
You may install this package using composer.
$ composer require bhittani/container --prefer-dist
This package implements the PSR-11 container interface, hence, you can easily swap any existing implementation with the container provided in this package.
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'
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.
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.
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
.
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
});
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 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 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;
});
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
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 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 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.
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.
Please see CHANGELOG for more information on what has changed.
git clone https://github.com/kamalkhan/container
cd container
composer install
composer test
Please see CONTRIBUTING and CONDUCT for details.
If you discover any security related issues, please email [email protected]
instead of using the issue tracker.
The MIT License (MIT). Please see the License File for more information.