PHP Academy · Lektion

Aggregates, repositories og factories

Beskyt invariants med aggregates, og persistér dem på en ren måde.

Lektion 2 af 413 trin

Aggregates, repositories og factories er en gratis PHP Academy-lektion på CoddyKit. Dette er lektion 2 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i PHP Academy, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. PHP Academy-kurset indeholder 4 lektioner i alt.

Beskyttelse af invariansbetingelser

Når du har entiteter og værdiobjekter, har du brug for mønstre, der holder klynger af dem konsistente og gemmer dem korrekt. DDD svarer med tre taktiske mønstre: aggregatet (en konsistensgrænse), repositoryet (en samlingslignende persistensabstraktion) og fabrikken (kompleks oprettelse). Denne lektion viser, hvordan de passer sammen i PHP.

Hvad er et aggregat

Et aggregat er en klynge af entiteter og værdiobjekter, der behandles som én enhed ved dataændringer. Én entitet er aggregatroden — det eneste medlem, som kode uden for aggregatet må holde en reference til. Alle ændringer går gennem roden, som håndhæver aggregatets invariansbetingelser. Aggregatet er også den transaktionelle grænse: Det indlæses og gemmes atomisk.

Roden beskytter helheden

Kaldende kode udefra tilgår aldrig de indre medlemmer direkte. Hvis du vil tilføje en linjepost, kalder du en metode på roden, som validerer og opretholder konsistensen, f.eks. totaler og grænser. Så holdes invariansbetingelserne samlet ét sted.

<?php
final class LineItem {
    public function __construct(
        public readonly string $sku,
        public readonly int $qty,
        public readonly int $unitCents
    ) {}
    public function subtotal(): int { return $this->qty * $this->unitCents; }
}

final class Order { // Aggregate Root
    /** @var LineItem[] */
    private array $items = [];
    public function __construct(public readonly string $id) {}

    public function addItem(string $sku, int $qty, int $unitCents): void {
        if ($qty < 1) { throw new DomainException('qty must be >= 1'); }
        $this->items[] = new LineItem($sku, $qty, $unitCents);
    }
    public function total(): int {
        return array_sum(array_map(fn(LineItem $i) => $i->subtotal(), $this->items));
    }
}
$o = new Order('o1');
$o->addItem('A', 2, 500);
$o->addItem('B', 1, 300);
echo $o->total(), PHP_EOL; // 1300

Udformning af små aggregater

En hyppig fejl er at gøre aggregater for store, f.eks. en Order, der også ejer hele Customer-grafen. Tommelfingerregler:

  • Hold aggregater små; referér til andre aggregater med id, og opbevar ikke selve objektet.
  • Én transaktion bør ændre ét aggregat; koordinér på tværs af aggregater med domænehændelser.
  • De invariansbetingelser, der altid skal gælde, definerer grænsen.

Referér med id

Ordren gemmer et customerId-værdiobjekt, ikke en Customer-entitet. Det holder konsistensgrænsen stram og undgår at indlæse enorme objektgrafer. Konsistens på tværs af aggregater bliver eventual, og den håndteres af hændelser i stedet for én gigantisk transaktion.

<?php
final class CustomerId {
    public function __construct(public readonly string $value) {}
}
final class Order {
    public function __construct(
        public readonly string $id,
        public readonly CustomerId $customerId // reference, not object
    ) {}
}
$order = new Order('o1', new CustomerId('cus_99'));
echo $order->customerId->value, PHP_EOL;

Repositoryets kontrakt

Et repository giver indtryk af at være en samling af aggregatrødder i hukommelsen. Domænet afhænger kun af grænsefladen; implementeringen (Doctrine, PDO eller hukommelsen) ligger i infrastrukturlaget. Repositories arbejder med hele aggregater, aldrig delvise rækker.

<?php
interface OrderRepository {
    public function ofId(string $id): ?Order;
    public function save(Order $order): void;
    public function nextIdentity(): string;
}

En implementering i hukommelsen

Et repository i hukommelsen er uvurderligt til hurtige enhedstest uden en database. Fordi domænet afhænger af grænsefladen, kan du frit udskifte implementeringer — afhængighedsinversion i praksis.

<?php
interface OrderRepository {
    public function ofId(string $id): ?object;
    public function save(object $order): void;
    public function nextIdentity(): string;
}
final class Order { public function __construct(public readonly string $id) {} }

