The obvious issue with dependency injection, of course, is that it becomes harder to encapsulate functionality. Injecting a Nest
into an Owl
's constructor requires that we build the Nest ourselves, instead of delegating it to the Owl. For classes with many, many dependencies, we can end up with a lot of code just to create an instance. Imagine an Owl that requires a Nest, that requires a Tree, that requires a Forest, etc.
As a more concrete example, let's look at the code required to create a new Monolog logging object:
// 1° Create formatter
$formatter = new LineFormatter(null, null, true);
// 2° Create Handler, give him the formatter
$handler = new StreamHandler('userfrosting.log');
$handler->setFormatter($formatter);
//3° Create the Logger, give him the handler
$logger = new Logger('debug');
$logger->pushHandler($handler);
Three main steps are required to create the object:
$logger
object requires the $handler
object, which we inject using pushHandler()
;$handler
object requires a $formatter
object, which we inject using setFormatter()
;$handler
object also requires the path to the log file.This is a lot of code to write just to create one measly object! It would be great if we could somehow encapsulate the creation of the object without creating tight couplings within the object itself.
This is where the dependency injection container (DIC) comes into play. The DIC handles basic management of dependencies, encapsulating their creation into simple callbacks. We will call these callbacks services.
You don't need a container to do dependency injection. However, a container can make injections easier.
UserFrosting uses PHP-DI 7 as it's DIC implementation since it provides many powerful features that we rely on:
Taken together, we can define our services without needing to worry about when and where their dependencies are created in our application's lifecycle.
Let's go back to our basic Owl example:
class Nest
{
// ...
}
class Owl
{
public function __construct(Nest $nest)
{
// ...
}
}
When using PHP-DI to create an Owl, the container detects that the constructor takes a Nest
object (using type declarations). Without any configuration, and as long as the constructor argument is properly typed, PHP-DI will create an Nest
instance (if it wasn't already created) and pass it as a constructor parameter. The equivalent code would now be :
$owl = $container->get(Owl::class);
It's very simple, doesn't require any configuration, and it just works !
Autowiring is an exotic word that represents something very simple: the ability of the container to automatically create and inject dependencies.
Sometimes classes might be a bit more complex to instantiate, especially third party ones (eg. the logger object from before). Or you might want to use a different class based on some configuration value. You might also want a class to be replaced by another one (eg. our ImprovedNest
). In these cases, autowiring cannot be used. This is where PHP-DI definition comes handy. PHP-DI loads the definitions you have written and uses them like instructions on how to create objects.
UserFrosting sets up its services through service provider classes. Each sprinkle can define as many service providers as it needs and register them in the Recipe. For example, the Services Provider class for the previous Logger
example would look like this:
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use UserFrosting\ServicesProvider\ServicesProviderInterface;
class LoggerServicesProvider implements ServicesProviderInterface
{
public function register(): array
{
return [
Logger::class => function (StreamHandler $handler, LineFormatter $formatter) {
$handler->setFormatter($formatter);
$logger = new Logger('debug');
$logger->pushHandler($handler);
return $logger;
},
StreamHandler::class => function () {
// 'userfrosting.log' could be fetched from a Config service here, for example.
return new StreamHandler('userfrosting.log');
},
LineFormatter::class => \DI\create()->constructor(null, null, true),
];
}
}
This definition uses the PHP-DI factories syntax. From the PHP-DI documentation:
Factories are PHP callables that return the instance. They allow to easily define objects lazily, i.e. each object will be created only when actually needed (because the callable will be called when actually needed).
Just like any other definition, factories are called once and the same result is returned every time the factory needs to be resolved.
Other services can be injected via type-hinting (as long as they are registered in the container or autowiring is enabled).
You'll notice that the callable used to create a Logger
object takes two parameters: StreamHandler
and LineFormatter
. This allows us to inject these services inside the definition. When Logger
is created (or injected), both StreamHandler
and LineFormatter
will be injected using their own definition.
LineFormatter
definition is different. It uses the object syntax instead of the factories syntax.Earlier we discussed the benefits of using interfaces, as the constructor can accept any class that implement the correct interface:
public function __construct(NestInterface $nest) // Accept both `Nest` and `ImprovedNest`
In this case, Autowiring can't help us since the NestInterface
cannot be instantiated: it's not a class, it's an interface! In this case, PHP Definitions can be used to match the interface with the correct class we want, using either a factory, or the Autowired object syntax:
return [
// mapping an interface to an implementation
NestInterface::class => \DI\autowire(ImprovedNest::class),
];
The "nest of choice" can now be selected in the service provider. It could also be selected using another kind of logic, for example using a Config
service and the new for PHP 8.0 match expression:
return [
// Inject Config to decide which nest to use, and the Container to get the actual class
NestInterface::class => function (ContainerInterface $ci, Config $config) {
return match ($config->get('nest.type')) {
'normal' => $ci->get(Nest::class),
'fancy' => $ci->get(ImprovedNest::class),
default => throw new \Exception("Bad nest configuration '{$config->get('nest.type')}' specified in configuration file."),
};
},
];
But why are interfaces really needed? If ImprovedNest
extends Nest
, wouldn't the constructor accept an ImprovedNest
anyway if you type-hinted against Nest
? Well, yes... But it won't work the other way around. For example :
// This will work
class AcceptNest {
public function __construct(protected Nest $nest)
{
// ...
}
}
$improvedNest = $this->ci->get(Nest::class); // Return `ImprovedNest`, because service is configured this way
$test = new AcceptNest($improvedNest); // Works, ImprovedNest is a subtype of Nest
// This wont
class AcceptImprovedNest {
public function __construct(protected ImprovedNest $nest)
{
// ...
}
}
$nest = $this->ci->get(Nest::class); // Return `Nest`
$test = new AcceptImprovedNest($nest); // Throws TypeError Exception, Nest is not a subtype of ImprovedNest
The next page shows a small list of the default services that ship with UserFrosting, as well as tips for using them. After that, we talk about how you can add your own services, extend existing services, or completely replace certain services in your own sprinkle.