Configuration
The package works with zero host configuration. The published config/shops.php in full (comments trimmed):
use RoundlyConsulting\Coupons\Models\Coupon;
use RoundlyConsulting\Shops\Discounts\CouponPackageDiscountResolver;
use RoundlyConsulting\Shops\Orders\NumberGenerators\DefaultNumberGenerator;
use RoundlyConsulting\Shops\Payments\NullPaymentGateway;
use RoundlyConsulting\Shops\Reviews\NullVerifiedPurchaseResolver;
use RoundlyConsulting\Shops\Shipping\FreeShippingMethod;
use RoundlyConsulting\Shops\Shops\Shop;
use RoundlyConsulting\Shops\Support\Tax\DatabaseTaxResolver;
return [
'shop_model' => env('SHOPS_SHOP_MODEL', Shop::class),
'key_type' => env('SHOPS_KEY_TYPE', 'bigint'),
'pricing' => [
'price_type' => env('SHOPS_PRICE_TYPE', 'gross'),
'default_currency' => env('SHOPS_DEFAULT_CURRENCY', 'EUR'),
],
'tax_classes' => [
'standard' => env('SHOPS_TAX_RATE', 20),
'reduced' => 10,
'zero' => 0,
],
'tax' => [
'resolver' => DatabaseTaxResolver::class,
],
'inventory' => [
'low_stock_threshold' => env('SHOPS_LOW_STOCK_THRESHOLD', 0),
],
'media' => [
'featured_bucket' => env('SHOPS_MEDIA_FEATURED_BUCKET', 'featured'),
'gallery_bucket' => env('SHOPS_MEDIA_GALLERY_BUCKET', 'gallery'),
'variant_bucket' => env('SHOPS_MEDIA_VARIANT_BUCKET', 'gallery'),
'banner_bucket' => env('SHOPS_MEDIA_BANNER_BUCKET', 'banner'),
'disk' => env('SHOPS_MEDIA_DISK'),
'public' => env('SHOPS_MEDIA_PUBLIC', true),
'responsive_widths' => [320, 640, 1024, 1600],
'max_file_size' => null,
],
'reviews' => [
'verified_purchase_resolver' => env(
'SHOPS_VERIFIED_PURCHASE_RESOLVER',
NullVerifiedPurchaseResolver::class,
),
],
'attributes' => [
'definitions' => [
// 'material' => ['type' => 'string'],
// 'weight' => ['type' => 'integer', 'rules' => ['min:0']],
],
],
'locales' => [
'fallback' => env('SHOPS_FALLBACK_LOCALE', config('app.fallback_locale', 'en')),
],
'slugs' => [
'history' => env('SHOPS_SLUG_HISTORY', false),
],
'payment' => [
'gateway' => env('SHOPS_PAYMENT_GATEWAY', NullPaymentGateway::class),
'allow_store_credit' => env('SHOPS_ALLOW_STORE_CREDIT', false),
'store_credit_bucket' => env('SHOPS_STORE_CREDIT_BUCKET', 'store_credit'),
'refund_to_store_credit' => env('SHOPS_REFUND_TO_STORE_CREDIT', false),
],
'shipping' => [
'method' => env('SHOPS_SHIPPING_METHOD', FreeShippingMethod::class),
],
'discounts' => [
'coupon_model' => env('SHOPS_COUPON_MODEL', Coupon::class),
'resolver' => CouponPackageDiscountResolver::class,
],
'addresses' => [
'billing_same_as_shipping' => env('SHOPS_BILLING_SAME_AS_SHIPPING', true),
],
'orders' => [
'number_generator' => DefaultNumberGenerator::class,
],
];Every key
| Key | Default | Env | Purpose |
|---|---|---|---|
shop_model | Shops\Shop | SHOPS_SHOP_MODEL | The tenant model every shop_id points to — any Eloquent model; a class that is not one throws InvalidConfigurationException naming the key. Extend Shop to keep the tenant features. |
key_type | bigint | SHOPS_KEY_TYPE | Key type of the polymorphic customer, owner and reference columns: bigint, uuid or ulid. Anything else throws InvalidConfigurationException when the migrations run. Fixed once they have run. |
pricing.price_type | gross | SHOPS_PRICE_TYPE | gross (tax is extracted from the price) or net (tax is added on top); anything else throws InvalidConfigurationException listing both. Orders snapshot it. |
pricing.default_currency | EUR | SHOPS_DEFAULT_CURRENCY | ISO 4217 fallback for a shop without its own currency, an order without a shop and a shop-less product’s default variant. |
tax_classes | standard 20, reduced 10, zero 0 | SHOPS_TAX_RATE | Whole-percent fallback floor used when a shop has no matching database rate. Each rate is 0–100, an int or a string such as "21"; "twenty" or "19.5" throws instead of becoming 0 %. A shipped class whose rate is not set — left out, null or blank (SHOPS_TAX_RATE=) — takes its shipped rate (standard 20 %, reduced 10 %, zero 0 %), never 0 %; one of your own classes that is not set uses the standard rate, and an empty map means the shipped rates. Set 0 explicitly for no tax. The env sets standard only. |
tax.resolver | DatabaseTaxResolver | — | Resolves the rate for a (shop, class, country) lookup. Bind ConfigTaxResolver for config-only tax. |
inventory.low_stock_threshold | 0 | SHOPS_LOW_STOCK_THRESHOLD | StockRanLow fires once when an adjustment takes a tracked variant’s available stock from above this level to at or below it. At least 0; a non-integer value throws. |
media.featured_bucket | featured | SHOPS_MEDIA_FEATURED_BUCKET | Product single-image bucket. |
media.gallery_bucket | gallery | SHOPS_MEDIA_GALLERY_BUCKET | Product multi-image bucket. |
media.variant_bucket | gallery | SHOPS_MEDIA_VARIANT_BUCKET | Per-variant image bucket. |
media.banner_bucket | banner | SHOPS_MEDIA_BANNER_BUCKET | Category banner bucket (single image). |
media.disk | null | SHOPS_MEDIA_DISK | Disk for every catalog bucket; not set (null or blank) uses media-library’s default. |
media.public | true | SHOPS_MEDIA_PUBLIC | Public (CDN, SEO) or private visibility for every catalog bucket. |
media.responsive_widths | [320, 640, 1024, 1600] | — | Responsive width ladder; each width is generated as the variant responsive-<width>. Every width must be a positive integer — a bad or blank entry throws rather than being dropped. |
media.max_file_size | null | — | Optional per-image cap in bytes, applied to every catalog bucket — product, variant and category. Not set (null or blank) for none; otherwise at least 1. |
reviews.verified_purchase_resolver | NullVerifiedPurchaseResolver | SHOPS_VERIFIED_PURCHASE_RESOLVER | Decides whether Product::review() pre-flags a review as a verified purchase. |
attributes.definitions | [] | — | Product spec-sheet definitions keyed by name, registered at boot. |
locales.fallback | app.fallback_locale | SHOPS_FALLBACK_LOCALE | Fallback locale for translations and slugs; the default-variant SKU derives from it. |
slugs.history | false | SHOPS_SLUG_HISTORY | Keep retired slugs and answer old URLs with a 301 to the current one. |
payment.gateway | NullPaymentGateway | SHOPS_PAYMENT_GATEWAY | Charges orders. Refunds are host-driven. |
payment.allow_store_credit | false | SHOPS_ALLOW_STORE_CREDIT | Shops::order($order)->charge() applies the buyer’s store credit before the gateway. |
payment.store_credit_bucket | store_credit | SHOPS_STORE_CREDIT_BUCKET | The credits bucket used; it must be denominated in the order currency. |
payment.refund_to_store_credit | false | SHOPS_REFUND_TO_STORE_CREDIT | On OrderRefunded, grant the order’s final price back as store credit. |
shipping.method | FreeShippingMethod | SHOPS_SHIPPING_METHOD | Your ShippingMethod; checkout quotes the shipping address through it. |
discounts.coupon_model | Coupon (coupons) | SHOPS_COUPON_MODEL | Model behind $order->coupon: the coupons Coupon or your subclass of it. Anything else throws InvalidConfigurationException naming the key. |
discounts.resolver | CouponPackageDiscountResolver | — | Turns a code and a goods subtotal into a DiscountResult. |
addresses.billing_same_as_shipping | true | SHOPS_BILLING_SAME_AS_SHIPPING | fromAddressBook() reuses the primary shipping address as billing when there is no billing address. |
orders.number_generator | DefaultNumberGenerator | — | Generates the order number; must implement NumberGenerator. |
Environment
The common knobs are env-driven, so you rarely need to publish the config at all:
SHOPS_PRICE_TYPE=gross
SHOPS_DEFAULT_CURRENCY=EUR
SHOPS_TAX_RATE=20
SHOPS_LOW_STOCK_THRESHOLD=5
SHOPS_MEDIA_DISK=s3
SHOPS_SLUG_HISTORY=true
SHOPS_ALLOW_STORE_CREDIT=true
SHOPS_PAYMENT_GATEWAY='App\Payments\AcmePayGateway'The bool switches accept true/false, 1/0, on/off and yes/no, so any env spelling works. Anything else throws an InvalidConfigurationException naming the key, so a typo never quietly becomes the default.
Strict values
The other settings are just as strict. A key that is not set — left out, null or blank ('' or whitespace, such as a SHOPS_DEFAULT_CURRENCY= line) — takes its default; a value of the wrong shape throws an InvalidConfigurationException naming the key — nothing is cast to 0 or swapped for the default:
- Integers take an int or a whole-number string such as "5" — never "five" or "5.5". Tax-class rates must be 0–100, the low-stock threshold at least 0, every responsive width and max_file_size at least 1.
- pricing.price_type must be gross or net; key_type must be bigint, uuid or ulid.
- Names — the currency, bucket names, the media disk and the fallback locale — must be strings; a blank one is not set and takes its default, so a blank SHOPS_MEDIA_DISK= means the media package’s disk.
- tax_classes, media.responsive_widths and attributes.definitions must be arrays, and each attribute definition an array of its own. Each responsive width must be a real width — a blank entry inside the list throws.
- A tax class whose rate is not set takes its shipped rate, so a blank SHOPS_TAX_RATE= still charges the shipped 20 % on the standard class — set SHOPS_TAX_RATE=0 to charge none.
- shop_model must be an Eloquent model; discounts.coupon_model must be the coupons Coupon or a subclass of it.
php artisan about shows a broken setting as INVALID instead of failing, so you can spot it at a glance.
Notes
- tax_classes is in whole percents (20 = 20 %) for authoring convenience; database tax rates are basis points (1900 = 19.00 %).
- A shop may override the default currency, and every order snapshots its currency on insert — changing pricing.default_currency never re-denominates placed orders.
- Store credit requires credits-for-laravel’s currencies map to denominate payment.store_credit_bucket.
- Driver keys accept any class implementing the contract — or bind the contract yourself in a service provider.
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.