---
name: platform-versioning
description: ClassVersion resolution and Versionierungs-Modell for Designer-generated code — 7-stage lookup chain via `LoadClassFromExtensions` with `segmentNames: ['', 'Platform']`, baseline vs. versioned (`v1`, `v2`, …) overrides, `ClassVersionConfig` setup, five Leitsätze (additiv vor Version, Version ändert Verhalten nie API, Datenbruch = neues Aggregat, Code-Rettung über Service-Schicht, one API surface per aggregate).
zone: post-active
persona: C
prerequisites: [platform-implementation]
next: []
---

### 1. ClassVersion resolution

`$this->handle(ClassName::class)` resolves an override through the reader `LoadClassFromExtensions`, called with `segmentNames: ['', 'Platform']`. The reader walks a 7-stage lookup chain. First hit wins; later stages are fallback only.

For active version `v_n` (returned by `version()` on the Domain facade — see below), the resolution order is:

1. `<Agg>\v_n\X` — Dev-Override v_n (aggregat-root `{Agg}/v{n}/`)
2. `<Agg>\Platform\v_n\X` — Platform v_n (generated under `{Agg}/Platform/v{n}/`)
3. `<Agg>\v_{n-1}\X` — Dev-Override of older version (fallback chain)
4. `<Agg>\Platform\v_{n-1}\X` — Platform older version
5. `<Agg>\X` — Dev-Baseline (the aggregate root `<Agg>\<Agg>` itself, or a direct override at the aggregate root)
6. `<Agg>\Platform\X` — Platform-Baseline (the regenerated class under `{Agg}/Platform/`)
7. Fallback: `class_exists($input)` check, otherwise `InvalidArgumentException`.

Stages 1–4 only run when `version()` returns a non-empty label and `ClassVersionConfig` lists the version with its fallback chain. Stages 5–6 always run. The `''` segment in `segmentNames` produces stages 1, 3, 5; the `'Platform'` segment produces stages 2, 4, 6 — interleaved per version.

```
Generator base (always under Platform/):
  MeterDevice\Counter\Counter\Platform\Command\Handler\Action\HydrateCreateCounterEntities

Dev baseline (override at aggregate root, no version):
  MeterDevice\Counter\Counter\Command\Handler\Action\HydrateCreateCounterEntities
  ↳ extends MeterDevice\Counter\Counter\Platform\Command\Handler\Action\HydrateCreateCounterEntities

Dev versioned (e.g. v2):
  MeterDevice\Counter\Counter\v2\Command\Handler\Action\HydrateCreateCounterEntities
  ↳ extends the baseline above (or directly the Platform base)

Platform versioned (Generator output for v2 — emitted only when a v2 generator variant exists):
  MeterDevice\Counter\Counter\Platform\v2\Command\Handler\Action\HydrateCreateCounterEntities
```

Namespace = `<Domain>\<BC>\<Agg>\…`. Both `Counter` segments above are real: first is BC, second is Aggregate. Dev paths sit directly under the aggregate (`<Agg>\…` or `<Agg>\v{n}\…`); generator paths always carry the `Platform` segment (`<Agg>\Platform\…` or `<Agg>\Platform\v{n}\…`).

**Baseline vs. versioned** — pick one:

- **Baseline** (versionless) covers the 80 % case: "I want this logic replaced, period." No `ClassVersionConfig` setup. Place the file directly at the aggregate root (mirroring the Platform sub-path), `extends` the Platform class, done.
- **Versioned** (`v1`, `v2`, …) for tenant / feature-flag scenarios. Activated by overriding `version()` on the Domain facade.

Setup for versioned resolution (once per domain, only when you need it):

```php
$config = new ClassVersionConfig(
    version:   ['v1' => ['v1'], 'v2' => ['v2'], 'v3' => ['v3']],
    fallbacks: ['v3' => ['v2', 'v1'], 'v2' => ['v1']]
);
```

