Pular para o conteúdo principal

Dependency Injection

Basics​

Dependency Injection (DI) is a design pattern that provides objects with their dependencies instead of having them create or find dependencies themselves.

In simple terms, rather than a class creating the objects it needs (its dependencies), those objects are "injected" from the outside.

Key benefits:

  • Easier testing: You can mock dependencies for isolated testing
  • Loose coupling: Classes aren't tightly linked to specific implementations
  • Improved maintainability: Dependencies can be changed without modifying the dependent class
  • Better reusability: Components can be used in different contexts

Using Dependency Injection​

Let's get by example the following classes:

<?php
namespace Example;

interface Area
{
public function calculate();
}

class Square implements Area
{
public function __construct($side)
{
// ...
}

//...
}

class RectangleTriangle implements Area
{
public function __construct($base, $height)
{
// ...
}

//...
}

We can create a definition for this classes:

<?php

use ByJG\Config\DependencyInjection as DI;

return [
\Example\Square::class => DI::bind(\Example\Square::class)
->withConstructorArgs([4])
->toInstance(),

\Example\RectangleTriangle::class => DI::bind(\Example\RectangleTriangle::class)
->withConstructorArgs([3, 4])
->toInstance(),
];

and to use in our code we just need to do:

<?php
$config = $definition->build();
$square = $config->get(\Example\Square::class);

Referencing other entries: Param​

Why it exists​

A configuration file is a PHP array, evaluated once when the configuration is loaded. At that moment the container is still being built: no service exists yet, and the Config facade is not ready. So a binding cannot simply call Config::get() to obtain another entry:

<?php
// ❌ Config::get() runs while the array is being evaluated, before the container exists
Example\Square::class => DI::bind(Example\Square::class)
->withConstructorArgs([Config::get('side')])
->toInstance(),

