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
| Kind | Resolves to | Use it for |
|---|---|---|
Param::get('key') | $container->get('key'), when the instance is built | another entry: a value (from .env or a config array) or a service |
Param::container() | the container itself | a service or a factory that resolves things by class name |
LazyParam::get(Class::class) | a proxy that asks the container on its first use | a 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 whateverConnectionFactory::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
Paramare accepted. A factory that has to resolve several collaborators can take the container:<?phpreturn [// PaymentMachine::build(ContainerInterface $container): FiniteStateMachineFiniteStateMachine::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.
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
DatabaseandLogger) 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 Instances cannot be used with:
withFactoryMethod()withInjectedConstructor()withInjectedLegacyConstructor()withConstructorNoArgs()
Delayed Instances also cannot be:
- Injected automatically to the constructor (classes with
withInjectedConstructor()orwithInjectedLegacyConstructor()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
;