Dependency Injection
Installation
Introduction
The framework uses Qubus/Injector which 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.
Basic Usage
To start using the injector, simply create a new instance of the Qubus\Injector\Injector
("the Injector") class:
<?php
$injector = new Qubus\Injector\Injector(
Qubus\Injector\Config\InjectorFactory::create([])
);
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 Qubus\Injector\Injector(
Qubus\Injector\Config\InjectorFactory::create([])
);
$obj1 = new App\MyClass;
$obj2 = $injector->make('App\MyClass');
var_dump($obj2 instanceof App\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
declare(strict_types=1);
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 Qubus\Injector\Injector(
Qubus\Injector\Config\InjectorFactory::create([])
);
$myObj = $injector->make('MyClass');
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
declare(strict_types=1);
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 Qubus\Injector\Injector(
Qubus\Injector\Config\InjectorFactory::create([])
);
$car = $injector->make('Car');
var_dump($car instanceof Car); // true
App-Bootstrapping
DICs should be used to wire together the disparate objects of your application into a cohesive functional unit (generally at the bootstrap or front-controller stage of the application). One such usage provides an elegant solution for one of the thorny problems in object-oriented (OO) web applications: how to instantiate classes in a routed environment where the dependencies are not known ahead of time. The CodefyPHP framework supplies a solution to the problem in the form of Service Providers.
Service Providers
To make it easier to use the Injector/Container/ServiceContainer
, the framework can be bootstrapped with ServiceProviders
.
Service providers are used to bootstrap your application with the injection of core classes and dependencies.
A service provider will have one or two methods: register()
and/or boot()
. The register method should only be used to define parameters, define new services or extend services.
The boot method is called after all service providers have been registered.
From here on, all the examples will show how to use Service Providers to inject your dependencies throughout the entire application. Codefy\Framework\Application
is the main part of the framework, and it extends the Injector. In the Service Providers, we call Codefy\Framework\Application
by using $this->codefy
.
Registering Service Providers
You can register service providers via bootstrap/providers.php
:
Or you can register service providers via config/app.php
using the providers
key:
<?php
'providers' => Codefy\Framework\Support\CodefyServiceProvider::defaultProviders()->merge([
// Application Service Providers...
App\Infrastructure\Providers\AppServiceProvider::class,
])->toArray(),
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
declare(strict_types=1);
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
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Car;
use Codefy\Framework\Support\CodefyServiceProvider;
use V8;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$this->codefy->define(Car::class, ['engine' => V8::class]);
}
}
<?php
// test it works
$car = Codefy\Framework\Helpers\app(Car::class);
var_dump($car instanceof Car); // true
The most important points to notice here are:
- A custom definition is an array whose 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
declare(strict_types=1);
class MyClass {
private $arg1;
private $arg2;
public function __construct(SomeConcreteClass $arg1, SomeInterface $arg2) {
$this->arg1 = $arg1;
$this->arg2 = $arg2;
}
}
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Codefy\Framework\Support\CodefyServiceProvider;
use MyClass;
use SomeImplementationClass;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$this->codefy->define(MyClass::class, ['arg2' => SomeImplementationClass::class]);
}
}
Info:
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
declare(strict_types=1);
interface SomeInterface {}
class SomeImplementation implements SomeInterface {}
class MyClass {
private $dependency;
public function __construct(SomeInterface $dependency) {
$this->dependency = $dependency;
}
}
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Codefy\Framework\Support\CodefyServiceProvider;
use MyClass;
use SomeImplementation;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$dependencyInstance = new SomeImplementation;
$this->codefy->define(MyClass::class, [':dependency' => $dependencyInstance]);
}
}
<?php
// test it works
$myObj = Codefy\Framework\Helpers\app(MyClass::class);
var_dump($myObj instanceof MyClass); // true
Info:
Since the 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: $this->codefy->define(MyClass::class, [$dependencyInstance]);
.
Specifying Injection Definitions On the Fly
You may also specify injection definitions at call-time with Qubus\Injector\Injector::make
. Consider:
<?php
declare(strict_types=1);
interface SomeInterface {}
class SomeImplementationClass implements SomeInterface {}
class MyClass {
private $dependency;
public function __construct(SomeInterface $dependency) {
$this->dependency = $dependency;
}
}
$myObj = Codefy\Framework\Helpers\app(MyClass::class, ['dependency' => SomeImplementationClass::class]);
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 Qubus\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
declare(strict_types=1);
interface Engine {}
class V8 implements Engine {}
class Car {
private $engine;
public function __construct(Engine $engine) {
$this->engine = $engine;
}
}
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Car;
use Codefy\Framework\Support\CodefyServiceProvider;
use Engine;
use V8;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
// Tell the Injector class to inject an instance of V8 any time
// it encounters an Engine type-hint
$this->codefy->alias(Engine::class, V8::class);
}
}
// test it works
$car = Codefy\Framework\Helpers\app(Car::class);
var_dump($car instanceof Car); // true
In this example we've demonstrated how to specify an alias class for any occurrence of a particular interface or abstract class type-hint. Once an implementation is assigned, the Injector will use it to provision any parameter with a matching type-hint.
Note:
If an injection definition is defined for a parameter covered by an implementation assignment, the definition takes precedence over the implementation.
Non-Class Parameters
All the previous examples have demonstrated how the Injector class instantiates parameters based on type-hints, class name definitions and existing instances. But what happens if we want to inject a scalar or other non-object variable into a class? First, let's establish the following behavioral rule:
Note:
The Injector assumes all named-parameter definitions are class names by default.
If you want the Injector to treat a named-parameter definition as a "raw" value and not a class name, you must prefix the parameter name in your definition with a colon character :
. For example, consider the following code in which we tell the Injector to share a PDO
database connection instance and define its scalar constructor parameters:
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Codefy\Framework\Support\CodefyServiceProvider;
use PDO;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$this->codefy->share(PDO::class);
$this->codefy->define(PDO::class, [
':dsn' => 'mysql:dbname=testdb;host=127.0.0.1',
':username' => 'dbuser',
':password' => 'dbpass'
]);
}
}
The colon character preceding the parameter names tells the Injector that the associated values ARE NOT class names. If the colons had been omitted above, Qubus Injector would attempt to instantiate classes of the names specified in the string and an exception would result. Also, note that we could just as easily specified arrays or integers or any other data type in the above definitions. As long as the parameter name is prefixed with a :
, Qubus Injector will inject the value directly without attempting to instantiate it.
Info:
As mentioned previously, since the define()
call is passing raw values, you may opt to assign the values by parameter order rather than name. Since PDO's first three parameters are $dsn
, $username
, and $password
, in that order, you could accomplish the same result by leaving out the array keys, like so: $this->codefy->define(PDO::class, ['mysql:dbname=testdb;host=127.0.0.1', 'dbuser', 'dbpass']);
.
Global Parameter Definitions
Sometimes applications may reuse the same value everywhere. However, it can be a hassle to manually specify definitions for this sort of thing everywhere it might be used in the app. Qubus Injector mitigates this problem by exposing the defineParam()
method. Consider the following example:
<?php
declare(strict_types=1);
class MyClass {
public $myValue;
public function __construct($myValue) {
$this->myValue = $myValue;
}
}
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Codefy\Framework\Support\CodefyServiceProvider;
use MyClass;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$myUniversalValue = 42;
$this->codefy->defineParam('myValue', $myUniversalValue);
}
}
// test it works
$obj = Codefy\Framework\Helpers\app(MyClass::class);
var_dump($obj->myValue === 42); // bool(true)
Because we specified a global definition for myValue
, all parameters that are not in some other way defined (as below) that match the specified parameter name are autofilled with the global value. If a parameter matches any of the following criteria the global value is not used:
- A typehint
- A predefined injection definition
- A custom call time definition
Advanced Usage
Instance Sharing
One of the more ubiquitous plagues in modern OOP is the Singleton anti-pattern. Coders looking to limit classes to a single instance often fall into the trap of using static Singleton implementations for things like configuration classes and database connections. While it's often necessary to prevent multiple instances of a class, the Singleton method spells death to testability and should generally be avoided. Qubus\Injector\Injector
makes sharing class instances across contexts a triviality while allowing maximum testability and API transparency.
Let's consider how a typical problem facing object-oriented web applications is easily solved by wiring together your application using the Injector. Here, we want to inject a single database connection instance across multiple layers of an application. We have a controller class that asks for a DataMapper
that requires a PDO
database connection instance:
<?php
declare(strict_types=1);
class DataMapper {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
}
class MyController {
private $mapper;
public function __construct(DataMapper $mapper) {
$this->mapper = $mapper;
}
}
<?php
namespace App\Infrastructure\Providers;
declare(strict_types=1);
use Codefy\Framework\Support\CodefyServiceProvider;
use PDO;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
$this->codefy->share($db);
}
}
In the above code, the DataMapper
instance will be provisioned with the same PDO
database connection instance we originally shared. This example is contrived and overly simple, but the implication should be clear:
Info:
By sharing an instance of a class, Qubus\Injector\Injector
will always use that instance when provisioning classes that type-hint the shared class.
A Simpler Example
Let's look at a simple proof of concept:
<?php
declare(strict_types=1);
class Person {
public $name = 'John Snow';
}
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Codefy\Framework\Support\CodefyServiceProvider;
use Person;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$this->codefy->share(Person::class);
}
}
// test it out
$person = Codefy\Framework\Helpers\app(Person::class);
var_dump($person->name); // John Snow
$person->name = 'Arya Stark';
$anotherPerson = Codefy\Framework\Helpers\app(Person::class);
var_dump($anotherPerson->name); // Arya Stark
var_dump($person === $anotherPerson); // bool(true) because it's the same instance!
Defining an object as shared will store the provisioned instance in the Injector's shared cache and all future requests to the provider for an injected instance of that class will return the originally created object. Note that in the above code, we shared the class name (Person
) instead of an actual instance. Sharing works with either a class name or an instance of a class. The difference is that when you specify a class name, the Injector will cache the shared instance the first time it is asked to create it.
Note:
Once the Injector caches a shared instance, call-time definitions passed to Qubus\Injector\Injector::make
will have no effect. Once shared, an instance will always be returned for instantiations of its type until the object is un-shared or refreshed:
Instantiation Delegates
Often factory classes/methods are used to prepare an object for use after instantiation. The Injector allows you to integrate factories and builders directly into the injection process by specifying callable instantiation delegates on a per-class basis. Let's look at a very basic example to demonstrate the concept of injection delegates:
<?php
declare(strict_types=1);
class MyComplexClass {
public $verification = false;
public function doSomethingAfterInstantiation() {
$this->verification = true;
}
}
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Codefy\Framework\Support\CodefyServiceProvider;
use MyComplexClass;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$complexClassFactory = function() {
$obj = new MyComplexClass();
$obj->doSomethingAfterInstantiation();
return $obj;
};
$this->codefy->delegate(MyComplexClass::class, $complexClassFactory);
}
}
// test it out
$obj = Codefy\Framework\Helpers\app(MyComplexClass::class);
var_dump($obj->verification); // bool(true)
In the above code we delegate instantiation of the MyComplexClass
class to a closure, $complexClassFactory
. Once this delegation is made, the Injector will return the results of the specified closure when asked to instantiate MyComplexClass
.
Available Delegate Types
Any valid PHP callable may be registered as a class instantiation delegate using Qubus\Injector\Injector::delegate
. Additionally, you may specify the name of a delegate class that specifies an __invoke
method, and it will be automatically provisioned and have its __invoke
method called at delegation time. Instance methods from uninstantiated classes may also be specified using the ['NonStaticClassName', 'factoryMethod']
construction. For example: visiting:
<?php
declare(strict_types=1);
class SomeClassWithDelegatedInstantiation {
public $value = 0;
}
class SomeFactoryDependency {}
class MyFactory {
private $dependency;
function __construct(SomeFactoryDependency $dep) {
$this->dependency = $dep;
}
function __invoke() {
$obj = new SomeClassWithDelegatedInstantiation;
$obj->value = 1;
return $obj;
}
function factoryMethod() {
$obj = new SomeClassWithDelegatedInstantiation;
$obj->value = 2;
return $obj;
}
}
// Works because MyFactory specifies a magic __invoke method
Codefy\Framework\Helpers\app()->delegate(SomeClassWithDelegatedInstantiation::class, MyFactory::class);
$obj = Codefy\Framework\Helpers\app(SomeClassWithDelegatedInstantiation::class);
var_dump($obj->value); // int(1)
// This also works
Codefy\Framework\Helpers\app()->delegate(SomeClassWithDelegatedInstantiation::class, 'MyFactory::factoryMethod');
$obj = Codefy\Framework\Helpers\app(SomeClassWithDelegatedInstantiation::class);
var_dump($obj->value); // int(2)
Prepares and Setter Injection
Constructor injection is almost always preferable to setter injection. However, some APIs require additional post-instantiation mutations. The Injector accommodates these use cases with its prepare()
method. Users may register any class or interface name for post-instantiation modification. Consider:
<?php
declare(strict_types=1);
class MyClass {
public $myProperty = 0;
}
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Codefy\Framework\Support\CodefyServiceProvider;
use MyClass;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$this->codefy->prepare(MyClass::class, function(int $myObj) {
$myObj->myProperty = 42;
});
}
}
// test it works
$myObj = Codefy\Framework\Helpers\app(MyClass::class);
var_dump($myObj->myProperty); // int(42)
While the above example is contrived, the usefulness should be clear.
Injecting for Execution
In addition to provisioning class instances using constructors, the Injector can also recursively instantiate the parameters of any valid PHP callable. The following examples all work:
<?php
declare(strict_types=1);
$injector = Codefy\Framework\Helpers\app();
$injector->execute(function(){});
$injector->execute([$objectInstance, 'methodName']);
$injector->execute('globalFunctionName');
$injector->execute('MyStaticClass::myStaticMethod');
$injector->execute(['MyStaticClass', 'myStaticMethod']);
$injector->execute(['MyChildStaticClass', 'parent::myStaticMethod']);
$injector->execute('ClassThatHasMagicInvoke');
$injector->execute($instanceOfClassThatHasMagicInvoke);
$injector->execute('MyClass::myInstanceMethod');
Additionally, you can pass in the name of a class for a non-static method and the injector will automatically provision an instance of the class (subject to any definitions or shared instances already stored by the injector) before provisioning and invoking the specified method:
<?php
declare(strict_types=1);
class Dependency {}
class AnotherDependency {}
class Example {
function __construct(Dependency $dep){}
function myMethod(AnotherDependency $arg1, $arg2) {
return $arg2;
}
}
$injector = Codefy\Framework\Helpers\app();
// outputs: int(42)
var_dump($injector->execute('Example::myMethod', $args = [':arg2' => 42]));
Dependency Resolution
The Injector resolves dependencies in the following order:
- If a shared instance exists for the class in question, the shared instance will always be returned.
- If a delegate callable is assigned for a class, its return result will always be used.
- If a call-time definition is passed to
Qubus\Injector\Injector::make
, that definition will be used. - If a pre-defined definition exists, it will be used.
- If a dependency is type-hinted, the Injector will recursively instantiate it subject to any implementations or definitions.
- If no type-hint exists and the parameter has a default value, the default value is injected.
- If a global parameter value is defined that value is used.
- Throw an exception because you did something stupid.
Avoiding Evil Singletons
A common difficulty in web applications is limiting the number of database connection instances. It's wasteful and slow to open up new connections each time we need to talk to a database. Unfortunately, using singletons to limit these instances makes code brittle and hard to test. Let's see how we can use a service provider to inject the same PDO instance across the entire scope of our application.
Say we have a service class that requires two separate data mappers to persist information to a database:
<?php
declare(strict_types=1);
use PDO;
use RecordNotFoundException;
class HouseMapper {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function find($houseId) {
$query = 'SELECT * FROM houses WHERE houseId = :houseId';
$stmt = $this->pdo->prepare($query);
$stmt->bindValue(':houseId', $houseId);
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Model\\Entities\\House');
$stmt->execute();
$house = $stmt->fetch(PDO::FETCH_CLASS);
if (false === $house) {
throw new RecordNotFoundException(
'No houses exist for the specified ID'
);
}
return $house;
}
// more data mapper methods here ...
}
class PersonMapper {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
// data mapper methods here
}
class SomeService {
private $houseMapper;
private $personMapper;
public function __construct(HouseMapper $hm, PersonMapper $pm) {
$this->houseMapper = $hm;
$this->personMapper = $pm;
}
public function doSomething() {
// do something with the mappers
}
}
In our wiring/bootstrap code, we simply instantiate the PDO instance once and share it in the context of the Injector:
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Codefy\Framework\Support\CodefyServiceProvider;
use MyClass;
use PDO;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$pdo = new PDO('sqlite:some_sqlite_file.sqlite');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->codefy->share($pdo);
}
}
// instantiate SomeService with the instantiated PDO instance
$service = Codefy\Framework\Helpers\app(SomeService::class);
In the above code, the DIC instantiates our service class. More importantly, the data mapper classes it generates to do so are injected with the same database connection instance we originally shared.
Of course, we don't have to manually instantiate our PDO
instance. We could just as easily seed the container with a definition for how to create the PDO
object and let it handle things for us:
<?php
declare(strict_types=1);
namespace App\Infrastructure\Providers;
use Codefy\Framework\Support\CodefyServiceProvider;
use MyClass;
use PDO;
final class ExampleServiceProvider extends CodefyServiceProvider
{
public function register(): void
{
$this->codefy->define(PDO::class, [
':dsn' => 'sqlite:some_sqlite_file.sqlite'
]);
$this->codefy->share(PDO::class);
}
}
// instantiate SomeService with the instantiated PDO instance
$service = Codefy\Framework\Helpers\app(SomeService::class);
In the above code, the injector will pass the string definition as the $dsn argument in the PDO::__construct
method and generate the shared PDO
instance automatically only if one of the classes it instantiates requires a PDO
instance!
PSR-11 Container
Codefy includes a PSR-11 compatible container which extends the Injector. You can type-hint the PSR-11 container interface, and it will return an Injector
instance.
<?php
return function (\Qubus\Routing\Psr7Router $router, Psr\Container\ContainerInterface $container) {
$user = $container->get(App\Infrastructure\Services\UserAuth);
//
};