What we'll cover
- How to use the Container outside Laravel
- Features of the Illuminate Container
- Basic binding
- Contextual binding
- Autowiring
- How these features work under the hood
- Changes in these features in recent Laravel versions
What we Won't Cover
- Laravel-specific container interactions
- Service Providers
- Facades
- Method/callable binding
- Controller actions
- Console commands
- See Matt Stauffer's presentation and the official docs
- Why to use dependency injection (see another presentation)
Looking under the hood...
Code has been edited to better fit on each slide. Actual Illuminate Cotainer code is PSR-2 compliant (or close to it), has comments, and doesn't include scalar parameter types.
For example
iansltx/raphple
Specifically, this comparison
composer require illuminate/container 6.x@dev
Wait...why do I have to install @dev?
- Current stable release required two functions from illuminate/support
- Laravel PR #30518 extracted those two functions, removing the dep
- Dropping illuminate/support results in way fewer dependencies
- illuminate/contracts
- psr/simple-cache (transitive from illuminate/contracts)
- PHP ^7.2
- 136 lines in composer.lock vs. 524
- This will be stable in v6.5.1 or v6.6.0; guessing we'll see a release ~Nov 12
Use it with Slim 4
$container = new Illuminate\Container\Container(); // if everything can't be autowired, bind here... \Slim\Factory\AppFactory::setContainer($container); // ...or here $app = AppFactory::create();
// ...or here $app->run();
This works with autowiring
class Auth
{
protected $rs;
public function __construct(RaffleService $rs) {
$this->rs = $rs;
}
// snip
}
$container->get(Auth::class); // instance of Auth
$container->get(Auth::class); // different instance
What does get() do?
public function get($id)
{
try {
return $this->resolve($id);
} catch (Exception $e) {
if ($this->has($id)) {
throw $e;
}
throw new EntryNotFoundException($id);
}
}
What does has() do?
public function has($id)
{
return $this->bound($id);
}
public function bound($abstract)
{
return isset($this->bindings[$abstract]) ||
isset($this->instances[$abstract]) ||
$this->isAlias($abstract);
}
Make sure you're using 5.7+ if you use the PSR-11 interface
Prior to version 5.7, the get() method would fail if a dependency was autowired rather than declared explicitly. Laravel PR #25870 fixed this.
...actually, we just want a single global Auth instance...
// in service config $container->singleton(Auth::class); // or you could use $container->bind(Auth::class, null, $shared = true);
// Use Laravel? You're probably used to...
$container->make(Auth::class); // ...or we can use ArrayAccess $container[Auth::class]; // same instance as above
Is make() any different than get()?
Yes, but it's identical to pulling a dependency via ArrayAccess.
public function offsetGet($key)
{
return $this->make($key);
}
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
Under the hood: singleton
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
// available as of 6.x
public function singletonIf($abstract, $concrete = null)
{
if (! $this->bound($abstract)) {
$this->singleton($abstract, $concrete);
}
}
Under the hood: Binding
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) $concrete = $abstract;
if (! $concrete instanceof Closure)
$concrete = $this->getClosure($abstract, $concrete);
$this->bindings[$abstract] =
compact('concrete', 'shared');
if ($this->resolved($abstract)) $this->rebound($abstract);
}
Under the hood: Binding
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) $concrete = $abstract;
if (! $concrete instanceof Closure)
$concrete = $this->getClosure($abstract, $concrete);
$this->bindings[$abstract] =
compact('concrete', 'shared');
if ($this->resolved($abstract)) $this->rebound($abstract);
}
Under the hood: Binding
protected function dropStaleInstances($abstract)
{
unset(
$this->instances[$abstract],
$this->aliases[$abstract]
);
}
Under the hood: Binding
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) $concrete = $abstract;
if (! $concrete instanceof Closure)
$concrete = $this->getClosure($abstract, $concrete);
$this->bindings[$abstract] =
compact('concrete', 'shared');
if ($this->resolved($abstract)) $this->rebound($abstract);
}
Under the hood: Binding
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) $concrete = $abstract;
if (! $concrete instanceof Closure)
$concrete = $this->getClosure($abstract, $concrete);
$this->bindings[$abstract] =
compact('concrete', 'shared');
if ($this->resolved($abstract)) $this->rebound($abstract);
}
Under the hood: Binding
protected function getClosure($abstract, $concrete)
{
return function ($container, $parameters = [])
use ($abstract, $concrete)
{
if ($abstract == $concrete) {
return $container->build($concrete);
}
// switched from using make() in 5.8
return $container->resolve(
$concrete, $parameters, $raiseEvents = false
);
};
}
If you want to bring your own Closure...
function (Container $c, $params = []) {
return new MyService(
'foo',
'bar',
$c->get(SomeDependency::class)
);
}
An example Closure from Raphple
$container->bind(
ExtendedPdo::class,
function () use ($env) {
return new \Aura\Sql\ExtendedPdo(
/* DSN here */,
$env['DB_USER'],
$env['DB_PASSWORD']
);
}
);
Under the hood: Binding
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) $concrete = $abstract;
if (! $concrete instanceof Closure)
$concrete = $this->getClosure($abstract, $concrete);
$this->bindings[$abstract] =
compact('concrete', 'shared');
if ($this->resolved($abstract)) $this->rebound($abstract);
}
Under the hood: Binding
// WARNING: not valid PHP code
protected $bindings = [
string => [
concrete => Closure(Container, array): mixed,
shared => boolean
], ...
];
Under the hood: Binding
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) $concrete = $abstract;
if (! $concrete instanceof Closure)
$concrete = $this->getClosure($abstract, $concrete);
$this->bindings[$abstract] =
compact('concrete', 'shared');
if ($this->resolved($abstract)) $this->rebound($abstract);
}
Under the hood: Binding
public function resolved(string $abstract)
{
if ($this->isAlias($abstract)) {
$abstract = $this->getAlias($abstract);
}
return isset($this->resolved[$abstract]) ||
isset($this->instances[$abstract]);
}
Under the hood: Binding
protected function rebound(string $abstract)
{
$instance = $this->make($abstract);
foreach (
$this->getReboundCallbacks($abstract) as $callback
) {
call_user_func($callback, $this, $instance);
}
}
protected function getReboundCallbacks($abstract) {
// set via rebinding($abstract, Closure)
return $this->reboundCallbacks[$abstract] ?? []; }
Is there a singletonIf() equivalent to bind()?
Yes, and it has been around forever.
public function bindIf(
$abstract,
$concrete = null,
$shared = false
) {
if (! $this->bound($abstract)) {
$this->bind($abstract, $concrete, $shared);
}
}
Let's use our Auth service in a Slim action closure
$app->post('/{id}', function ($req, $res, $args) {
$id = $args['id'];
// snip
if (!$this->auth->isAuthorized($req, $id))
return $res->withRedirect('/');
// snip
}
...wait...
- How can we access the auth service via a property?
- How is it accessible as all-lower-case "auth"?
How can we access the auth service via a property?
Slim binds the container to $this in route closures, and...
public function __get($key)
{
return $this[$key];
}
How is it accessible as all-lower-case "auth"?
$container->alias(Auth::class, 'auth');
public function alias($abstract, $alias)
{
// check moved from getAlias() in 5.8
if ($alias === $abstract)
throw new LogicException(
"[{$abstract}] is aliased to itself."
);
// forward lookup
$this->aliases[$alias] = $abstract;
// reverse lookup
$this->abstractAliases[$abstract][] = $alias;
}
Under the Hood: Aliases
public function isAlias($name)
{
return isset($this->aliases[$name]);
}
public function getAlias($abstract)
{
if (! isset($this->aliases[$abstract])) {
return $abstract;
}
return $this->getAlias($this->aliases[$abstract]);
}
Under the Hood: Aliases in use via Resolve()
protected function resolve(
string $abstract,
array $parameters = [],
bool $raiseEvents = true
) {
$abstract = $this->getAlias($abstract);
// do things with the un-aliased abstract identifier
}
getAlias() is used in 10 other places in the Container class, including in getAlias()
...but I'm in a legacy app that pre-defines a dependency!
$container->instance(MyClass::class, $preBuiltInstance);
public function instance($abstract, $instance)
{
$this->removeAbstractAlias($abstract);
$isBound = $this->bound($abstract);
unset($this->aliases[$abstract]);
$this->instances[$abstract] = $instance;
if ($isBound) $this->rebound($abstract);
return $instance;
}
not to be confused with...
public static function getInstance()
{
if (is_null(static::$instance)) {
static::$instance = new static;
}
return static::$instance;
}
public static function setInstance(
ContainerContract $container = null
) {
return static::$instance = $container;
}
This can't be autowired without some help
class RaffleService {
// properties for dependencies
public function __construct(
ExtendedPdoInterface $db, // interface
SMS $sms, // interface
$phone_number // string
) {
// set properties
}
// snip
}
Parameter #1: Bind abstract to concrete
$container->bind(
ExtendedPdoInterface::class,
ExtendedPdo::class
);
Parameter #2: Bind abstract To closure
$container[SMS::class] = function() use ($env) {
if (isset($env['TWILIO_SID']))
return new TwilioSMS(/* env params */);
if (isset($env['NEXMO_KEY']))
return new NexmoSMS(/* env params */);
if (isset($env['DUMMY_SMS_WAIT_MS']))
return new DummySMS($env['DUMMY_SMS_WAIT_MS']);
throw new InvalidArgumentException(/* some message */);
};
Under the hood: array setting is slightly different
public function offsetSet($key, $value)
{
$this->bind(
$key,
$value instanceof Closure
? $value
: function () use ($value) { return $value; }
);
}
Parameter #3: Contextual Binding
$container
->when(RaffleService::class) // can be an array as of 5.7
->needs('$phone_number')
->give($env['PHONE_NUMBER']); // can be a Closure
// or, for a simple cases like this...
$container->addContextualBinding(
RaffleService::class,
'$phone_number',
$env['PHONE_NUMBER']
);
Under the Hood: Contextual Binding
public function when($concrete)
{
$aliases = [];
foreach (Util::arrayWrap($concrete) as $c) {
$aliases[] = $this->getAlias($c);
}
return new ContextualBindingBuilder($this, $aliases);
}
Under the Hood: Contextual Binding
// inside ContextualBindingBuilder
public function needs($abstract) {
$this->needs = $abstract;
return $this;
}
public function give($implementation) {
foreach (Util::arrayWrap($this->concrete) as $concrete)
$this->container->addContextualBinding(
$concrete,
$this->needs,
$implementation
);
}
Under the Hood: Contextual Binding
// back inside Container
public function addContextualBinding(
$concrete,
$abstract,
$implementation
) {
$this->contextual[$concrete][$this->getAlias($abstract)]
= $implementation;
}
...but how do we resolve() dependencies?
Under the Hood: Resolve()
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = !empty($parameters)
|| !is_null($this->getContextualConcrete($abstract));
if (isset($this->instances[$abstract])
&& !$needsContextualBuild)
return $this->instances[$abstract];
// snip
}
Under the Hood: Resolve()
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = !empty($parameters)
|| !is_null($this->getContextualConcrete($abstract));
if (isset($this->instances[$abstract])
&& !$needsContextualBuild)
return $this->instances[$abstract];
// snip
}
Under the Hood: getContextualConcrete()
protected function getContextualConcrete($abstract)
{
// e.g. RaffleService::class
if (! is_null($binding = $this->findInContextualBindings($abstract)))
return $binding;
if (empty($this->abstractAliases[$abstract]))
return;
// e.g. "raffleService"
foreach ($this->abstractAliases[$abstract] as $alias)
if (! is_null($binding = $this->findInContextualBindings($alias)))
return $binding;
}
Under the Hood: FindInContextualBindings()
protected function findInContextualBindings($abstract)
{
return $this->contextual[end($this->buildStack)][$abstract]
?? null;
}
Under the Hood: Resolve()
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = !empty($parameters)
|| !is_null($this->getContextualConcrete($abstract));
if (isset($this->instances[$abstract])
&& !$needsContextualBuild)
return $this->instances[$abstract];
// snip
}
Under the Hood: Resolve()
protected function resolve(/* snip */) { // snip $this->with[] = $parameters; $concrete = $this->getConcrete($abstract); // snip }
Under the Hood: Resolve()
protected function resolve(/* snip */) { // snip $this->with[] = $parameters; $concrete = $this->getConcrete($abstract); // snip }
Under the Hood: Resolve()
protected function getConcrete(string $abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
// if we have an explicit binding, use it
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
// otherwise, let autowiring do its job
return $abstract;
}
Under the Hood: Resolve()
protected function resolve(/* snip */)
{
// $concrete was just set to $this->getConcrete($abstract)
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// snip
}
Under the Hood: Resolve()
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract
|| $concrete instanceof Closure;
}
Under the Hood: Resolve()
protected function resolve(/* snip */) { // snip if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete); } else { // non-Closure contextual concrete $object = $this->make($concrete); } // snip }
get ready for some Reflection
Reflection
Under the Hood: Build()
public function build($concrete)
{
if ($concrete instanceof Closure)
return $concrete($this, $this->getLastParameterOverride());
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e) { // try-catch as of 6.x
throw new BindingResolutionException(
"Target class [$concrete] does not exist.", 0, $e
);
}
if (! $reflector->isInstantiable()) // abstract or interface
return $this->notInstantiable($concrete);
// snip
}
Under the Hood: Build()
public function build($concrete)
{
// snip
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
// snip
}
Under the Hood: Build()
public function build($concrete)
{
// snip
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
// snip
}
Under the Hood: Build()
public function build($concrete) { // snip $dependencies = $constructor->getParameters(); try { $instances = $this->resolveDependencies($dependencies); } catch (BindingResolutionException $e) { array_pop($this->buildStack); throw $e; } array_pop($this->buildStack); return $reflector->newInstanceArgs($instances); }
Under the Hood: Build()
public function build($concrete) { // snip $dependencies = $constructor->getParameters(); try { $instances = $this->resolveDependencies($dependencies); } catch (BindingResolutionException $e) { array_pop($this->buildStack); throw $e; } array_pop($this->buildStack); return $reflector->newInstanceArgs($instances); }
Under the Hood: Build()
protected function resolveDependencies(array $dependencies)
{
$results = [];
foreach ($dependencies as $dependency) {
if ($this->hasParameterOverride($dependency)) {
$results[] = $this->getParameterOverride($dependency);
continue;
}
$results[] = is_null($dependency->getClass())
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
}
return $results;
}
Under the Hood: Build()
protected function hasParameterOverride($dependency) {
return array_key_exists(
$dependency->name, $this->getLastParameterOverride()
);
}
protected function getLastParameterOverride() {
return count($this->with) ? end($this->with) : [];
}
protected function getParameterOverride($dependency) {
return $this->getLastParameterOverride()[$dependency->name];
}
Under the Hood: Build()
protected function resolveDependencies(array $dependencies)
{
$results = [];
foreach ($dependencies as $dependency) {
if ($this->hasParameterOverride($dependency)) {
$results[] = $this->getParameterOverride($dependency);
continue;
}
$results[] = is_null($dependency->getClass())
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
}
return $results;
}
Under the Hood: Resolving primitives
protected function resolvePrimitive(ReflectionParameter $parameter)
{
if (! is_null(
$concrete = $this->getContextualConcrete('$'.$parameter->name)
)) {
return $concrete instanceof Closure ? $concrete($this) : $concrete;
}
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
$this->unresolvablePrimitive($parameter);
}
Under the Hood: Resolving primitives
protected function resolvePrimitive(ReflectionParameter $parameter)
{
if (! is_null(
$concrete = $this->getContextualConcrete('$'.$parameter->name)
)) {
return $concrete instanceof Closure ? $concrete($this) : $concrete;
}
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
$this->unresolvablePrimitive($parameter);
}
Under the Hood: Resolving primitives
protected function resolvePrimitive(ReflectionParameter $parameter)
{
if (! is_null(
$concrete = $this->getContextualConcrete('$'.$parameter->name)
)) {
return $concrete instanceof Closure ? $concrete($this) : $concrete;
}
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
$this->unresolvablePrimitive($parameter);
}
Under the Hood: building Classes
protected function resolveClass(ReflectionParameter $parameter)
{
try {
return $this->make($parameter->getClass()->name);
} catch (BindingResolutionException $e) {
if ($parameter->isOptional()) {
return $parameter->getDefaultValue();
}
throw $e;
}
}
Under the Hood: building Classes
protected function resolveClass(ReflectionParameter $parameter)
{
try {
return $this->make($parameter->getClass()->name);
} catch (BindingResolutionException $e) {
if ($parameter->isOptional()) {
return $parameter->getDefaultValue();
}
throw $e;
}
}
...and that's how Container::Build() works!
Under the Hood: Resolve()
protected function resolve(/* snip */)
{
// $object = $this->build($concrete) or $this->make($concrete)
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
// snip
}
Under the Hood: Resolve()
protected function resolve(/* snip */)
{
// $object = $this->build($concrete) or $this->make($concrete)
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
// snip
}
Under the Hood: Extenders
protected function getExtenders($abstract)
{
$abstract = $this->getAlias($abstract);
return $this->extenders[$abstract] ?? [];
}
See Extender docs for how to use, and this code block for the extend() implementation
Under the Hood: Resolve()
protected function resolve(/* snip */)
{
// $object = $this->build($concrete) or $this->make($concrete)
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
// snip
}
Under the Hood: Resolve()
protected function resolve(/* snip */) { // snip; object has been extended and persisted to instances if needed if ($raiseEvents) { $this->fireResolvingCallbacks($abstract, $object); } $this->resolved[$abstract] = true; array_pop($this->with); return $object; }
Under the Hood: Resolution callbacks
protected function fireResolvingCallbacks($abstract, $object) {
// set by resolving(Closure)
$this->fireCallbackArray($object, $this->globalResolvingCallbacks);
$this->fireCallbackArray( // set by resolving($abstract, Closure)
$object,
$this->getCallbacksForType(
$abstract,
$object,
$this->resolvingCallbacks
)
);
// set by afterResolving(Closure) && afterResolving($abstract, Closure)
$this->fireAfterResolvingCallbacks($abstract, $object);
}
Under the Hood: Resolve()
protected function resolve(/* snip */) { // snip; object has been extended and persisted to instances if needed if ($raiseEvents) { $this->fireResolvingCallbacks($abstract, $object); } $this->resolved[$abstract] = true; array_pop($this->with); return $object; }
...and that's how you resolve a dependency
What we learned
- How to use a few of the Illuminate Container's major features
- How those features work under the hood
- How these features have changed in recent releases
- How to use the Container outside Laravel
Bonus: Other changes in versions
Bonus: Method Calls
Method bindings
- bindMethod($method, $callback)
- $method can be Class@method or array-style
- hasMethodBinding()
- callMethodBinding($method, $instance)
- $instance is first param passed to the bound method
- Container is second
injecting callables with call() and BoundMethod::call()
You can also curry closures with wrap($closure, $params = [])
Thanks! Questions?
- ian.im/icbg19 - these slides
- ian.im/icraphple - sample code
- joind.in/talk/377b0 - rate this talk!
- github.com/iansltx - my code
- @iansltx - my tweets
Looking into the Illuminate Container - Bulgaria PHP 2019
By Ian Littman
Looking into the Illuminate Container - Bulgaria PHP 2019
If you use Laravel, you’re taking advantage of a feature-filled, somewhat complex depedency injection container. In this presentation we’ll pull back the curtain on the magic behind the container’s auto-wiring, contextual binding, and other such features, stopping along the way to highlight upgrades that the package has gotten over succesive versions of Laravel since 5.5. If you aren’t a Laravel dev and need to dependency-inject your application, you might even come out of this presentation deciding that the Illuminate container is the best solution for your particular use case!
- 2,157