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-Strategien —
LoadClassFromSubDirectoryfür das klassischev2/-Layout,LoadClassFromExtensionsfür Multi-Segment-Layouts mit klarer Trennung zwischen Generator- und Custom-Code (Default in Jardis-Builder-Projekten:segmentNames: ['', 'Platform']) - Fallback-Chain —
v2nicht vorhanden? Automatisch aufv1oder 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
ClassResolutionCachememoisiert Treffer und Misses und vermeidet wiederholteclass_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:
// 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:
| Sprint | v2-Overrides | Effekt |
|---|---|---|
| 1 | Payment, PaymentValidator | Neue Zahlungslogik, Rest bleibt v1 |
| 2 | Invoice, InvoiceRenderer | Neues Rechnungsformat |
| 3 | Customer | Erweiterte Kundendaten |
| 4 | Label 'v2' → 'current' | v2 wird Standard, v1 wird 'legacy' |
Jeder Sprint ist einzeln deployer und rollbar. Kein Alles-oder-nichts-Umstieg.
Installation
composer require jardissupport/classversionGitHub: 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:
| Resolver | Injektionspunkt | Typisches Layout |
|---|---|---|
LoadClassFromSubDirectory | direkt vor dem Klassennamen | App\Domain\v2\Payment |
LoadClassFromExtensions | an konfigurierbarer Tiefe, mit ein oder mehreren Zwischen-Segmenten | App\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\CalculatorDie Klasse muss nur im richtigen Namespace deklariert sein: PSR-4 erledigt den Rest.
Grundlegende Nutzung
Konfiguration
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
// 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 → returnWenn 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:
$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
$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
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\CreateOrderWichtig: 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 untersrc/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:
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.
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:
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-BasisBei 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 zuPlatform/) - 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:
// 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östDer 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-TrailVerzeichnisstruktur
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-DecoratorAPI-Referenz
ClassVersion
new ClassVersion(
ClassVersionConfigInterface $versionConfig,
ClassVersionInterface $classFinder,
?ClassVersionInterface $proxyClassFinder = null,
?ClassResolutionCache $cache = null,
)| Methode | Signatur | Rückgabe |
|---|---|---|
__invoke | (string $className, ?string $version = null): mixed | object (Proxy) oder class-string |
ClassVersionConfig
| Methode | Signatur | Beschreibung |
|---|---|---|
__construct | (array $version = [], array $fallbacks = []) | Labels- und Fallback-Map |
version | (?string $version = null): ?string | Label → Versions-Key auflösen |
fallbackChain | (?string $version = null): array | Vollständige Fallback-Kette |
LoadClassFromSubDirectory
| Methode | Signatur | Beschreibung |
|---|---|---|
__construct | (ClassVersionConfigInterface $config) | — |
__invoke | (string $className, ?string $version = null): mixed | FQCN der versionierten Klasse |
LoadClassFromExtensions
| Methode | Signatur | Beschreibung |
|---|---|---|
__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): mixed | FQCN aus Extensions- oder Basis-Layer |
LoadClassFromProxy
| Methode | Signatur | Beschreibung |
|---|---|---|
__invoke | (string $className, ?string $version = null): mixed | Proxy-Instanz oder null |
addProxy | (string $className, object $proxy, ?string $version = null): self | Instanz registrieren |
removeProxy | (string $className, ?string $version = null): self | Instanz entfernen |
ClassResolutionCache
| Methode | Signatur | Beschreibung |
|---|---|---|
remember | (string $key, callable $producer): mixed | Liefert gecachten Wert oder ruft Producer einmalig |
clear | (): void | Leert Hits und Misses |
TracingClassVersion
| Methode | Signatur | Beschreibung |
|---|---|---|
__invoke | (string $className, ?string $version = null): mixed | Delegiert + Trace |
getTrace | (): array | Alle Trace-Einträge |
clearTrace | (): void | Trace zurücksetzen |
Vollständiges Beispiel
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;
}