Injector
A recursive dependency injector used to bootstrap and wire together S.O.L.I.D., object-oriented PHP applications.
Install / Use
/learn @amphp/InjectorREADME
injector
A recursive dependency injector to bootstrap and wire together S.O.L.I.D., object-oriented PHP applications.
How It Works
Among other things, the injector recursively instantiates class dependencies based on the parameter type-hints specified in class constructor signatures. This requires the use of Reflection. You may have heard that "reflection is slow". Let's clear something up: anything can be "slow" if you're doing it wrong. Reflection is an order of magnitude faster than disk access and several orders of magnitude faster than retrieving information (for example) from a remote database. Additionally, each reflection offers the opportunity to cache the results if you're worried about speed. The injector caches any reflections it generates to minimize the potential performance impact.
The injector is NOT a Service Locator. DO NOT turn it into one by passing the injector into your application classes. Service Locator is an anti-pattern; it hides class dependencies, makes code more difficult to maintain and makes a liar of your API! You should only use an injector for wiring together the disparate parts of your application during your bootstrap phase.
The Guide
Basic Usage
- Basic Instantiation
- Injection Definitions
- Type-Hint Aliasing
- Non-Class Parameters
- Global Parameter Definitions
Advanced Usage
- Instance Sharing
- Instantiation Delegates
- Prepares and Setter Injection
- Injecting for Execution
- Dependency Resolution
Example Use Cases
Requirements and Installation
- Requires PHP 8.0 or higher.
Installation
Composer
You may also use composer to include the project as a dependency in your projects composer.json. The relevant package is amphp/injector.
Alternatively require the package using composer cli:
composer require amphp/injector
Basic Usage
To start using the injector, simply create a new instance of the Amp\Injector\Injector ("the Injector") class:
<?php
$injector = new Amp\Injector\Injector;
Basic Instantiation
If a class doesn't specify any dependencies in its constructor signature there's little point in using the Injector to generate it. However, for the sake of completeness consider that you can do the following with equivalent results:
<?php
$injector = new Amp\Injector\Injector;
$obj1 = new SomeNamespace\MyClass;
$obj2 = $injector->make(SomeNamespace\MyClass::class);
var_dump($obj2 instanceof SomeNamespace\MyClass); // true
Concrete Type-hinted Dependencies
If a class only asks for concrete dependencies you can use the Injector to inject them without
specifying any injection definitions. For example, in the following scenario you can use the
Injector to automatically provision MyClass with the required SomeDependency and AnotherDependency
class instances:
<?php
class SomeDependency {}
class AnotherDependency {}
class MyClass {
public $dep1;
public $dep2;
public function __construct(SomeDependency $dep1, AnotherDependency $dep2) {
$this->dep1 = $dep1;
$this->dep2 = $dep2;
}
}
$injector = new Amp\Injector\Injector;
$myObj = $injector->make(MyClass::class);
var_dump($myObj->dep1 instanceof SomeDependency); // true
var_dump($myObj->dep2 instanceof AnotherDependency); // true
Recursive Dependency Instantiation
One of the Injector's key attributes is that it recursively traverses class dependency trees to
instantiate objects. This is just a fancy way of saying, "if you instantiate object A which asks for
object B, the Injector will instantiate any of object B's dependencies so that B can be instantiated
and provided to A". This is perhaps best understood with a simple example. Consider the following
classes in which a Car asks for Engine and the Engine class has concrete dependencies of its
own:
<?php
class Car {
private $engine;
public function __construct(Engine $engine) {
$this->engine = $engine;
}
}
class Engine {
private $sparkPlug;
private $piston;
public function __construct(SparkPlug $sparkPlug, Piston $piston) {
$this->sparkPlug = $sparkPlug;
$this->piston = $piston;
}
}
$injector = new Amp\Injector\Injector;
$car = $injector->make(Car::class);
var_dump($car instanceof Car); // true
Injection Definitions
You may have noticed that the previous examples all demonstrated instantiation of classes with explicit, type-hinted, concrete constructor parameters. Obviously, many of your classes won't fit this mold. Some classes will type-hint interfaces and abstract classes. Some will specify scalar parameters which offer no possibility of type-hinting in PHP. Still other parameters will be arrays, etc. In such cases we need to assist the Injector by telling it exactly what we want to inject.
Defining Class Names for Constructor Parameters
Let's look at how to provision a class with non-concrete type-hints in its constructor signature.
Consider the following code in which a Car needs an Engine and Engine is an interface:
<?php
interface Engine {}
class V8 implements Engine {}
class Car {
private $engine;
public function __construct(Engine $engine) {
$this->engine = $engine;
}
}
To instantiate a Car in this case, we simply need to define an injection definition for the class
ahead of time:
<?php
$injector = new Amp\Injector\Injector;
$injector->define(Car::class, ['engine' => 'V8']);
$car = $injector->make(Car::class);
var_dump($car instanceof Car); // true
The most important points to notice here are:
- A custom definition is an
arraywhose keys match constructor parameter names - The values in the definition array represent the class names to inject for the specified parameter key
Because the Car constructor parameter we needed to define was named $engine, our definition
specified an engine key whose value was the name of the class (V8) that we want to inject.
Custom injection definitions are only necessary on a per-parameter basis. For example, in the
following class we only need to define the injectable class for $arg2 because $arg1 specifies a
concrete class type-hint:
<?php
class MyClass {
private $arg1;
private $arg2;
public function __construct(SomeConcreteClass $arg1, SomeInterface $arg2) {
$this->arg1 = $arg1;
$this->arg2 = $arg2;
}
}
$injector = new Amp\Injector\Injector;
$injector->define(MyClass::class, ['arg2' => 'SomeImplementationClass']);
$myObj = $injector->make(MyClass::class);
NOTE: Injecting instances where an abstract class is type-hinted works in exactly the same way as the above examples for interface type-hints.
Using Existing Instances in Injection Definitions
Injection definitions may also specify a pre-existing instance of the requisite class instead of the string class name:
<?php
interface SomeInterface {}
class SomeImplementation implements SomeInterface {}
class MyClass {
private $dependency;
public function __construct(SomeInterface $dependency) {
$this->dependency = $dependency;
}
}
$injector = new Amp\Injector\Injector;
$dependencyInstance = new SomeImplementation;
$injector->define(MyClass::class, [':dependency' => $dependencyInstance]);
$myObj = $injector->make(MyClass::class);
var_dump($myObj instanceof MyClass); // true
NOTE: Since this
define()call is passing raw values (as evidenced by the colon:usage), you can achieve the same result by omitting the array key(s) and relying on parameter order rather than name. Like so:$injector->define('MyClass', [$dependencyInstance]);
Specifying Injection Definitions On the Fly
You may also specify injection definitions at call-time with Amp\Injector\Injector::make. Consider:
<?php
interface SomeInterface {}
class SomeImplementationClass implements SomeInterface {}
class MyClass {
private $dependency;
public function __construct(SomeInterface $dependency) {
$this->dependency = $dependency;
}
}
$injector = new Amp\Injector\Injector;
$myObj = $injector->make('MyClass', ['dependency' => 'SomeImplementationClass']);
var_dump($myObj instanceof MyClass); // true
The above code shows how even though we haven't called the Injector's define method, the
call-time specification allows us to instantiate MyClass.
NOTE: on-the-fly instantiation definitions will override a pre-defined definition for the specified class, but only in the context of that particular call to
Amp\Injector\Injector::make.
Type-Hint Aliasing
Programming to interfaces is one of the most useful concepts in object-oriented design (OOD), and well-designed code should type-hint interfaces whenever possible. But does this mean we have to assign injection definitions for every class in our application to reap the benefits of abstracted dependencies? Thankfully the answer to this question is, "NO." The Injector accommodates this goal by accepting "aliases". Consider:
<?php
interface Engine {}
class V8 implements Engine {}
class Car {
private $engine;
public function __construct(Engine $engine) {
$this->engine = $engine;
}
}
$injector = new Amp\Injector\Injector;
// Tell the Injector class to inject an instance of V8 any time
// it encounters an Engine type-hint
$injector->alias('Engine', 'V8');
$car = $injector->make('Car');
var_dump($car instanceof Car); // bool(true)
In this example we've demonstrated how to