Then activate in the Domain facade:

```php
final class MeterDevice extends JardisApp
{
    protected function version(): string
    {
        return 'v2';   // or derive from tenant, feature flag, …
    }
}
```

Override skeleton (baseline example — null setup, file lives directly at the aggregate root):

```php
declare(strict_types=1);
namespace MeterDevice\Counter\Counter\Command\Handler\Action;

use MeterDevice\Counter\Counter\Platform\Command\Handler\Action\HydrateCreateCounterEntities as Base;
use MeterDevice\Counter\Counter\Platform\Command\Counter as CommandCounter;
use MeterDevice\Counter\Counter\Platform\Aggregate\Counter;
use MeterDevice\Counter\Counter\ValueObject\ObisCode;

final class HydrateCreateCounterEntities extends Base
{
    protected function hydrateCounterRegister(Counter $handler, CommandCounter $counter): void
    {
        parent::hydrateCounterRegister($handler, $counter);
        foreach ($counter->counterRegister as $reg) {
            $this->handle(ObisCode::class, $reg->obis);
        }
    }
}
```

Versioned variant — same class, namespace adds `\v2\` between `<Agg>` and the rest:

```php
namespace MeterDevice\Counter\Counter\v2\Command\Handler\Action;
```

### 2. Versionierungs-Modell

Versions in Jardis are about behaviour, not data shape. Five Leitsätze govern when to reach for `v{N}`, when to extend the schema, and when to spin up a new aggregate.

**Leitsätze:**

- **Additiv geht vor Version.** New nullable fields, new enum members, new optional tables go into the base schema without bumping a version.
- **Version ändert Verhalten, nie API.** `v1` and `v2` of the same aggregate share identical Commands / Queries / Events / Payloads. Only the implementation differs. There is exactly one API surface per aggregate.
- **Datenbruch = neues Aggregat.** A removed field, flipped type, or shifted semantic of a load-bearing field is honest enough to warrant `<Agg>V4` — its own name, its own tree, its own spec, its own entities.
- **Code-Rettung läuft über Abstraktion, nicht über Mechanik.** When data breaks, behaviour is rescued by Domain Services (`{Agg}/Service/`), not by version-merge tricks. Entity-agnostic logic survives any data break; entity-bound logic on the broken fields does not.
- **One API surface per aggregate — punkt.** No v{N}-API matrices, no delta-merge variants.

**Daumenregel — when to use what:**

| Change | Path |
|---|---|
| New nullable column / new enum member / new optional relation | Schema additiv erweitern, no version. Goes into base definition; FieldMap learns the new key; rebuild. |
| New business rule, different calculation, tenant variant, tightened validation | **`v{N}` Dev-Override** under `{Agg}/v{N}/`. Activate via `version()` on the Domain facade. Spec stays invariant. |
| Field removed, type flips, semantic of load-bearing field changes | **Neues Aggregat `<Agg>V4`**, eigener Tree, eigene Spec. Generator regeneriert separat. |

**Service-Schicht-Hinweis.** Versionsfreier, entity-agnostischer Code gehört nach `{Agg}/Service/` — direkt am Aggregat-Root, keine `v{N}/`-Variante. Diese Service-Schicht wird von `v1` / `v2` / `v3` UND einem späteren `<Agg>V4` genutzt. Die Konsequenz von Säule 4 (`rules-architecture` — Data-Behavior-Separation): Logik, die nicht entity-typ-abhängig sein muss, jetzt schon als Service extrahieren — sie überlebt jeden Datenbruch.

### Anchors

- `platform-implementation` (Generated baseline layout, override targets, prohibitions).
- `platform-cookbook` (Recipe 1 — tenant-variant baseline override picked up via stage 5 of §1 above).
- `support-classversion` (the resolver implementation itself).
- `rules-architecture` (Pillar 4 — Data-Behavior-Separation drives the Service-Schicht-Hinweis above).