final class InMemoryOrderRepository implements OrderRepository {
    private array $store = [];
    public function ofId(string $id): ?object { return $this->store[$id] ?? null; }
    public function save(object $order): void { $this->store[$order->id] = $order; }
    public function nextIdentity(): string { return 'o_' . bin2hex(random_bytes(4)); }
}
$repo = new InMemoryOrderRepository();
$repo->save(new Order('o1'));
var_dump($repo->ofId('o1') !== null);

Repository er ikke en DAO

Et repository er ikke en generisk CRUD-DAO. Det stiller domænemeningsfulde forespørgsler til rådighed, f.eks. findOverdueOrders(), og gendanner hele aggregater med deres invariansbetingelser intakte. Det skjuler bevidst SQL- og ORM-detaljer, så domænet forbliver uafhængigt af persistens. Undgå at lække forespørgselsbyggere eller generiske metoder som save($anyEntity) ind i domænet.

Fabrikker til kompleks oprettelse

Når oprettelsen af et aggregat indebærer reel logik — generering af identitet, sammensætning af værdiobjekter og håndhævelse af invariansbetingelser ved oprettelsen — skal du flytte den til en fabrik (en dedikeret klasse eller en statisk navngiven konstruktør). Det holder entitetens konstruktør enkel og samler reglerne for gyldig oprettelse ét sted.

<?php
final class Order {
    private function __construct(
        public readonly string $id,
        public readonly string $customerId
    ) {}
    public static function place(string $customerId): self {
        if ($customerId === '') { throw new DomainException('customer required'); }
        return new self('o_' . bin2hex(random_bytes(4)), $customerId);
    }
}
$order = Order::place('cus_1');
echo $order->id, PHP_EOL;

Sådan samarbejder de

Det typiske forløb i en applikationstjeneste:

  • En fabrik (eller en navngiven konstruktør) opretter et gyldigt aggregat.
  • Aggregatrodens metoder håndhæver invariansbetingelser under brug.
  • Et repository gemmer senere hele aggregatet og gendanner det.

Applikationstjenesten orkestrerer disse trin i én transaktion pr. aggregat og afhænger kun af grænseflader.

Håndhævelse af en invariansbetingelse for hele aggregatet

Rodens egentlige værdi er, at den håndhæver invariansbetingelser, der går på tværs af medlemmerne. Her afviser ordren en linjepost, hvis den ville bringe totalen over en kreditgrænse — en regel, som ingen enkelt LineItem kunne håndhæve alene. Fordi alle ændringer går gennem roden, kan reglen aldrig omgås.

<?php
final class Order {
    private array $items = [];
    public function __construct(
        public readonly string $id,
        private int $creditLimitCents
    ) {}
    public function addItem(int $cents): void {
        if ($this->total() + $cents > $this->creditLimitCents) {
            throw new DomainException('Exceeds credit limit');
        }
        $this->items[] = $cents;
    }
    public function total(): int { return array_sum($this->items); }
}
$o = new Order('o1', 1000);
$o->addItem(600);
try { $o->addItem(600); } catch (DomainException $e) { echo $e->getMessage(), PHP_EOL; }
echo $o->total(), PHP_EOL; // 600

Hurtigt tjek

Udformning af aggregater.

Opsamling

Du har lært at beskytte invariansbetingelser og håndtere persistens korrekt. Aggregater udgør en konsistens- og transaktionel grænse, ændres kun gennem deres rod og holdes små ved at referere til andre aggregater med id. Repositories præsenterer aggregatrødder som en samling bag en domænegrænseflade, skjuler ORM og SQL og muliggør testdobler i hukommelsen. Fabrikker samler kompleks oprettelse, der håndhæver invariansbetingelser. Tilsammen holder de din domænemodel konsistent og uafhængig af persistens.

Gratis at komme i gang

Lær PHP med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
49
Lektioner
195

Ofte stillede spørgsmål

Er lektionen “Aggregates, repositories og factories” gratis?

Ja — hele teksten til “Aggregates, repositories og factories” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af PHP Academy-kurset, skal du opgradere til CoddyKit PRO. PHP Academy-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Aggregates, repositories og factories”?

Beskyt invariants med aggregates, og persistér dem på en ren måde. Du øver dig i PHP Academy med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på PHP Academy?

Der kræves ingen tidligere erfaring. PHP Academy på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 2 af 4.

Hvor lang tid tager lektionen “Aggregates, repositories og factories”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne PHP Academy-lektion?

Ja. Alle PHP Academy-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. DDD-byggesten: Entities og value objects
  2. Aggregates, repositories og factories
  3. Domain events og domain services
  4. Bounded contexts og context mapping
← Tilbage til PHP Academy