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
MarketplaceBackendinterface. 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 MarketplaceBackend → RemoteMarketplaceBackend. 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.