Skip to content

Yii Dependency Injection


Latest Stable VersionTotal DownloadsBuild statusScrutinizer Code QualityCode CoverageMutation testing badgestatic analysistype-coverage

PSR-11 compatible dependency injection container that's able to instantiate and configure classes resolving dependencies.


Features

  • PSR-11 compatible.
  • Supports property injection, constructor injection and method injection.
  • Detects circular references.
  • Accepts array definitions. You can use it with mergeable configs.
  • Provides optional autoload fallback for classes without explicit definition.
  • Allows delegated lookup and has a composite container.
  • Supports aliasing.
  • Supports service providers.
  • Has state resetter for long-running workers serving many requests, such as RoadRunner or Swoole.
  • Supports container delegates.

Requirements

  • PHP 8.0 or higher.
  • Multibyte String PHP extension.

Installation

You could install the package with composer:

shell
composer require yiisoft/di

Using the container

Usage of the DI container is simple: You first initialize it with an array of definitions. The array keys are usually interface names. It will then use these definitions to create an object whenever the application requests that type. This happens, for example, when fetching a type directly from the container somewhere in the application. But objects are also created implicitly if a definition has a dependency to another definition.

Usually one uses a single container for the whole application. It's often configured either in the entry script such as index.php or a configuration file:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions($definitions);

$container = new Container($config);

You could store the definitions in a .php file that returns an array:

php
return [
    EngineInterface::class => EngineMarkOne::class,
    'full_definition' => [
        'class' => EngineMarkOne::class,
        '__construct()' => [42], 
        '$propertyName' => 'value',
        'setX()' => [42],
    ],
    'closure' => fn (SomeFactory $factory) => $factory->create('args'),
    'static_call_preferred' => fn () => MyFactory::create('args'),
    'static_call_supported' => [MyFactory::class, 'create'],
    'object' => new MyClass(),
];

You can define an object in several ways:

  • In the simple case, an interface definition maps an id to a particular class.
  • A full definition describes how to instantiate a class in more detail:
    • class has the name of the class to instantiate.
    • __construct() holds an array of constructor arguments.
    • The rest of the config are property values (prefixed with $) and method calls, postfixed with (). They're set/called in the order they appear in the array.
  • Closures are useful if instantiation is tricky and can better done in code. When using these, arguments are auto-wired by type. ContainerInterface could be used to get current container instance.
  • If it's even more complicated, it's a good idea to move such a code into a factory and reference it as a static call.
  • While it's usually not a good idea, you can also set an already instantiated object into the container.

See yiisoft/definitions for more information.

After you configure the container, you can obtain a service can via get():

php
/** @var \Yiisoft\Di\Container $container */
$object = $container->get('interface_name');

Note, however, that it's bad practice using a container directly. It's much better to rely on auto-wiring as provided by the Injector available from the yiisoft/injector package.


Using aliases

The DI container supports aliases via the Yiisoft\Definitions\Reference class. This way you can retrieve objects by a more handy name:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkOne::class,
        'engine_one' => EngineInterface::class,
    ]);

$container = new Container($config);
$object = $container->get('engine_one');

Composite containers

A composite container combines many containers in a single container. When using this approach, you should fetch objects only from the composite container.

php
use Yiisoft\Di\CompositeContainer;
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$composite = new CompositeContainer();

$carConfig = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkOne::class,
        CarInterface::class => Car::class
    ]);
$carContainer = new Container($carConfig);

$bikeConfig = ContainerConfig::create()
    ->withDefinitions([
        BikeInterface::class => Bike::class
    ]);

$bikeContainer = new Container($bikeConfig);
$composite->attach($carContainer);
$composite->attach($bikeContainer);

// Returns an instance of a `Car` class.
$car = $composite->get(CarInterface::class);
// Returns an instance of a `Bike` class.
$bike = $composite->get(BikeInterface::class);

Note, that containers attached earlier override dependencies of containers attached later.

php
use Yiisoft\Di\CompositeContainer;
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$carConfig = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkOne::class,
        CarInterface::class => Car::class
    ]);

$carContainer = new Container($carConfig);

$composite = new CompositeContainer();
$composite->attach($carContainer);

// Returns an instance of a `Car` class.
$car = $composite->get(CarInterface::class);
// Returns an instance of a `EngineMarkOne` class.
$engine = $car->getEngine();

$engineConfig = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkTwo::class,
    ]);

$engineContainer = new Container($engineConfig);

$composite = new CompositeContainer();
$composite->attach($engineContainer);
$composite->attach($carContainer);

// Returns an instance of a `Car` class.
$car = $composite->get(CarInterface::class);
// Returns an instance of a `EngineMarkTwo` class.
$engine = $composite->get(EngineInterface::class);

Using service providers

