From 38fa0e2e457c6641356198ee096500b15143d484 Mon Sep 17 00:00:00 2001 From: Nurmuhammet Allanov Date: Thu, 5 Feb 2026 01:08:07 +0500 Subject: [PATCH] Add properties filtering and relationships in Product model --- .../Commands/SyncProductPropertiesJson.php | 47 +++++++++++++++++ .../Product/Filter/ProductFilterer.php | 13 +++++ .../Resources/ProductIndexResource.php | 1 + .../Product/Concerns/HasPropertiesJson.php | 50 +++++++++++++++++++ .../Ecommerce/Product/Product/Product.php | 7 +++ .../Property/ProductAttributeValue.php | 8 +++ .../ProductAttributeValueObserver.php | 32 ++++++++++++ app/Observers/ProductPropertyObserver.php | 32 ++++++++++++ app/Providers/AppServiceProvider.php | 2 +- app/Providers/EventServiceProvider.php | 7 ++- ..._add_properties_json_to_products_table.php | 30 +++++++++++ 11 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/SyncProductPropertiesJson.php create mode 100644 app/Models/Ecommerce/Product/Product/Concerns/HasPropertiesJson.php create mode 100644 app/Observers/ProductAttributeValueObserver.php create mode 100644 app/Observers/ProductPropertyObserver.php create mode 100644 database/migrations/2026_02_05_005932_add_properties_json_to_products_table.php diff --git a/app/Console/Commands/SyncProductPropertiesJson.php b/app/Console/Commands/SyncProductPropertiesJson.php new file mode 100644 index 0000000..74231e7 --- /dev/null +++ b/app/Console/Commands/SyncProductPropertiesJson.php @@ -0,0 +1,47 @@ +info('Starting sync of product properties JSON...'); + + // Using cursor to be memory efficient + $products = Product::cursor(); + $count = Product::count(); + + $bar = $this->output->createProgressBar($count); + $bar->start(); + + foreach ($products as $product) { + $product->syncPropertiesJson(); + $bar->advance(); + } + + $bar->finish(); + $this->newLine(); + $this->info('Sync completed successfully!'); + } +} diff --git a/app/Helpers/Ecommerce/Product/Filter/ProductFilterer.php b/app/Helpers/Ecommerce/Product/Filter/ProductFilterer.php index 805a091..b634daa 100644 --- a/app/Helpers/Ecommerce/Product/Filter/ProductFilterer.php +++ b/app/Helpers/Ecommerce/Product/Filter/ProductFilterer.php @@ -48,6 +48,7 @@ class ProductFilterer 'min_price' => ['nullable', 'numeric'], 'max_price' => ['nullable', 'numeric'], 'backorder' => ['nullable', 'in:0,1'], + 'properties' => ['nullable', 'array'], ]); } @@ -60,6 +61,18 @@ class ProductFilterer return $this->queryBuilder; } + if ($this->request->filled('properties')) { + foreach ($this->request->input('properties') as $attributeSlug => $values) { + $valuesArray = explode(',', $values); + + $this->queryBuilder->where(function ($query) use ($attributeSlug, $valuesArray) { + foreach ($valuesArray as $value) { + $query->orWhereJsonContains("properties_json->{$attributeSlug}", $value); + } + }); + } + } + if ($this->request->filled('brands')) { $this->queryBuilder->whereIntegerInRaw('products.brand_id', explode(',', $this->request->brands)); } diff --git a/app/Http/Controllers/Api/V1/Product/Resources/ProductIndexResource.php b/app/Http/Controllers/Api/V1/Product/Resources/ProductIndexResource.php index a21f6ad..2c607f2 100644 --- a/app/Http/Controllers/Api/V1/Product/Resources/ProductIndexResource.php +++ b/app/Http/Controllers/Api/V1/Product/Resources/ProductIndexResource.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\V1\Product\Resources; use App\Http\Resources\MediaResource; use App\Repositories\Ecommerce\Product\Property\PropertyRepository; +use App\Http\Controllers\Api\V1\Product\Resources\Variant\ProductVariantResource; use Illuminate\Http\Resources\Json\JsonResource; class ProductIndexResource extends JsonResource diff --git a/app/Models/Ecommerce/Product/Product/Concerns/HasPropertiesJson.php b/app/Models/Ecommerce/Product/Product/Concerns/HasPropertiesJson.php new file mode 100644 index 0000000..e62cb95 --- /dev/null +++ b/app/Models/Ecommerce/Product/Product/Concerns/HasPropertiesJson.php @@ -0,0 +1,50 @@ +casts['properties_json'] = 'array'; + } + + /** + * Sync properties to JSON column + */ + public function syncPropertiesJson(): void + { + $this->load(['properties.attribute', 'properties.values.value']); + + $propertiesJson = []; + + foreach ($this->properties as $property) { + $attributeSlug = $property->attribute->slug; + + if (! isset($propertiesJson[$attributeSlug])) { + $propertiesJson[$attributeSlug] = []; + } + + foreach ($property->values as $value) { + // We want to store both the key (for standard values) and the custom value + // so we can filter by either. + if ($value->value) { + $propertiesJson[$attributeSlug][] = $value->value->key; + } + + if ($value->product_custom_value) { + $propertiesJson[$attributeSlug][] = $value->product_custom_value; + } + } + + // Unique values just in case + $propertiesJson[$attributeSlug] = array_values(array_unique($propertiesJson[$attributeSlug])); + } + + $this->properties_json = $propertiesJson; + $this->saveQuietly(); + } +} diff --git a/app/Models/Ecommerce/Product/Product/Product.php b/app/Models/Ecommerce/Product/Product/Product.php index 60dc72b..c4bc4be 100644 --- a/app/Models/Ecommerce/Product/Product/Product.php +++ b/app/Models/Ecommerce/Product/Product/Product.php @@ -3,6 +3,7 @@ namespace App\Models\Ecommerce\Product\Product; use App\Models\Concerns\HasSchemalessAttributes; +use App\Models\Ecommerce\Product\Product\Concerns\HasPropertiesJson; use App\Models\Ecommerce\Product\Product\Concerns\ProductFrontEndHelpers; use App\Models\Ecommerce\Product\Product\Concerns\ProductMedia; use App\Models\Ecommerce\Product\Product\Concerns\ProductProperties; @@ -31,6 +32,11 @@ class Product extends Model implements HasMedia, Viewable */ use HasSchemalessAttributes; + /** + * Has Properties Json + */ + use HasPropertiesJson; + /** * Has Slug (spatie/laravel-sluggable) */ @@ -117,5 +123,6 @@ class Product extends Model implements HasMedia, Viewable 'is_visible', 'colour', 'size', + 'properties_json', ]; } diff --git a/app/Models/Ecommerce/Product/Property/ProductAttributeValue.php b/app/Models/Ecommerce/Product/Property/ProductAttributeValue.php index 9a17277..20dde7c 100644 --- a/app/Models/Ecommerce/Product/Property/ProductAttributeValue.php +++ b/app/Models/Ecommerce/Product/Property/ProductAttributeValue.php @@ -69,4 +69,12 @@ class ProductAttributeValue extends Model { return $this->belongsTo(AttributeValue::class, 'attribute_value_id'); } + + /** + * Product Property + */ + public function productProperty(): BelongsTo + { + return $this->belongsTo(ProductProperty::class, 'product_attribute_id'); + } } diff --git a/app/Observers/ProductAttributeValueObserver.php b/app/Observers/ProductAttributeValueObserver.php new file mode 100644 index 0000000..09a4639 --- /dev/null +++ b/app/Observers/ProductAttributeValueObserver.php @@ -0,0 +1,32 @@ +productProperty->product->syncPropertiesJson(); + } + + /** + * Handle the ProductAttributeValue "updated" event. + */ + public function updated(ProductAttributeValue $productAttributeValue): void + { + $productAttributeValue->productProperty->product->syncPropertiesJson(); + } + + /** + * Handle the ProductAttributeValue "deleted" event. + */ + public function deleted(ProductAttributeValue $productAttributeValue): void + { + $productAttributeValue->productProperty->product->syncPropertiesJson(); + } +} diff --git a/app/Observers/ProductPropertyObserver.php b/app/Observers/ProductPropertyObserver.php new file mode 100644 index 0000000..152b9da --- /dev/null +++ b/app/Observers/ProductPropertyObserver.php @@ -0,0 +1,32 @@ +product->syncPropertiesJson(); + } + + /** + * Handle the ProductProperty "updated" event. + */ + public function updated(ProductProperty $productProperty): void + { + $productProperty->product->syncPropertiesJson(); + } + + /** + * Handle the ProductProperty "deleted" event. + */ + public function deleted(ProductProperty $productProperty): void + { + $productProperty->product->syncPropertiesJson(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1802e95..3f3efe4 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -28,7 +28,7 @@ class AppServiceProvider extends ServiceProvider Relation::morphMap(config('ecommerce.models')); $this->loadMigrationsFrom($this->findModuleMigrations()); - // $this->listenDB(); + $this->listenDB(); } /** diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 186b2e9..02b5dd9 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -8,6 +8,10 @@ use App\Events\Ecommerce\Product\Order\OrderCreated; use App\Listeners\Ecommerce\Category\UpdateCategoryProductsPriceByTax; use App\Listeners\Ecommerce\Channel\HideChannelProducts; use App\Listeners\Ecommerce\Order\SendOrderCreatedNotification; +use App\Models\Ecommerce\Product\Property\ProductAttributeValue; +use App\Models\Ecommerce\Product\Property\ProductProperty; +use App\Observers\ProductAttributeValueObserver; +use App\Observers\ProductPropertyObserver; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider @@ -39,7 +43,8 @@ class EventServiceProvider extends ServiceProvider */ public function boot(): void { - // + ProductProperty::observe(ProductPropertyObserver::class); + ProductAttributeValue::observe(ProductAttributeValueObserver::class); } /** diff --git a/database/migrations/2026_02_05_005932_add_properties_json_to_products_table.php b/database/migrations/2026_02_05_005932_add_properties_json_to_products_table.php new file mode 100644 index 0000000..7b30881 --- /dev/null +++ b/database/migrations/2026_02_05_005932_add_properties_json_to_products_table.php @@ -0,0 +1,30 @@ +jsonb('properties_json')->nullable(); + $table->index('properties_json', 'products_properties_json_gin', 'gin'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + $table->dropIndex('products_properties_json_gin'); + $table->dropColumn('properties_json'); + }); + } +};