Skip to content

Factory

PSR-11 container with three-stage resolution, from pre-registration through backend delegation to reflection.

Introduction

There are many dependency injection containers. Most require extensive configuration, YAML files, PHP definitions, annotations. For a platform package like Jardis, that is too much overhead: most services need no configuration, some are wired manually, and only a few live in an external container.

jardissupport/factory solves exactly this problem with a three-stage resolution chain: first pre-registered instances are checked, then an optional backend container (Symfony, Laravel, PHP-DI), and as a fallback the factory instantiates the class via reflection. No YAML, no annotations, no magic, three files, one interface.

  • PSR-11 compatible — works everywhere ContainerInterface is expected
  • Three-stage resolution — Instances → Backend → Reflection
  • Zero configuration — classes without required parameters are automatically instantiated
  • Backend integration — Symfony, Laravel or any other PSR-11 container as fallback
  • Immutable — not modifiable after construction, readonly properties

Installation

bash
composer require jardissupport/factory

GitHub: jardisSupport/factory

Basic Usage

Simplest Case — Reflection

php
use JardisSupport\Factory\Factory;

$factory = new Factory();

// Classes without required parameters are automatically instantiated
$service = $factory->get(SimpleService::class);

Pre-Registration

php
$factory = new Factory(instances: [
    LoggerInterface::class  => $psrLogger,
    CacheInterface::class   => $cacheService,
    'app.config'            => ['debug' => true],
]);

$logger = $factory->get(LoggerInterface::class);  // $psrLogger
$config = $factory->get('app.config');             // ['debug' => true]

null and false are valid values, the check uses array_key_exists, not isset.

With Backend Container

php
// Symfony container as fallback
$factory = new Factory(
    container: $symfonyContainer,
    instances: [
        CacheInterface::class => $customCache,  // overrides Symfony
    ],
);

Instances always take precedence over the backend container.

Resolution Chain

get() checks three stages, the first one that matches wins:

get('MyService')
   ├── 1. Instances[$id] present?          → return instance
   ├── 2. Backend container->has($id)?     → Backend->get($id)
   └── 3. class_exists($id)?              → new $id() via reflection
         └── No stage matches?             → NotFoundException

Stage 3 — Reflection

The reflection stage instantiates classes only if they have no required constructor parameters:

php
// Works — no constructor or only optional parameters
class SimpleService {}
class ConfigurableService {
    public function __construct(private string $name = 'default') {}
}

// Throws ContainerException — required parameters
class DatabaseService {
    public function __construct(private PDO $pdo) {}
}

For classes with required parameters: use create() or register as instance.

create() — Explicit Instantiation

create() bypasses the resolution chain entirely and instantiates via reflection with parameters:

php
// With parameters
$command = $factory->create(ProcessOrder::class, $orderId, $userId);

// Without parameters (like get(), but bypasses instances and backend)
$service = $factory->create(SimpleService::class);

get() vs. create()

get($id)create($class, ...$params)
Checks instancesYesNo
Checks backendYesNo
Accepts parametersNoYes
New instance guaranteedOnly on reflectionAlways

Error Handling

ExceptionCausePSR-11 Interface
NotFoundExceptionNone of the three stages finds $idNotFoundExceptionInterface
ContainerExceptionClass has required parameters or does not existContainerExceptionInterface
php
use JardisSupport\Factory\NotFoundException;
use JardisSupport\Factory\ContainerException;

try {
    $service = $factory->get('unknown.service');
} catch (NotFoundException $e) {
    // "Entry "unknown.service" not found in container."
}

try {
    $db = $factory->get(DatabaseService::class);
} catch (ContainerException $e) {
    // "Class DatabaseService has required constructor parameters.
    //  Register it as instance or use create()."
}

Architecture

Three files, deliberately minimal:

src/
├── Factory.php              ← PSR-11 container (orchestrator)
├── ContainerException.php   ← Technical errors
└── NotFoundException.php    ← ID not found

The factory is immutable. $instances and $container are readonly properties. No register(), no set() after construction.

API Reference

Factory

MethodSignatureDescription
getget(string $id): mixedThree-stage resolution (PSR-11)
hashas(string $id): boolChecks if $id would be resolvable (PSR-11)
createcreate(string $className, mixed ...$params): objectDirect instantiation via reflection

Complete Example

php
use JardisSupport\Factory\Factory;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;

// Prepare services
$logger = new FileLogger('/var/log/app.log');
$cache  = new RedisCache($redis);
$pdo    = new PDO('mysql:host=localhost;dbname=shop', 'root', '');

// Configure factory
$factory = new Factory(instances: [
    LoggerInterface::class  => $logger,
    CacheInterface::class   => $cache,
    PDO::class              => $pdo,
]);

// Resolution
$factory->get(LoggerInterface::class);  // $logger (stage 1: instance)
$factory->get(SimpleValidator::class);  // new SimpleValidator() (stage 3: reflection)

// Explicit creation with parameters
$handler = $factory->create(
    OrderHandler::class,
    $factory->get(PDO::class),
    $factory->get(CacheInterface::class),
);