Install
The runtime has one dependency: the PSR-11 container contract.
composer require dvictorjhg/php-injector
PHP 8.3+ / PSR-11
A compact dependency injector for classes and callables. Reflection does the inspection; you decide the explicit inputs, providers, and lifecycle.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\DI\Injector;
final class MyService
{
}
$service = Injector::inject(MyService::class);
echo get_class($service);
MyService
Register only the values that are genuinely external to the object graph. Let type declarations describe the rest.
The runtime has one dependency: the PSR-11 container contract.
composer require dvictorjhg/php-injector
A provider is a stored value or a lazy class string. Closures are supplied as values, not executed as factories.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\DI\Injector;
final class Logger
{
}
$logger = new Logger();
$dsn = 'mysql:host=db;dbname=app';
$injector = new Injector();
$injector->addProvider(Logger::class, $logger);
$injector->addProvider('db.dsn', $dsn);
echo Injector::inject('db.dsn');
mysql:host=db;dbname=app
| Identifier | How it is used |
|---|---|
| Logger::class | Matches a parameter type. |
| db.dsn | A parameter marked #[Inject('db.dsn')] receives this value. |
| Logger::class => $logger | A supplied value is returned as registered. |
A new injector becomes globally active. It checks local providers first, then its explicit parent or the previously active injector. Constructing an empty injector does not reset the chain.
The injector caches concrete classes by default. Use the Transient attribute when each resolution needs a fresh object, or implement the Singleton contract when the class should own its instance.
Add #[Transient] to skip injector caching. Each resolution that constructs the class gets a fresh instance.
Implement Singleton when the class should own a shared instance. The injector calls getInstance() instead of its constructor.
Pass an explicit instance when one resolution needs a fresh dependency. #[Transient] applies to the whole class and does not replace Singleton.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\DI\Attributes\Transient;
use PHPInjector\DI\Injector;
#[Transient]
final class RequestStamp
{
}
$first = Injector::inject(RequestStamp::class);
$second = Injector::inject(RequestStamp::class);
var_dump($first === $second);
bool(false)
The resolver follows one ordered path. The first matching source wins, which keeps a complex graph inspectable in a debugger or a code review.
The optional second argument to Injector::inject() is checked first. Keys can be a type, parameter name, or numeric position.
$args[Logger::class]
type
$args['logger']
name
$args[0]
position
A parameter-level identifier selects a named provider after no explicit argument matches.
The declared class or interface name is looked up in the active injector and then its parent chain.
A declared default value is used when no explicit value, attribute, or typed provider is available.
A required parameter with no source fails loudly. There is no hidden source or service locator detour.
Step 1 maps the same dependency by type key, parameter name, or numeric position. Use one form per call.
Logger::class => $logger
type key
'logger' => $logger
parameter name
0 => $logger
numeric position
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\DI\Injector;
final class Logger
{
public function __construct(public string $channel)
{
}
}
$logger = new Logger('worker');
$target = static fn (Logger $logger): string => $logger->channel;
$byType = Injector::inject($target, [Logger::class => $logger]);
$byName = Injector::inject($target, ['logger' => $logger]);
$byPosition = Injector::inject($target, [$logger]);
echo $byType . PHP_EOL . $byName . PHP_EOL . $byPosition;
worker
worker
worker
Run the same target twice: first with an explicit report, then without it. The first available source supplies each parameter, and the final call shows the required-parameter exception.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\DI\Attributes\Inject;
use PHPInjector\DI\Injector;
use PHPInjector\Exceptions\InjectorException;
final class Logger
{
public function __construct(public string $channel = 'app')
{
}
}
final class ReportRunner
{
public function run(
Logger $logger,
#[Inject('env')] string $environment,
string $report = 'summary',
): string {
return "{$logger->channel}:{$environment}:{$report}";
}
}
$injector = new Injector([
Logger::class,
'env' => 'production',
]);
echo Injector::inject([ReportRunner::class, 'run'], [
'report' => 'sales',
]) . PHP_EOL;
echo Injector::inject([ReportRunner::class, 'run']) . PHP_EOL;
$required = static fn (string $token): string => $token;
try {
Injector::inject($required);
} catch (InjectorException $exception) {
echo $exception->getMessage();
}
app:production:sales
app:production:summary
No provider or value found for parameter: $token at position 0.
$report uses the explicit per-call $args value.$environment uses #[Inject('env')] to find production.$logger uses the registered Logger provider.summary for $report.$token has no source, so PHPInjector throws InjectorException.The public entry point accepts the common PHP callable shapes, then routes each one through the same parameter resolver.
strlen.
__invoke method.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\DI\Injector;
final class Logger
{
public function __construct(public string $channel = 'app')
{
}
}
final class ReportJobs
{
public static function warm(Logger $logger): string
{
return "warming {$logger->channel}";
}
}
$injector = new Injector([Logger::class]);
echo Injector::inject('ReportJobs::warm') . PHP_EOL;
echo Injector::inject(static fn (Logger $logger): string => $logger->channel);
warming app
app
For function sum(int ...$values), numeric values from the variadic position onward are collected and passed as separate arguments.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\DI\Injector;
echo Injector::inject(
fn (int ...$values): int => array_sum($values),
[2, 3, 5],
);
10
Run each complete PHP example independently from the project root after composer install. The documentation check compares actual output with the output shown here.
Register constructor dependencies explicitly. Direct class targets can be constructed without registration, but their required typed parameters need providers.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\DI\Attributes\Inject;
use PHPInjector\DI\Injector;
interface Formatter
{
public function format(string $value): string;
}
final class TextFormatter implements Formatter
{
public function format(string $value): string
{
return strtoupper($value);
}
}
final class Report
{
public function __construct(
public Formatter $formatter,
#[Inject('env')] public string $environment,
) {
}
}
$injector = new Injector([
Formatter::class => TextFormatter::class,
'env' => 'production',
]);
$report = Injector::inject(Report::class);
echo $report->formatter->format($report->environment) . PHP_EOL;
var_dump(Injector::inject(TextFormatter::class) === $report->formatter);
PRODUCTION
bool(true)
Container stores values without constructing classes or invoking closures. Null is a present value; missing identifiers throw NotFoundException. set() replaces entries and unset() removes them.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\Container\Container;
use Psr\Container\NotFoundExceptionInterface;
$container = new Container(['env' => 'dev', 'optional' => null]);
var_dump($container->has('optional'));
$container->set('env', 'production');
echo $container->get('env') . PHP_EOL;
$container->unset('env');
echo count($container) . PHP_EOL;
try {
$container->get('env');
} catch (NotFoundExceptionInterface $exception) {
echo $exception->getMessage();
}
bool(true)
production
1
No entry was found for env identifier.
Resolving a closure provider returns the closure itself. Invoke it in your consumer, or pass a callable directly to Injector::inject() to execute it with resolved parameters.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use PHPInjector\DI\Injector;
$format = static fn (string $value): string => strtoupper($value);
$injector = new Injector([Closure::class => $format]);
var_dump(Injector::inject(Closure::class) === $format);
echo Injector::inject(
static fn (Closure $format): string => $format('ready'),
);
bool(true)
READY
The API is intentionally short. The useful detail is in the contracts between the injector, its providers, and reflection.
Injector::inject Resolve a class, callable, closure, method array, or invokable object. The second argument supplies per-call values.
$injector->addProvider(string $id, mixed $value): voidRegister or replace a stored value or lazy class string in this injector.
Sourcenew Injector(array|Container $providers = [], ?Injector $parent = null)Creates the active injector. The default parent is the previously active injector; an explicit parent selects another chain.
SourceInjector::inject($target, array $args = []): mixedResolves through the active injector. Missing string targets or required parameters raise InjectorException. There is no public getProvider() or hasProvider() API.
SourceContainerUse the PSR-11 container directly when a provider map is the right abstraction. It supports get, has, and set.
SourceThis project keeps its feedback loop deliberately plain: Composer scripts, strict static analysis, and a focused unit suite.
composer validate --strict
composer check-platform-reqs
php tools/check-docs.php
composer analyse
composer test
composer test:coverage
composer coverage:check
The documentation check requires PHP DOM and proc_open. It executes marked HTML examples and README examples with expected output in separate PHP processes. Coverage additionally requires Xdebug or PCOV.