Skip to content

ClassVersion

Versionierte Klassen per Namespace-Injection, ohne Umbenennung, ohne Konfigurationsdateien, ohne Magie.

Einführung

Wenn sich eine Klasse weiterentwickelt (neue Felder, andere Validierung, geändertes Verhalten), steht man vor einem Dilemma: Die bestehende Klasse ändern und Abwärtskompatibilität riskieren? Oder eine neue Klasse unter neuem Namen einführen und an hundert Stellen den Import anpassen?

jardissupport/classversion löst das elegant: Der Klassenname bleibt gleich, die Version wird als Namespace-Segment injiziert. App\Domain\Payment wird zu App\Domain\v2\Payment, automatisch, mit Fallback-Chain und Proxy-Support. Der aufrufende Code ändert sich nicht.

Das ist das Herzstück von Jardis' Evolutionsstrategie: Der Builder erzeugt seinen Output unter {Agg}/Platform/, Custom-Code lebt direkt unter {Agg}/ parallel zu Platform/, wahlweise versionslos (Baseline-Override) oder pro Version. Bei Regeneration wird ausschließlich Platform/ neu geschrieben, alles parallel dazu bleibt unangetastet. Alte und neue Versionen koexistieren: kein Alles-oder-nichts-Umstieg.

  • Zwei Resolver-StrategienLoadClassFromSubDirectory für das klassische v2/-Layout, LoadClassFromExtensions für Multi-Segment-Layouts mit klarer Trennung zwischen Generator- und Custom-Code (Default in Jardis-Builder-Projekten: segmentNames: ['', 'Platform'])
  • Fallback-Chainv2 nicht vorhanden? Automatisch auf v1 oder Generator-Basis zurückfallen
  • Labels/Aliase'current', 'legacy', '2.0' statt hartcodierter Verzeichnisnamen
  • Proxy-Registry — vorab registrierte Instanzen überschreiben die Klassen-Auflösung
  • Auflösungs-Cache — optional ClassResolutionCache memoisiert Treffer und Misses und vermeidet wiederholte class_exists()-Syscalls
  • Tracing-Decorator — jeden Auflösungsschritt protokollieren (Debugging)

Systemweite Versionierung

ClassVersion ist kein Tool für einzelne Klassen. Es ist eine Versionierungsstrategie für komplette Software-Systeme. Der entscheidende Punkt: Die Version wird nicht pro Klasse festgelegt, sondern pro Aufrufkontext. Ein BoundedContext übergibt eine Version an handle(), und ClassVersion löst jede Klasse in diesem Kontext zur richtigen Version auf.

Jede Klasse evolves unabhängig

Ein System mit 50 Klassen braucht keine 50 koordinierten Upgrades. Wird Payment auf v2 erweitert, bleiben die anderen 49 Klassen auf ihrer aktuellen Version. Die Fallback-Chain greift automatisch:

handle('Payment', 'v2')        → App\Domain\v2\Payment       (v2 existiert)
handle('Invoice', 'v2')        → App\Domain\v1\Invoice        (kein v2, Fallback v1)
handle('Customer', 'v2')       → App\Domain\Customer           (kein v2/v1, Base-Class)

Runtime-Koexistenz

Verschiedene Versionen können gleichzeitig im selben Prozess laufen. Ein v1-API-Endpoint nutzt 'v1', ein v2-Endpoint nutzt 'v2': jede Klasse wird zur richtigen Implementierung aufgelöst:

php
// API v1 — Legacy-Clients
class OrderV1Controller
{
    public function create(array $data): DomainResponse
    {
        return (new PlaceOrderContext($this->kernel(), $data, version: 'v1'))();
    }
}

// API v2 — Neue Clients
class OrderV2Controller
{
    public function create(array $data): DomainResponse
    {
        return (new PlaceOrderContext($this->kernel(), $data, version: 'v2'))();
    }
}

Derselbe PlaceOrderContext, aber intern werden Validator, Repository, Hydrator jeweils in der passenden Version aufgelöst. Kein if/else, kein Feature-Flag. Die Architektur übernimmt das.

Graduelle Migration

Statt Big-Bang-Releases wird Klasse für Klasse migriert:

Sprintv2-OverridesEffekt
1Payment, PaymentValidatorNeue Zahlungslogik, Rest bleibt v1
2Invoice, InvoiceRendererNeues Rechnungsformat
3CustomerErweiterte Kundendaten
4Label 'v2''current'v2 wird Standard, v1 wird 'legacy'