A service provider is a special class that's responsible for providing complex services or groups of dependencies for the container and extensions of existing services.

A provider should extend from Yiisoft\Di\ServiceProviderInterface and must contain a getDefinitions() and getExtensions() methods. It should only provide services for the container and therefore should only contain code that's related to this task. It should never implement any business logic or other functionality such as environment bootstrap or applying changes to a database.

A typical service provider could look like:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ServiceProviderInterface;

class CarFactoryProvider extends ServiceProviderInterface
{
    public function getDefinitions(): array
    {
        return [
            CarFactory::class => [
                'class' => CarFactory::class,
                '$color' => 'red',
            ], 
            EngineInterface::class => SolarEngine::class,
            WheelInterface::class => [
                'class' => Wheel::class,
                '$color' => 'black',
            ],
            CarInterface::class => [
                'class' => BMW::class,
                '$model' => 'X5',
            ],
        ];    
    }
     
    public function getExtensions(): array
    {
        return [
            // Note that Garage should already be defined in a container 
            Garage::class => function(ContainerInterface $container, Garage $garage) {
                $car = $container
                    ->get(CarFactory::class)
                    ->create();
                $garage->setCar($car);
                
                return $garage;
            }
        ];
    } 
}

Here you created a service provider responsible for bootstrapping of a car factory with all its dependencies.

An extension is callable that returns a modified service object. In this case you get existing Garage service and put a car into the garage by calling the method setCar(). Thus, before applying this provider, you had an empty garage and with the help of the extension you fill it.

To add this service provider to a container, you can pass either its class or a configuration array in the extra config:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withProviders([CarFactoryProvider::class]);

$container = new Container($config);

When you add a service provider, DI calls its getDefinitions() and getExtensions() methods immediately and both services and their extensions get registered into the container.

Container tags

You can tag services in the following way:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions([  
        BlueCarService::class => [
            'class' => BlueCarService::class,
            'tags' => ['car'], 
        ],
        RedCarService::class => [
            'definition' => fn () => new RedCarService(),
            'tags' => ['car'],
        ],
    ]);

$container = new Container($config);

Now you can get tagged services from the container in the following way:

php
$container->get('tag@car');

The result is an array that has two instances: BlueCarService and RedCarService.

Another way to tag services is setting tags via container constructor:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions([  
        BlueCarService::class => [
            'class' => BlueCarService::class,
        ],
        RedCarService::class => fn () => new RedCarService(),
    ])
    ->withTags([
        // "car" tag has references to both blue and red cars
        'car' => [BlueCarService::class, RedCarService::class]
    ]);

$container = new Container($config);

Resetting services state

Despite stateful services isn't a great practice, these are often inevitable. When you build long-running applications with tools like Swoole or RoadRunner you should reset the state of such services every request. For this purpose you can use StateResetter with resetters callbacks:

php
$resetter = new StateResetter($container);
$resetter->setResetters([
    MyServiceInterface::class => function () {
        $this->reset(); // a method of MyServiceInterface
    },
]);

The callback has access to the private and protected properties of the service instance, so you can set initial state of the service efficiently without creating a new instance.

You should trigger the reset itself after each request-response cycle. For RoadRunner, it would look like the following:

php
while ($request = $psr7->acceptRequest()) {
    $response = $application->handle($request);
    $psr7->respond($response);
    $application->afterEmit($response);
    $container
        ->get(\Yiisoft\Di\StateResetter::class)
        ->reset();
    gc_collect_cycles();
}

Setting resetters in definitions

You define the reset state for each service by providing "reset" callback in the following way:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkOne::class,
        EngineMarkOne::class => [
            'class' => EngineMarkOne::class,
            'setNumber()' => [42],
            'reset' => function () {
                $this->number = 42;
            },
        ],
    ]);

$container = new Container($config);

Note:

resetters from definitions work only if you don't set StateResetter in definition or service providers.

Configuring StateResetter manually

To manually add resetters or in case you use Yii DI composite container with a third party container that doesn't support state reset natively, you could configure state resetter separately. The following example is PHP-DI:

php
MyServiceInterface::class => function () {
    // ...
},
StateResetter::class => function (ContainerInterface $container) {
    $resetter = new StateResetter($container);
    $resetter->setResetters([
        MyServiceInterface::class => function () {
            $this->reset(); // a method of MyServiceInterface
        },
    ]);
    return $resetter;
}

Specifying metadata for non-array definitions

To specify some metadata, such as in cases of "resetting services state" or "container tags," for non-array definitions, you could use the following syntax:

php
LogTarget::class => [
    'definition' => static function (LoggerInterface $logger) use ($params) {
        $target = ...
        return $target;
    },
    'reset' => function () use ($params) {
        ...
    },
],

Now you've explicitly moved the definition itself to "definition" key.

