Last update: 30 Mar 2024

Container

Powerful auto wiring dependency injection container including PSR-11.

Installation

To get started, install the container repository via the Composer package manager:

composer require zaphyr-org/container

Basic usage

The container enables you to conveniently register services, whether they have dependencies or not, and access them at a later time. It functions as a registry, which, when utilized effectively, empowers you to implement the Dependency Injection design pattern.

Let's take a look at the following example:

class Foo
{
    public function __construct(public Bar $bar)
    {
    }
}

class Bar{}

In the example above, we have two classes: Foo and Bar. Foo has a dependency on Bar because it expects an instance of Bar to be passed to its constructor.

Normally, you would need to perform the following steps in order to return a fully configured instance of Foo:

$bar = new Bar();
$foo = new Foo($bar);

When dealing with nested dependencies, this process can quickly become cumbersome and difficult to manage. However, with the container, obtaining a fully configured instance of Foo is as straightforward as requesting it from the container:

$container = new Zaphyr\Container\Container();

$foo = $container->get(Foo::class);
$foo->bar;

Thanks to automatic dependency injection (auto-wiring), the container can automatically resolve the dependencies of objects in many cases.

However, if you create classes that implement an interface, and you want to type-hint that interface, for example in a class constructor, you need to instruct the container on how to resolve that interface.

Additionally, when building larger applications with many dependencies, it is advisable to use service providers.

Binding

Binding aliases

The container allows you to define aliases for your container bindings. To create an alias, you pass a string as the first parameter to the bind method. This string becomes your alias. As the second parameter, you pass the class-string or closure that should be associated with this container binding to the bind method:

$container->bind('foo', Foo::class);
$container->get('foo');

Binding factories

The container has also the ability to accept any callable function that acts as a factory to resolve your classes. This approach offers the highest performance when resolving objects since it avoids the need for inspecting the definitions. However, it does limit the level of flexibility you can leverage. If your container bindings become more complex, you should consider using service providers.

To illustrate this concept using the previous example, you can define it within the container in the following manner:

$container->bind(Foo::class, function (Zaphyr\Container\Container $container) {
    return new Foo($container->get(Bar::class));
});

Binding interfaces

Let's take a look at the following code example:

class Foo
{
    public function __construct(public BarInterface $bar)
    {
    }
}

interface BarInterface{}
class Bar implements BarInterface{}

The Foo class represents a class that has a dependency on BarInterface. The BarInterface interface serves as a contract that classes must adhere to. The Bar class implements the BarInterface interface.

We now bind the BarInterface to the Bar class within the container using the bind method. This means that whenever the container needs to resolve an instance of BarInterface, it will use the Bar class:

$container->bind(BarInterface::class, Bar::class);
$container->get(Foo::class);

We now have Foo depending on an implementation of BarInterface.

Binding singletons

The bindSingleton method in the container binds a class or interface that should be resolved only once. When a singleton binding is resolved, the same object instance will be returned whenever the container is called again for that binding:

$container->bindSingleton(BarInterface::class, Bar::class);

To check if a container binding is a singleton object, you can use the isSingleton method:

$container->isSingleton(BarInterface::class);

Binding instances

Available since v1.1.0

The bindInstance method in the container binds a class or interface to an already existing instance. When a binding is resolved, the same object instance will be returned whenever the container is called again for that binding:

$container->bindInstance(BarInterface::class, new Bar());
$container->get(BarInterface::class);

Tagging

Container services can also be tagged. For example, if you want to associate all resolved bindings of a specific "category", you can do so using the tag method:

$container->bind(Foo::class);
$container->bind(Bar::class);

$container->tag([Foo::class, Bar::class], ['group']);

Once you have tagged all the desired services, you can easily resolve them using the tagged method:

$container->bind(Baz::class, function (Zaphyr\Container\Container $container) {
    return new Baz(...$container->tagged('group'));
});

You can also add multiple tags:

$container->tag(Foo::class, ['groupOne', 'groupTwo']);

Extending Bindings

The extend method allows for modifying resolved services. When a service is resolved, you can execute additional code to decorate or configure the service. The extend method accepts two arguments: the service class that you are extending and a closure that should return the modified service. The closure receives the resolved service and the container instance as parameters:

$container->extend(Service::class, function (Service $service, Zaphyr\Container\Container $container) {
    return new Decorator($service);
});

$container->get(Service::class);

Method injection

Occasionally, you may wish to call a method on an object instance while enabling the container to automatically inject the dependencies required by that method. For instance, considering the following class:

class Foo
{
    public function processBar(Bar $bar): array
    {
        return [
            // …
        ]
    }
}

Now you can invoke the processBar method using the call method of the container:

$result = $container->call([Foo::class, 'processBar']);

The call method accepts any PHP callable. You can also use the container's call method to invoke a closure and automatically inject its dependencies:

$container->call(function (Foo $foo) {
    // …
});

Service providers

Service providers offer the advantage of organizing your container definitions while also improving the performance of larger applications. This is because the definitions registered within a service provider are lazily registered at the moment when a service is accessed.

Creating a service provider is straightforward - you simply need to extend the Zaphyr\Container\AbstractServiceProvider and specify what you want to register:

class ExampleServiceProvider extends Zaphyr\Container\AbstractServiceProvider
{
    protected array $provides = [
        'key',
        'Some\Class',
    ];

    public function register(): void
    {
        $this->container
            ->bind('key', fn () => 'value')
            ->bind('Some\Class');
    }
}

To register this service provider with the container, you need to pass an instance of your provider to the registerServiceProvider method:

$container->registerServiceProvider(new ExampleServiceProvider());

The register method is only called when one of the aliases in the $provides array is requested by the container service. As a result, the items provided by the service provider are not actually registered until they are needed. This approach enhances the performance of larger applications, particularly as the dependency map expands.

Bootable service providers

If there is specific functionality that needs to be executed when the service provider is added to the container, e.g. including configuration files, we can make the service provider bootable by implementing the Zaphyr\Container\Contracts\BootableServiceProviderInterface:

class ExampleServiceProvider
    extends Zaphyr\Container\AbstractServiceProvider
    implements Zaphyr\Container\Contracts\BootableServiceProviderInterface
{
    protected array $provides = [];

    public function boot(): void
    {
        // …
    }

    public function register(): void
    {
        // …
    }
}