This is an overview about Laravel’s DI container ( Illuminate\Container\Container ) and its functionality. It is interesting to have a look since the logic behind most of the DI containers is more or less the same (not to be surprised. After all, the goal is the same, too.)
(a) It is a singleton.
There can be only one! It makes sense, right ?
(b) It implements the ArrayAccess interface for the container services.
offsetGet() -> Allows us to get an instance of a service object like that: $app['myservice']
offsetSet() -> Allows us to bind a name to a service object like that: $app[$serviceName] = $serviceObj
And similar for the rest:
offsetExists() -> …
offsetUnset() -> …
(c) It provides dynamic access to registered services through the use of magic methods:
__get()
__set()
In a way, it allows us to consider services as properties of the container object.
So, app('router')
and app()->router;
will give the same result.
(d) It supports auto-wiring (automatic injection)
Auto-wiring is achieved through the make() and makeWith() methods.
public function make($abstract, array $parameters = [])
These methods resolve the dependencies of a class and instantiate the class. Keep in mind that you don’t need to register a class to the container as a service in order to be able to autowire the class in a method signature. This need comes under special circumstances. For example, if:
- you need some special configuration and initialization of the class (e.g read some information from configuration files and pass it as input to the class constructor)
- we want this service class to be replaceable, so we use an interface for type-hinting and register the class as the default implementation of this interface
(e) It provides service registration.
Generally, service registration makes a service object (an object that offers some functionality. Don’t be scared!) available through the container. Registration happens by mapping the service object to a name. The name is just a label that we can use to get the service object when we need it. Usually, lazy instantiation of the service objects is preferred. So, what we really map, usually, is a name to a function that instantiates the object and returns it. We will call it a “builder function”. If eager instantiation is required/preferred for a service, then we will instantiate the service and just map the name to this instance.
Service registration for lazy services happens mainly through the bind() method:
public function bind($abstract, $concrete = null, $shared = false)
This method just adds an entry to the $bindings property of the container that maps a name (short name, class name, interface name,…) to a function (a builder function) that returns an instance (object) of the relevant service.
Generally, we would bind an abstract type (abstract class name, interface name) to a concrete type (e.g the class name of the implementation of the interface). If no abstract type is needed for a service class, we can omit the 2nd argument and pass the concrete type as the first argument.
If we want to say that the object of a service should be a singleton, we can use the singleton()
method, instead of bind()
. The first time this service object will be requested (e.g directly through container $app['serviceName']
or through auto-wiring), the created instance will also be mapped to this name by putting an entry to the $instance property of the container.
In case we need eager instantiation of a service, we can create an object and bind a name (abstract or not) to this (existing) object through the instance()
method. This will put directly an entry to the $instance property.
In any way, the bind() method is smart. If, instead of a builder function, we provide something else (e.g the instance itself) as the second argument, bind() will wrap the instance with a Closure.
Why register more than one names to a service:
Registering short names are useful because, if we want to access the Router service, we can write:
app('router')
instead of:
app('Illuminate\Routing\Router')
Binding interfaces/contracts is useful for auto-wiring a service while being able at the same time to decouple the dependency from the implementation that we have chosen to use.
Binding a class name is the way to go if we are not interested in abstracting a service (making sure a specific concrete class will be used).
What happens if we bind a new concrete type to a name that is already bound ?
The new concrete type will become the default implementation for this name.
(f) It provides “rebounding” functionality.
If a name is re-registered after an instance of this service has been returned by the container, we can update the returned instance(s) by using the rebounding functionality (the rebound listeners are triggered in order to update/replace the service instances).
(g) It provides contextual binding
Allows us to bind different builder functions to the same name, based on which class has the dependency to this name.