Laravel als e-commerce middleware: architectuurpatronen die werken
Terug naar blog

Laravel als e-commerce middleware: architectuurpatronen die werken

AuthorRuthger Idema
31 maart 202612 min leestijd

Laravel als lijm tussen je webshop, ERP, WMS en PIM. Niet als monoliet, maar als event-driven middleware met adapters. Een concrete architectuurgids met patterns die op schaal werken.

Laravel als e-commerce middleware: architectuurpatronen die werken

Een webshop koppelen aan een ERP is geen integratieproject. Het is een architectuurvraagstuk. Hoe zorg je dat data betrouwbaar stroomt tussen systemen die elk een eigen taal spreken, elk hun eigen tempo hebben, en elk op een ander moment offline kunnen zijn?

Laravel is bij uitstek geschikt als middleware voor e-commerce integraties. Niet als monoliet die alles doet, maar als event-driven hub die adapters, queues en transformers combineert tot een betrouwbare integratilaag. Dit artikel beschrijft de architectuurpatronen die werken.

Wat je leert in dit artikel

  • Waarom Laravel als middleware verstandig is
  • Het Adapter Pattern voor systeemonafhankelijke integraties
  • Event-driven architectuur voor asynchrone datastroom
  • Queue-based sync: waarom alles via een queue moet
  • Concrete Laravel-implementaties van elk patroon

Waarom Laravel als middleware?

De alternatieven voor e-commerce middleware zijn: een iPaaS zoals Alumio of MuleSoft, een custom oplossing in een andere taal, of directe punt-tot-punt koppelingen zonder centrale laag.

Laravel wint voor de meeste mid-market e-commerce situaties op vier punten:

Ecosysteem. Laravel heeft native ondersteuning voor queues, events, HTTP-clients, database-ORM, caching en jobs. Alles wat middleware nodig heeft, zit in de standaard installatie. Testbaarheid. PHP unit tests, Laravel Dusk voor integraties, factory states voor testdata. Je kunt elk onderdeel van je middleware geïsoleerd testen. Deployability. Een Laravel-applicatie deploy je op elke VPS, Forge-server of cloud. Geen vendor lock-in op de infrastructuur. Aanpasbaarheid. Geen licentiekosten, geen platform-beperkingen. Complexe logica die niet past in een visuele configuratietool, schrijf je gewoon in PHP.

Het Adapter Pattern

De meest voorkomende fout in integratieprojecten: businesslogica die direct aan een specifiek systeem is gekoppeld. Je schrijft code die AFAS-specifieke API-calls maakt, vermengt met de logica die orders transformeert.

Zodra de klant van AFAS naar SAP wisselt, moet je alles herschrijven.

Het Adapter Pattern lost dit op. Definieer een interface die de abstractie beschrijft, en schrijf per systeem een concrete implementatie.

php
// De abstracte interface — systeemonafhankelijk
interface ErpAdapterInterface
{
    public function createSalesOrder(Order $order): string;
    public function getInventoryLevel(string $sku): int;
    public function updateOrderStatus(string $erpOrderId, string $status): void;
}

// AFAS-implementatie
class AfasErpAdapter implements ErpAdapterInterface
{
    public function __construct(
        private AfasRestClient $client
    ) {}

    public function createSalesOrder(Order $order): string
    {
        $payload = $this->transformToAfasFormat($order);
        $response = $this->client->post('/ProfitRestServices/connectors/SalesOrders', $payload);
        return $response['rows'][0]['OrderId'];
    }

    public function getInventoryLevel(string $sku): int
    {
        $response = $this->client->get('/ProfitRestServices/connectors/ItemStock', [
            'ItemCode' => $sku,
        ]);
        return (int) $response['rows'][0]['Stock'];
    }

    public function updateOrderStatus(string $erpOrderId, string $status): void
    {
        $this->client->put("/ProfitRestServices/connectors/SalesOrders/{$erpOrderId}", [
            'Status' => $status,
        ]);
    }
}

