Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Master Product Laravel Package

vanilo/master-product

Vanilo Master Product adds master/variant product support to Laravel e-commerce catalogs, letting you group product variants under a single master product with shared attributes, pricing, and media.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal steps

composer require vanilo/master-product
  1. Register the module with Concord in config/concord.php, after the modules it depends on (vanilo/product, vanilo/channel, and the AppShell if you use the admin):
// config/concord.php
'modules' => [
    // ... AppShell, Channel, Product modules first
    Vanilo\MasterProduct\Providers\ModuleServiceProvider::class,
],
  1. Run migrations. The module ships its own migration; there is no separate vendor:publish step for the schema, only for config/views if you need to change them:
php artisan migrate
php artisan concord:modules   # confirm it is registered and enabled
  1. Where to look first, in this order:

    • src/Providers/ModuleServiceProvider.php — the single source of truth for what the module registers ($models, $controllers, $bindings, menu/quick-create entries). Everything else follows from here.
    • src/Contracts/ — the interfaces you should code against (MasterProduct, and any variant-aware contract).
    • src/Models/ — the Eloquent implementation plus its *Proxy class.
    • database/migrations/ — read these before reading the docs; for a module this small, the schema is the spec.
  2. First use case: create one master product with two variants, then load it back through the proxy to confirm relationships and inheritance work in your app:

use Vanilo\MasterProduct\Contracts\MasterProduct;
use Vanilo\MasterProduct\Proxy\MasterProductProxy;

$master = MasterProductProxy::modelClass()::create([
    'name' => 'T-Shirt',
    'slug' => 't-shirt',
    // ...
]);

// variants are ordinary products pointing at the master
$master->variants()->create(['name' => 'T-Shirt / S', 'slug' => 't-shirt-s', /* ... */]);

$product = MasterProductProxy::modelClass()::findBySlug('t-shirt');
dump($product->variants, $product->priceRange());

If that round-trip works, the module is wired correctly and you can start building on it.

Implementation Patterns

Always resolve through the contract or proxy, never new MasterProduct. The whole Vanilo stack is built on Concord's swap-out mechanism; hardcoding the concrete class breaks the moment you or a future package version replaces the model.

$class = MasterProductProxy::modelClass();
$master = $class::findBySlug($slug);

Create master + variants atomically. A master product with zero variants is a valid but useless record, and partial writes are the most common source of "why is this product missing from the catalog" bugs:

DB::transaction(function () use ($data) {
    $master = MasterProductProxy::modelClass()::create($data['master']);

    foreach ($data['variants'] as $variant) {
        $master->variants()->create($variant);
    }

    return $master;
});

Put variants in the cart, masters on the listing page. The master product is a merchandising/aggregation concept. Anything that touches price, stock, SKU, or an order line must reference the variant product, otherwise you end up with order items that can never be fulfilled or that ignore per-size stock.

Aggregate, don't persist. Master-level price and stock are derived from variants. Compute them at read time (min/max price, sum of stock) or cache them with an explicit invalidation hook on variant save, rather than storing a denormalized value you have to keep in sync across every write path.

Extend through Concord, not by editing vendor code:

// AppServiceProvider::boot()
concord()->registerModel(
    Vanilo\MasterProduct\Contracts\MasterProduct::class,
    App\Models\MasterProduct::class
);

Your subclass then needs to be discoverable by the module's own relations, which is why you should also check the module's ModuleServiceProvider::$models array and mirror the mapping in a small app-level module if you go beyond a simple swap.

Admin integration (if vanilo/admin is installed): the module registers its menu and quick-create entries in the service provider against Konekt\AppShell\Shell/AppShell. If you need a custom field on the master product form, the extension point is the AppShell form/table registration, not a Blade override. Guard any admin-specific code in your own provider with class_exists() on the AppShell class so the module stays usable in headless/API-only installs.

Testing: use the module's factories (MasterProduct::factory()) in your own feature tests, and add at least a smoke test asserting variants() returns the expected count after a factory create — this catches migration-ordering regressions early.

Gotchas and Tips

  • Migration ordering is the #1 failure mode. The master product table has a foreign key into products; if you register the module before vanilo/product in config/concord.php, php artisan migrate fails on a fresh database. Order the modules array exactly as the module's composer.json requires.
  • concord:modules reports the truth, not your config file. If the module is listed but shows as disabled, a later entry in the array is overriding it — Concord keys merges by class name.
  • Model swap + opcache. After concord()->registerModel(...), clear config:cache and opcache; Concord resolves proxies lazily and stale container bindings are a classic "my subclass is ignored" red herring.
  • Deleting a master. Check the migration's onDelete behavior before writing a delete path. A restrict FK means $master->delete() throws once variants exist; a cascade means you silently delete order-referenced products. Explicitly detach or soft-delete variants and handle the decision in your service layer.
  • Slug collisions. Masters and variants live in the same products table space in Vanilo, so the slug unique index applies to both. Generate t-shirt-s style variant slugs, not bare s.
  • Variants of variants. The data model does not forbid it; your domain should. Add a guard/validation rule that a product used as a master cannot itself have a master_product_id.
  • N+1 on catalog pages. MasterProduct::with('variants') is effectively mandatory on any listing; the aggregate price accessor will otherwise fire one query per card.
  • Low adoption (2 stars, 0 dependents) is a real risk. Pin the exact version in composer.json, cover the behaviours you depend on with your own feature tests, and keep your integration behind the Contracts namespace so that forking the module later is a one-line swap. Read database/migrations/ and the service provider before trusting any blog post — for a module this size, the source is genuinely the documentation.
  • Debugging tip: when a master product loads with an empty variants collection, dump the raw query log (DB::listen) and compare against select * from products where master_product_id = ?. Nine times out of ten it's a variant saved with the wrong type value or a channel/scope restriction filtering it out.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource