PHP 8.3+ / PSR-11

Injection without the fog.

A compact dependency injector for classes and callables. Reflection does the inspection; you decide the explicit inputs, providers, and lifecycle.

A useful first line
<?php

declare(strict_types=1);

use PHPInjector\DI\Injector;

final class MyService
{
}

$service = Injector::inject(MyService::class);
echo get_class($service);
Expected output
MyService
01 / Start here

Keep the graph legible.

Register only the values that are genuinely external to the object graph. Let type declarations describe the rest.

Install

The runtime has one dependency: the PSR-11 container contract.

composer require dvictorjhg/php-injector

Providers

A provider is a value or factory stored under an identifier. Types are the natural default; named identifiers are useful for interfaces and configuration.

<?php

declare(strict_types=1);

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');
Expected output
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.
One container, one graph

Child injectors consult their own providers first, then walk toward the parent. A provider is never silently pulled from an unrelated global container.

02 / Lifecycle

Choose where identity lives.

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.

Transient

Add #[Transient] to skip injector caching. Each resolution that constructs the class gets a fresh instance.

Singleton

Implement Singleton when the class should own a shared instance. The injector calls getInstance() instead of its constructor.

Per-call

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);

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);
Expected output
bool(false)
03 / Resolution

Every parameter has a trail.

The resolver follows one ordered path. The first matching source wins, which keeps a complex graph inspectable in a debugger or a code review.

01 first

Explicit arguments

stop

The optional second argument to Injector::inject() is checked first. Keys can be a type, parameter name, or numeric position.

  1. 01 $args[Logger::class]

    type

  2. 02 $args['logger']

    name

  3. 03 $args[0]

    position

Contextual input

$args belongs to this call only; it is separate from the injector's persistent provider map.

<?php

declare(strict_types=1);

use PHPInjector\DI\Injector;

$args = ['string' => 'cache'];
$length = Injector::inject('strlen', $args);
echo $length;
Expected output
5
next
02 fallback

#[Inject('id')]

chain

A parameter-level identifier selects a named provider after no explicit argument matches.

next
03 type

Typed provider

chain

The declared class or interface name is looked up in the active injector and then its parent chain.

next
04 fallback

PHP default

stop

A declared default value is used when no explicit value, attribute, or typed provider is available.

next
05 error

InjectorException

throw

A required parameter with no source fails loudly. There is no hidden fallback or service locator detour.

One rule of thumb: make the ordinary path obvious, then use explicit arguments or attributes only where the graph needs a deliberate exception.

Supported targets include class names, object/class method arrays, callable strings, closures, and invokable objects.

04 / Targets

Reflect the thing you already have.

The public entry point accepts the common PHP callable shapes, then routes each one through the same parameter resolver.

  • Class string Constructs the class and resolves its constructor.
  • Method array Invokes an instance or static method with resolved parameters.
  • Callable string Reflects a function name such as strlen.
  • Closure Reflects and invokes an inline function.
  • Invokable object Uses the object's __invoke method.
<?php

declare(strict_types=1);

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);
Expected output
warming app
app

Variadics stay positional

For function sum(int ...$values), numeric values from the variadic position onward are collected and passed as separate arguments.

<?php

declare(strict_types=1);

use PHPInjector\DI\Injector;

echo Injector::inject(
    fn (int ...$values): int => array_sum($values),
    [2, 3, 5],
);
Expected output
10
05 / API

Small surface, sharp edges.

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()

Store a value, class string, or callable factory under a string identifier.

Source
Injector::getProvider()

Read a provider from the current injector or its parent chain. Missing identifiers throw a container exception.

Source
Injector::hasProvider()

Check whether an identifier exists in the current injector or its parent chain.

Source
PHPInjector\DI\inject()

Global function entry point for Injector::inject().

Source
Container

Use the PSR-11 container directly when a provider map is the right abstraction. It supports get, has, and set.

Source
#[Inject('id')]

Annotate one parameter with a named provider identifier.

Source
#[Transient]

Override singleton caching for one class declaration.

Source
Singleton

Implement the marker interface to cache the constructed concrete instance.

Source
06 / Development

Read the source. Run the checks.

This project keeps its feedback loop deliberately plain: Composer scripts, strict static analysis, and a focused unit suite.

Commands

composer analyse
composer test
composer test:coverage

Project links