Delegates

Each delegate is a callable returning a container instance that's used in case DI can't find a service in a primary container:

php
function (ContainerInterface $container): ContainerInterface
{

}

To configure delegates use extra config:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDelegates([
        function (ContainerInterface $container): ContainerInterface {
            // ...
        }
    ]);


$container = new Container($config);

Tuning for production

By default, the container validates definitions right when they're set. In the production environment, it makes sense to turn it off:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withValidate(false);

$container = new Container($config);

Strict mode

Container may work in a strict mode, that's when you should define everything in the container explicitly. To turn it on, use the following code:

php
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withStrictMode(true);

$container = new Container($config);

Further reading

Benchmarks

To run benchmarks execute the next command

composer require phpbench/phpbench$ ./vendor/bin/phpbench run

Result example

\Yiisoft\Di\Tests\Benchmark\ContainerBench

benchConstructStupid....................I4 [μ Mo]/r: 438.566 435.190 (μs) [μSD μRSD]/r: 9.080μs 2.07%
benchConstructSmart.....................I4 [μ Mo]/r: 470.958 468.942 (μs) [μSD μRSD]/r: 2.848μs 0.60%
benchSequentialLookups # 0..............R5 I4 [μ Mo]/r: 2,837.000 2,821.636 (μs) [μSD μRSD]/r: 34.123μs 1.20%
benchSequentialLookups # 1..............R1 I0 [μ Mo]/r: 12,253.600 12,278.859 (μs) [μSD μRSD]/r: 69.087μs 0.56%
benchRandomLookups # 0..................R5 I4 [μ Mo]/r: 3,142.200 3,111.290 (μs) [μSD μRSD]/r: 87.639μs 2.79%
benchRandomLookups # 1..................R1 I2 [μ Mo]/r: 13,298.800 13,337.170 (μs) [μSD μRSD]/r: 103.891μs 0.78%
benchRandomLookupsComposite # 0.........R1 I3 [μ Mo]/r: 3,351.600 3,389.104 (μs) [μSD μRSD]/r: 72.516μs 2.16%
benchRandomLookupsComposite # 1.........R1 I4 [μ Mo]/r: 13,528.200 13,502.881 (μs) [μSD μRSD]/r: 99.997μs 0.74%
\Yiisoft\Di\Tests\Benchmark\ContainerMethodHasBench

benchPredefinedExisting.................R1 I4 [μ Mo]/r: 0.115 0.114 (μs) [μSD μRSD]/r: 0.001μs 1.31%
benchUndefinedExisting..................R5 I4 [μ Mo]/r: 0.436 0.432 (μs) [μSD μRSD]/r: 0.008μs 1.89%
benchUndefinedNonexistent...............R5 I4 [μ Mo]/r: 0.946 0.942 (μs) [μSD μRSD]/r: 0.006μs 0.59%
8 subjects, 55 iterations, 5,006 revs, 0 rejects, 0 failures, 0 warnings 
(best [mean mode] worst) = 0.113 [4,483.856 4,486.051] 0.117 (μs) 
⅀T: 246,612.096μs μSD/r 43.563μs μRSD/r: 1.336%

Warning!

These summary statistics can be misleading. You should always verify the individual subject statistics before drawing any conclusions.

Legend

  • μ: Mean time taken by all iterations in variant.
  • Mo: Mode of all iterations in variant.
  • μSD: μ standard deviation.
  • μRSD: μ relative standard deviation.
  • best: Maximum time of all iterations (minimal of all iterations).
  • mean: Mean time taken by all iterations.
  • mode: Mode of all iterations.
  • worst: Minimum time of all iterations (minimal of all iterations).

Command examples

  • Default report for all benchmarks that outputs the result to CSV-file

$ ./vendor/bin/phpbench run --report=default --progress=dots --output=csv_file

Generated MD-file example

DI benchmark report

suite: 1343b1dc0589cb4e985036d14b3e12cb430a975b, date: 2020-02-21, stime: 16:02:45

