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!
- 1,840