Jeder Sprint ist einzeln deployer und rollbar. Kein Alles-oder-nichts-Umstieg.

Installation

bash
composer require jardissupport/classversion

GitHub: jardisSupport/classversion

Das Konzept

ClassVersion nutzt PSR-4 Autoloading: das Versions-Segment wird zum Unterverzeichnis. Welche Stelle im Namespace das Segment landet, entscheidet die Resolver-Strategie:

ResolverInjektionspunktTypisches Layout
LoadClassFromSubDirectorydirekt vor dem KlassennamenApp\Domain\v2\Payment
LoadClassFromExtensionsan konfigurierbarer Tiefe, mit ein oder mehreren Zwischen-SegmentenApp\Sales\Orders\v2\Command\CreateOrder (Custom) bzw. App\Sales\Orders\Platform\Command\CreateOrder (Generator)

Für simple Bibliotheken und Kleinstpakete reicht LoadClassFromSubDirectory. Für generierte Codebases mit klarer Trennung zwischen Generator-Basis und Custom-Code (Jardis-Builder-Projekte) ist LoadClassFromExtensions die richtige Wahl.

Namespace-Injection (SubDirectory)

Der Algorithmus ist einfach: Das Versions-Segment wird vor den Klassennamen im Namespace eingefügt:

App\Domain\Payment + v2  →  App\Domain\v2\Payment
App\Service\Calculator + v1  →  App\Service\v1\Calculator

Die Klasse muss nur im richtigen Namespace deklariert sein: PSR-4 erledigt den Rest.

Grundlegende Nutzung

Konfiguration

php
use JardisSupport\ClassVersion\ClassVersion;
use JardisSupport\ClassVersion\Data\ClassVersionConfig;
use JardisSupport\ClassVersion\Reader\LoadClassFromSubDirectory;

$config = new ClassVersionConfig(
    version: [
        'v2' => ['v2', '2.0', 'current'],
        'v1' => ['v1', '1.0', 'legacy'],
    ],
    fallbacks: [
        'v2' => ['v1'],  // v2 nicht vorhanden → v1 versuchen
    ],
);

$classVersion = new ClassVersion(
    $config,
    new LoadClassFromSubDirectory($config),
);

Klasse auflösen

php
// Gibt den FQCN der versionierten Klasse zurück
$class = $classVersion('App\Domain\Payment', 'v2');
// → 'App\Domain\v2\Payment' (wenn class_exists)

// Labels funktionieren
$class = $classVersion('App\Domain\Payment', 'current');
// → 'App\Domain\v2\Payment' ('current' ist Alias für 'v2')

// Instanz erzeugen
$payment = new $class(...$args);

Fallback-Chain

Wenn App\Domain\v2\Payment nicht existiert:

1. App\Domain\v2\Payment     → class_exists? NEIN
2. App\Domain\v1\Payment     → class_exists? JA → return

Wenn auch v1 nicht existiert, wird die Base-Class App\Domain\Payment verwendet. Existiert auch die nicht, wird InvalidArgumentException geworfen.

Konfiguration im Detail

Labels (Aliase)

Labels mappen lesbare Namen auf Versions-Keys:

php
$config = new ClassVersionConfig(
    version: [
        'v2' => ['v2', '2.0', 'current', 'latest'],
        'v1' => ['v1', '1.0', 'legacy'],
    ],
);

$config->version('current');  // 'v2'
$config->version('legacy');   // 'v1'
$config->version('2.0');      // 'v2'
$config->version('unknown');  // 'unknown' (Passthrough)

Fallback-Ketten

php
$config = new ClassVersionConfig(
    version: [
        'v3' => ['v3', 'next'],
        'v2' => ['v2', 'current'],
        'v1' => ['v1', 'legacy'],
    ],
    fallbacks: [
        'v3' => ['v2', 'v1'],  // v3 → v2 → v1 → Base-Class
        'v2' => ['v1'],         // v2 → v1 → Base-Class
    ],
);

$config->fallbackChain('v3');  // ['v3', 'v2', 'v1']
$config->fallbackChain('v2');  // ['v2', 'v1']
$config->fallbackChain('v1');  // ['v1'] (kein Fallback definiert)

Die Base-Class ist immer der implizite letzte Fallback. Sie muss nicht in der Chain stehen.

