revolution/self-ordering
A Laravel package that adds self-ordering behavior to Eloquent models. Use its trait to keep records in a custom sequence and move items up or down, making it simple to manage ordered lists like menus, categories, or FAQs without manual position bookkeeping.
revolution/self-ordering is a Laravel application-style package (not a thin library) that gives you a QR-code-driven, table-side ordering flow: a guest scans a code, browses a menu, builds a cart, and submits an order that staff can fulfil. Treat it as a reference implementation you fork and adapt, not as a drop-in SaaS component.
Minimal setup:
composer require revolution/self-ordering
php artisan vendor:publish --provider="Revolution\SelfOrdering\SelfOrderingServiceProvider"
php artisan migrate --seed
npm install && npm run build
Then, in order:
composer.json of the package — as of 3.0.0 this is a Livewire 3 package, so it now requires PHP 8.1+ and Laravel 10/11, and explicitly drops PHP 8.0 and Laravel 9. Confirm the constraints against your app before installing; if you are still on Laravel 9, 2.x is your ceiling.routes/ — this is the map of the whole system. You will find the guest ordering routes, the cart endpoints, and the staff/admin routes. Read these before touching anything else.src/Models/ — the domain is small: something like Menu, MenuCategory, Order, OrderItem, Table/Seat, and a cart abstraction. Understand the relationships before you seed real data.config/self-ordering.php (after publishing) — currency, tax rate, route prefixes, middleware, and any QR/base-URL settings live here.src/Livewire/ and resources/views/livewire/ — the 3.0 UI is Livewire 3 components rendered through Blade, with Alpine and Tailwind on top. There is no Inertia/Vue layer to wire up any more, but you still need a JS build step for Tailwind/Alpine and Livewire's own assets must be reachable.First use case to wire up: a single-store, single-language lunch menu. Seed categories and items, generate a QR code pointing at the guest route with a table token, scan it on a phone on the same network, place an order, and confirm it appears in the staff view. That end-to-end loop exercises migrations, config, routing, the Livewire cart component, and the order lifecycle in one pass — do that before customising a single view.
Table-bound QR sessions. The core pattern is that the QR URL carries a table (and often a session) identifier. Keep that token opaque and validated server-side; do not let the client post an arbitrary table_id with the order. If the package does not already scope the cart to the token, add a middleware that resolves the token to a table and stashes it on the request/session — Livewire's mount() is a good place to resolve and re-validate it per component load.
Cart in session, order in the database. Expect the cart to be session-backed and only persisted as an Order + OrderItem rows on submit. In the Livewire world the cart usually lives as a public property on the cart/order component and is rehydrated from the session between requests. This is good for guests (no auth) but means carts die on session expiry — acceptable for a restaurant, painful for a slow corporate cafeteria. If you need resumable carts, add a cart table keyed by the QR token.
Order status as an explicit state machine. Rather than a free-text status column, wrap transitions in methods (markAsAccepted(), markAsServed()) and dispatch domain events (OrderPlaced, OrderServed). This is the cleanest extension point: listeners for kitchen printing, Slack notifications, or POS sync attach here without touching package code.
Kitchen/staff display via Livewire polling first, broadcasting second. Livewire 3 makes wire:poll trivial (wire:poll.5s on the staff order list). Start there before reaching for Reverb/Pusher. Self-ordering traffic is bursty and low-volume; polling is operationally simpler and avoids a websocket dependency in a restaurant with flaky Wi-Fi. Livewire also gives you a clean upgrade path to wire:poll → Echo → Reverb without a rewrite.
Admin CRUD by extension, not by editing vendor code. Publish the views and models you need, then subclass or decorate. With Livewire, extend the published component class (or override the view via $view) rather than editing the vendor component in place; a subclass registered through config or a route override will survive composer update, an edited Blade file will not.
Multi-store / multi-tenant. Out of the box this is almost certainly single-tenant. The cheapest path to multi-store is adding a store_id to menus, tables, and orders, a global scope on the models, then resolving the store from the QR token or subdomain in middleware. Do this before you take a second customer.
Payments and POS integration. Insert a payment step between "order submitted" and "order accepted" — most real deployments want pay-first for kiosk-style ordering and pay-later for dine-in. Model this as a payment_status orthogonal to the fulfilment status so the same order can be paid and cancelled independently.
Localisation and money. Store amounts as integers in the smallest currency unit and format at the edge. If the package assumes JPY and a 10% consumption tax, generalise tax as a configurable rate plus an optional inclusive/exclusive flag; retrofitting this after you have historical orders is miserable.
It is a demo app wearing a package costume. Still a low-star, low-adoption package with a tiny dependent count — there is no community to lean on. 3.0.0 shows someone is still maintaining it, which is reassuring, but assume you will be reading source, not docs, and that your fork is the long-term maintenance plan. Check the license (MIT — fine) and pin the version in composer.json.
3.0 is a front-end rewrite, not a bump. The jump to Livewire 3 means the UI layer changed wholesale. If you were running 2.x (the Inertia + Vue version), treat 3.0 as a different package for upgrade purposes: your customised pages, JS entrypoints, and Inertia resolvers do not carry over — only the models, migrations, and routes are likely to translate. Plan a fresh publish into a branch and diff, not an in-place composer update.
Version constraints are the first blocker. Run composer why-not revolution/self-ordering against your app. 3.0.0 requires PHP 8.1+ and Laravel 10+; if your app is on PHP 8.0 or Laravel 9, stay on 2.x or fork and widen the constraint, then run the test suite (if any) to catch framework API drift.
Livewire version collision. Livewire 3 support means the package almost certainly requires livewire/livewire:^3.0. If your host app still runs Livewire 2 (Filament 2, an older Nova package, an in-house admin), Composer will refuse to install or you will get class/asset mismatches at runtime. Audit your Livewire constraint before you start; upgrading the whole app to Livewire 2→3 is a separate project.
No built assets. Application-style packages frequently gitignore public/build. If npm run build fails or the pages render unstyled and non-reactive, check resources/js for the Alpine/Tailwind entrypoint and confirm Livewire's JS is on the page (@livewireScripts in your layout, or the published Livewire config if you load it manually). Livewire's own assets are served from the package's public path — if you deploy with a read-only filesystem or a CDN, publish them (php artisan livewire:publish --assets) or the components will silently fail to hydrate.
Migrations and seeders are destructive. migrate --seed on an existing database can wipe menus and orders. Run seeders only on a fresh database, and check whether the package's migrations are guarded with Schema::hasTable — if not, publishing them into a mature app risks table-name collisions (orders, tables, menus are common names). Prefix or rename before migrating.
QR codes and hostnames. The most common "it doesn't work" report is a QR pointing at localhost or 127.0.0.1. Drive the QR base URL from an env var (APP_URL or a dedicated SELF_ORDERING_BASE_URL) and generate codes at request time, not at seed time, so staging and production differ cleanly.
Session and CSRF across devices. Guests scan, order, and sometimes re-scan on a second device. If the cart is session-bound, SESSION_DRIVER=file behind multiple app servers will lose carts. Use Redis or a database session driver in any multi-node deployment. Livewire's AJAX round-trips make a stale CSRF token surface as a confusing "component not found" or 419 mid-cart — check the session config before you blame the package.
Route and middleware collisions. The package will register guest routes, possibly with a web group and its own prefix. If your app already owns /order or /menu, publish the config and change the prefix rather than fighting route precedence. Livewire's own update endpoint (/livewire/update) also needs to be reachable and must not be caught by a catch-all route or an over-zealous auth middleware. Similarly, check which auth guard the staff routes use — it may assume the default web guard.
Debugging order flow. Log every status transition with the order ID and the acting user/device. When a waiter says "the order never arrived," you want a timeline, not a guess. php artisan telescope locally plus a structured log channel in production covers this. For Livewire-specific failures, APP_DEBUG=true plus the browser network tab on the /livewire/update payload will usually show the validation or hydration error the UI swallowed.
Extension points to prefer, in order: config file → published Blade views → Livewire component subclassing → event listeners → forking. Reach for a fork only for the ordering core itself. Anything you edit in vendor/ is a bug you have scheduled for six months from now.
Front-end upgrade risk. Livewire 3 and Alpine 3 move quickly; a pinned @livewireScripts/Alpine version that lags your Tailwind or Vite version will produce subtle reactivity bugs (listeners not firing, wire:model not updating). Update the JS toolchain as a single, contained task before you build features, and commit the lockfile.
How can I help you explore Laravel packages today?