DI Container

Basics

A dependency injection (DI) container gives you a way of telling your app "When you need an instance of IFoo, use this implementation".

use Aphiria\DependencyInjection\Container;
use App\UserService;
use App\IUserService;

$container = new Container();
$container->bindInstance(IUserService::class, new UserService());

// ...

// Will be an instance of UserService
$userService = $container->resolve(IUserService::class);

Bindings

A binding is a way of telling the container what instance to use when resolving an interface. There are a few different ways of registering bindings:

// Whenever you need IUserService, always use the same instance of UserService
$container->bindInstance(IUserService::class, new UserService());

// Whenever you need IUserService, run the factory to get a new instance
$container->bindFactory(IUserService::class, fn () => new UserService());

// Whenever you need IUserService, run the factory and use that instance every time after
$container->bindFactory(IUserService::class, fn () => new UserService(), true);

Note: The factory must be parameterless.

// Whenever you need IUserService, use auto-wiring to return the same instance of UserService
$container->bindSingleton(IUserService::class, UserService::class);

// Whenever you need IUserService, use auto-wiring to return a new instance of UserService
$container->bindPrototype(IUserService::class, UserService::class);

Note: If you attempt to resolve an interface, but the container does not have a binding or it cannot auto-wire it, a ResolutionException will be thrown.

You can check if the container has a particular binding:

if ($container->hasBinding(IUserService::class)) {
    // ...
}

You can also remove a binding:

$container->unbind(IUserService::class);

If you'd like to try resolving a binding without an exception being thrown, use tryResolve():

$userService = null;

if (!$container->tryResolve(IUserService::class, $userService)) {
    $userService = new UserService(new UserRepository());
}

// ...

Targeted Bindings

If you only want to use a specific binding when resolving a type, you can use what's called a targeted binding. Let's say that UserService looked like this:

final class UserService implements IUserService
{
    private IUserRepository $userRepository;

    public function __construct(IUserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    // ...
}

You can tell the container to use a specific instance of IUserRepository when resolving UserService:

$container->for(
    UserService::class,
    fn ($container) => $container->bindInstance(IUserRepository::class, new UserRepository())
);

Calling $container->resolve(UserService::class) will now automatically inject an instance of UserRepository.

Auto-Wiring

Auto-wiring is when you let the container use reflection to scan the constructor and attempt to automatically instantiate each parameter. Let's build off of the targeted binding example.

$container->bindInstance(IUserRepository::class, new UserRepository());
$userService = $container->resolve(UserService::class);

The container will scan UserService::__construct(), see the IUserRepository parameter, and either check to see if it has a binding or attempt to auto-wire an instance of it if possible.

Bootstrappers

A bootstrapper is a simple class that registers bindings to the container for a particular area of your domain. For example, you might have a bootstrapper called UserBootstrapper to centralize all the bindings related to your user domain.

use Aphiria\DependencyInjection\Bootstrappers\Bootstrapper;
use Aphiria\DependencyInjection\IContainer;

final class UserBootstrapper extends Bootstrapper
{
    public function registerBindings(IContainer $container): void
    {
        $container->bindInstance(IUserRepository::class, new UserRepository());
        $container->bindSingleton(IUserService::class, UserService::class);
    }
}

For more info on bootstrappers, read their documentation.