Extensions-Resolver

LoadClassFromExtensions ist der zweite Resolver, zugeschnitten auf generierte Codebases mit klarer Trennung zwischen Generator-Basis und Custom-Overrides. Statt das Versions-Segment direkt vor den Klassennamen zu setzen, werden an einer konfigurierbaren Namespace-Tiefe ein oder mehrere Zwischen-Segmente (Extensions/, Overrides/, Platform/, …) injiziert. Darunter liegen optional versionierte Subverzeichnisse.

Konfiguration

php
use JardisSupport\ClassVersion\ClassVersion;
use JardisSupport\ClassVersion\Data\ClassVersionConfig;
use JardisSupport\ClassVersion\Reader\LoadClassFromExtensions;

$config = new ClassVersionConfig(
    version: [
        'v2' => ['v2', 'current'],
        'v1' => ['v1', 'legacy'],
    ],
    fallbacks: ['v2' => ['v1']],
);

$resolver = new ClassVersion(
    $config,
    new LoadClassFromExtensions(
        depth: 3,
        segmentNames: ['Extensions'],
        versionConfig: $config,
    ),
);

depth gibt an, wie viele Namespace-Segmente vom Anfang den "Root" bilden, unter dem die Segmente aus segmentNames injiziert werden. segmentNames ist eine Liste von Segment-Namen (Default ['Extensions']), die in der angegebenen Reihenfolge (entsprechend ihrer Priorität) geprüft werden. Der Leerstring '' ist als Segment-Name zulässig und bedeutet "kein Zwischen-Segment einfügen". versionConfig ist optional: ohne Config wird die Version eins-zu-eins als Kette verwendet (keine Labels, keine Fallbacks).

Lookup-Reihenfolge

Für den Klassennamen App\Sales\Orders\Command\Handler\CreateOrder mit depth: 3 (Root = App\Sales\Orders) und segmentNames: ['Extensions']:

1. App\Sales\Orders\Extensions\v2\Command\Handler\CreateOrder   (versionierter Override, mit Fallback-Kette)
2. App\Sales\Orders\Extensions\Command\Handler\CreateOrder      (Baseline-Override, versionslos)
3. App\Sales\Orders\Command\Handler\CreateOrder                 (Generator-Basis)

Klassen mit weniger als depth + 1 Segmenten überspringen die Schritte 1–2 und werden direkt gegen die Generator-Basis aufgelöst: es gibt keinen "Rest" unterhalb des Roots.

Existiert weder Override noch Baseline noch Basis, wird InvalidArgumentException geworfen. Die Exception-Nachricht listet alle probierten Kandidaten auf.

Multi-Segment-Layouts (Versions-first)

segmentNames darf mehrere Einträge enthalten. Das ist nützlich, um neben dem projekteigenen Override-Layer einen weiteren Layer (z.B. eine Plattform- oder Vendor-Schicht) parallel zu unterstützen. Der Resolver folgt dabei strikt einer "Versions-first across segments"-Strategie: erst wird die komplette Versions-Fallback-Kette über alle Segmente probiert, anschließend werden die Baselines aller Segmente geprüft, zuletzt die Generator-Basis.

Beispiel mit segmentNames: ['', 'Platform'] (Leerstring = direkter Override am Root, ohne Zwischen-Segment) und Version v2 mit Fallback v1:

Versionierte Layer (alle Segmente, alle Versionen der Fallback-Kette):
1. App\Sales\Orders\v2\Command\Handler\CreateOrder            (segment='',         version='v2')
2. App\Sales\Orders\Platform\v2\Command\Handler\CreateOrder   (segment='Platform', version='v2')
3. App\Sales\Orders\v1\Command\Handler\CreateOrder            (segment='',         version='v1')
4. App\Sales\Orders\Platform\v1\Command\Handler\CreateOrder   (segment='Platform', version='v1')

Baseline-Layer (versionslos, alle Segmente):
5. App\Sales\Orders\Command\Handler\CreateOrder               (segment='',         versionslos = Generator-Basis-Form)
6. App\Sales\Orders\Platform\Command\Handler\CreateOrder      (segment='Platform', versionslos)

Generator-Basis (impliziter letzter Fallback):
7. App\Sales\Orders\Command\Handler\CreateOrder

