PHP Academy · Lektion

Fra layered til clean architecture

Forstå, hvorfor dependencies bør pege indad.

Lektion 1 af 413 trin

Fra layered til clean architecture er en gratis PHP Academy-lektion på CoddyKit. Dette er lektion 1 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.

Hvorfor Clean Architecture

Du kender allerede den klassiske PHP-stak med tre lag: Controller → Tjeneste → Repository → Database. Den fungerer, men forretningslogikken bliver koblet til Eloquent, Doctrine, HTTP-anmodningen og rammeværkets livscyklus. Clean Architecture vender afhængighedsretningen, så dit domæne ikke ved noget om infrastrukturen. Gevinsten er testbare anvendelsestilfælde, udskiftelige adaptere og en kodebase, der overlever opgraderinger af rammeværket.

Afhængighedsreglen

Den eneste regel i Clean Architecture er: Afhængigheder i kildekoden peger kun indad. Indre cirkler (entiteter, anvendelsestilfælde) må aldrig referere til ydre cirkler (controllere, ORM'er, rammeværker). Ved kørsel løber styringen udad via grænseflader, men på tidspunktet for kompilering/import importerer intet indre noget ydre.

  • Entiteter: forretningsregler på virksomhedsniveau
  • Anvendelsestilfælde: applikationsregler
  • Adaptere: controllere, præsentatorer, gateways
  • Rammeværker og drivere: DB, HTTP, webben

Et tæt koblet lagdelt eksempel

Her er den slags tjeneste, som de fleste PHP-apps indeholder. Bemærk, hvordan domænelogikken er filtret sammen med Eloquent og HTTP-svaret. Du kan ikke enhedsteste rabatreglen uden en database og et rammeværk.

<?php
class OrderService
{
    public function place(Request $request)
    {
        $user = User::find($request->user_id); // Eloquent
        $total = 0;
        foreach ($request->items as $i) {
            $total += Product::find($i['id'])->price * $i['qty'];
        }
        if ($user->is_vip) {
            $total *= 0.9; // business rule trapped in infra code
        }
        Order::create(['user_id' => $user->id, 'total' => $total]);
        return response()->json(['total' => $total]);
    }
}

Entiteter: rammeværksfrit domæne

En entitet indkoder regler, der gælder på tværs af virksomheden, og afhænger ikke af noget. Almindelig PHP, ingen annotations og ingen basisklasse fra ORM'en. Den kan oprettes fuldt ud i en test.

<?php
final class Money
{
    public function __construct(public readonly int $cents) {
        if ($cents < 0) throw new InvalidArgumentException('negative money');
    }
    public function multiply(float $factor): self {
        return new self((int) round($this->cents * $factor));
    }
}

final class Order
{
    /** @param array<int,int> $lineCents */
    public function __construct(private array $lineCents, private bool $vip) {}
    public function total(): Money {
        $sum = array_sum($this->lineCents);
        $money = new Money($sum);
        return $this->vip ? $money->multiply(0.9) : $money;
    }
}

echo (new Order([1000, 2000], true))->total()->cents, PHP_EOL; // 2700

Use cases ejer arbejdsgangen

Et use case (interactor) orkestrerer entiteter og taler kun med omverdenen gennem grænseflader (porte). Det modtager en request-DTO og returnerer en response-DTO — aldrig et HTTP-objekt.

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

final class PlaceOrder
{
    public function __construct(private OrderRepository $orders) {}

    public function execute(array $lineCents, bool $vip): int {
        $order = new Order($lineCents, $vip);
        $this->orders->save($order);
        return $order->total()->cents;
    }
}

Grænsen er en grænseflade

Use caset erklærer den OrderRepository-grænseflade, det har brug for. Grænsefladen ligger i den indre cirkel; den konkrete Eloquent/Doctrine-implementering ligger udenfor og afhænger indad. Det er princippet om afhængighedsinversion anvendt ved en arkitektonisk grænse.

Retningen for kildekodeafhængigheden: EloquentOrderRepository → OrderRepository (grænseflade), aldrig omvendt.

<?php
// Lives in infrastructure layer, points INWARD to the domain interface
final class EloquentOrderRepository implements OrderRepository
{
    public function save(Order $order): void {
        OrderModel::create(['total' => $order->total()->cents]);
    }
}

Test uden infrastruktur

Fordi use caset afhænger af en grænseflade, injicerer tests en testdobbelt. Ingen database, ingen opstart af rammeværk — mikrosekundhurtige enhedstests, der verificerer ren forretningsadfærd.

<?php
final class InMemoryOrders implements OrderRepository {
    public array $saved = [];
    public function save(Order $o): void { $this->saved[] = $o; }
}

$repo = new InMemoryOrders();
$useCase = new PlaceOrder($repo);
$total = $useCase->execute([1000, 2000], true);

assert($total === 2700);
assert(count($repo->saved) === 1);
echo "PASS total=$total saved=" . count($repo->saved) . PHP_EOL;

Controllere bliver tynde adaptere

Controlleren er nu en adapter: Den oversætter HTTP til et kald på et use case og resultatet tilbage til HTTP. Den indeholder ingen forretningsregler. Skift REST ud med CLI eller en køarbejder, så forbliver use caset uændret.

<?php
final class OrderController
{
    public function __construct(private PlaceOrder $placeOrder) {}

    public function store(Request $request): JsonResponse {
        $total = $this->placeOrder->execute(
            lineCents: $request->input('lineCents'),
            vip: (bool) $request->input('vip'),
        );
        return new JsonResponse(['total' => $total], 201);
    }
}

Skrigende arkitektur

Mappestrukturen skal råbe domænet, ikke rammeværket. Undgå Controllers/ og Models/ øverst. Organiser efter kapacitet, så en ny person straks kan se, hvad appen gør.

  • src/Ordering/Domain/ — entiteter, værdiobjekter
  • src/Ordering/Application/ — anvendelsestilfælde, portgrænseflader
  • src/Ordering/Infrastructure/ — Eloquent-repositories, HTTP-controllere

Hver afgrænset kontekst er en mappe på øverste niveau; rammeværket lever ved kanterne.

Håndhævelse af afhængighedsreglen

Uden værktøjer forsvinder disciplinen gradvist. Brug deptrac eller phparkitect i CI til at få buildet til at fejle, når Domain importerer Infrastructure. Reglen bliver en garanti ved kompilering i stedet for et håb baseret på kodegennemgang.

# deptrac.yaml
deptrac:
  layers:
    - name: Domain
      collectors: [{ type: directory, value: src/.*/Domain/.* }]
    - name: Application
      collectors: [{ type: directory, value: src/.*/Application/.* }]
    - name: Infrastructure
      collectors: [{ type: directory, value: src/.*/Infrastructure/.* }]
  ruleset:
    Domain: []                       # Domain may depend on nothing
    Application: [Domain]
    Infrastructure: [Application, Domain]

Krydsning af grænser med DTO'er

For at forhindre entiteter i at lække udad transporteres data, der krydser en grænse, som en simpel DTO — ikke som en entitet eller en ORM-model. Use caset returnerer en flad struktur, som adapteren kan serialisere, så domæneobjektet aldrig forlader kernen, og det ydre lag aldrig får en reference til den interne tilstand.

<?php
final class OrderSummary // boundary DTO, no behavior, no domain types
{
    public function __construct(
        public readonly string $orderId,
        public readonly int $totalCents,
    ) {}
}

final class PlaceOrderV2 {
    public function __construct(private OrderRepository $orders) {}
    public function execute(array $lineCents, bool $vip): OrderSummary {
        $order = new Order($lineCents, $vip);
        $this->orders->save($order);
        return new OrderSummary('ord_1', $order->total()->cents);
    }
}

Hurtigt tjek

Hvilken afhængighedsretning er tilladt under afhængighedsreglen?

Opsamling

Du er gået fra en tæt koblet lagdelt stak til Clean Architecture:

  • Afhængighedsreglen: Kildekodeafhængigheder peger kun indad.
  • Entiteter indeholder forretningsregler i rammeværksfri PHP.
  • Use cases orkestrerer via portgrænseflader og returnerer DTO'er, ikke HTTP.
  • Controllere og ORM-repositories er ydre adaptere, der afhænger indad (DIP).
  • Strukturen skal råbe domænet, og værktøjer som deptrac håndhæver reglen i CI.
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 “Fra layered til clean architecture” gratis?

Ja — hele teksten til “Fra layered til clean architecture” 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 “Fra layered til clean architecture”?

Forstå, hvorfor dependencies bør pege indad. 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 1 af 4.

Hvor lang tid tager lektionen “Fra layered til clean architecture”?

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. Fra layered til clean architecture
  2. Ports og adapters forklaret
  3. Use cases og application services
  4. Dependency inversion i praksis
← Tilbage til PHP Academy