roundly-consulting/shops-for-laravel
Shops for Laravel adds lightweight shop and storefront support to Laravel apps, with simple configuration and an Eloquent-friendly API for managing shops and related data.
This release (1.1.0) is about model resolution: the package now gives you a single, canonical way to ask "which Shop class is actually in play?" instead of assuming it's the packaged Shop.
Minimal steps:
Shop is enough, you can stop: ShopModel::shop() returns Shop and the packaged ShopFactory builds Shop.App\Models\Shop extends Vendor\Package\Models\Shop, point the config at it, and run your migrations.ShopModel::shop() anywhere you'd otherwise hardcode Shop::class — relations, seeders, policies, form requests, test factories.First use case — a fresh install with a tenant-specific relation:
use Vendor\Package\Support\ShopModel;
Schema::create('products', function (Blueprint $table) {
$table->foreignIdFor(ShopModel::shop())->constrained();
});
The guarantee you get from 1.1.0: ShopModel::shop() returns your subclass only if it extends Shop. If the configured class is unrelated, missing, or misconfigured, you silently get Shop back rather than a fatal error. That's a deliberate fallback, not a validation failure — treat it as such.
Swap the model, then resolve it — never both.
// config/shop.php (verify the exact key in your published config)
'model' => App\Models\Shop::class,
// App\Models\Shop
class Shop extends Vendor\Package\Models\Shop
{
public function owner(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Resolve at the call site, not at class-load time. Model resolution belongs inside methods and closures so it survives config changes and test overrides:
// Good
public function shops()
{
return $this->hasMany(ShopModel::shop());
}
// Bad — evaluated once, frozen forever
protected $shopClass = ShopModel::shop();
Seeders and factories. The packaged ShopFactory now builds whatever the config points at, so Shop::factory() and ShopModel::shop()::factory() produce the same class. Prefer the latter in package-agnostic code:
ShopModel::shop()::factory()->count(3)->create();
If your subclass has required columns, define states on your own factory and extend the packaged one:
class ShopFactory extends Vendor\Package\Database\Factories\ShopFactory
{
public function definition(): array
{
return [...parent::definition(), 'plan' => 'basic'];
}
}
Remember to wire it up via newFactory() on your model, or the packaged factory will still be used.
Testing the swap. Because the fallback is silent, add a test that asserts resolution rather than assuming it:
public function test_configured_shop_model_is_used(): void
{
config()->set('shop.model', App\Models\Shop::class);
$this->assertSame(App\Models\Shop::class, ShopModel::shop());
}
Integration tip — policies and validation. Anything keyed by model class name (Gate::policy, Rule::exists, morphMap) must use the resolved class too, otherwise you get rules validated against the base table while your subclass writes elsewhere.
Shop, or a swapped-in model that fails to autoload all degrade to Shop without complaint. The only reliable signal is the helper's return value — assert it in a test and log it once at boot in non-production.ShopModel::shop() checks is_subclass_of(..., Shop::class). A model that re-implements the behaviour from scratch but extends Model directly gets ignored. Also note the check is against Shop itself, not the packaged base class via an interface — subclass chains are fine, sibling implementations are not.hasMany(ShopModel::shop()) is safe — but only if the method is called after config is loaded. Config cached via config:cache is still fine; a config()->set() in a service provider that runs after the model is first resolved is not.Shop::class in seeders, remove those workarounds — you'll now get double-application or class mismatches.Shop::class and Shop::factory(), replace with ShopModel::shop(), then run your suite. There are no breaking changes here, but any code that relied on the factory always returning the packaged model will behave differently now.ShopModel::shop() and get_class() on a fetched instance side by side. Mismatches almost always trace back to the config key or a missing extends.ShopModel::shop() is the seam the package has committed to. Route your own resolution through it rather than reading config directly — that way the package can change how the class is determined without breaking you, and you only ever have one source of truth.How can I help you explore Laravel packages today?