Wichtig: Der versionierte Treffer eines spät genannten Segments schlägt die Baseline eines früheren Segments. Wer 'v2' anfordert und nur Platform/v2/... existiert (Schritt 2), bekommt diese Klasse, auch wenn unter dem leeren Segment ein versionsloser Override (Schritt 5) liegen würde. Versionen haben Vorrang vor Baselines, segmentübergreifend.

Einsatzzweck

Das Extensions-Layout ist ideal, wenn:

  • generierter Code und Custom-Code physisch getrennt sein sollen (z.B. src/Sales/Orders/Platform/ als ausschließlich generator-eigenes Verzeichnis, alles parallel dazu unter src/Sales/Orders/ als Team-Bereich)
  • Baseline-Overrides (versionslos) parallel zu versionierten Overrides existieren dürfen
  • mehrere Override-Layer mit klarer Priorität benötigt werden (Projekt-Ebene + Plattform-Ebene, Customizing + Vendor, …)
  • Regenerations-Läufe nur die Generator-Basis überschreiben sollen, ohne die Override-Verzeichnisse anzutasten

In Jardis-Projekten ist dies der Default, der generierte {Domain}Context::classVersion() nutzt depth: 3 mit segmentNames: ['', 'Platform']: Projekt-Overrides liegen direkt am Aggregate-Root (Leersegment), Plattform-Overrides unter Platform/. Versionen werden segmentübergreifend mit Vorrang gegenüber Baselines aufgelöst.

Proxy-Registry

Vorab registrierte Objekt-Instanzen überschreiben die Klassen-Auflösung, ideal für Tests oder Hot-Swap zur Laufzeit:

php
use JardisSupport\ClassVersion\Reader\LoadClassFromProxy;

$proxy = new LoadClassFromProxy($config);

// Instanz registrieren
$mockPayment = new MockPayment();
$proxy->addProxy('App\Domain\Payment', $mockPayment, 'v2');

// Liefert die registrierte Instanz statt einen FQCN
$result = $proxy('App\Domain\Payment', 'v2');
// → $mockPayment (Objekt, kein class-string)

// Proxy entfernen
$proxy->removeProxy('App\Domain\Payment', 'v2');

Der Proxy-Finder wird im Konstruktor von ClassVersion als optionaler dritter Parameter übergeben. Wird er weggelassen, legt ClassVersion selbst eine leere LoadClassFromProxy-Instanz an. Der Cache ist also immer verfügbar und hat Vorrang vor dem eigentlichen Class-Finder.

Auflösungs-Cache

Der optionale ClassResolutionCache memoisiert Auflösungs-Ergebnisse und Exceptions. Ziel: wiederholte class_exists()- und stat()-Syscalls für immer gleiche Klassen-Lookups vermeiden, besonders wertvoll für negative Lookups (existierende Basis-Klasse, kein Override), die in generiertem Code der Normalfall sind.

php
use JardisSupport\ClassVersion\ClassVersion;
use JardisSupport\ClassVersion\Support\ClassResolutionCache;

$cv = new ClassVersion(
    $config,
    new LoadClassFromExtensions(depth: 3, segmentNames: ['Extensions'], versionConfig: $config),
    null,                          // Proxy-Finder optional
    cache: new ClassResolutionCache(),
);

// Erster Aufruf: class_exists() + stat() Syscalls
$cv(App\Sales\Orders\Command\CreateOrder::class, 'v2');

// Zweiter Aufruf: Treffer aus dem Cache, kein Syscall
$cv(App\Sales\Orders\Command\CreateOrder::class, 'v2');

// Cache leeren (z. B. in Tests nach Autoload-Änderungen)
$cv->__construct(...);  // frische Instanz, oder:
$cache = new ClassResolutionCache();
$cache->clear();

Der Cache speichert Hits (Ergebnisse) und Misses (Exceptions) getrennt. Ein gecachter Miss wirft beim zweiten Aufruf die zuvor geworfene Exception erneut, ohne den Producer noch einmal auszuführen. Das macht den Cache transparent, selbst wenn Auflösungen fehlschlagen.

ClassResolutionCache implementiert ClassVersionInterface nicht. Er ist ein reiner Hilfs-Cache für ClassVersion::__invoke() und hat keine eigene Auflösungslogik.

Tracing (Debugging)

Der TracingClassVersion-Decorator protokolliert jeden Auflösungsschritt:

php
use JardisSupport\ClassVersion\Support\TracingClassVersion;

