Skip to content

Factory

PSR-11 Container mit dreistufiger Auflösung, von Pre-Registration über Backend-Delegation bis Reflection.

Einführung

Dependency Injection Container gibt es viele. Die meisten erfordern umfangreiche Konfiguration, YAML-Dateien, PHP-Definitionen, Annotations. Für ein Platform-Package wie Jardis ist das zu viel Overhead: Die meisten Services brauchen keine Konfiguration, manche werden manuell verdrahtet, und nur wenige leben in einem externen Container.

jardissupport/factory löst genau dieses Problem mit einer dreistufigen Auflösungskette: Zuerst werden vorab registrierte Instanzen geprüft, dann ein optionaler Backend-Container (Symfony, Laravel, PHP-DI), und als Fallback instanziiert die Factory die Klasse per Reflection selbst. Kein YAML, keine Annotations, keine Magie, drei Dateien, ein Interface.

  • PSR-11 kompatibel — funktioniert überall wo ContainerInterface erwartet wird
  • Dreistufige Auflösung — Instances → Backend → Reflection
  • Null Konfiguration — Klassen ohne required Parameter werden automatisch instanziiert
  • Backend-Integration — Symfony, Laravel oder jeder andere PSR-11 Container als Fallback
  • Immutable — nach dem Konstruktor nicht änderbar, readonly Properties

Installation

bash
composer require jardissupport/factory

GitHub: jardisSupport/factory

Grundlegende Nutzung

Einfachster Fall — Reflection

php
use JardisSupport\Factory\Factory;

$factory = new Factory();

// Klassen ohne required Parameter werden automatisch instanziiert
$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 und false sind gültige Werte, die Prüfung erfolgt per array_key_exists, nicht isset.

Mit Backend-Container

php
// Symfony-Container als Fallback
$factory = new Factory(
    container: $symfonyContainer,
    instances: [
        CacheInterface::class => $customCache,  // überschreibt Symfony
    ],
);

Instances haben immer Vorrang vor dem Backend-Container.

Auflösungskette

get() prüft drei Stufen, die erste die trifft, gewinnt:

get('MyService')
   ├── 1. Instances[$id] vorhanden?        → Instanz zurückgeben
   ├── 2. Backend-Container->has($id)?     → Backend->get($id)
   └── 3. class_exists($id)?              → new $id() per Reflection
         └── Keine Stufe trifft?           → NotFoundException

Stufe 3 — Reflection

Die Reflection-Stufe instanziiert Klassen nur wenn sie keine required Konstruktor-Parameter haben:

php
// Funktioniert — kein Konstruktor oder nur optionale Parameter
class SimpleService {}
class ConfigurableService {
    public function __construct(private string $name = 'default') {}
}

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

Für Klassen mit required Parametern: create() verwenden oder als Instance registrieren.

create() — Explizite Instanziierung

create() umgeht die Auflösungskette komplett und instanziiert per Reflection mit Parametern:

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

// Ohne Parameter (wie get(), aber bypassed Instances und Backend)
$service = $factory->create(SimpleService::class);

get() vs. create()

get($id)create($class, ...$params)
Prüft InstancesJaNein
Prüft BackendJaNein
Akzeptiert ParameterNeinJa
Neue Instanz garantiertNur bei ReflectionImmer

Fehlerbehandlung

ExceptionUrsachePSR-11 Interface
NotFoundExceptionKeine der drei Stufen findet $idNotFoundExceptionInterface
ContainerExceptionKlasse hat required Parameter oder existiert nichtContainerExceptionInterface
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()."
}

Architektur

Drei Dateien, bewusst minimal:

src/
├── Factory.php              ← PSR-11 Container (Orchestrator)
├── ContainerException.php   ← Technische Fehler
└── NotFoundException.php    ← ID nicht gefunden

Die Factory ist immutable. $instances und $container sind readonly Properties. Kein register(), kein set() nach der Konstruktion.

API-Referenz

Factory

MethodeSignaturBeschreibung
getget(string $id): mixedDreistufige Auflösung (PSR-11)
hashas(string $id): boolPrüft ob $id auflösbar wäre (PSR-11)
createcreate(string $className, mixed ...$params): objectDirekte Instanziierung per Reflection

Vollständiges Beispiel

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

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

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

// Auflösung
$factory->get(LoggerInterface::class);  // $logger (Stufe 1: Instance)
$factory->get(SimpleValidator::class);  // new SimpleValidator() (Stufe 3: Reflection)

// Explizite Erstellung mit Parametern
$handler = $factory->create(
    OrderHandler::class,
    $factory->get(PDO::class),
    $factory->get(CacheInterface::class),
);