benchmarksubjectsetrevsitermem_peaktime_revcomp_z_valuecomp_deviation
ContainerBenchbenchConstructStupid0100001,416,784b210.938μs-1.48σ-1.1%
ContainerBenchbenchConstructStupid0100011,416,784b213.867μs+0.37σ+0.27%
ContainerBenchbenchConstructStupid0100021,416,784b212.890μs-0.25σ-0.18%
ContainerBenchbenchConstructStupid0100031,416,784b215.820μs+1.60σ+1.19%
ContainerBenchbenchConstructStupid0100041,416,784b212.891μs-0.25σ-0.18%
ContainerBenchbenchConstructSmart0100001,426,280b232.422μs-1.03σ-0.5%
ContainerBenchbenchConstructSmart0100011,426,280b232.422μs-1.03σ-0.5%
ContainerBenchbenchConstructSmart0100021,426,280b233.398μs-0.17σ-0.08%
ContainerBenchbenchConstructSmart0100031,426,280b234.375μs+0.69σ+0.33%
ContainerBenchbenchConstructSmart0100041,426,280b235.351μs+1.54σ+0.75%
... skipped........................
ContainerMethodHasBenchbenchPredefinedExisting0100001,216,144b81.055μs-0.91σ-1.19%
ContainerMethodHasBenchbenchPredefinedExisting0100011,216,144b83.985μs+1.83σ+2.38%
ContainerMethodHasBenchbenchPredefinedExisting0100021,216,144b82.032μs0.00σ0.00%
ContainerMethodHasBenchbenchPredefinedExisting0100031,216,144b82.031μs0.00σ0.00%
ContainerMethodHasBenchbenchPredefinedExisting0100041,216,144b81.055μs-0.91σ-1.19%
... skipped........................

Legend

  • benchmark: Benchmark class.
  • subject: Benchmark class method.
  • set: Set of data (provided by ParamProvider).
  • revs: Number of revolutions (represent the number of times that the code is executed).
  • iter: Number of iteration.
  • mem_peak: (mean) Peak memory used by iteration as retrieved by memory_get_peak_usage.
  • time_rev: Mean time taken by all iterations in variant.
  • comp_z_value: Z-score.
  • comp_deviation: Relative deviation (margin of error).
  • Aggregate report for the lookup group that outputs the result to console and CSV-file

$ ./vendor/bin/phpbench run --report=aggregate --progress=dots --output=csv_file --output=console --group=lookup

Notice

Available groups: construct lookup has

Generated MD-file example

DI benchmark report

suite: 1343b1d2654a3819c72a96d236302b70a504dac7, date: 2020-02-21, stime: 13:27:32

benchmarksubjectsetrevsitsmem_peakbestmeanmodeworststdevrstdevdiff
ContainerBenchbenchSequentialLookups0100051,454,024b168.945μs170.117μs169.782μs171.875μs0.957μs0.56%1.00x
ContainerBenchbenchSequentialLookups1100051,445,296b3,347.656μs3,384.961μs3,390.411μs3,414.062μs21.823μs0.64%19.90x
ContainerBenchbenchSequentialLookups2100051,445,568b3,420.898μs3,488.477μs3,447.260μs3,657.227μs85.705μs2.46%20.51x
ContainerBenchbenchRandomLookups0100051,454,024b169.922μs171.875μs171.871μs173.828μs1.381μs0.80%1.01x
ContainerBenchbenchRandomLookups1100051,445,296b3,353.515μs3,389.844μs3,377.299μs3,446.289μs31.598μs0.93%19.93x
ContainerBenchbenchRandomLookups2100051,445,568b3,445.313μs3,587.696μs3,517.823μs3,749.023μs115.850μs3.23%21.09x
ContainerBenchbenchRandomLookupsComposite0100051,454,032b297.852μs299.610μs298.855μs302.734μs1.680μs0.56%1.76x
ContainerBenchbenchRandomLookupsComposite1100051,445,880b3,684.570μs3,708.984μs3,695.731μs3,762.695μs28.297μs0.76%21.80x
ContainerBenchbenchRandomLookupsComposite2100051,446,152b3,668.946μs3,721.680μs3,727.407μs3,765.625μs30.881μs0.83%21.88x

Legend

  • benchmark: Benchmark class.
  • subject: Benchmark class method.
  • set: Set of data (provided by ParamProvider).
  • revs: Number of revolutions (represent the number of times that the code is executed).
  • its: Number of iterations (one measurement for each iteration).
  • mem_peak: (mean) Peak memory used by each iteration as retrieved by memory_get_peak_usage.
  • best: Maximum time of all iterations in variant.
  • mean: Mean time taken by all iterations in variant.
  • mode: Mode of all iterations in variant.
  • worst: Minimum time of all iterations in variant.
  • stdev: Standard deviation.
  • rstdev: The relative standard deviation.
  • diff: Difference between variants in a single group.

Testing

Unit testing

The package is tested with PHPUnit. To run tests:

shell
./vendor/bin/phpunit

Mutation testing

The package tests are checked with Infection mutation framework with Infection Static Analysis Plugin. To run it:

shell
./vendor/bin/roave-infection-static-analysis-plugin

Static analysis

The code is statically analyzed with Psalm. To run static analysis:

shell
./vendor/bin/psalm

License

The Yii Dependency Injection is free software. It's released under the terms of the BSD License. Please see LICENSE for more information.

Maintained by Yii Software.

Support the project

Open Collective

Follow updates

Official websiteTwitterTelegramFacebookSlack