$traced = new TracingClassVersion($classVersion);

$traced('App\Domain\Payment', 'v2');
$traced('App\Service\Calculator', 'v1');

$trace = $traced->getTrace();
// [
//     [
//         'requested' => 'App\Domain\Payment',
//         'version'   => 'v2',
//         'resolved'  => 'App\Domain\v2\Payment',
//         'type'      => 'class-string',
//     ],
//     [
//         'requested' => 'App\Service\Calculator',
//         'version'   => 'v1',
//         'resolved'  => 'App\Service\v1\Calculator',
//         'type'      => 'class-string',
//     ],
// ]

$traced->clearTrace();

Jardis-Kontext: Builder + Platform-Dir

In Jardis-Builder-Projekten ist ClassVersion der Mechanismus, der die Koexistenz von generiertem und Custom-Code ermöglicht. Der Builder erzeugt seinen Output ausschließlich unter {Agg}/Platform/, Custom-Code lebt direkt unter {Agg}/ parallel zu Platform/:

src/Sales/Orders/Order/
├── Command/
│   └── Handler/
│       └── CreateOrder.php            ← Custom-Override (Baseline, versionslos)
├── v2/
│   └── Command/
│       └── Handler/
│           └── CreateOrder.php        ← Custom-Override (versionsspezifisch)
└── Platform/                          ← ★ ausschließlich Generator-Output, NIE editieren
    ├── Command/
    │   └── Handler/
    │       └── CreateOrder.php        ← Generator-Basis (bei jedem Build neu)
    └── Repository/
        └── HydrateOrder.php           ← Generator-Basis

Bei einem erneuten Builder-Lauf wird ausschließlich Platform/ neu geschrieben. Alles parallel dazu unter {Agg}/ bleibt unangetastet: das ist das Team-Verzeichnis. Pro Klasse entscheidet das Team:

  • Kein Override nötig → Generator-Basis unter Platform/<Rest> wird direkt verwendet
  • Projektweiter Override → Baseline-Datei direkt unter <Agg>/<Rest> (Leersegment, parallel zu Platform/)
  • Versionsspezifischer Override → Datei unter <Agg>/v{N}/<Rest>

Integration mit dem generierten Context

Die Version fließt automatisch durch den gesamten Kontext. Jeder handle()-Aufruf innerhalb eines generierten {Domain}Context nutzt die Version, die beim Einstieg über die Fassade übergeben wurde:

php
// Einstieg über die Domain-/BC-Fassade: neuer Kontext mit expliziter Version
return $sales->context(PlaceOrderContext::class, $data, version: 'v2');

// In PlaceOrderContext::__invoke()
$validator = $this->handle(ValidateOrder::class);      // → <Agg>\v2\…\ValidateOrder (wenn vorhanden)
$order     = $this->handle(CreateOrder::class, $data); // → <Agg>\v2\…\CreateOrder
$hydrator  = $this->handle(HydrateOrder::class);       // → <Agg>\Platform\…\HydrateOrder (Generator-Basis, kein Override)
// Alle drei werden automatisch über die Fallback-Kette aufgelöst

Der aufrufende Code kennt keine Versionen. Er arbeitet mit logischen Klassennamen. Die Version ist eine Eigenschaft des Kontexts, nicht der einzelnen Klasse.

Architektur

ClassVersion                        ← Composite-Orchestrator
├── LoadClassFromProxy              ← Strategy 1: Proxy-Registry (Vorrang)
├── LoadClassFromSubDirectory | LoadClassFromExtensions
│                                    ← Strategy 2: Class-Finder
│   └── ClassVersionConfig          ← Labels, Aliase, Fallback-Ketten
└── ClassResolutionCache            ← optional: Memoization von Hits & Misses

Support/TracingClassVersion         ← Decorator: Audit-Trail

Verzeichnisstruktur

src/
├── ClassVersion.php                    ← Orchestrator
├── Data/
│   └── ClassVersionConfig.php          ← Konfiguration (Labels, Fallbacks)
├── Reader/
│   ├── LoadClassFromProxy.php          ← Proxy-Registry
│   ├── LoadClassFromSubDirectory.php   ← Namespace-Injection
│   └── LoadClassFromExtensions.php     ← Extensions-Layout-Resolver
└── Support/
    ├── ClassResolutionCache.php        ← Memoization-Helper
    └── TracingClassVersion.php         ← Tracing-Decorator

