Shops & tenancy
A Shop is the concrete tenant every owned record belongs to through a plain, nullable shop_id foreign key. A single-shop app can ignore it entirely; a multi-shop app creates shops and scopes data to them:
use RoundlyConsulting\Shops\Facades\Shops;
use RoundlyConsulting\Shops\Products\Product;
use RoundlyConsulting\Shops\Shops\Shop;
$shop = Shop::create(['name' => 'Acme EU', 'currency' => 'EUR']);
$shop->currency(); // Currency (EUR) — the shop's own, or the configured default when null
// Explicit ownership always wins:
$product = Product::create(['shop_id' => $shop->id, 'name' => 'Sparkling Water']);
// Or bind a current shop and let shop_id auto-fill on create:
Shops::current()->set($shop); // a Shop or its id
Product::create(['name' => 'Still Water']); // shop_id auto-filled
Shops::current()->id(); // ?int
Shops::current()->forget();
// Scoped block — restores the previous binding afterwards (even on exception):
Shops::current()->run($shop, function () {
Product::create(['name' => 'Tonic']); // belongs to $shop
});
Shops::current()->get(); // ?Shop bound to the current context
Shop::current(); // the same, typed to the package's Shop model
Product::query()->forShop($shop)->get(); // scope by model
Product::query()->forShop($shop->id)->get(); // or by idThe Shop model
Shop has a translatable name and slug, a nullable ISO currency and soft deletes. Its slug is unique across all shops and is the route key:
$shop->setTranslation('name', 'sk', 'Acme EU (SK)')->save();
$shop->products(); // HasMany<Product> — also what ->scopeBindings() walks
$shop->categories(); // HasMany<Category> — ditto
$shop->taxRates(); // HasMany<TaxRate>
$shop->currency(); // Currency — the shop's, else shops.pricing.default_currency
$shop->currentSlug(); // sluggable readers: slugMap(), slugFor($locale)
Shop::resolveModelClass(); // the configured shops.shop_model class
Shop::current(); // ?Shop — the CurrentShop binding, only when it is a ShopCurrentShop
Shops::current() returns CurrentShop, which holds the active tenant. It is bound per request and per queued job (a scoped binding): Octane drops it between requests and the queue worker before each job, so one tenant never leaks into the next. Nothing sets it for you — set it in your own middleware, at the top of a job or in a scoped block. Without the facade, call ->current() on an injected ShopsManager or resolve CurrentShop itself:
| Method | Description |
|---|---|
set(Model|int|null $shop): void | Bind a model or a key. |
get(): ?Model | The bound model; a bound key is loaded once. |
id(): ?int | The bound key. |
forget(): void | Clear the binding. |
run(Model|int $shop, Closure $callback): mixed | Bind for the callback, restore the previous binding afterwards (also on exceptions), return the callback’s value. |
use Closure;
use Illuminate\Http\Request;
use RoundlyConsulting\Shops\Facades\Shops;
final class SetCurrentShop
{
public function handle(Request $request, Closure $next)
{
Shops::current()->set($request->route('shop')); // a model or a key
return $next($request);
}
}
// In a queued job or console command, scope the work instead:
$product = Shops::current()->run($shop, fn () => Product::create(['name' => 'Chair']));BelongsToShop
- Used by Product, Category, Cart, Order and order Item. TaxRate has its own shop() relation.
- On creating, a null shop_id is filled from CurrentShop. An explicit shop_id always wins; with nothing bound, nothing is filled.
- shop() — a BelongsTo to the configured tenant model; forShop($shopOrId) — a where on shop_id.
- Variants, options, stock adjustments and cart items hang off their parent and carry no shop_id.
Your own tenant model
shops.shop_model accepts any Eloquent model — owned records only need its primary key. Features that need the package’s Shop (Shop::current(), per-shop currency, per-shop database tax rates, the cart and order price shop context) engage only when the tenant is a Shop or a subclass; otherwise the config default currency and config tax classes apply. Extending Shop keeps them all:
use RoundlyConsulting\Shops\Shops\Shop;
// Extend the packaged model to keep currency, tax rates, translations,
// slugs and Shop::current():
class Store extends Shop
{
// your own relations and helpers
}
// config/shops.php
'shop_model' => App\Models\Store::class,The published migrations constrain every shop_id to the shops table. A tenant model backed by another table needs those foreign keys adjusted in your published migrations.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.