Skip to content

Architecture

The master diagrams of the Flyokai Marketplace suite, in one place.

Three plugins, one seam

                     ┌─────────────────────────────────────────┐
                     │  FlyokaiMarketplace  (Base, mandatory)    │
                     │  schema · cart · portal API · OAuth ·     │
                     │  admin Vue · mail · DAL extensions        │
                     │                                           │
                     │        MarketplaceBackend (interface)     │
                     │           ▲                  ▲            │
                     └───────────┼──────────────────┼───────────┘
                                 │                  │
              binds by default   │                  │  rebinds on install
                                 │                  │
                  LocalMarketplaceBackend     RemoteMarketplaceBackend
                  (in-process, marketplace-     (channel-dispatcher →
                   embedded)                      Flyokai cluster)
                                                  └── FlyokaiMarketplaceRemote
                                                      (optional add-on)

      ┌──────────────────────────────────────────────────────────────┐
      │  FlyokaiFujinShuttleMarketplace (optional) — seller-scoped     │
      │  CSV bulk import/export onto /flyok-portal/bulk/*              │
      └──────────────────────────────────────────────────────────────┘
  • Base owns everything Shopware-facing and defines the MarketplaceBackend interface. It works completely on its own.
  • Remote changes only how a request is dispatched — it rebinds the interface to a cluster client. It changes nothing else.
  • Bulk is orthogonal: it adds import/export endpoints and reuses Base's portal auth and ownership model.
Plugin Package Namespace Requires
FlyokaiMarketplace flyokai/sw-marketplace Flyokai\Marketplace
FlyokaiMarketplaceRemote flyokai/sw-marketplace-remote Flyokai\MarketplaceRemote Base
FlyokaiFujinShuttleMarketplace flyokai/sw-fujin-shuttle-marketplace Flyokai\FujinShuttleMarketplace Base + flyokai/sw-fujin-shuttle

The MarketplaceBackend seam

Every piece of marketplace business logic is expressed as a Request DTO that travels through one interface:

interface MarketplaceBackend
{
    public function sendRequest(Request $request, float $timeoutSeconds = 5.0): ?Response;
}

A controller builds a ServiceRequest\* (e.g. GetSellerMe, CreateSellerMyOffer, ListOffersForProduct, GenerateStatement, OrderPlaced), calls sendRequest, and renders the ServiceResponse. The controller does not know — or care — whether the handler ran in this PHP process or on a remote cluster.

Binding Provided by Dispatch
LocalMarketplaceBackend Base Calls marketplace-embedded's in-process dispatcher. Same PHP-FPM worker that's serving the request.
RemoteMarketplaceBackend Remote Serializes the Request over amp-channel-dispatcher to a Flyokai cluster worker.

The handler map, repositories and domain services behind both bindings come from the same framework packages (flyokai/marketplace-core, marketplace-service-message). Local and Remote run identical business logic; only the transport differs. This is why marketplace:ping and marketplace:seller:list work unchanged against either backend.

Base: everything in Shopware

Browser (seller-portal SPA + admin Vue + storefront)
   ▼  /flyok-portal/*  ·  /flyok-app/oauth/access_token  ·  /api/_action/marketplace/*
Shopware (FlyokaiMarketplace)
   ├─ Cart processor + storefront overrides   (buy-box, per-seller deliveries)
   ├─ /flyok-portal/* Symfony controllers      (seller REST, JWT-guarded)
   ├─ /api/_action/marketplace/* controllers   (operator REST, admin-token-guarded)
   └─ MarketplaceBackend → LocalMarketplaceBackend (in-process)
                          marketplace-embedded dispatcher
                          → marketplace-core repositories
                          → Shopware MySQL (flyokai_* tables)

Marketplace work runs synchronously inside the PHP-FPM worker handling the Shopware request. No fibers, no AMPHP, no cluster. Simple to deploy; the cost is that expensive calls (filtered offer lists, statement generation) tie up a PHP-FPM worker and contend with shop traffic for Shopware's MySQL. That ceiling is what Remote lifts.

Remote: offload to a cluster

Install FlyokaiMarketplaceRemote and its build() registers a CompilerPass that rebinds MarketplaceBackendRemoteMarketplaceBackend. The install itself is the switch — there is no env flag to flip.

Shopware (Base owns these, unchanged)        Flyokai cluster
   ├─ /flyok-portal/* → Symfony                ├─ channel listener  :1439
   ├─ OAuth → Symfony                          ├─ HTTP listener     :1339
   ├─ admin Vue, cart, mail                    └─ workers run the same handler map
   └─ MarketplaceBackend → RemoteBackend ──────►  (marketplace-core)
                              channel-dispatcher

Where the data lives then becomes a second, independent decision — dataLocation = shopware | cluster — covered in Going remote and Data migration. The identity plane never moves, so JWTs issued by Base stay valid regardless.

Two data planes

The plugin keeps two clearly separated groups of tables. This separation is what lets Remote move the data plane without touching auth.

Plane Tables Owner Moves with dataLocation?
Identity flyokai_user, OAuth client/token tables, mail outbox Always Shopware-side (Base) Never
Data flyokai_seller*, flyokai_seller_offer*, flyokai_purchase_order*, flyokai_statement*, flyokai_payout, flyokai_shipping_*, flyokai_chain/flyokai_mail* Base on install Yes (Remote only)

See the Domain model for the full table catalogue.

Schema management

Marketplace tables are declarative. They're defined as Flyokai Solid DTOs tagged with #[Table] (in flyokai/marketplace-core) and reconciled into Shopware's MySQL by the Base plugin:

marketplace-core Solid DTOs  (#[Table] on SellerSolid, SellerOfferSolid, …)
   ▼  on plugin install / update / marketplace:local:schema:apply
SchemaApplyService over a SyncConnectionPool
   ▼  diff declared vs. existing, emit CREATE / ALTER
Shopware MySQL  (flyokai_* tables)

23 DTOs own the schema. The apply is idempotent — safe to re-run anytime via php bin/console marketplace:local:schema:apply. DAL extensions on Shopware's own tables (product, order_line_item, order_delivery) are added by ordinary Shopware migrations.

DAL extensions on Shopware tables

Shopware table Added Purpose
product flyokaiCommissionPercent field Per-product commission override (falls back to the global default).
product flyokai_product_owner_id The owning seller, or NULL for operator-owned products.
order_line_item 1:1 extension entity Snapshots seller_id, offer_id, commission and unit price onto each purchased line.
order_delivery 1:1 extension entity Per-delivery seller and shipping-service binding.

The line-item snapshot is what makes statements reproducible: later edits to a product or offer never change an order that already happened.

Request lifecycle (one screen)

Buyer adds to cart
   ▼  BeforeLineItemAddedEvent → OfferAttacher resolves the winning offer,
      stamps seller_id / offer_id / commission / unit_price on the line
   ▼  Cart calculation → FlyokaiCartProcessor builds per-seller deliveries
      and the shipping matrix (seller × service tier)
   ▼  CheckoutOrderPlacedEvent → OrderPlaced request materialises one
      purchase order per seller (flyokai_purchase_order[_line])
   ▼  Delivery state → shipped → PO status advances
   ▼  Period close → marketplace:statement:generate → statements per seller
   ▼  marketplace:payout:execute → payout per closed statement

Each arrow is detailed in Cart & checkout and Statements & payouts.