// SAP-implementatie — zelfde interface, ander systeem
class SapErpAdapter implements ErpAdapterInterface
{
    public function createSalesOrder(Order $order): string
    {
        // SAP Service Layer API-call
        $response = $this->client->post('/b1s/v1/Orders', [
            'CardCode'   => $order->customer->erpCode,
            'DocDate'    => now()->format('Y-m-d'),
            'DocumentLines' => $this->mapLinesToSap($order->lines),
        ]);
        return (string) $response['DocNum'];
    }
    // ...
}

De service die orders verwerkt, werkt met de interface — niet met de concrete implementatie. Je bindt de juiste adapter via Laravel's service container.

php
// In AppServiceProvider of een dedicated IntegrationServiceProvider
$this->app->bind(ErpAdapterInterface::class, function ($app) {
    return match(config('integrations.erp.driver')) {
        'afas' => new AfasErpAdapter(new AfasRestClient(...)),
        'sap'  => new SapErpAdapter(new SapServiceLayerClient(...)),
        'exact' => new ExactErpAdapter(new ExactOnlineClient(...)),
        default => throw new \InvalidArgumentException('Onbekende ERP driver'),
    };
});

Wissel je van ERP? Verander één configuratieregel. De rest van je code raakt je niet aan.

Event-driven architectuur

Een event-driven aanpak ontkoppelt de componenten van je middleware verder. In plaats van directe aanroepen, publiceert een component een event. Andere componenten luisteren naar dat event en handelen ernaar.

php
// Event aanmaken
class OrderPlacedInShopify
{
    public function __construct(
        public readonly array $shopifyOrder,
        public readonly \DateTimeImmutable $occurredAt,
    ) {}
}

// Listeners registreren in EventServiceProvider
protected $listen = [
    OrderPlacedInShopify::class => [
        SendOrderToErp::class,
        SendOrderToWms::class,
        UpdateInventoryReservation::class,
        NotifyFulfillmentTeam::class,
    ],
];

// Listener die asynchroon werkt
class SendOrderToErp implements ShouldQueue
{
    public function handle(OrderPlacedInShopify $event): void
    {
        $order = Order::fromShopifyPayload($event->shopifyOrder);
        $erpOrderId = app(ErpAdapterInterface::class)->createSalesOrder($order);

        // ERP order-ID opslaan voor latere referentie
        OrderMapping::create([
            'shopify_order_id' => $order->shopifyId,
            'erp_order_id'     => $erpOrderId,
            'erp_system'       => config('integrations.erp.driver'),
        ]);
    }
}

Het voordeel: je kunt listeners toevoegen of verwijderen zonder bestaande code te wijzigen. Wil je een nieuwe actie uitvoeren bij een nieuwe order? Voeg een listener toe. Wil je een listener tijdelijk uitschakelen? Verwijder hem uit de $listen array.

Queue-based sync: de enige juiste aanpak

Alles wat je middleware doet, moet via een queue. Geen synchrone API-calls in een webhook-handler, geen directe database-writes op het kritieke pad.

Waarom? Drie redenen:

Externe systemen zijn niet altijd beschikbaar. Een ERP die een onderhoudswindow heeft, een WMS dat langzaam reageert, een Shopify rate limit die je raakt. Als je synchroon werkt, faalt de hele keten als één schakel tijdelijk uitvalt. Webhooks hebben een timeout. Shopify geeft je 5 seconden. Als je order dan nog niet verwerkt is, krijg je een retry. En nog een. En nog een. Schaal. Op Black Friday komen orders sneller binnen dan een synchrone verwerking aankan. Een queue is van nature schaalbaar: voeg meer workers toe en de verwerking versnelt.
php
// Webhook-controller: snel een 200 geven en doorgaan
class ShopifyWebhookController extends Controller
{
    public function orderCreated(Request $request): JsonResponse
    {
        $this->validateHmac($request);

        // Event dispatchen naar de queue
        event(new OrderPlacedInShopify(
            shopifyOrder: $request->json()->all(),
            occurredAt: new \DateTimeImmutable(),
        ));

        return response()->json(['status' => 'accepted'], 200);
    }
}

Retry logic en idempotency

Externe API-calls falen. Netwerk timeouts, service hiaten, rate limits. Je retry-strategie bepaalt hoe je middleware hiermee omgaat.

Laravel's queue systeem heeft ingebouwde retry-ondersteuning. Combineer dat met exponential backoff:

php
class SendOrderToErp implements ShouldQueue
{
    public int $tries = 5;
    public int $maxExceptions = 3;

    public function backoff(): array
    {
        // Wacht 1, 5, 25, 125, 625 seconden tussen pogingen
        return [1, 5, 25, 125, 625];
    }

    public function failed(\Throwable $exception): void
    {
        // Notificeer het team bij definitieve mislukking
        Notification::route('slack', config('services.slack.webhook'))
            ->notify(new OrderSyncFailedNotification(
                $this->shopifyOrderId,
                $exception->getMessage()
            ));
    }
}

Idempotency is de andere kant van retry logic. Een job die meerdere keren wordt uitgevoerd, moet hetzelfde resultaat geven als een job die één keer wordt uitgevoerd.

php
class SendOrderToErp implements ShouldQueue
{
    public function handle(): void
    {
        // Controleer of de order al bestaat in het ERP
        $existing = OrderMapping::where('shopify_order_id', $this->shopifyOrderId)
            ->where('erp_system', config('integrations.erp.driver'))
            ->first();

        if ($existing) {
            Log::info("Order {$this->shopifyOrderId} al in ERP, skipping");
            return;
        }

        // Verwerk de order...
    }
}

Datamodel voor integratietracking

Een middleware zonder logging is een zwarte doos. Bouw een integratietracking-tabel die bijhoudt wat er is verstuurd, wanneer, met welk resultaat.

php
// Database migratie
Schema::create('integration_logs', function (Blueprint $table) {
    $table->id();
    $table->string('source_system');         // 'shopify'
    $table->string('target_system');         // 'afas'
    $table->string('entity_type');           // 'order'
    $table->string('source_id');             // Shopify order-ID
    $table->string('target_id')->nullable(); // AFAS order-ID
    $table->string('status');                // 'pending', 'success', 'failed'
    $table->json('payload')->nullable();     // Verzonden data
    $table->text('error_message')->nullable();
    $table->integer('attempt_count')->default(0);
    $table->timestamp('last_attempted_at')->nullable();
    $table->timestamps();

    $table->index(['source_system', 'entity_type', 'source_id']);
    $table->index(['status', 'created_at']);
});

Met deze tabel kun je in één query zien welke orders vastgelopen zijn, hoeveel pogingen er gedaan zijn en wat de foutmelding was. Dat is het verschil tussen debuggen in 5 minuten en debuggen in 2 uur.

Conclusie

Laravel als e-commerce middleware is geen compromis. Het is een bewuste architectuurkeuze die flexibiliteit, testbaarheid en controle geeft die een iPaaS niet biedt.

De patronen in dit artikel — Adapter Pattern, event-driven listeners, queue-based verwerking en idempotency — vormen samen een middleware-architectuur die betrouwbaar is bij schaal en onderhoudbaar blijft bij wijzigingen.


De patronen in dit artikel bouwen op Laravel's native Queue-systeem en Event-systeem. Het Adapter Pattern is een van de meest toepasbare design patterns in integratie-architectuur.

Bouw je een e-commerce middleware of wil je je bestaande integratiearchitectuur verbeteren? Bekijk onze Laravel-diensten of neem contact op. Gerelateerde artikelen:
Ruthger Idema

Geschreven door Ruthger Idema

15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.

Meer over ons team →
Deel dit artikel:

Wil je jouw e-commerce naar het volgende niveau?

Plan een vrijblijvend gesprek met onze experts over Magento, Shopify of Laravel maatwerk.

Plan een Tech Check