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

Properties Laravel Package

vanilo/properties

Vanilo Properties adds flexible product attributes to Laravel e-commerce stores: define properties and values, attach them to products, and manage catalog attributes for display, filtering, and organization. Integrates with Vanilo Products.

View on GitHub
Deep Wiki
Context7

Getting Started

vanilo/properties is the dynamic-attribute layer of the Vanilo e-commerce suite. It gives any Eloquent model (in practice: Product) a set of named Property records — Color, Material, Wattage, Country of Origin — each owning a controlled list of PropertyValue records, and attaches those values to models through a polymorphic pivot. It is a specification/filtering system, not a variant generator: it answers "what is this product like", not "which SKU is this".

Minimum steps:

  1. composer require vanilo/properties (pin it to the same major as the rest of your Vanilo packages — this module versions in lockstep with the suite).
  2. Publish and run migrations. You should end up with three tables: properties, property_values, and model_property_values (polymorphic pivot with model_type, model_id, property_value_id).
  3. Read src/Contracts/ first — Property, PropertyValue — then src/Models/, then src/Traits/. The service provider is auto-discovered; there is no publishable config worth touching on day one.
  4. Add the trait to your product model:
use Illuminate\Database\Eloquent\Model;
use Vanilo\Properties\Traits\HasPropertyValues; // verify exact name in src/Traits

class Product extends Model
{
    use HasPropertyValues;
}
  1. Seed one property and use it:
use Vanilo\Properties\Models\Property;

$color = Property::create([
    'name' => 'Color',
    'slug' => 'color',
    'type' => 'select',
]);

$red  = $color->values()->create(['value' => 'red',  'title' => 'Red']);
$blue = $color->values()->create(['value' => 'blue', 'title' => 'Blue']);

$product->propertyValues()->sync([$red->id, $blue->id]);

First real use case: a category listing page with a "Color" filter and a spec table on the product detail page. That covers create, attach, read, and query in one pass.

Implementation Patterns

Treat properties as reference data, not user content. Seed them in migrations/seeders or manage them in the admin UI; the storefront only ever reads them. Keep slug stable — it is your lookup key in views and query strings, and renaming it breaks saved filters.

Assign values, not properties. The pivot points at property_value_id. A common early bug is passing a property_id into sync(). Wrap the assignment in a small service method so callers can express intent in terms of slugs:

public function setProperty(Product $product, string $propertySlug, array $valueSlugs): void
{
    $property = Property::where('slug', $propertySlug)->firstOrFail();
    $ids = $property->values()->whereIn('value', $valueSlugs)->pluck('id');

    $product->propertyValues()->syncWithoutDetaching($ids->all());
}

Read with the property eager-loaded. The pivot gives you values; the label and type live on the parent Property, so two levels are needed:

$product->load('propertyValues.property');

$specs = $product->propertyValues
    ->groupBy(fn ($v) => $v->property->slug)
    ->map(fn ($group) => $group->pluck('title')->implode(', '));

Filtering. Use whereHas on the morph relation and constrain on the value column. For multi-select filters, group the orWhereHas clauses:

Product::query()
    ->when($colors, fn ($q) => $q->whereHas('propertyValues', fn ($q) =>
        $q->whereIn('value', $colors)
    ))
    ->get();

For a filter sidebar, one query per property facet is usually cheaper and clearer than a single mega-join.

Render by type, validate generically. The type column is a UI hint consumed by the PropertyTypes registry (see src/PropertyTypes.php). Map each type to a Blade partial — select/multiselect render a dropdown from $property->values, text renders an input, number a numeric field. On submit, validate propertyValues.* as array of existing property_values.id; the package will not validate cardinality or type for you.

Custom types. Register your own type class in a service provider's boot() (before any form is rendered), then set properties.type to that key. Typical candidates: color (swatch UI), dimension (value + unit), range (min/max). Keep the class thin — label, input view, and a cast() if you want typed access.

Integration points. If you use vanilo/admin, the property CRUD and the product form's property tab come for free; match your custom types to the admin's expectations or hide them from the UI. If you build your own admin, the properties index is a plain resource controller — nothing exotic.

Gotchas and Tips

  • Morph map consistency. The pivot stores model_type. If you register a morph map (Relation::enforceMorphMap), do it before writing any property data, or existing rows will point at FQCNs that no longer resolve. This is the single most common cause of "values disappeared" after a refactor.
  • Property deletion is destructive. Deleting a Property cascades to its PropertyValue rows; whether the pivot rows are cleaned up depends on the FK definition in the published migration. Audit the migration and add cascadeOnDelete() to property_value_id if it's missing — otherwise you get orphaned pivot rows and broken eager loads.
  • type is immutable in practice. Changing a property from select to number does not rewrite existing values. Treat the type as fixed after creation, or write a migration that normalizes the values.
  • No value-level uniqueness unless you add it. Nothing stops two PropertyValue rows with value = 'red' on the same property. Add a unique index on (property_id, value) in your own migration and use firstOrCreate in seeders.
  • N+1 lives here. Any product grid that prints specs must with('propertyValues.property'). This is easy to miss because the first page with 12 products looks fine and the category page with 200 does not.
  • Facet queries scale poorly without indexes. Add composite indexes on model_property_values (model_type, model_id) (usually present) and property_values (property_id, value). Filtering by value across a large catalog becomes a sequential scan otherwise.
  • The package is young and thinly used (single-digit stars, no dependents at time of writing). There is no substantial third-party documentation — the test suite in tests/ is your best reference for edge-case behaviour. Read it before assuming a helper method exists; several common operations (sync-by-slug, typed casting, facet counts) are not provided and belong in your own application layer.
  • Extension points are the registry and the contracts. If you need different storage, bind your own implementation to Vanilo\Properties\Contracts\Property in a service provider. Avoid subclassing the Eloquent models directly — the suite assumes the concrete classes in a few places.
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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums