From c54d912647b402d046ab42264f5c1052509c030a Mon Sep 17 00:00:00 2001 From: indy koning Date: Fri, 20 Jun 2025 13:34:39 +0200 Subject: [PATCH 01/10] Get non-flat table attributes using relations instead of join --- src/Http/Controllers/ProductController.php | 2 +- src/Models/Product.php | 54 +++++++++++++++++++ src/Models/ProductAttribute.php | 37 +++++++++++++ .../Traits/Product/SelectAttributeScopes.php | 2 +- 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/Models/ProductAttribute.php diff --git a/src/Http/Controllers/ProductController.php b/src/Http/Controllers/ProductController.php index cc11e2460..9876023fa 100644 --- a/src/Http/Controllers/ProductController.php +++ b/src/Http/Controllers/ProductController.php @@ -12,7 +12,7 @@ public function show(int $productId) $productModel = config('rapidez.models.product'); $product = $productModel::selectForProductPage() ->withEventyGlobalScopes('productpage.scopes') - ->with('options') + ->with('options', 'attrs') ->findOrFail($productId); $attributes = [ diff --git a/src/Models/Product.php b/src/Models/Product.php index b15cb8acb..a2216a9d9 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -233,6 +233,60 @@ public function getThumbnailAttribute($image): ?string return $this->getImageAttribute($image); } + public function attrs(): HasMany + { + return $this->hasMany( + ProductAttribute::class, + 'entity_id', + 'entity_id', + ); + } + + public function getAttribute($key) + { + if (($value = parent::getAttribute($key)) !== null || $this->hasAttribute($key)) { + return $value; + } + + // TOOD: Not sure if this is very efficient, first we're + // searching for the attribute by code for the id and + // after that we're searching for the attribute id + // between the product attributes for the value. + $attributeModel = config('rapidez.models.attribute'); + $attributes = $attributeModel::getCachedWhere(function ($attribute) use ($key) { + return $attribute['code'] == $key; + }); + + if (!count($attributes) || !$attribute = reset($attributes)) { + return null; + } + + $this->loadMissing('attrs'); + // TODO: Check for a custom value for a store. So if store 1 overwrites store 0. + if (!$value = optional($this->attrs->firstWhere('attribute_id', $attribute['id']))->value) { + return null; + } + + if ($attribute['input'] == 'multiselect') { + foreach (explode(',', $value) as $optionValueId) { + $values[] = OptionValue::getCachedByOptionId($optionValueId); + } + $this->setAttribute($key, $values); + return $values; + } + + if ($attribute['input'] == 'select' && $attribute['type'] == 'int' && !$attribute['system']) { + $value = OptionValue::getCachedByOptionId($value); + } + + if ($key == 'url_key') { + return '/' . $value . Config::getValue('catalog/seo/product_url_suffix', options: ['default' => '.html']); + } + + $this->setAttribute($key, $value); + return $value; + } + protected function breadcrumbCategories(): Attribute { return Attribute::make( diff --git a/src/Models/ProductAttribute.php b/src/Models/ProductAttribute.php new file mode 100644 index 000000000..f3785c25c --- /dev/null +++ b/src/Models/ProductAttribute.php @@ -0,0 +1,37 @@ +select([ + 'entity_id', + 'attribute_id', + 'store_id', + 'value', + ]) + ->whereIn('store_id', [config('rapidez.store'), 0]) + ->whereNotNull('value'); + + $baseQuery = clone $builder->getQuery(); + foreach (['int', 'text', 'decimal'] as $type) { + $typeTable = 'catalog_product_entity_'.$type; + $typeQuery = (clone $baseQuery)->from($typeTable); + $typeQuery->wheres[0]['column'] = $typeTable.'.entity_id'; + $builder->unionAll($typeQuery); + } + }); + } +} diff --git a/src/Models/Traits/Product/SelectAttributeScopes.php b/src/Models/Traits/Product/SelectAttributeScopes.php index eadbaab5d..febbda8ea 100644 --- a/src/Models/Traits/Product/SelectAttributeScopes.php +++ b/src/Models/Traits/Product/SelectAttributeScopes.php @@ -19,7 +19,7 @@ public function scopeSelectForProductPage(Builder $query): Builder { $attributeModel = config('rapidez.models.attribute'); $this->attributesToSelect = Arr::pluck($attributeModel::getCachedWhere(function ($attribute) { - return $attribute['productpage'] || in_array($attribute['code'], [ + return $attribute['flat'] && $attribute['productpage'] || in_array($attribute['code'], [ 'name', 'meta_title', 'meta_description', From 8d43426b5820b1a6711cede887002ade1305b894 Mon Sep 17 00:00:00 2001 From: indykoning Date: Fri, 20 Jun 2025 11:35:10 +0000 Subject: [PATCH 02/10] Apply fixes from Duster --- src/Models/Product.php | 8 +++++--- src/Models/ProductAttribute.php | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index a2216a9d9..76e6602f5 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -257,13 +257,13 @@ public function getAttribute($key) return $attribute['code'] == $key; }); - if (!count($attributes) || !$attribute = reset($attributes)) { + if (! count($attributes) || ! $attribute = reset($attributes)) { return null; } $this->loadMissing('attrs'); // TODO: Check for a custom value for a store. So if store 1 overwrites store 0. - if (!$value = optional($this->attrs->firstWhere('attribute_id', $attribute['id']))->value) { + if (! $value = optional($this->attrs->firstWhere('attribute_id', $attribute['id']))->value) { return null; } @@ -272,10 +272,11 @@ public function getAttribute($key) $values[] = OptionValue::getCachedByOptionId($optionValueId); } $this->setAttribute($key, $values); + return $values; } - if ($attribute['input'] == 'select' && $attribute['type'] == 'int' && !$attribute['system']) { + if ($attribute['input'] == 'select' && $attribute['type'] == 'int' && ! $attribute['system']) { $value = OptionValue::getCachedByOptionId($value); } @@ -284,6 +285,7 @@ public function getAttribute($key) } $this->setAttribute($key, $value); + return $value; } diff --git a/src/Models/ProductAttribute.php b/src/Models/ProductAttribute.php index f3785c25c..28b6d35f7 100644 --- a/src/Models/ProductAttribute.php +++ b/src/Models/ProductAttribute.php @@ -3,8 +3,6 @@ namespace Rapidez\Core\Models; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Rapidez\Core\Models\Model; class ProductAttribute extends Model { @@ -27,9 +25,9 @@ protected static function boot(): void $baseQuery = clone $builder->getQuery(); foreach (['int', 'text', 'decimal'] as $type) { - $typeTable = 'catalog_product_entity_'.$type; + $typeTable = 'catalog_product_entity_' . $type; $typeQuery = (clone $baseQuery)->from($typeTable); - $typeQuery->wheres[0]['column'] = $typeTable.'.entity_id'; + $typeQuery->wheres[0]['column'] = $typeTable . '.entity_id'; $builder->unionAll($typeQuery); } }); From 46a59694066b29584f4634b045bfe5d10cd04b4f Mon Sep 17 00:00:00 2001 From: indy koning Date: Fri, 20 Jun 2025 13:39:34 +0200 Subject: [PATCH 03/10] Still allow select type being joined --- src/Models/Traits/Product/SelectAttributeScopes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Traits/Product/SelectAttributeScopes.php b/src/Models/Traits/Product/SelectAttributeScopes.php index febbda8ea..c3c085d60 100644 --- a/src/Models/Traits/Product/SelectAttributeScopes.php +++ b/src/Models/Traits/Product/SelectAttributeScopes.php @@ -19,7 +19,7 @@ public function scopeSelectForProductPage(Builder $query): Builder { $attributeModel = config('rapidez.models.attribute'); $this->attributesToSelect = Arr::pluck($attributeModel::getCachedWhere(function ($attribute) { - return $attribute['flat'] && $attribute['productpage'] || in_array($attribute['code'], [ + return ($attribute['flat'] || $attribute['type'] === 'select') && $attribute['productpage'] || in_array($attribute['code'], [ 'name', 'meta_title', 'meta_description', From 0dd2d78f86cc18a48ab2cf5b89cee8c48a6506d9 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:20:18 +0200 Subject: [PATCH 04/10] Fixed error for some attributes --- src/Models/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index 76e6602f5..790c2eac0 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -276,7 +276,7 @@ public function getAttribute($key) return $values; } - if ($attribute['input'] == 'select' && $attribute['type'] == 'int' && ! $attribute['system']) { + if ($attribute['input'] == 'select' && $attribute['type'] == 'int' && ! ($attribute['system'] ?? false)) { $value = OptionValue::getCachedByOptionId($value); } From fe9da860a5d9b54141e30b3b0bea5f03d7786bb6 Mon Sep 17 00:00:00 2001 From: Jade Date: Wed, 20 Aug 2025 10:26:29 +0200 Subject: [PATCH 05/10] Fix options being taken from the wrong attribute ID --- src/Models/OptionValue.php | 8 +++++--- src/Models/Product.php | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Models/OptionValue.php b/src/Models/OptionValue.php index faadc4b89..d270856cd 100644 --- a/src/Models/OptionValue.php +++ b/src/Models/OptionValue.php @@ -10,17 +10,19 @@ class OptionValue extends Model protected $primaryKey = 'value_id'; - public static function getCachedByOptionId(int $optionId): string + public static function getCachedByOptionId(int $optionId, ?int $attributeId = null, mixed $default = false): string { $cacheKey = 'optionvalues.' . config('rapidez.store'); $cache = Cache::store('rapidez:multi')->get($cacheKey, []); if (! isset($cache[$optionId])) { - $cache[$optionId] = html_entity_decode(self::where('option_id', $optionId) + $cache[$optionId] = html_entity_decode(self::where('eav_attribute_option_value.option_id', $optionId) ->whereIn('store_id', [config('rapidez.store'), 0]) + ->join('eav_attribute_option', 'eav_attribute_option.option_id', '=', 'eav_attribute_option_value.option_id') ->orderByDesc('store_id') + ->when($attributeId, fn($query) => $query->where('attribute_id', $attributeId)) ->first('value') - ->value ?? false); + ->value ?? $default); Cache::store('rapidez:multi')->forever($cacheKey, $cache); } diff --git a/src/Models/Product.php b/src/Models/Product.php index 790c2eac0..f3893d4b0 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -269,7 +269,7 @@ public function getAttribute($key) if ($attribute['input'] == 'multiselect') { foreach (explode(',', $value) as $optionValueId) { - $values[] = OptionValue::getCachedByOptionId($optionValueId); + $values[] = OptionValue::getCachedByOptionId($optionValueId, $attribute['id'], $optionValueId); } $this->setAttribute($key, $values); @@ -277,7 +277,7 @@ public function getAttribute($key) } if ($attribute['input'] == 'select' && $attribute['type'] == 'int' && ! ($attribute['system'] ?? false)) { - $value = OptionValue::getCachedByOptionId($value); + $value = OptionValue::getCachedByOptionId($value, $attribute['id'], $value); } if ($key == 'url_key') { @@ -292,7 +292,7 @@ public function getAttribute($key) protected function breadcrumbCategories(): Attribute { return Attribute::make( - get: function () { + get: function (): iterable { if (! $path = session('latest_category_path')) { return []; } From 8d69a0bb966bcfe20a7e6bc3b28c3570e7279ee8 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Wed, 20 Aug 2025 08:26:56 +0000 Subject: [PATCH 06/10] Apply fixes from Duster --- src/Models/OptionValue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/OptionValue.php b/src/Models/OptionValue.php index d270856cd..3c27bf86d 100644 --- a/src/Models/OptionValue.php +++ b/src/Models/OptionValue.php @@ -20,7 +20,7 @@ public static function getCachedByOptionId(int $optionId, ?int $attributeId = nu ->whereIn('store_id', [config('rapidez.store'), 0]) ->join('eav_attribute_option', 'eav_attribute_option.option_id', '=', 'eav_attribute_option_value.option_id') ->orderByDesc('store_id') - ->when($attributeId, fn($query) => $query->where('attribute_id', $attributeId)) + ->when($attributeId, fn ($query) => $query->where('attribute_id', $attributeId)) ->first('value') ->value ?? $default); Cache::store('rapidez:multi')->forever($cacheKey, $cache); From 7903b10a2dd73b7f5db22cb1191df10d5e2093d6 Mon Sep 17 00:00:00 2001 From: Jade Date: Wed, 20 Aug 2025 10:29:04 +0200 Subject: [PATCH 07/10] Remove extra change --- src/Models/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index f3893d4b0..8bbcdf273 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -292,7 +292,7 @@ public function getAttribute($key) protected function breadcrumbCategories(): Attribute { return Attribute::make( - get: function (): iterable { + get: function () { if (! $path = session('latest_category_path')) { return []; } From c11cd16cce74a47dbe574a5d7041c837ef93f4a9 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:28:14 +0200 Subject: [PATCH 08/10] Update url_key code --- src/Models/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index 8bbcdf273..46b0175fc 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -281,7 +281,7 @@ public function getAttribute($key) } if ($key == 'url_key') { - return '/' . $value . Config::getValue('catalog/seo/product_url_suffix', options: ['default' => '.html']); + return '/' . ($value ? $value . Rapidez::config('catalog/seo/product_url_suffix') : 'catalog/product/view/id/' . $this->entity_id) } $this->setAttribute($key, $value); From a285298bfd648122cba1e41ca499b9bb6017feeb Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:29:29 +0200 Subject: [PATCH 09/10] Fixed typo --- src/Models/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index 46b0175fc..eab8e21f7 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -281,7 +281,7 @@ public function getAttribute($key) } if ($key == 'url_key') { - return '/' . ($value ? $value . Rapidez::config('catalog/seo/product_url_suffix') : 'catalog/product/view/id/' . $this->entity_id) + return '/' . ($value ? $value . Rapidez::config('catalog/seo/product_url_suffix') : 'catalog/product/view/id/' . $this->entity_id); } $this->setAttribute($key, $value); From d0e7cb9ae99498ec6314a1ae9cb2d6370146f02c Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Fri, 5 Dec 2025 08:31:07 +0100 Subject: [PATCH 10/10] Implemented cache tags to prevent clearing all caches every reindex (#1104) --- composer.json | 2 +- src/Commands/IndexCommand.php | 3 ++- src/Models/Attribute.php | 2 +- src/Models/Config.php | 4 ++-- src/Models/OptionSwatch.php | 2 +- src/Models/OptionValue.php | 4 ++-- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 338cabb50..0cf3a27af 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "matchish/laravel-scout-elasticsearch": "^7.11", "rapidez/blade-components": "^1.10", "rapidez/blade-directives": "^1.1", - "rapidez/laravel-multi-cache": "^2.0", + "rapidez/laravel-multi-cache": "^2.1", "tormjens/eventy": "^0.8" }, "require-dev": { diff --git a/src/Commands/IndexCommand.php b/src/Commands/IndexCommand.php index 5d6d52fba..6b4b9fb9c 100644 --- a/src/Commands/IndexCommand.php +++ b/src/Commands/IndexCommand.php @@ -3,6 +3,7 @@ namespace Rapidez\Core\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Cache; use Rapidez\Core\Events\IndexAfterEvent; use Rapidez\Core\Events\IndexBeforeEvent; use Rapidez\Core\Facades\Rapidez; @@ -27,7 +28,7 @@ public function handle() ? Rapidez::getStores(explode(',', $this->option('store'))) : Rapidez::getStores(); - $this->call('cache:clear'); + Cache::driver('rapidez:multi')->tags(['attributes', 'swatches'])->flush(); IndexBeforeEvent::dispatch($this); diff --git a/src/Models/Attribute.php b/src/Models/Attribute.php index ed2056e57..6e525256a 100644 --- a/src/Models/Attribute.php +++ b/src/Models/Attribute.php @@ -46,7 +46,7 @@ protected function prefix(): CastsAttribute public static function getCachedWhere(callable $callback): array { - $attributes = Cache::store('rapidez:multi')->rememberForever('attributes.' . config('rapidez.store'), function () { + $attributes = Cache::store('rapidez:multi')->tags('attributes')->rememberForever('attributes.' . config('rapidez.store'), function () { return self::all()->toArray(); }); diff --git a/src/Models/Config.php b/src/Models/Config.php index b16d9ee08..79c931be3 100644 --- a/src/Models/Config.php +++ b/src/Models/Config.php @@ -86,7 +86,7 @@ public static function getValue( } if ($options['cache'] ?? true) { - $configCache = Cache::driver('rapidez:multi')->get('magento.config', []); + $configCache = Cache::driver('rapidez:multi')->tags('config')->get('magento.config', []); $cacheKey = implode( '.', [ @@ -126,7 +126,7 @@ public static function getValue( if (($options['cache'] ?? true) && isset($cacheKey)) { Arr::set($configCache, $cacheKey, $resultObject ? $result : false); - Cache::driver('rapidez:multi')->set('magento.config', $configCache); + Cache::driver('rapidez:multi')->tags('config')->set('magento.config', $configCache); } return (bool) $options['decrypt'] && is_string($result) ? static::decrypt($result) : $result; diff --git a/src/Models/OptionSwatch.php b/src/Models/OptionSwatch.php index 2196c367a..326afa443 100644 --- a/src/Models/OptionSwatch.php +++ b/src/Models/OptionSwatch.php @@ -22,7 +22,7 @@ public static function getCachedSwatchValues(): array return $attribute['text_swatch'] || $attribute['visual_swatch']; }), 'id', 'code'); - return Cache::rememberForever('swatchvalues', function () use ($swatchAttributes) { + return Cache::store('rapidez:multi')->tags('swatches')->rememberForever('swatchvalues', function () use ($swatchAttributes) { return self::select('eav_attribute.attribute_code') ->selectRaw('JSON_OBJECTAGG(`eav_attribute_option_value`.`option_id`, JSON_OBJECT( "label", COALESCE(`eav_attribute_option_value_store`.`value`, `eav_attribute_option_value`.`value`), diff --git a/src/Models/OptionValue.php b/src/Models/OptionValue.php index 3c27bf86d..69538b895 100644 --- a/src/Models/OptionValue.php +++ b/src/Models/OptionValue.php @@ -13,7 +13,7 @@ class OptionValue extends Model public static function getCachedByOptionId(int $optionId, ?int $attributeId = null, mixed $default = false): string { $cacheKey = 'optionvalues.' . config('rapidez.store'); - $cache = Cache::store('rapidez:multi')->get($cacheKey, []); + $cache = Cache::store('rapidez:multi')->tags('attributes')->get($cacheKey, []); if (! isset($cache[$optionId])) { $cache[$optionId] = html_entity_decode(self::where('eav_attribute_option_value.option_id', $optionId) @@ -23,7 +23,7 @@ public static function getCachedByOptionId(int $optionId, ?int $attributeId = nu ->when($attributeId, fn ($query) => $query->where('attribute_id', $attributeId)) ->first('value') ->value ?? $default); - Cache::store('rapidez:multi')->forever($cacheKey, $cache); + Cache::store('rapidez:multi')->tags('attributes')->forever($cacheKey, $cache); } return $cache[$optionId];