Without an environment this fails at once (RunTimeException: Environment isn't build yet). In an application with a ConfigBootstrap, the call recurses into auto-initialization and fails the same way.

A Param is the answer: a reference to an entry, stored in the binding and resolved later, when the binding builds its instance. By then the container exists and holds every entry, so a binding can refer to entries defined after it, in another file, or in an environment it inherits.

The three kinds​

KindResolves toUse it for
Param::get('key')$container->get('key'), when the instance is builtanother entry: a value (from .env or a config array) or a service
Param::container()the container itselfa service or a factory that resolves things by class name
LazyParam::get(Class::class)a proxy that asks the container on its first usea dependency that is expensive or creates a cycle

They are accepted wherever a binding takes arguments: withConstructorArgs(), withMethodCall(), withFactoryMethod() and the values of withInjectedConstructorOverrides(). Any other value is passed as it is.

A value​

<?php
use ByJG\Config\DependencyInjection as DI;
use ByJG\Config\Param;

return [
'DB_HOST' => 'localhost',
'DB_PORT' => 3306,

Example\Database::class => DI::bind(Example\Database::class)
->withConstructorArgs([Param::get('DB_HOST'), Param::get('DB_PORT')])
->toSingleton(),
];

A Param refers to a whole entry and cannot be indexed: Param::get('database')['host'] fails with Cannot use object of type ByJG\Config\Param as array. When the values are grouped in an array, pass the whole array and let the class read it:

<?php
return [
'database' => ['host' => 'localhost', 'port' => 3306],

Example\Database::class => DI::bind(Example\Database::class)
->withConstructorArgs([Param::get('database')]) // __construct(array $config)
->toSingleton(),
];

Another service​

Param::get() takes any key, so it also refers to another binding. The container builds it (or returns the singleton) when the instance that needs it is built:

<?php
return [
Example\UserRepository::class => DI::bind(Example\UserRepository::class)
->withConstructorArgs([Param::get(Example\Database::class)])
->toInstance(),
];

withInjectedConstructor() does the same for every class-typed constructor parameter automatically, see Injecting automatically the constructor arguments.

Injecting the container itself​

Some services have to resolve collaborators on their own — a router that instantiates controllers by class name, for example. Param::container() hands such a service the container, and works anywhere a Param is accepted:

<?php
use ByJG\Config\DependencyInjection as DI;
use ByJG\Config\Param;

return [
Example\Server::class => DI::bind(Example\Server::class)
->withConstructorArgs([Param::get(Psr\Log\LoggerInterface::class)])
->withMethodCall('withContainer', [Param::container()])
->toSingleton(),
];

Prefer this over reaching for Config::getContainer() inside a configuration file. The facade is only populated after Definition::build() returns, while Container::__construct() resolves eager singletons before that — so a facade call made during the build recurses into auto-initialization and fails. Param::container() uses the container instance that is already injected into every binding, so it is safe even in an eager singleton.

It is also cache-safe: the container cache serializes your bindings, and Param::container() stores a stateless marker rather than a live container. After Container::createFromCache(), the restored binding receives the new container.

Use Config::getContainer() only from code built outside dependency injection — a test harness assembling its own objects, for instance — where no Param can reach.

Postponing a dependency​

LazyParam::get() takes the place of Param::get() when the dependency should not be built together with the instance that needs it: it passes a proxy that satisfies the type-hint and asks the container for the real object only on its first method or property call. See Lazy parameters for eager singletons.

Factory method​

withFactoryMethod() builds the instance by calling a static method of the bound class, instead of its constructor:

<?php
use ByJG\Config\DependencyInjection as DI;
use ByJG\Config\Param;

return [
'DB_URI' => 'mysql://root@localhost/app',

// ConnectionFactory::fromUri(string $uri): Connection
Example\Connection::class => DI::bind(Example\ConnectionFactory::class)
->withFactoryMethod('fromUri', [Param::get('DB_URI')])
->toSingleton(),
];
  • The method's return value is the entry. get(Example\Connection::class) returns whatever ConnectionFactory::fromUri() returns; the container does not check its type. So the key and the bound class can differ: the key names what you ask for (an interface, a class), the bound class is only where the static method lives. A static constructor of the class itself (DI::bind(Money::class)->withFactoryMethod('fromCents', [100])) works the same way.

  • The arguments are like any other. Literal values and every kind of Param are accepted. A factory that has to resolve several collaborators can take the container:

    <?php
    return [
    // PaymentMachine::build(ContainerInterface $container): FiniteStateMachine
    FiniteStateMachine::class => DI::bind(PaymentMachine::class)
    ->withFactoryMethod('build', [Param::container()])
    ->toSingleton(),
    ];
  • withMethodCall() still applies, to the object the factory returned.

  • It cannot be combined with toDelayedInstance(), see Delayed Instance Limitations.

Injecting automatically the constructor arguments​

Let's figure it out this class:

<?php
namespace Example;

class SumAreas implements Area
{
/**
* SumAreas constructor.
* @param Example\RectangleTriangle $triangle
* @param Example\Square $square
*/
public function __construct($triangle, $square)
{
$this->triangle = $triangle;
$this->square = $square;
}

//...
}

Note that this class needs instances of objects previously defined in our container definition. In that case we just need add this:

<?php
use ByJG\Config\DependencyInjection as DI;

return [
// ....

\Example\SumAreas::class => DI::bind(\Example\SumAreas::class)
->withInjectedConstructor()
->toInstance(),
];

When use the method withInjectedConstructor() the container will try to inject the constructor automatically based on its type declaration. Since we previously defined the classes Square and RectangleTriangle the container will inject the instances automatically.

observação

This component uses type hinting and PHP reflection to determine the classes that are required, not PHPDoc. If you're using older PHP versions or code without type declarations, you can use withInjectedLegacyConstructor() which uses PHPDoc comments to determine the types.

Union types (Foo|Bar) are supported — the first non-builtin type of the union is injected. Intersection types (Foo&Bar) cannot be auto-injected because there is no single class to resolve; provide those parameters through withInjectedConstructorOverrides() instead.

Mixing automatic injection with manual parameters​

Sometimes you need to inject most dependencies automatically but provide specific values for certain parameters (like configuration strings, API keys, or numeric values). The withInjectedConstructorOverrides() method gives you the best of both worlds:

<?php
namespace Example;

class UserService
{
public function __construct(
Database $db, // Will be auto-injected
Logger $logger, // Will be auto-injected
string $apiKey, // Must be provided manually
int $timeout = 30 // Optional, uses default if not overridden
) {
// ...
}
}

Configuration:

<?php
use ByJG\Config\DependencyInjection as DI;

return [
Database::class => DI::bind(Database::class)
->withConstructorArgs(['localhost', 'mydb'])
->toSingleton(),

Logger::class => DI::bind(Logger::class)
->withConstructorArgs(['/var/log/app.log'])
->toSingleton(),

UserService::class => DI::bind(UserService::class)
->withInjectedConstructorOverrides([
'apiKey' => 'my-secret-api-key',
'timeout' => 60 // Optional: override the default
])
->toInstance(),
];

How it works:

  • Auto-injected: Class type-hinted parameters (like Database and Logger) are automatically resolved from the container
  • Manual override: Built-in types (like string, int, bool) must be provided in the overrides array (unless they have default values)
  • Default values: Parameters with default values can be omitted from overrides
  • Flexible overrides: You can also override class dependencies using Param::get() if you need a different instance
<?php
// Override a dependency to use a different container entry
UserService::class => DI::bind(UserService::class)
->withInjectedConstructorOverrides([
'apiKey' => 'my-secret-key',
'logger' => Param::get('FileLogger') // Use specific logger instead of default
])
->toInstance(),

Get a singleton object​

The DependencyInjection class with the parameter toInstance() will return a new instance every time you require a new object.

However, you can return always the same object by adding toSingleton() instead of toInstance().

Eager Singleton​

Eager Singleton looks like a Singleton, but it creates the instance immediately after the definition.

It is useful when you need a specific object to be created before the application starts.

<?php
use ByJG\Config\DependencyInjection as DI;

return [
// ....

\Example\Square::class => DI::bind(\Example\Square::class)
->withConstructorArgs([4])
->toEagerSingleton(),
];

You can also register a class to be loaded as eager singleton regardless of how it was defined in the container:

<?php
use ByJG\Config\Container;

// Register a class to be loaded as eager singleton
Container::addEagerSingleton(\Example\Square::class);

Lazy parameters for eager singletons​

Sometimes an eager singleton needs to keep its own constructor or initializer type-hinted (so static analyzers remain happy) but still postpone the creation of heavy collaborators until they are really used.
LazyParam::get() solves this by returning a lightweight proxy that satisfies the original type-hint yet asks the container for the real object only on the first method/property call.

<?php
use ByJG\Config\DependencyInjection as DI;
use ByJG\Config\LazyParam;

return [
Example\Service::class => DI::bind(Example\Service::class)
->withMethodCall('boot', [LazyParam::get(Example\ExpensiveDependency::class)])
->toEagerSingleton(),
];

Because LazyParam still resolves through the container, the dependency is tracked normally, but it avoids the upfront instantiation cost that eager singletons would otherwise incur.

Autowiring a family of classes​

Some classes are terminal — nothing else depends on them. A REST controller is the usual example: it is named directly by the router, it is always per-request, and its constructor arguments are type-hinted services that are themselves explicitly bound. The per-class binding encodes no decision, so listing every one of them is ceremony.

Autowire replaces those entries with a single rule. The config key is the pattern, where * matches any run of characters:

<?php
use ByJG\Config\Autowire;

return [
'App\Controller\*' => Autowire::rule()
->withInjectedConstructor()
->toInstance(),
];

Any class matching the pattern is now resolvable with no entry of its own, and has() reports it as available. A class that declares no constructor degrades to withConstructorNoArgs() automatically, so an ActiveRecord-style controller needs no special case.

An explicit binding always wins. Add one for the odd class that needs different treatment and the rule steps aside:

'App\Controller\*' => Autowire::rule()->withInjectedConstructor()->toInstance(),

App\Controller\ReportController::class => DI::bind(App\Controller\ReportController::class)
->withInjectedConstructorOverrides(['format' => 'pdf'])
->toInstance(),

Scope patterns to a namespace​

Prefer App\Controller\* over a bare *Controller. A bare suffix also matches classes in your vendor directory, and because has() consults these rules, that can quietly change the meaning of feature checks written as:

$logger = Config::has(LoggerInterface::class) ? Config::get(LoggerInterface::class) : new NullLogger();

Where not to use it​

Services and repositories are a poor fit. Their bindings carry real decisions — which implementation, singleton or not, scalar constructor arguments — and a pattern would be convention standing in for a choice that genuinely has to be made. Bind those explicitly.

Delayed Instance​

The delayed instance allows you to pass custom arguments to the constructor for every instance you get from the container.

You should prefer to use toInstance() or toSingleton() instead of toDelayedInstance().

Only use toDelayedInstance() when you need to pass custom arguments to the constructor for every instance you get from the container.

<?php
return [
// ....

Square::class => DI::bind(Square::class)
->toDelayedInstance(),
];

And then you can get the instance with custom arguments:

<?php

// Recommended: Pass arguments directly to get()
$square1 = $config->get(Square::class, 5);
$square2 = $config->get(Square::class, 7);

// Deprecated: Using getInstance() (still works but will be removed in future versions)
$square1 = $config->get(Square::class)->getInstance(5);
$square2 = $config->get(Square::class)->getInstance(7);
Delayed Instance Limitations

Delayed Instances cannot be used with:

  • withFactoryMethod()
  • withInjectedConstructor()
  • withInjectedLegacyConstructor()
  • withConstructorNoArgs()

Delayed Instances also cannot be:

  • Injected automatically to the constructor (classes with withInjectedConstructor() or withInjectedLegacyConstructor() pointing to a delayed instance will fail)

All options (bind)​

<?php

\ByJG\Config\DependencyInjection::bind("classname")
// To create a new instance choose *only* one below:
// --------------------------------------------------
->withInjectedConstructor() // If you want inject the constructor automatically using reflection
->withInjectedConstructorOverrides(array) // Auto-inject dependencies but override specific parameters (e.g., ['apiKey' => 'value'])
->withInjectedLegacyConstructor() // If you want inject the constructor automatically using PHP annotation
->withNoConstructor() // The class has no constructor
->withConstructorNoArgs() // The constructor's class has no arguments
->withConstructorArgs(array) // The constructor's class arguments
->withFactoryMethod("method", array_of_args) // Build it with a static method of the class; its return value is the entry (see "Factory method")

// Call methods after you have a instance
// --------------------------------------
->withMethodCall("methodName", array_of_args)

// How will you get a instance?
// ----------------------------
->toInstance() // get a new instance for every time you get from the container
->toSingleton() // get the same instance for every time you get from the container
->toEagerSingleton() // same as singleton however get a new instance immediately after the definition.
->toDelayedInstance() // get a new instance for every time you get from the container,
// however you can force the constructor parameters
;

Open source ByJG