NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Shops for Laravel

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

KeyDefaultEnvPurpose
shop_modelShops\ShopSHOPS_SHOP_MODELThe 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_typebigintSHOPS_KEY_TYPEKey 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_typegrossSHOPS_PRICE_TYPEgross (tax is extracted from the price) or net (tax is added on top); anything else throws InvalidConfigurationException listing both. Orders snapshot it.
pricing.default_currencyEURSHOPS_DEFAULT_CURRENCYISO 4217 fallback for a shop without its own currency, an order without a shop and a shop-less product’s default variant.
tax_classesstandard 20, reduced 10, zero 0SHOPS_TAX_RATEWhole-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.resolverDatabaseTaxResolver—Resolves the rate for a (shop, class, country) lookup. Bind ConfigTaxResolver for config-only tax.
inventory.low_stock_threshold0SHOPS_LOW_STOCK_THRESHOLDStockRanLow 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_bucketfeaturedSHOPS_MEDIA_FEATURED_BUCKETProduct single-image bucket.
media.gallery_bucketgallerySHOPS_MEDIA_GALLERY_BUCKETProduct multi-image bucket.
media.variant_bucketgallerySHOPS_MEDIA_VARIANT_BUCKETPer-variant image bucket.
media.banner_bucketbannerSHOPS_MEDIA_BANNER_BUCKETCategory banner bucket (single image).
media.disknullSHOPS_MEDIA_DISKDisk for every catalog bucket; not set (null or blank) uses media-library’s default.
media.publictrueSHOPS_MEDIA_PUBLICPublic (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_sizenull—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_resolverNullVerifiedPurchaseResolverSHOPS_VERIFIED_PURCHASE_RESOLVERDecides whether Product::review() pre-flags a review as a verified purchase.
attributes.definitions[]—Product spec-sheet definitions keyed by name, registered at boot.
locales.fallbackapp.fallback_localeSHOPS_FALLBACK_LOCALEFallback locale for translations and slugs; the default-variant SKU derives from it.
slugs.historyfalseSHOPS_SLUG_HISTORYKeep retired slugs and answer old URLs with a 301 to the current one.
payment.gatewayNullPaymentGatewaySHOPS_PAYMENT_GATEWAYCharges orders. Refunds are host-driven.
payment.allow_store_creditfalseSHOPS_ALLOW_STORE_CREDITShops::order($order)->charge() applies the buyer’s store credit before the gateway.
payment.store_credit_bucketstore_creditSHOPS_STORE_CREDIT_BUCKETThe credits bucket used; it must be denominated in the order currency.
payment.refund_to_store_creditfalseSHOPS_REFUND_TO_STORE_CREDITOn OrderRefunded, grant the order’s final price back as store credit.
shipping.methodFreeShippingMethodSHOPS_SHIPPING_METHODYour ShippingMethod; checkout quotes the shipping address through it.
discounts.coupon_modelCoupon (coupons)SHOPS_COUPON_MODELModel behind $order->coupon: the coupons Coupon or your subclass of it. Anything else throws InvalidConfigurationException naming the key.
discounts.resolverCouponPackageDiscountResolver—Turns a code and a goods subtotal into a DiscountResult.
addresses.billing_same_as_shippingtrueSHOPS_BILLING_SAME_AS_SHIPPINGfromAddressBook() reuses the primary shipping address as billing when there is no billing address.
orders.number_generatorDefaultNumberGenerator—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 crypto

By 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.