API-Referenz

ClassVersion

php
new ClassVersion(
    ClassVersionConfigInterface $versionConfig,
    ClassVersionInterface $classFinder,
    ?ClassVersionInterface $proxyClassFinder = null,
    ?ClassResolutionCache $cache = null,
)
MethodeSignaturRückgabe
__invoke(string $className, ?string $version = null): mixedobject (Proxy) oder class-string

ClassVersionConfig

MethodeSignaturBeschreibung
__construct(array $version = [], array $fallbacks = [])Labels- und Fallback-Map
version(?string $version = null): ?stringLabel → Versions-Key auflösen
fallbackChain(?string $version = null): arrayVollständige Fallback-Kette

LoadClassFromSubDirectory

MethodeSignaturBeschreibung
__construct(ClassVersionConfigInterface $config)
__invoke(string $className, ?string $version = null): mixedFQCN der versionierten Klasse

LoadClassFromExtensions

MethodeSignaturBeschreibung
__construct(int $depth, array $segmentNames = ['Extensions'], ?ClassVersionConfigInterface $versionConfig = null)Layout-Konvention explizit angeben; segmentNames listet ein oder mehrere Override-Layer in Prioritäts-Reihenfolge, '' = kein Zwischen-Segment
__invoke(string $className, ?string $version = null): mixedFQCN aus Extensions- oder Basis-Layer

LoadClassFromProxy

MethodeSignaturBeschreibung
__invoke(string $className, ?string $version = null): mixedProxy-Instanz oder null
addProxy(string $className, object $proxy, ?string $version = null): selfInstanz registrieren
removeProxy(string $className, ?string $version = null): selfInstanz entfernen

ClassResolutionCache

MethodeSignaturBeschreibung
remember(string $key, callable $producer): mixedLiefert gecachten Wert oder ruft Producer einmalig
clear(): voidLeert Hits und Misses

TracingClassVersion

MethodeSignaturBeschreibung
__invoke(string $className, ?string $version = null): mixedDelegiert + Trace
getTrace(): arrayAlle Trace-Einträge
clearTrace(): voidTrace zurücksetzen

Vollständiges Beispiel

php
use JardisSupport\ClassVersion\ClassVersion;
use JardisSupport\ClassVersion\Data\ClassVersionConfig;
use JardisSupport\ClassVersion\Reader\LoadClassFromExtensions;
use JardisSupport\ClassVersion\Reader\LoadClassFromProxy;
use JardisSupport\ClassVersion\Support\ClassResolutionCache;
use JardisSupport\ClassVersion\Support\TracingClassVersion;

// Konfiguration
$config = new ClassVersionConfig(
    version: [
        'v2' => ['v2', 'current'],
        'v1' => ['v1', 'legacy'],
    ],
    fallbacks: [
        'v2' => ['v1'],
    ],
);

// Platform-Dir-Layout (Jardis-Default) mit Proxy-Registry und Auflösungs-Cache
$cv = new ClassVersion(
    $config,
    new LoadClassFromExtensions(depth: 3, segmentNames: ['', 'Platform'], versionConfig: $config),
    new LoadClassFromProxy($config),
    cache: new ClassResolutionCache(),
);

// Optional: Tracing für Debug-Session
$cv = new TracingClassVersion($cv);

// Fall 1: versionierter Custom-Override existiert (segment='', Version v2)
$class = $cv('App\Sales\Orders\Repository\HydrateOrder', 'current');
// → 'App\Sales\Orders\v2\Repository\HydrateOrder'

// Fall 2: kein v2-Override, versionsloser Custom-Override (Baseline) vorhanden
$class = $cv('App\Sales\Orders\Repository\ValidateOrder', 'current');
// → 'App\Sales\Orders\Repository\ValidateOrder' (segment='', versionslos)

// Fall 3: kein Custom-Override → Generator-Basis unter Platform/
$class = $cv('App\Sales\Orders\Repository\PersistOrder', 'current');
// → 'App\Sales\Orders\Platform\Repository\PersistOrder'

// Instanz erzeugen
$hydrator = new $class();

// Trace auswerten
foreach ($cv->getTrace() as $entry) {
    echo $entry['requested'] . ' [' . ($entry['version'] ?? 'default') . ']'
        . ' → ' . $entry['type'] . PHP_EOL;
}