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, but without creating tight couplings by doing that 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 help you.
UserFrosting uses PHP-DI 7 has it's DIC implementation since it provides many powerful features that we rely on:
Taken together, this means 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 the 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 theses 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 provider it need and register them in their 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 definitions uses 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 theses services inside this 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 interface really needed? If ImprovedNest
extends Nest
, wouldn't the constructor accept an ImprovedNest
anyway if you typed-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
In most case it's considered "best practice" to type-hint against interfaces, unless you explicitly required an
The next page shows a small list the default services that ship with UserFrosting, as well as tips and trick to replace. After that, we talk about how you can add your own services, extend existing services, or completely replace certain services in your own Sprinkle.