From 841685b5e36d6a48dbc6dc588495ef6ec12cd1af Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 09:25:51 +0200 Subject: [PATCH 01/45] Implement EAV structure for product entity --- config/rapidez/attribute-models.php | 5 + src/Models/AttributeDateTime.php | 8 ++ src/Models/AttributeDecimal.php | 8 ++ src/Models/AttributeInt.php | 10 ++ src/Models/AttributeModels/ArrayBackend.php | 11 ++ src/Models/AttributeModels/AttributeModel.php | 8 ++ src/Models/AttributeOption.php | 19 +++ src/Models/AttributeText.php | 10 ++ src/Models/AttributeVarchar.php | 10 ++ src/Models/EavAttribute.php | 46 ++++++++ src/Models/Product.php | 2 +- src/Models/ProductEntity.php | 52 +++++++++ src/Models/ProductStock.php | 10 ++ src/Models/Traits/HasAttributeOptions.php | 22 ++++ src/Models/Traits/HasCustomAttributes.php | 108 ++++++++++++++++++ src/RapidezServiceProvider.php | 1 + 16 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 config/rapidez/attribute-models.php create mode 100644 src/Models/AttributeDateTime.php create mode 100644 src/Models/AttributeDecimal.php create mode 100644 src/Models/AttributeInt.php create mode 100644 src/Models/AttributeModels/ArrayBackend.php create mode 100644 src/Models/AttributeModels/AttributeModel.php create mode 100644 src/Models/AttributeOption.php create mode 100644 src/Models/AttributeText.php create mode 100644 src/Models/AttributeVarchar.php create mode 100644 src/Models/EavAttribute.php create mode 100644 src/Models/ProductEntity.php create mode 100644 src/Models/ProductStock.php create mode 100644 src/Models/Traits/HasAttributeOptions.php create mode 100644 src/Models/Traits/HasCustomAttributes.php diff --git a/config/rapidez/attribute-models.php b/config/rapidez/attribute-models.php new file mode 100644 index 000000000..18c4e0252 --- /dev/null +++ b/config/rapidez/attribute-models.php @@ -0,0 +1,5 @@ + Rapidez\Core\Models\AttributeModels\ArrayBackend::class, +]; diff --git a/src/Models/AttributeDateTime.php b/src/Models/AttributeDateTime.php new file mode 100644 index 000000000..1b07e6999 --- /dev/null +++ b/src/Models/AttributeDateTime.php @@ -0,0 +1,8 @@ + 'datetime']; +} diff --git a/src/Models/AttributeDecimal.php b/src/Models/AttributeDecimal.php new file mode 100644 index 000000000..39129b106 --- /dev/null +++ b/src/Models/AttributeDecimal.php @@ -0,0 +1,8 @@ + 'float']; +} diff --git a/src/Models/AttributeInt.php b/src/Models/AttributeInt.php new file mode 100644 index 000000000..4d5b8e0f7 --- /dev/null +++ b/src/Models/AttributeInt.php @@ -0,0 +1,10 @@ +map(fn($value) => $attribute->options[$value]?->value ?? $value); + } +} diff --git a/src/Models/AttributeModels/AttributeModel.php b/src/Models/AttributeModels/AttributeModel.php new file mode 100644 index 000000000..a04ca1d72 --- /dev/null +++ b/src/Models/AttributeModels/AttributeModel.php @@ -0,0 +1,8 @@ +leftJoin('eav_attribute_option_value', $builder->qualifyColumn('option_id'), '=', 'eav_attribute_option_value.option_id'); + }); + } +} diff --git a/src/Models/AttributeText.php b/src/Models/AttributeText.php new file mode 100644 index 000000000..a1c799951 --- /dev/null +++ b/src/Models/AttributeText.php @@ -0,0 +1,10 @@ +leftJoin('eav_attribute', $builder->qualifyColumn('attribute_id'), '=', 'eav_attribute.attribute_id'); + }); + + if (isset(static::$hasAttributeOptions)) { + static::addGlobalScope('attributeOptions', function (Builder $builder) { + $builder->with('attributeOptions'); + }); + } + } + + public function getTable() + { + // Overwritten to always return table, so that qualifyColumn and such work + return $this->table; + } + + protected function value(): Attribute + { + return Attribute::get(function($value) { + if ($this->frontend_input === 'select') { + return $this->options[$value] ?? $value; + } + + $class = config('rapidez.attribute-models')[$this->backend_model]; + if ($class) { + return $class::value($value, $this); + } + + return $value; + }); + } +} diff --git a/src/Models/Product.php b/src/Models/Product.php index 9c26ea97f..2a885f407 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -27,7 +27,7 @@ use Rapidez\Core\Models\Traits\Product\SelectAttributeScopes; use TorMorten\Eventy\Facades\Eventy; -class Product extends Model +class Product1 extends Model { use CastMultiselectAttributes; use CastSuperAttributes; diff --git a/src/Models/ProductEntity.php b/src/Models/ProductEntity.php new file mode 100644 index 000000000..a354badc5 --- /dev/null +++ b/src/Models/ProductEntity.php @@ -0,0 +1,52 @@ + 'datetime', + self::CREATED_AT => 'datetime', + ]; + + protected static function boot(): void + { + parent::boot(); + + static::addGlobalScope('customAttributes', function (Builder $builder) { + $builder->withCustomAttributes(); + }); + } + + public function gallery(): BelongsToMany + { + return $this->belongsToMany( + config('rapidez.models.product_image'), + 'catalog_product_entity_media_gallery_value_to_entity', + 'entity_id', + 'value_id', + ); + } + + public function getAttribute($key) + { + if (! $key) { + return; + } + + if ($value = parent::getAttribute($key)) { + return $value; + } + + return $this->getCustomAttribute($key)?->value; + } +} diff --git a/src/Models/ProductStock.php b/src/Models/ProductStock.php new file mode 100644 index 000000000..2a1a85700 --- /dev/null +++ b/src/Models/ProductStock.php @@ -0,0 +1,10 @@ + $this->attributeOptions->keyBy('value_id')); + } + + public function attributeOptions(): HasMany + { + return $this->hasMany(AttributeOption::class, 'attribute_id', 'attribute_id'); + } +} diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php new file mode 100644 index 000000000..c4c0fe472 --- /dev/null +++ b/src/Models/Traits/HasCustomAttributes.php @@ -0,0 +1,108 @@ +with([ + 'attributeDateTime', + 'attributeDecimal', + 'attributeInt', + 'attributeText', + 'attributeVarchar', + ]); + } + + public function attributeDateTime(): HasMany + { + return $this->hasManyWithAttributeTypeTable( + AttributeDateTime::class, + 'datetime', + 'entity_id', + 'entity_id', + ); + } + + public function attributeDecimal(): HasMany + { + return $this->hasManyWithAttributeTypeTable( + AttributeDecimal::class, + 'decimal', + 'entity_id', + 'entity_id', + ); + } + + public function attributeInt(): HasMany + { + return $this->hasManyWithAttributeTypeTable( + AttributeInt::class, + 'int', + 'entity_id', + 'entity_id', + ); + } + + public function attributeText(): HasMany + { + return $this->hasManyWithAttributeTypeTable( + AttributeText::class, + 'text', + 'entity_id', + 'entity_id', + ); + } + + public function attributeVarchar(): HasMany + { + return $this->hasManyWithAttributeTypeTable( + AttributeVarchar::class, + 'varchar', + 'entity_id', + 'entity_id', + ); + } + + public function hasManyWithAttributeTypeTable($class, $type, $foreignKey = null, $localKey = null) + { + $table = ($this->attributesTablePrefix ?? $this->table) . '_' . $type; + + $relation = $this->hasMany($class, $foreignKey, $localKey); + $relation->getModel()->setTable($table); + $relation->getQuery()->from($table); + + return $relation; + } + + public function customAttributes(): Attribute + { + return Attribute::get(function() { + if (@!$this->attributeDateTime) { + return collect(); + } + return collect() + ->concat($this->attributeDateTime) + ->concat($this->attributeDecimal) + ->concat($this->attributeInt) + ->concat($this->attributeText) + ->concat($this->attributeVarchar) + ->keyBy('attribute_code'); + }); + } + + public function getCustomAttribute($key) + { + return $this->customAttributes[$key] ?? null; + } +} diff --git a/src/RapidezServiceProvider.php b/src/RapidezServiceProvider.php index fa56e12c4..ff3cf90b1 100644 --- a/src/RapidezServiceProvider.php +++ b/src/RapidezServiceProvider.php @@ -45,6 +45,7 @@ class RapidezServiceProvider extends ServiceProvider { protected $configFiles = [ + 'attribute-models', 'frontend', 'healthcheck', 'jwt', From 78a47ec6bc88219c6e8781fafd38b70b7ea8ad94 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Thu, 24 Jul 2025 07:26:15 +0000 Subject: [PATCH 02/45] Apply fixes from Duster --- src/Models/AttributeModels/ArrayBackend.php | 6 ++++-- src/Models/EavAttribute.php | 2 +- src/Models/Traits/HasAttributeOptions.php | 2 +- src/Models/Traits/HasCustomAttributes.php | 5 +++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Models/AttributeModels/ArrayBackend.php b/src/Models/AttributeModels/ArrayBackend.php index bff11752e..8f6544ce0 100644 --- a/src/Models/AttributeModels/ArrayBackend.php +++ b/src/Models/AttributeModels/ArrayBackend.php @@ -4,8 +4,10 @@ class ArrayBackend implements AttributeModel { - public static function value($value, $attribute) { + public static function value($value, $attribute) + { $values = explode(',', $value); - return collect($values)->map(fn($value) => $attribute->options[$value]?->value ?? $value); + + return collect($values)->map(fn ($value) => $attribute->options[$value]?->value ?? $value); } } diff --git a/src/Models/EavAttribute.php b/src/Models/EavAttribute.php index 061f6f637..eace6ab97 100644 --- a/src/Models/EavAttribute.php +++ b/src/Models/EavAttribute.php @@ -30,7 +30,7 @@ public function getTable() protected function value(): Attribute { - return Attribute::get(function($value) { + return Attribute::get(function ($value) { if ($this->frontend_input === 'select') { return $this->options[$value] ?? $value; } diff --git a/src/Models/Traits/HasAttributeOptions.php b/src/Models/Traits/HasAttributeOptions.php index d220dd764..5356748db 100644 --- a/src/Models/Traits/HasAttributeOptions.php +++ b/src/Models/Traits/HasAttributeOptions.php @@ -12,7 +12,7 @@ trait HasAttributeOptions protected function options(): Attribute { - return Attribute::get(fn() => $this->attributeOptions->keyBy('value_id')); + return Attribute::get(fn () => $this->attributeOptions->keyBy('value_id')); } public function attributeOptions(): HasMany diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index c4c0fe472..269cacfc3 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -87,10 +87,11 @@ public function hasManyWithAttributeTypeTable($class, $type, $foreignKey = null, public function customAttributes(): Attribute { - return Attribute::get(function() { - if (@!$this->attributeDateTime) { + return Attribute::get(function () { + if (@! $this->attributeDateTime) { return collect(); } + return collect() ->concat($this->attributeDateTime) ->concat($this->attributeDecimal) From 77c9a41e5229edc910ba7e522e298daf7c576cf6 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 09:54:09 +0200 Subject: [PATCH 03/45] Fix model name --- 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 2a885f407..9c26ea97f 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -27,7 +27,7 @@ use Rapidez\Core\Models\Traits\Product\SelectAttributeScopes; use TorMorten\Eventy\Facades\Eventy; -class Product1 extends Model +class Product extends Model { use CastMultiselectAttributes; use CastSuperAttributes; From 42a1a7e6efa948e87590c2c494088830ed94c692 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 11:17:37 +0200 Subject: [PATCH 04/45] Get store specific values --- src/Models/AttributeOption.php | 6 +++++- src/Models/EavAttribute.php | 7 +++++-- src/Models/Traits/HasAttributeOptions.php | 7 ++++++- src/Models/Traits/HasCustomAttributes.php | 18 +++++++++++++++++- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/Models/AttributeOption.php b/src/Models/AttributeOption.php index 2cd1bc14f..376b3f0c4 100644 --- a/src/Models/AttributeOption.php +++ b/src/Models/AttributeOption.php @@ -3,6 +3,7 @@ namespace Rapidez\Core\Models; use Illuminate\Contracts\Database\Eloquent\Builder; +use Illuminate\Database\Query\JoinClause; class AttributeOption extends Model { @@ -13,7 +14,10 @@ protected static function boot(): void parent::boot(); static::addGlobalScope('values', function (Builder $builder) { - $builder->leftJoin('eav_attribute_option_value', $builder->qualifyColumn('option_id'), '=', 'eav_attribute_option_value.option_id'); + $builder->leftJoin('eav_attribute_option_value', function (JoinClause $join) use ($builder) { + $join->on($builder->qualifyColumn('option_id'), '=', 'eav_attribute_option_value.option_id') + ->whereIn('store_id', [0, config('rapidez.store')]); + }); }); } } diff --git a/src/Models/EavAttribute.php b/src/Models/EavAttribute.php index eace6ab97..e1b0eeeb5 100644 --- a/src/Models/EavAttribute.php +++ b/src/Models/EavAttribute.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Rapidez\Core\Models\Scopes\ForCurrentStoreWithoutLimitScope; class EavAttribute extends Model { @@ -11,6 +12,8 @@ protected static function boot(): void { parent::boot(); + static::addGlobalScope(new ForCurrentStoreWithoutLimitScope('value_id')); + static::addGlobalScope('attribute', function (Builder $builder) { $builder->leftJoin('eav_attribute', $builder->qualifyColumn('attribute_id'), '=', 'eav_attribute.attribute_id'); }); @@ -32,10 +35,10 @@ protected function value(): Attribute { return Attribute::get(function ($value) { if ($this->frontend_input === 'select') { - return $this->options[$value] ?? $value; + return $this->options[$value]?->value ?? $value; } - $class = config('rapidez.attribute-models')[$this->backend_model]; + $class = config('rapidez.attribute-models')[$this->backend_model] ?? null; if ($class) { return $class::value($value, $this); } diff --git a/src/Models/Traits/HasAttributeOptions.php b/src/Models/Traits/HasAttributeOptions.php index 5356748db..064d2f3e9 100644 --- a/src/Models/Traits/HasAttributeOptions.php +++ b/src/Models/Traits/HasAttributeOptions.php @@ -12,7 +12,12 @@ trait HasAttributeOptions protected function options(): Attribute { - return Attribute::get(fn () => $this->attributeOptions->keyBy('value_id')); + // Sort by store_id first to always get the higher store id if there are two. + return Attribute::get(fn () => + $this->attributeOptions + ->sortBy('store_id') + ->keyBy('option_id') + ); } public function attributeOptions(): HasMany diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 269cacfc3..0e13fea97 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -2,7 +2,7 @@ namespace Rapidez\Core\Models\Traits; -use Illuminate\Contracts\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; use Rapidez\Core\Models\AttributeDateTime; @@ -24,6 +24,22 @@ public function scopeWithCustomAttributes(Builder $builder) ]); } + public function scopeWhereValueHas(Builder $builder, $callback) + { + return $builder->whereHas('attributeDateTime', $callback) + ->orWhereHas('attributeDecimal', $callback) + ->orWhereHas('attributeInt', $callback) + ->orWhereHas('attributeText', $callback) + ->orWhereHas('attributeVarchar', $callback); + } + + public function scopeWhereValue(Builder $builder, string $attribute, $operator = null, $value = null) + { + return $builder->whereValueHas(fn ($query) => + $query->where('value', $operator, $value)->where('attribute_code', $attribute) + ); + } + public function attributeDateTime(): HasMany { return $this->hasManyWithAttributeTypeTable( From ee340e90f1f8d316c4b16206659ce8151aefd29d Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Thu, 24 Jul 2025 09:18:04 +0000 Subject: [PATCH 05/45] Apply fixes from Duster --- src/Models/Traits/HasAttributeOptions.php | 7 +++---- src/Models/Traits/HasCustomAttributes.php | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Models/Traits/HasAttributeOptions.php b/src/Models/Traits/HasAttributeOptions.php index 064d2f3e9..dec409b47 100644 --- a/src/Models/Traits/HasAttributeOptions.php +++ b/src/Models/Traits/HasAttributeOptions.php @@ -13,10 +13,9 @@ trait HasAttributeOptions protected function options(): Attribute { // Sort by store_id first to always get the higher store id if there are two. - return Attribute::get(fn () => - $this->attributeOptions - ->sortBy('store_id') - ->keyBy('option_id') + return Attribute::get(fn () => $this->attributeOptions + ->sortBy('store_id') + ->keyBy('option_id') ); } diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 0e13fea97..5fdb92586 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -35,8 +35,7 @@ public function scopeWhereValueHas(Builder $builder, $callback) public function scopeWhereValue(Builder $builder, string $attribute, $operator = null, $value = null) { - return $builder->whereValueHas(fn ($query) => - $query->where('value', $operator, $value)->where('attribute_code', $attribute) + return $builder->whereValueHas(fn ($query) => $query->where('value', $operator, $value)->where('attribute_code', $attribute) ); } From 9cfed198d3846d4c79e6b412a123c99721cf4535 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 13:26:00 +0200 Subject: [PATCH 06/45] Allow for values to be queried on, filter products based on website and enabled status --- src/Models/EavAttribute.php | 15 ++++++++--- src/Models/ProductEntity.php | 7 ++--- .../ForCurrentStoreWithoutLimitScope.php | 27 ++++++++++++++----- .../Scopes/Product/ForCurrentWebsiteScope.php | 16 +++++++++++ src/Models/Traits/HasCustomAttributes.php | 22 +++++++-------- 5 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 src/Models/Scopes/Product/ForCurrentWebsiteScope.php diff --git a/src/Models/EavAttribute.php b/src/Models/EavAttribute.php index e1b0eeeb5..8703d5ca5 100644 --- a/src/Models/EavAttribute.php +++ b/src/Models/EavAttribute.php @@ -4,6 +4,8 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; use Rapidez\Core\Models\Scopes\ForCurrentStoreWithoutLimitScope; class EavAttribute extends Model @@ -12,19 +14,26 @@ protected static function boot(): void { parent::boot(); - static::addGlobalScope(new ForCurrentStoreWithoutLimitScope('value_id')); + static::addGlobalScope(new ForCurrentStoreWithoutLimitScope(['attribute_id', 'entity_id'])); static::addGlobalScope('attribute', function (Builder $builder) { $builder->leftJoin('eav_attribute', $builder->qualifyColumn('attribute_id'), '=', 'eav_attribute.attribute_id'); }); - if (isset(static::$hasAttributeOptions)) { + if (isset(static::$hasAttributeOptions)) { // @phpstan-ignore-line static::addGlobalScope('attributeOptions', function (Builder $builder) { - $builder->with('attributeOptions'); + $builder->with('attributeOptions'); // @phpstan-ignore-line }); } } + public static function getCached() + { + return Cache::rememberForever('eav_attributes', function() { + return DB::table('eav_attribute')->get()->keyBy('attribute_code'); + }); + } + public function getTable() { // Overwritten to always return table, so that qualifyColumn and such work diff --git a/src/Models/ProductEntity.php b/src/Models/ProductEntity.php index a354badc5..61af8818d 100644 --- a/src/Models/ProductEntity.php +++ b/src/Models/ProductEntity.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; use Rapidez\Core\Models\Traits\HasCustomAttributes; class ProductEntity extends Model @@ -22,9 +23,9 @@ protected static function boot(): void { parent::boot(); - static::addGlobalScope('customAttributes', function (Builder $builder) { - $builder->withCustomAttributes(); - }); + static::addGlobalScope(ForCurrentWebsiteScope::class); + static::addGlobalScope('customAttributes', fn (Builder $builder) => $builder->withCustomAttributes()); + static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereValue('status', 1)); } public function gallery(): BelongsToMany diff --git a/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php b/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php index 66434b3c0..cf296acc9 100644 --- a/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php +++ b/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php @@ -5,13 +5,18 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; +use Illuminate\Support\Facades\DB; /** * Remove results from the default store when store view specific is found. */ class ForCurrentStoreWithoutLimitScope implements Scope { - public function __construct(public $uniquePerStoreKey, public $storeIdColumn = 'store_id') {} + public array $uniquePerStoreKeys; + + public function __construct(string|array $uniquePerStoreKey, public $storeIdColumn = 'store_id') { + $this->uniquePerStoreKeys = is_array($uniquePerStoreKey) ? $uniquePerStoreKey : [$uniquePerStoreKey]; + } public function apply(Builder $query, Model $model) { @@ -20,18 +25,26 @@ public function apply(Builder $query, Model $model) ->where($query->qualifyColumn($this->storeIdColumn), 0); } + $scope = $this; + return $query // Pre-filter results to be default and current store only. ->whereIn($query->qualifyColumn($this->storeIdColumn), [0, config('rapidez.store')]) // Remove values from the default store where values for the current store exist. ->where(fn ($query) => $query // Remove values where we already have values in the current store. - ->whereNotIn($query->qualifyColumn($this->uniquePerStoreKey), fn ($query) => $query - ->select($model->qualifyColumn($this->uniquePerStoreKey)) - ->from($model->getTable()) - ->whereColumn($model->qualifyColumn($this->uniquePerStoreKey), $model->qualifyColumn($this->uniquePerStoreKey)) - ->where($model->qualifyColumn($this->storeIdColumn), config('rapidez.store')) - ) + ->where(function ($query) use ($scope, $model) { + $query + ->whereNotExists(function ($query) use ($scope, $model) { + $query + ->select(DB::raw(1)) + ->from($model->getTable() . ' as comparison') + ->where('comparison.'.$this->storeIdColumn, config('rapidez.store')); + foreach($scope->uniquePerStoreKeys as $uniquePerStoreKey) { + $query->whereColumn('comparison.'.$uniquePerStoreKey, $model->qualifyColumn($uniquePerStoreKey)); + } + }); + }) // Unless the value IS the current store. ->orWhere($query->qualifyColumn($this->storeIdColumn), config('rapidez.store')) ); diff --git a/src/Models/Scopes/Product/ForCurrentWebsiteScope.php b/src/Models/Scopes/Product/ForCurrentWebsiteScope.php new file mode 100644 index 000000000..15a051940 --- /dev/null +++ b/src/Models/Scopes/Product/ForCurrentWebsiteScope.php @@ -0,0 +1,16 @@ +leftJoin('catalog_product_website', 'product_id', '=', $model->getQualifiedKeyName()); + $builder->where('website_id', config('rapidez.website')); + } +} diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 5fdb92586..b2d1e43f8 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -5,11 +5,13 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; +use Rapidez\Core\Models\Attribute as ModelsAttribute; use Rapidez\Core\Models\AttributeDateTime; use Rapidez\Core\Models\AttributeDecimal; use Rapidez\Core\Models\AttributeInt; use Rapidez\Core\Models\AttributeText; use Rapidez\Core\Models\AttributeVarchar; +use Rapidez\Core\Models\EavAttribute; trait HasCustomAttributes { @@ -24,18 +26,16 @@ public function scopeWithCustomAttributes(Builder $builder) ]); } - public function scopeWhereValueHas(Builder $builder, $callback) + public function scopeWhereValue(Builder $builder, string $attributeCode, $operator = null, $value = null) { - return $builder->whereHas('attributeDateTime', $callback) - ->orWhereHas('attributeDecimal', $callback) - ->orWhereHas('attributeInt', $callback) - ->orWhereHas('attributeText', $callback) - ->orWhereHas('attributeVarchar', $callback); - } - - public function scopeWhereValue(Builder $builder, string $attribute, $operator = null, $value = null) - { - return $builder->whereValueHas(fn ($query) => $query->where('value', $operator, $value)->where('attribute_code', $attribute) + $type = EavAttribute::getCached()[$attributeCode]->backend_type ?? 'varchar'; + $relation = match($type) { + 'datetime' => 'attributeDateTime', + default => 'attribute' . ucfirst($type), + }; + + return $builder->whereHas($relation, fn ($query) => + $query->where('value', $operator, $value)->where('attribute_code', $attributeCode) ); } From 30c975e3b7c96823fb888a6718d39c730b2eaabc Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Thu, 24 Jul 2025 11:26:21 +0000 Subject: [PATCH 07/45] Apply fixes from Duster --- src/Models/EavAttribute.php | 2 +- .../Scopes/ForCurrentStoreWithoutLimitScope.php | 11 ++++++----- src/Models/Traits/HasCustomAttributes.php | 8 +++----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Models/EavAttribute.php b/src/Models/EavAttribute.php index 8703d5ca5..611317a81 100644 --- a/src/Models/EavAttribute.php +++ b/src/Models/EavAttribute.php @@ -29,7 +29,7 @@ protected static function boot(): void public static function getCached() { - return Cache::rememberForever('eav_attributes', function() { + return Cache::rememberForever('eav_attributes', function () { return DB::table('eav_attribute')->get()->keyBy('attribute_code'); }); } diff --git a/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php b/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php index cf296acc9..cb0321d18 100644 --- a/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php +++ b/src/Models/Scopes/ForCurrentStoreWithoutLimitScope.php @@ -14,7 +14,8 @@ class ForCurrentStoreWithoutLimitScope implements Scope { public array $uniquePerStoreKeys; - public function __construct(string|array $uniquePerStoreKey, public $storeIdColumn = 'store_id') { + public function __construct(string|array $uniquePerStoreKey, public $storeIdColumn = 'store_id') + { $this->uniquePerStoreKeys = is_array($uniquePerStoreKey) ? $uniquePerStoreKey : [$uniquePerStoreKey]; } @@ -39,11 +40,11 @@ public function apply(Builder $query, Model $model) $query ->select(DB::raw(1)) ->from($model->getTable() . ' as comparison') - ->where('comparison.'.$this->storeIdColumn, config('rapidez.store')); - foreach($scope->uniquePerStoreKeys as $uniquePerStoreKey) { - $query->whereColumn('comparison.'.$uniquePerStoreKey, $model->qualifyColumn($uniquePerStoreKey)); + ->where('comparison.' . $this->storeIdColumn, config('rapidez.store')); + foreach ($scope->uniquePerStoreKeys as $uniquePerStoreKey) { + $query->whereColumn('comparison.' . $uniquePerStoreKey, $model->qualifyColumn($uniquePerStoreKey)); } - }); + }); }) // Unless the value IS the current store. ->orWhere($query->qualifyColumn($this->storeIdColumn), config('rapidez.store')) diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index b2d1e43f8..7880b50ed 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -5,7 +5,6 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; -use Rapidez\Core\Models\Attribute as ModelsAttribute; use Rapidez\Core\Models\AttributeDateTime; use Rapidez\Core\Models\AttributeDecimal; use Rapidez\Core\Models\AttributeInt; @@ -29,13 +28,12 @@ public function scopeWithCustomAttributes(Builder $builder) public function scopeWhereValue(Builder $builder, string $attributeCode, $operator = null, $value = null) { $type = EavAttribute::getCached()[$attributeCode]->backend_type ?? 'varchar'; - $relation = match($type) { + $relation = match ($type) { 'datetime' => 'attributeDateTime', - default => 'attribute' . ucfirst($type), + default => 'attribute' . ucfirst($type), }; - return $builder->whereHas($relation, fn ($query) => - $query->where('value', $operator, $value)->where('attribute_code', $attributeCode) + return $builder->whereHas($relation, fn ($query) => $query->where('value', $operator, $value)->where('attribute_code', $attributeCode) ); } From 573f3bf1fbf44de2e35e5ab1a2ca480b13decfc3 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 13:30:13 +0200 Subject: [PATCH 08/45] Rename EavAttribute to AbstractAttribute --- .../{EavAttribute.php => AbstractAttribute.php} | 11 +---------- src/Models/Attribute.php | 8 ++++++++ src/Models/AttributeDateTime.php | 2 +- src/Models/AttributeDecimal.php | 2 +- src/Models/AttributeInt.php | 2 +- src/Models/AttributeText.php | 2 +- src/Models/AttributeVarchar.php | 2 +- src/Models/Traits/HasCustomAttributes.php | 4 ++-- 8 files changed, 16 insertions(+), 17 deletions(-) rename src/Models/{EavAttribute.php => AbstractAttribute.php} (82%) diff --git a/src/Models/EavAttribute.php b/src/Models/AbstractAttribute.php similarity index 82% rename from src/Models/EavAttribute.php rename to src/Models/AbstractAttribute.php index 611317a81..60de699a7 100644 --- a/src/Models/EavAttribute.php +++ b/src/Models/AbstractAttribute.php @@ -4,11 +4,9 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\DB; use Rapidez\Core\Models\Scopes\ForCurrentStoreWithoutLimitScope; -class EavAttribute extends Model +class AbstractAttribute extends Model { protected static function boot(): void { @@ -27,13 +25,6 @@ protected static function boot(): void } } - public static function getCached() - { - return Cache::rememberForever('eav_attributes', function () { - return DB::table('eav_attribute')->get()->keyBy('attribute_code'); - }); - } - public function getTable() { // Overwritten to always return table, so that qualifyColumn and such work diff --git a/src/Models/Attribute.php b/src/Models/Attribute.php index ed2056e57..5768ad9c4 100644 --- a/src/Models/Attribute.php +++ b/src/Models/Attribute.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute as CastsAttribute; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; use Rapidez\Core\Models\Scopes\Attribute\OnlyProductAttributesScope; class Attribute extends Model @@ -44,6 +45,13 @@ protected function prefix(): CastsAttribute )->shouldCache(); } + public static function getCached() + { + return Cache::rememberForever('eav_attributes', function () { + return DB::table('eav_attribute')->get()->keyBy('attribute_code'); + }); + } + public static function getCachedWhere(callable $callback): array { $attributes = Cache::store('rapidez:multi')->rememberForever('attributes.' . config('rapidez.store'), function () { diff --git a/src/Models/AttributeDateTime.php b/src/Models/AttributeDateTime.php index 1b07e6999..28c799116 100644 --- a/src/Models/AttributeDateTime.php +++ b/src/Models/AttributeDateTime.php @@ -2,7 +2,7 @@ namespace Rapidez\Core\Models; -class AttributeDateTime extends EavAttribute +class AttributeDateTime extends AbstractAttribute { protected $casts = ['value' => 'datetime']; } diff --git a/src/Models/AttributeDecimal.php b/src/Models/AttributeDecimal.php index 39129b106..401872b2a 100644 --- a/src/Models/AttributeDecimal.php +++ b/src/Models/AttributeDecimal.php @@ -2,7 +2,7 @@ namespace Rapidez\Core\Models; -class AttributeDecimal extends EavAttribute +class AttributeDecimal extends AbstractAttribute { protected $casts = ['value' => 'float']; } diff --git a/src/Models/AttributeInt.php b/src/Models/AttributeInt.php index 4d5b8e0f7..ac4eb82d8 100644 --- a/src/Models/AttributeInt.php +++ b/src/Models/AttributeInt.php @@ -4,7 +4,7 @@ use Rapidez\Core\Models\Traits\HasAttributeOptions; -class AttributeInt extends EavAttribute +class AttributeInt extends AbstractAttribute { use HasAttributeOptions; } diff --git a/src/Models/AttributeText.php b/src/Models/AttributeText.php index a1c799951..05dd4211b 100644 --- a/src/Models/AttributeText.php +++ b/src/Models/AttributeText.php @@ -4,7 +4,7 @@ use Rapidez\Core\Models\Traits\HasAttributeOptions; -class AttributeText extends EavAttribute +class AttributeText extends AbstractAttribute { use HasAttributeOptions; } diff --git a/src/Models/AttributeVarchar.php b/src/Models/AttributeVarchar.php index 884db569f..10196c043 100644 --- a/src/Models/AttributeVarchar.php +++ b/src/Models/AttributeVarchar.php @@ -4,7 +4,7 @@ use Rapidez\Core\Models\Traits\HasAttributeOptions; -class AttributeVarchar extends EavAttribute +class AttributeVarchar extends AbstractAttribute { use HasAttributeOptions; } diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 7880b50ed..8602888c4 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -10,7 +10,7 @@ use Rapidez\Core\Models\AttributeInt; use Rapidez\Core\Models\AttributeText; use Rapidez\Core\Models\AttributeVarchar; -use Rapidez\Core\Models\EavAttribute; +use Rapidez\Core\Models\Attribute as AttributeModel; trait HasCustomAttributes { @@ -27,7 +27,7 @@ public function scopeWithCustomAttributes(Builder $builder) public function scopeWhereValue(Builder $builder, string $attributeCode, $operator = null, $value = null) { - $type = EavAttribute::getCached()[$attributeCode]->backend_type ?? 'varchar'; + $type = AttributeModel::getCached()[$attributeCode]->backend_type ?? 'varchar'; $relation = match ($type) { 'datetime' => 'attributeDateTime', default => 'attribute' . ucfirst($type), From 9184a383b144a3dd921a97034cb4c82987939d42 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Thu, 24 Jul 2025 11:30:38 +0000 Subject: [PATCH 09/45] Apply fixes from Duster --- src/Models/Traits/HasCustomAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 8602888c4..91e6ea7c0 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -5,12 +5,12 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; +use Rapidez\Core\Models\Attribute as AttributeModel; use Rapidez\Core\Models\AttributeDateTime; use Rapidez\Core\Models\AttributeDecimal; use Rapidez\Core\Models\AttributeInt; use Rapidez\Core\Models\AttributeText; use Rapidez\Core\Models\AttributeVarchar; -use Rapidez\Core\Models\Attribute as AttributeModel; trait HasCustomAttributes { From 0cda5c092dc7512e987e75a26e0fd75b61f19509 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 14:03:48 +0200 Subject: [PATCH 10/45] Add stock data to products --- config/rapidez/magento-defaults.php | 3 +++ config/rapidez/system.php | 17 +++++++++++++++++ src/Models/ProductEntity.php | 12 ++++++++++++ src/Models/ProductStock.php | 18 ++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/config/rapidez/magento-defaults.php b/config/rapidez/magento-defaults.php index ae516fcc5..b7a45b148 100644 --- a/config/rapidez/magento-defaults.php +++ b/config/rapidez/magento-defaults.php @@ -9,6 +9,9 @@ 'catalog/seo/category_url_suffix' => '.html', 'catalog/seo/product_url_suffix' => '.html', 'cataloginventory/item_options/backorders' => '0', + 'cataloginventory/item_options/min_sale_qty' => '1', + 'cataloginventory/item_options/max_sale_qty' => '10000', + 'cataloginventory/item_options/qty_increments' => '1', 'cataloginventory/options/show_out_of_stock' => '0', 'checkout/cart/redirect_to_cart' => '0', 'currency/options/default' => 'USD', diff --git a/config/rapidez/system.php b/config/rapidez/system.php index 09db30437..fcb9eb1bd 100644 --- a/config/rapidez/system.php +++ b/config/rapidez/system.php @@ -25,6 +25,23 @@ // Should the stock qty be exposed and indexed within Elasticsearch? 'expose_stock' => false, + // Which columns in the `cataloginventory_stock_item` table should be exposed? + // Add `qty` to this to expose the current stock to the frontend + 'exposed_stock_columns' => [ + 'min_sale_qty', + 'max_sale_qty', + 'qty_increments', + 'backorders', + + // These are necessary to make sure we use the config value when available + 'use_config_min_sale_qty', + 'use_config_max_sale_qty', + 'use_config_qty_increments', + 'use_config_backorders', + + 'is_in_stock', + ], + 'standalone_checkout' => [ // What cache store should be used to store temporary standalone checkout credentials 'cache_store' => env('STANDALONE_CHECKOUT_CACHE_STORE', config('cache.default')), diff --git a/src/Models/ProductEntity.php b/src/Models/ProductEntity.php index 61af8818d..50af915f6 100644 --- a/src/Models/ProductEntity.php +++ b/src/Models/ProductEntity.php @@ -3,6 +3,7 @@ namespace Rapidez\Core\Models; use Illuminate\Contracts\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; use Rapidez\Core\Models\Traits\HasCustomAttributes; @@ -19,6 +20,8 @@ class ProductEntity extends Model self::CREATED_AT => 'datetime', ]; + protected $with = ['stock']; + protected static function boot(): void { parent::boot(); @@ -38,6 +41,15 @@ public function gallery(): BelongsToMany ); } + public function stock(): BelongsTo + { + return $this->belongsTo( + ProductStock::class, + 'entity_id', + 'product_id', + ); + } + public function getAttribute($key) { if (! $key) { diff --git a/src/Models/ProductStock.php b/src/Models/ProductStock.php index 2a1a85700..32dc33263 100644 --- a/src/Models/ProductStock.php +++ b/src/Models/ProductStock.php @@ -2,9 +2,27 @@ namespace Rapidez\Core\Models; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Rapidez\Core\Facades\Rapidez; class ProductStock extends Model { protected $table = 'cataloginventory_stock_item'; + + public static function boot() + { + parent::boot(); + + static::addGlobalScope('onlyExposedColumns', fn(Builder $builder) => $builder->select(['product_id', ...config('rapidez.exposed_stock_columns')])); + } + + public function __get($key) + { + if ($this->hasAttribute('use_config_' . $key) && $this->getAttribute('use_config_' . $key) == 1) { + return Rapidez::config('cataloginventory/item_options/' . $key); + } + + return parent::__get($key); + } } From 9766b9535cace68596e96dfff61c6408e205d641 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Thu, 24 Jul 2025 12:04:12 +0000 Subject: [PATCH 11/45] Apply fixes from Duster --- src/Models/ProductStock.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/ProductStock.php b/src/Models/ProductStock.php index 32dc33263..96bf90a7d 100644 --- a/src/Models/ProductStock.php +++ b/src/Models/ProductStock.php @@ -14,7 +14,7 @@ public static function boot() { parent::boot(); - static::addGlobalScope('onlyExposedColumns', fn(Builder $builder) => $builder->select(['product_id', ...config('rapidez.exposed_stock_columns')])); + static::addGlobalScope('onlyExposedColumns', fn (Builder $builder) => $builder->select(['product_id', ...config('rapidez.exposed_stock_columns')])); } public function __get($key) From dbe6403b3d1a31e720b88634c66d8139e097d46f Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 14:48:44 +0200 Subject: [PATCH 12/45] Rename Datetime --- src/Models/AttributeDateTime.php | 2 +- src/Models/AttributeDatetime.php | 8 ++++++++ src/Models/Traits/HasCustomAttributes.php | 20 +++++++++----------- 3 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 src/Models/AttributeDatetime.php diff --git a/src/Models/AttributeDateTime.php b/src/Models/AttributeDateTime.php index 28c799116..f174bce10 100644 --- a/src/Models/AttributeDateTime.php +++ b/src/Models/AttributeDateTime.php @@ -2,7 +2,7 @@ namespace Rapidez\Core\Models; -class AttributeDateTime extends AbstractAttribute +class AttributeDatetime extends AbstractAttribute { protected $casts = ['value' => 'datetime']; } diff --git a/src/Models/AttributeDatetime.php b/src/Models/AttributeDatetime.php new file mode 100644 index 000000000..f174bce10 --- /dev/null +++ b/src/Models/AttributeDatetime.php @@ -0,0 +1,8 @@ + 'datetime']; +} diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 91e6ea7c0..a90664651 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; use Rapidez\Core\Models\Attribute as AttributeModel; -use Rapidez\Core\Models\AttributeDateTime; +use Rapidez\Core\Models\AttributeDatetime; use Rapidez\Core\Models\AttributeDecimal; use Rapidez\Core\Models\AttributeInt; use Rapidez\Core\Models\AttributeText; @@ -17,7 +17,7 @@ trait HasCustomAttributes public function scopeWithCustomAttributes(Builder $builder) { $builder->with([ - 'attributeDateTime', + 'attributeDatetime', 'attributeDecimal', 'attributeInt', 'attributeText', @@ -28,19 +28,17 @@ public function scopeWithCustomAttributes(Builder $builder) public function scopeWhereValue(Builder $builder, string $attributeCode, $operator = null, $value = null) { $type = AttributeModel::getCached()[$attributeCode]->backend_type ?? 'varchar'; - $relation = match ($type) { - 'datetime' => 'attributeDateTime', - default => 'attribute' . ucfirst($type), - }; - return $builder->whereHas($relation, fn ($query) => $query->where('value', $operator, $value)->where('attribute_code', $attributeCode) + return $builder->whereHas( + 'attribute' . ucfirst($type), + fn ($query) => $query->where('value', $operator, $value)->where('attribute_code', $attributeCode), ); } - public function attributeDateTime(): HasMany + public function attributeDatetime(): HasMany { return $this->hasManyWithAttributeTypeTable( - AttributeDateTime::class, + AttributeDatetime::class, 'datetime', 'entity_id', 'entity_id', @@ -101,12 +99,12 @@ public function hasManyWithAttributeTypeTable($class, $type, $foreignKey = null, public function customAttributes(): Attribute { return Attribute::get(function () { - if (@! $this->attributeDateTime) { + if (@! $this->attributeDatetime) { return collect(); } return collect() - ->concat($this->attributeDateTime) + ->concat($this->attributeDatetime) ->concat($this->attributeDecimal) ->concat($this->attributeInt) ->concat($this->attributeText) From e6b9338d0e89ee229fa57971df44c44bd716ac32 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 14:49:48 +0200 Subject: [PATCH 13/45] Delete src/Models/AttributeDateTime.php --- src/Models/AttributeDateTime.php | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/Models/AttributeDateTime.php diff --git a/src/Models/AttributeDateTime.php b/src/Models/AttributeDateTime.php deleted file mode 100644 index f174bce10..000000000 --- a/src/Models/AttributeDateTime.php +++ /dev/null @@ -1,8 +0,0 @@ - 'datetime']; -} From 22b4949eda61d19b31e4b08108f95497792c7503 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 15:05:15 +0200 Subject: [PATCH 14/45] Cast value properly when getting --- src/Models/AbstractAttribute.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Models/AbstractAttribute.php b/src/Models/AbstractAttribute.php index 60de699a7..55498938d 100644 --- a/src/Models/AbstractAttribute.php +++ b/src/Models/AbstractAttribute.php @@ -35,7 +35,7 @@ protected function value(): Attribute { return Attribute::get(function ($value) { if ($this->frontend_input === 'select') { - return $this->options[$value]?->value ?? $value; + return $this->options[$value]?->value ?? $this->castAttribute('value', $value);; } $class = config('rapidez.attribute-models')[$this->backend_model] ?? null; @@ -43,7 +43,7 @@ protected function value(): Attribute return $class::value($value, $this); } - return $value; + return $this->castAttribute('value', $value); }); } } From 521423d76a5de41d2d494666cc08546f2597dcda Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Thu, 24 Jul 2025 13:05:39 +0000 Subject: [PATCH 15/45] Apply fixes from Duster --- src/Models/AbstractAttribute.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Models/AbstractAttribute.php b/src/Models/AbstractAttribute.php index 55498938d..2240d6a8c 100644 --- a/src/Models/AbstractAttribute.php +++ b/src/Models/AbstractAttribute.php @@ -35,7 +35,7 @@ protected function value(): Attribute { return Attribute::get(function ($value) { if ($this->frontend_input === 'select') { - return $this->options[$value]?->value ?? $this->castAttribute('value', $value);; + return $this->options[$value]?->value ?? $this->castAttribute('value', $value); } $class = config('rapidez.attribute-models')[$this->backend_model] ?? null; From b6fef4c15470ab90710d115c70ad0093c3c96e3f Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 15:46:40 +0200 Subject: [PATCH 16/45] Small fixes --- src/Models/AbstractAttribute.php | 4 ++-- src/Models/Scopes/Product/ForCurrentWebsiteScope.php | 2 +- src/Models/Traits/HasCustomAttributes.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Models/AbstractAttribute.php b/src/Models/AbstractAttribute.php index 2240d6a8c..1209b7c79 100644 --- a/src/Models/AbstractAttribute.php +++ b/src/Models/AbstractAttribute.php @@ -35,7 +35,7 @@ protected function value(): Attribute { return Attribute::get(function ($value) { if ($this->frontend_input === 'select') { - return $this->options[$value]?->value ?? $this->castAttribute('value', $value); + return $this->options[$value]?->value ?? $value; } $class = config('rapidez.attribute-models')[$this->backend_model] ?? null; @@ -43,7 +43,7 @@ protected function value(): Attribute return $class::value($value, $this); } - return $this->castAttribute('value', $value); + return array_key_exists('value', $this->getCasts()) ? $this->castAttribute('value', $value) : $value; }); } } diff --git a/src/Models/Scopes/Product/ForCurrentWebsiteScope.php b/src/Models/Scopes/Product/ForCurrentWebsiteScope.php index 15a051940..3860b7a7c 100644 --- a/src/Models/Scopes/Product/ForCurrentWebsiteScope.php +++ b/src/Models/Scopes/Product/ForCurrentWebsiteScope.php @@ -10,7 +10,7 @@ class ForCurrentWebsiteScope implements Scope { public function apply(Builder $builder, Model $model) { - $builder->leftJoin('catalog_product_website', 'product_id', '=', $model->getQualifiedKeyName()); + $builder->leftJoin('catalog_product_website', 'catalog_product_website.product_id', '=', $model->getQualifiedKeyName()); $builder->where('website_id', config('rapidez.website')); } } diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index a90664651..a3869aeae 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -99,7 +99,7 @@ public function hasManyWithAttributeTypeTable($class, $type, $foreignKey = null, public function customAttributes(): Attribute { return Attribute::get(function () { - if (@! $this->attributeDatetime) { + if (!$this->relationLoaded('attributeDatetime')) { return collect(); } From 24c785d851a75400709fe6a541ceda60ac01f5f1 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 15:52:35 +0200 Subject: [PATCH 17/45] Add parent, children, super attributes & super attribute values --- src/Models/ProductEntity.php | 49 ++++++++++++++++++++++++++++++++++- src/Models/SuperAttribute.php | 20 ++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/Models/SuperAttribute.php diff --git a/src/Models/ProductEntity.php b/src/Models/ProductEntity.php index 50af915f6..d0243d457 100644 --- a/src/Models/ProductEntity.php +++ b/src/Models/ProductEntity.php @@ -3,8 +3,12 @@ namespace Rapidez\Core\Models; use Illuminate\Contracts\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; use Rapidez\Core\Models\Traits\HasCustomAttributes; @@ -20,7 +24,7 @@ class ProductEntity extends Model self::CREATED_AT => 'datetime', ]; - protected $with = ['stock']; + protected $with = ['stock', 'superAttributes']; protected static function boot(): void { @@ -41,6 +45,26 @@ public function gallery(): BelongsToMany ); } + public function parent(): HasOneThrough + { + return $this->hasOneThrough( + ProductEntity::class, + config('rapidez.models.product_link'), + 'product_id', 'entity_id', + 'entity_id', 'parent_id' + ); + } + + public function children(): HasManyThrough + { + return $this->hasManyThrough( + ProductEntity::class, + config('rapidez.models.product_link'), + 'parent_id', 'entity_id', + 'entity_id', 'product_id' + ); + } + public function stock(): BelongsTo { return $this->belongsTo( @@ -50,6 +74,29 @@ public function stock(): BelongsTo ); } + public function superAttributes(): HasMany + { + return $this->hasMany( + SuperAttribute::class, + 'product_id', + )->orderBy('position'); + } + + public function superAttributeValues(): Attribute + { + return Attribute::get(function() { + return $this->superAttributes->pluck('attribute_code') + ->mapWithKeys(fn ($attribute) => [ + $attribute => $this->children->mapWithKeys(function ($child) use ($attribute) { + return [$child->entity_id => [ + 'label' => $child->{$attribute}, + 'value' => $child->customAttributes[$attribute]->value_id, + ]]; + }) + ]); + }); + } + public function getAttribute($key) { if (! $key) { diff --git a/src/Models/SuperAttribute.php b/src/Models/SuperAttribute.php new file mode 100644 index 000000000..3f3e35372 --- /dev/null +++ b/src/Models/SuperAttribute.php @@ -0,0 +1,20 @@ +leftJoin('eav_attribute', $builder->qualifyColumn('attribute_id'), '=', 'eav_attribute.attribute_id'); + }); + } +} From b72b189d7880b799e46a69de0122b974908c7c7a Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Thu, 24 Jul 2025 13:53:13 +0000 Subject: [PATCH 18/45] Apply fixes from Duster --- src/Models/ProductEntity.php | 4 ++-- src/Models/Traits/HasCustomAttributes.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Models/ProductEntity.php b/src/Models/ProductEntity.php index d0243d457..6ff461fe6 100644 --- a/src/Models/ProductEntity.php +++ b/src/Models/ProductEntity.php @@ -84,7 +84,7 @@ public function superAttributes(): HasMany public function superAttributeValues(): Attribute { - return Attribute::get(function() { + return Attribute::get(function () { return $this->superAttributes->pluck('attribute_code') ->mapWithKeys(fn ($attribute) => [ $attribute => $this->children->mapWithKeys(function ($child) use ($attribute) { @@ -92,7 +92,7 @@ public function superAttributeValues(): Attribute 'label' => $child->{$attribute}, 'value' => $child->customAttributes[$attribute]->value_id, ]]; - }) + }), ]); }); } diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index a3869aeae..53dd5d654 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -99,7 +99,7 @@ public function hasManyWithAttributeTypeTable($class, $type, $foreignKey = null, public function customAttributes(): Attribute { return Attribute::get(function () { - if (!$this->relationLoaded('attributeDatetime')) { + if (! $this->relationLoaded('attributeDatetime')) { return collect(); } From f5fcd829258b9db59e9fb0893d14cfa00a573321 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 16:17:33 +0200 Subject: [PATCH 19/45] Add consts --- src/Models/ProductEntity.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Models/ProductEntity.php b/src/Models/ProductEntity.php index 6ff461fe6..b46897cb8 100644 --- a/src/Models/ProductEntity.php +++ b/src/Models/ProductEntity.php @@ -16,6 +16,14 @@ class ProductEntity extends Model { use HasCustomAttributes; + public const VISIBILITY_NOT_VISIBLE = 1; + public const VISIBILITY_IN_CATALOG = 2; + public const VISIBILITY_IN_SEARCH = 3; + public const VISIBILITY_BOTH = 4; + + public const STATUS_ENABLED = 1; + public const STATUS_DISABLED = 2; + protected $table = 'catalog_product_entity'; protected $primaryKey = 'entity_id'; @@ -32,7 +40,7 @@ protected static function boot(): void static::addGlobalScope(ForCurrentWebsiteScope::class); static::addGlobalScope('customAttributes', fn (Builder $builder) => $builder->withCustomAttributes()); - static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereValue('status', 1)); + static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereValue('status', static::STATUS_ENABLED)); } public function gallery(): BelongsToMany From 1ec5263f81b94bd0c46c24417cbcd50c99af0ae1 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 24 Jul 2025 16:40:56 +0200 Subject: [PATCH 20/45] Fix sorting on super attributes & change whereValue to whereAttribute --- src/Models/AbstractAttribute.php | 8 ++++++++ src/Models/ProductEntity.php | 12 +++++++----- src/Models/Traits/HasCustomAttributes.php | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Models/AbstractAttribute.php b/src/Models/AbstractAttribute.php index 1209b7c79..36f17dbd3 100644 --- a/src/Models/AbstractAttribute.php +++ b/src/Models/AbstractAttribute.php @@ -46,4 +46,12 @@ protected function value(): Attribute return array_key_exists('value', $this->getCasts()) ? $this->castAttribute('value', $value) : $value; }); } + + protected function sortOrder(): Attribute + { + return Attribute::get(function() { + $value = $this->getAttributeFromArray('value'); + return $this->options[$value]->sort_order ?? null; + }); + } } diff --git a/src/Models/ProductEntity.php b/src/Models/ProductEntity.php index b46897cb8..2bd0c7744 100644 --- a/src/Models/ProductEntity.php +++ b/src/Models/ProductEntity.php @@ -40,7 +40,7 @@ protected static function boot(): void static::addGlobalScope(ForCurrentWebsiteScope::class); static::addGlobalScope('customAttributes', fn (Builder $builder) => $builder->withCustomAttributes()); - static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereValue('status', static::STATUS_ENABLED)); + static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereAttribute('status', static::STATUS_ENABLED)); } public function gallery(): BelongsToMany @@ -93,12 +93,14 @@ public function superAttributes(): HasMany public function superAttributeValues(): Attribute { return Attribute::get(function () { - return $this->superAttributes->pluck('attribute_code') + return $this->superAttributes + ->sortBy('position') ->mapWithKeys(fn ($attribute) => [ - $attribute => $this->children->mapWithKeys(function ($child) use ($attribute) { + $attribute->attribute_code => $this->children->mapWithKeys(function ($child) use ($attribute) { return [$child->entity_id => [ - 'label' => $child->{$attribute}, - 'value' => $child->customAttributes[$attribute]->value_id, + 'label' => $child->{$attribute->attribute_code}, + 'value' => $child->customAttributes[$attribute->attribute_code]->value_id, + 'sort_order' => $child->customAttributes[$attribute->attribute_code]->sort_order, ]]; }), ]); diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 53dd5d654..81dd6077d 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -25,7 +25,7 @@ public function scopeWithCustomAttributes(Builder $builder) ]); } - public function scopeWhereValue(Builder $builder, string $attributeCode, $operator = null, $value = null) + public function scopeWhereAttribute(Builder $builder, string $attributeCode, $operator = null, $value = null) { $type = AttributeModel::getCached()[$attributeCode]->backend_type ?? 'varchar'; From 8594af01d6336265df3c72fa250f37b3f8a83f84 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Thu, 24 Jul 2025 14:41:20 +0000 Subject: [PATCH 21/45] Apply fixes from Duster --- src/Models/AbstractAttribute.php | 3 ++- src/Models/ProductEntity.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Models/AbstractAttribute.php b/src/Models/AbstractAttribute.php index 36f17dbd3..5bbbcad26 100644 --- a/src/Models/AbstractAttribute.php +++ b/src/Models/AbstractAttribute.php @@ -49,8 +49,9 @@ protected function value(): Attribute protected function sortOrder(): Attribute { - return Attribute::get(function() { + return Attribute::get(function () { $value = $this->getAttributeFromArray('value'); + return $this->options[$value]->sort_order ?? null; }); } diff --git a/src/Models/ProductEntity.php b/src/Models/ProductEntity.php index 2bd0c7744..5016873c9 100644 --- a/src/Models/ProductEntity.php +++ b/src/Models/ProductEntity.php @@ -98,8 +98,8 @@ public function superAttributeValues(): Attribute ->mapWithKeys(fn ($attribute) => [ $attribute->attribute_code => $this->children->mapWithKeys(function ($child) use ($attribute) { return [$child->entity_id => [ - 'label' => $child->{$attribute->attribute_code}, - 'value' => $child->customAttributes[$attribute->attribute_code]->value_id, + 'label' => $child->{$attribute->attribute_code}, + 'value' => $child->customAttributes[$attribute->attribute_code]->value_id, 'sort_order' => $child->customAttributes[$attribute->attribute_code]->sort_order, ]]; }), From 46f18549d0da8ee5019e739ae43890655bb84bc2 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 6 Aug 2025 10:49:15 +0200 Subject: [PATCH 22/45] Rename product, use config rapidez.model, add callbacks, abstractify the whereHas --- src/Models/Product.php | 295 ++++----------------- src/Models/ProductEntity.php | 122 --------- src/Models/ProductOld.php | 305 ++++++++++++++++++++++ src/Models/Traits/HasCustomAttributes.php | 48 ++-- 4 files changed, 394 insertions(+), 376 deletions(-) delete mode 100644 src/Models/ProductEntity.php create mode 100644 src/Models/ProductOld.php diff --git a/src/Models/Product.php b/src/Models/Product.php index 9c26ea97f..39d0f6d91 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -2,46 +2,29 @@ namespace Rapidez\Core\Models; -use Illuminate\Database\Eloquent\Builder; +use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasOneThrough; -use Rapidez\Core\Casts\Children; -use Rapidez\Core\Casts\CommaSeparatedToArray; -use Rapidez\Core\Casts\CommaSeparatedToIntegerArray; -use Rapidez\Core\Casts\DecodeHtmlEntities; -use Rapidez\Core\Facades\Rapidez; -use Rapidez\Core\Models\Scopes\Product\WithProductAttributesScope; -use Rapidez\Core\Models\Scopes\Product\WithProductCategoryInfoScope; -use Rapidez\Core\Models\Scopes\Product\WithProductChildrenScope; -use Rapidez\Core\Models\Scopes\Product\WithProductGroupedScope; -use Rapidez\Core\Models\Scopes\Product\WithProductRelationIdsScope; -use Rapidez\Core\Models\Scopes\Product\WithProductStockScope; -use Rapidez\Core\Models\Scopes\Product\WithProductSuperAttributesScope; -use Rapidez\Core\Models\Traits\HasAlternatesThroughRewrites; -use Rapidez\Core\Models\Traits\Product\CastMultiselectAttributes; -use Rapidez\Core\Models\Traits\Product\CastSuperAttributes; -use Rapidez\Core\Models\Traits\Product\Searchable; -use Rapidez\Core\Models\Traits\Product\SelectAttributeScopes; -use TorMorten\Eventy\Facades\Eventy; +use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; +use Rapidez\Core\Models\Traits\HasCustomAttributes; class Product extends Model { - use CastMultiselectAttributes; - use CastSuperAttributes; - use HasAlternatesThroughRewrites; - use Searchable; - use SelectAttributeScopes; + use HasCustomAttributes; public const VISIBILITY_NOT_VISIBLE = 1; public const VISIBILITY_IN_CATALOG = 2; public const VISIBILITY_IN_SEARCH = 3; public const VISIBILITY_BOTH = 4; - public array $attributesToSelect = []; + public const STATUS_ENABLED = 1; + public const STATUS_DISABLED = 2; + protected $table = 'catalog_product_entity'; protected $primaryKey = 'entity_id'; protected $casts = [ @@ -49,53 +32,15 @@ class Product extends Model self::CREATED_AT => 'datetime', ]; - protected $appends = ['url']; + protected $with = ['stock', 'superAttributes']; - protected static function booting(): void + protected static function boot(): void { - static::addGlobalScope(new WithProductAttributesScope); - static::addGlobalScope(new WithProductSuperAttributesScope); - static::addGlobalScope(new WithProductStockScope); - static::addGlobalScope(new WithProductCategoryInfoScope); - static::addGlobalScope(new WithProductRelationIdsScope); - static::addGlobalScope(new WithProductChildrenScope); - static::addGlobalScope(new WithProductGroupedScope); - static::addGlobalScope('defaults', function (Builder $builder) { - $builder - ->whereNotIn($builder->getQuery()->from . '.type_id', ['bundle']) - ->groupBy($builder->getQuery()->from . '.entity_id'); - }); - } - - public function getTable(): string - { - return 'catalog_product_flat_' . config('rapidez.store'); - } + parent::boot(); - public function getCasts(): array - { - if (! isset($this->casts['name'])) { - $this->casts = array_merge( - parent::getCasts(), - [ - 'name' => DecodeHtmlEntities::class, - 'category_ids' => CommaSeparatedToIntegerArray::class, - 'category_paths' => CommaSeparatedToArray::class, - 'relation_ids' => CommaSeparatedToIntegerArray::class, - 'upsell_ids' => CommaSeparatedToIntegerArray::class, - 'children' => Children::class, - 'grouped' => Children::class, - 'qty_increments' => 'int', - 'min_sale_qty' => 'int', - 'max_sale_qty' => 'int', - ], - $this->getSuperAttributeCasts(), - $this->getMultiselectAttributeCasts(), - Eventy::filter(static::getModelName() . '.casts', []), - ); - } - - return $this->casts; + static::addGlobalScope(ForCurrentWebsiteScope::class); + static::addGlobalScope('customAttributes', fn (Builder $builder) => $builder->withCustomAttributes()); + static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereAttribute('status', static::STATUS_ENABLED)); } public function gallery(): BelongsToMany @@ -108,47 +53,6 @@ public function gallery(): BelongsToMany ); } - public function views(): HasMany - { - return $this->hasMany( - config('rapidez.models.product_view'), - 'product_id', - ); - } - - public function options(): HasMany - { - return $this->hasMany( - config('rapidez.models.product_option'), - 'product_id', - ); - } - - public function categoryProducts(): HasMany - { - return $this - ->hasMany( - config('rapidez.models.category_product'), - 'product_id', - ); - } - - public function reviewSummary(): HasOne - { - return $this->hasOne( - config('rapidez.models.product_review_summary', \Rapidez\Core\Models\ProductReviewSummary::class), - 'entity_pk_value' - ); - } - - public function rewrites(): HasMany - { - return $this - ->hasMany(config('rapidez.models.rewrite'), 'entity_id') - ->withoutGlobalScope('store') - ->where('entity_type', 'product'); - } - public function parent(): HasOneThrough { return $this->hasOneThrough( @@ -156,150 +60,63 @@ public function parent(): HasOneThrough config('rapidez.models.product_link'), 'product_id', 'entity_id', 'entity_id', 'parent_id' - )->withoutGlobalScopes(); - } - - public function scopeByIds(Builder $query, array $productIds): Builder - { - return $query->whereIn($this->getQualifiedKeyName(), $productIds); - } - - protected function price(): Attribute - { - return Attribute::make( - get: function (?float $price): ?float { - if ($this->type_id == 'configurable') { - return collect($this->children)->min->price; - } - - if ($this->type_id == 'grouped') { - return collect($this->grouped)->min->price; - } - - return $price; - } - ); - } - - protected function specialPrice(): Attribute - { - return Attribute::make( - get: function (?float $specialPrice): ?float { - if (! in_array($this->type_id, ['configurable', 'grouped'])) { - if ($this->special_from_date && $this->special_from_date > now()->toDateTimeString()) { - return null; - } - - if ($this->special_to_date && $this->special_to_date < now()->toDateTimeString()) { - return null; - } - - return $specialPrice < $this->price ? $specialPrice : null; - } - - return collect($this->type_id == 'configurable' ? $this->children : $this->grouped)->filter(function ($child) { - if (! $child->special_price) { - return false; - } - - if (isset($child->special_from_date) && $child->special_from_date > now()->toDateTimeString()) { - return false; - } - - if (isset($child->special_to_date) && $child->special_to_date < now()->toDateTimeString()) { - return false; - } - - return true; - })->min->special_price; - } - ); - } - - protected function minSaleQty(): Attribute - { - return Attribute::make( - get: function (?int $minSaleQty): ?int { - if (! $this->qty_increments) { - return $minSaleQty; - } - $remainder = $minSaleQty % $this->qty_increments; - if ($remainder === 0) { - return $minSaleQty; - } - - return $minSaleQty - $remainder + $this->qty_increments; - } - ); - } - - protected function url(): Attribute - { - return Attribute::make( - get: fn (): string => '/' . ($this->url_key ? $this->url_key . Rapidez::config('catalog/seo/product_url_suffix') : 'catalog/product/view/id/' . $this->entity_id) ); } - protected function images(): Attribute + public function children(): HasManyThrough { - return Attribute::make( - get: fn (): array => $this->gallery->sortBy('productImageValue.position')->pluck('value')->toArray() - )->shouldCache(); - } - - private function getImageFrom(?string $image): ?string - { - return $image !== 'no_selection' ? $image : null; - } - - protected function image(): Attribute - { - return Attribute::make( - get: $this->getImageFrom(...) + return $this->hasManyThrough( + config('rapidez.models.product'), + config('rapidez.models.product_link'), + 'parent_id', 'entity_id', + 'entity_id', 'product_id' ); } - protected function smallImage(): Attribute + public function stock(): BelongsTo { - return Attribute::make( - get: $this->getImageFrom(...) + return $this->belongsTo( + ProductStock::class, + 'entity_id', + 'product_id', ); } - protected function thumbnail(): Attribute + public function superAttributes(): HasMany { - return Attribute::make( - get: $this->getImageFrom(...) - ); + return $this->hasMany( + SuperAttribute::class, + 'product_id', + )->orderBy('position'); + } + + public function superAttributeValues(): Attribute + { + return Attribute::get(function () { + return $this->superAttributes + ->sortBy('position') + ->mapWithKeys(fn ($attribute) => [ + $attribute->attribute_code => $this->children->mapWithKeys(function ($child) use ($attribute) { + return [$child->entity_id => [ + 'label' => $child->{$attribute->attribute_code}, + 'value' => $child->customAttributes[$attribute->attribute_code]->value_id, + 'sort_order' => $child->customAttributes[$attribute->attribute_code]->sort_order, + ]]; + }), + ]); + }); } - protected function breadcrumbCategories(): Attribute + public function getAttribute($key) { - return Attribute::make( - get: function (): iterable { - if (! $path = session('latest_category_path')) { - return []; - } - - $categoryIds = explode('/', $path); - $categoryIds = array_slice($categoryIds, array_search(config('rapidez.root_category_id'), $categoryIds) + 1); - - if (! in_array(end($categoryIds), $this->category_ids)) { - return []; - } - - $categoryModel = config('rapidez.models.category'); - $categoryTable = (new $categoryModel)->getTable(); + if (! $key) { + return; + } - return Category::whereIn($categoryTable . '.entity_id', $categoryIds) - ->orderByRaw('FIELD(' . $categoryTable . '.entity_id,' . implode(',', $categoryIds) . ')') - ->get(); - }, - )->shouldCache(); - } + if ($value = parent::getAttribute($key)) { + return $value; + } - public static function exist($productId): bool - { - return self::withoutGlobalScopes()->where('entity_id', $productId)->exists(); + return $this->getCustomAttribute($key)?->value; } } diff --git a/src/Models/ProductEntity.php b/src/Models/ProductEntity.php deleted file mode 100644 index 5016873c9..000000000 --- a/src/Models/ProductEntity.php +++ /dev/null @@ -1,122 +0,0 @@ - 'datetime', - self::CREATED_AT => 'datetime', - ]; - - protected $with = ['stock', 'superAttributes']; - - protected static function boot(): void - { - parent::boot(); - - static::addGlobalScope(ForCurrentWebsiteScope::class); - static::addGlobalScope('customAttributes', fn (Builder $builder) => $builder->withCustomAttributes()); - static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereAttribute('status', static::STATUS_ENABLED)); - } - - public function gallery(): BelongsToMany - { - return $this->belongsToMany( - config('rapidez.models.product_image'), - 'catalog_product_entity_media_gallery_value_to_entity', - 'entity_id', - 'value_id', - ); - } - - public function parent(): HasOneThrough - { - return $this->hasOneThrough( - ProductEntity::class, - config('rapidez.models.product_link'), - 'product_id', 'entity_id', - 'entity_id', 'parent_id' - ); - } - - public function children(): HasManyThrough - { - return $this->hasManyThrough( - ProductEntity::class, - config('rapidez.models.product_link'), - 'parent_id', 'entity_id', - 'entity_id', 'product_id' - ); - } - - public function stock(): BelongsTo - { - return $this->belongsTo( - ProductStock::class, - 'entity_id', - 'product_id', - ); - } - - public function superAttributes(): HasMany - { - return $this->hasMany( - SuperAttribute::class, - 'product_id', - )->orderBy('position'); - } - - public function superAttributeValues(): Attribute - { - return Attribute::get(function () { - return $this->superAttributes - ->sortBy('position') - ->mapWithKeys(fn ($attribute) => [ - $attribute->attribute_code => $this->children->mapWithKeys(function ($child) use ($attribute) { - return [$child->entity_id => [ - 'label' => $child->{$attribute->attribute_code}, - 'value' => $child->customAttributes[$attribute->attribute_code]->value_id, - 'sort_order' => $child->customAttributes[$attribute->attribute_code]->sort_order, - ]]; - }), - ]); - }); - } - - public function getAttribute($key) - { - if (! $key) { - return; - } - - if ($value = parent::getAttribute($key)) { - return $value; - } - - return $this->getCustomAttribute($key)?->value; - } -} diff --git a/src/Models/ProductOld.php b/src/Models/ProductOld.php new file mode 100644 index 000000000..6af1583ee --- /dev/null +++ b/src/Models/ProductOld.php @@ -0,0 +1,305 @@ + 'datetime', + self::CREATED_AT => 'datetime', + ]; + + protected $appends = ['url']; + + protected static function booting(): void + { + static::addGlobalScope(new WithProductAttributesScope); + static::addGlobalScope(new WithProductSuperAttributesScope); + static::addGlobalScope(new WithProductStockScope); + static::addGlobalScope(new WithProductCategoryInfoScope); + static::addGlobalScope(new WithProductRelationIdsScope); + static::addGlobalScope(new WithProductChildrenScope); + static::addGlobalScope(new WithProductGroupedScope); + static::addGlobalScope('defaults', function (Builder $builder) { + $builder + ->whereNotIn($builder->getQuery()->from . '.type_id', ['bundle']) + ->groupBy($builder->getQuery()->from . '.entity_id'); + }); + } + + public function getTable(): string + { + return 'catalog_product_flat_' . config('rapidez.store'); + } + + public function getCasts(): array + { + if (! isset($this->casts['name'])) { + $this->casts = array_merge( + parent::getCasts(), + [ + 'name' => DecodeHtmlEntities::class, + 'category_ids' => CommaSeparatedToIntegerArray::class, + 'category_paths' => CommaSeparatedToArray::class, + 'relation_ids' => CommaSeparatedToIntegerArray::class, + 'upsell_ids' => CommaSeparatedToIntegerArray::class, + 'children' => Children::class, + 'grouped' => Children::class, + 'qty_increments' => 'int', + 'min_sale_qty' => 'int', + 'max_sale_qty' => 'int', + ], + $this->getSuperAttributeCasts(), + $this->getMultiselectAttributeCasts(), + Eventy::filter(static::getModelName() . '.casts', []), + ); + } + + return $this->casts; + } + + public function gallery(): BelongsToMany + { + return $this->belongsToMany( + config('rapidez.models.product_image'), + 'catalog_product_entity_media_gallery_value_to_entity', + 'entity_id', + 'value_id', + ); + } + + public function views(): HasMany + { + return $this->hasMany( + config('rapidez.models.product_view'), + 'product_id', + ); + } + + public function options(): HasMany + { + return $this->hasMany( + config('rapidez.models.product_option'), + 'product_id', + ); + } + + public function categoryProducts(): HasMany + { + return $this + ->hasMany( + config('rapidez.models.category_product'), + 'product_id', + ); + } + + public function reviewSummary(): HasOne + { + return $this->hasOne( + config('rapidez.models.product_review_summary', \Rapidez\Core\Models\ProductReviewSummary::class), + 'entity_pk_value' + ); + } + + public function rewrites(): HasMany + { + return $this + ->hasMany(config('rapidez.models.rewrite'), 'entity_id') + ->withoutGlobalScope('store') + ->where('entity_type', 'product'); + } + + public function parent(): HasOneThrough + { + return $this->hasOneThrough( + config('rapidez.models.product'), + config('rapidez.models.product_link'), + 'product_id', 'entity_id', + 'entity_id', 'parent_id' + )->withoutGlobalScopes(); + } + + public function scopeByIds(Builder $query, array $productIds): Builder + { + return $query->whereIn($this->getQualifiedKeyName(), $productIds); + } + + protected function price(): Attribute + { + return Attribute::make( + get: function (?float $price): ?float { + if ($this->type_id == 'configurable') { + return collect($this->children)->min->price; + } + + if ($this->type_id == 'grouped') { + return collect($this->grouped)->min->price; + } + + return $price; + } + ); + } + + protected function specialPrice(): Attribute + { + return Attribute::make( + get: function (?float $specialPrice): ?float { + if (! in_array($this->type_id, ['configurable', 'grouped'])) { + if ($this->special_from_date && $this->special_from_date > now()->toDateTimeString()) { + return null; + } + + if ($this->special_to_date && $this->special_to_date < now()->toDateTimeString()) { + return null; + } + + return $specialPrice < $this->price ? $specialPrice : null; + } + + return collect($this->type_id == 'configurable' ? $this->children : $this->grouped)->filter(function ($child) { + if (! $child->special_price) { + return false; + } + + if (isset($child->special_from_date) && $child->special_from_date > now()->toDateTimeString()) { + return false; + } + + if (isset($child->special_to_date) && $child->special_to_date < now()->toDateTimeString()) { + return false; + } + + return true; + })->min->special_price; + } + ); + } + + protected function minSaleQty(): Attribute + { + return Attribute::make( + get: function (?int $minSaleQty): ?int { + if (! $this->qty_increments) { + return $minSaleQty; + } + $remainder = $minSaleQty % $this->qty_increments; + if ($remainder === 0) { + return $minSaleQty; + } + + return $minSaleQty - $remainder + $this->qty_increments; + } + ); + } + + protected function url(): Attribute + { + return Attribute::make( + get: fn (): string => '/' . ($this->url_key ? $this->url_key . Rapidez::config('catalog/seo/product_url_suffix') : 'catalog/product/view/id/' . $this->entity_id) + ); + } + + protected function images(): Attribute + { + return Attribute::make( + get: fn (): array => $this->gallery->sortBy('productImageValue.position')->pluck('value')->toArray() + )->shouldCache(); + } + + private function getImageFrom(?string $image): ?string + { + return $image !== 'no_selection' ? $image : null; + } + + protected function image(): Attribute + { + return Attribute::make( + get: $this->getImageFrom(...) + ); + } + + protected function smallImage(): Attribute + { + return Attribute::make( + get: $this->getImageFrom(...) + ); + } + + protected function thumbnail(): Attribute + { + return Attribute::make( + get: $this->getImageFrom(...) + ); + } + + protected function breadcrumbCategories(): Attribute + { + return Attribute::make( + get: function (): iterable { + if (! $path = session('latest_category_path')) { + return []; + } + + $categoryIds = explode('/', $path); + $categoryIds = array_slice($categoryIds, array_search(config('rapidez.root_category_id'), $categoryIds) + 1); + + if (! in_array(end($categoryIds), $this->category_ids)) { + return []; + } + + $categoryModel = config('rapidez.models.category'); + $categoryTable = (new $categoryModel)->getTable(); + + return Category::whereIn($categoryTable . '.entity_id', $categoryIds) + ->orderByRaw('FIELD(' . $categoryTable . '.entity_id,' . implode(',', $categoryIds) . ')') + ->get(); + }, + )->shouldCache(); + } + + public static function exist($productId): bool + { + return self::withoutGlobalScopes()->where('entity_id', $productId)->exists(); + } +} diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 81dd6077d..7d807203c 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -3,9 +3,9 @@ namespace Rapidez\Core\Models\Traits; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Casts\Attribute as AttributeCast; use Illuminate\Database\Eloquent\Relations\HasMany; -use Rapidez\Core\Models\Attribute as AttributeModel; +use Rapidez\Core\Models\Attribute; use Rapidez\Core\Models\AttributeDatetime; use Rapidez\Core\Models\AttributeDecimal; use Rapidez\Core\Models\AttributeInt; @@ -14,24 +14,42 @@ trait HasCustomAttributes { - public function scopeWithCustomAttributes(Builder $builder) + public function scopeWithCustomAttributes(Builder $builder, ?callable $callback = null) { - $builder->with([ - 'attributeDatetime', - 'attributeDecimal', - 'attributeInt', - 'attributeText', - 'attributeVarchar', - ]); + if ($callback) { + $builder->with([ + 'attributeDatetime' => $callback, + 'attributeDecimal' => $callback, + 'attributeInt' => $callback, + 'attributeText' => $callback, + 'attributeVarchar' => $callback, + ]); + } else { + $builder->with([ + 'attributeDatetime', + 'attributeDecimal', + 'attributeInt', + 'attributeText', + 'attributeVarchar', + ]); + } } - public function scopeWhereAttribute(Builder $builder, string $attributeCode, $operator = null, $value = null) + public function scopeAttributeHas(Builder $builder, string $attributeCode, callable $callback) { - $type = AttributeModel::getCached()[$attributeCode]->backend_type ?? 'varchar'; + $type = Attribute::getCached()[$attributeCode]->backend_type ?? 'varchar'; return $builder->whereHas( 'attribute' . ucfirst($type), - fn ($query) => $query->where('value', $operator, $value)->where('attribute_code', $attributeCode), + $callback, + ); + } + + public function scopeWhereAttribute(Builder $builder, string $attributeCode, $operator = null, $value = null) + { + return $builder->attributeHas( + $attributeCode, + fn ($query) => $query->where('value', $operator, $value)->where('attribute_code', $attributeCode) ); } @@ -96,9 +114,9 @@ public function hasManyWithAttributeTypeTable($class, $type, $foreignKey = null, return $relation; } - public function customAttributes(): Attribute + public function customAttributes(): AttributeCast { - return Attribute::get(function () { + return AttributeCast::get(function () { if (! $this->relationLoaded('attributeDatetime')) { return collect(); } From ab8aeda24c0a179de892351ef83126a096d80821 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 6 Aug 2025 10:51:00 +0200 Subject: [PATCH 23/45] Still return falsy values --- src/Models/Product.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index 39d0f6d91..bc9fe76ea 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -113,7 +113,8 @@ public function getAttribute($key) return; } - if ($value = parent::getAttribute($key)) { + $value = parent::getAttribute($key); + if ($value !== null) { return $value; } From 0bab9eb1c00073dba7fc28b7b15e3d858d59fcf8 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 6 Aug 2025 11:20:27 +0200 Subject: [PATCH 24/45] Remove old scopes, update HasAlternates to include rewrites relation --- src/Models/Category.php | 14 +- src/Models/Product.php | 24 + src/Models/ProductOld.php | 606 +++++++++--------- .../Product/WithProductAttributesScope.php | 93 --- .../Product/WithProductCategoryInfoScope.php | 24 - .../Product/WithProductChildrenScope.php | 62 -- .../Product/WithProductGroupedScope.php | 47 -- .../Product/WithProductRelationIdsScope.php | 23 - .../Scopes/Product/WithProductStockScope.php | 35 - .../WithProductSuperAttributesScope.php | 60 -- .../Traits/HasAlternatesThroughRewrites.php | 11 + .../Product/CastMultiselectAttributes.php | 23 - .../Traits/Product/CastSuperAttributes.php | 24 - .../Traits/Product/SelectAttributeScopes.php | 70 -- 14 files changed, 343 insertions(+), 773 deletions(-) delete mode 100644 src/Models/Scopes/Product/WithProductAttributesScope.php delete mode 100644 src/Models/Scopes/Product/WithProductCategoryInfoScope.php delete mode 100644 src/Models/Scopes/Product/WithProductChildrenScope.php delete mode 100644 src/Models/Scopes/Product/WithProductGroupedScope.php delete mode 100644 src/Models/Scopes/Product/WithProductRelationIdsScope.php delete mode 100644 src/Models/Scopes/Product/WithProductStockScope.php delete mode 100644 src/Models/Scopes/Product/WithProductSuperAttributesScope.php delete mode 100644 src/Models/Traits/Product/CastMultiselectAttributes.php delete mode 100644 src/Models/Traits/Product/CastSuperAttributes.php delete mode 100644 src/Models/Traits/Product/SelectAttributeScopes.php diff --git a/src/Models/Category.php b/src/Models/Category.php index d98c0a485..8df24d0d4 100644 --- a/src/Models/Category.php +++ b/src/Models/Category.php @@ -4,7 +4,6 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Rapidez\Core\Models\Scopes\IsActiveScope; use Rapidez\Core\Models\Traits\HasAlternatesThroughRewrites; @@ -58,6 +57,11 @@ protected static function booting() }); } + protected static function getEntityType(): string + { + return 'category'; + } + public function getTable() { return 'catalog_category_flat_store_' . config('rapidez.store'); @@ -90,14 +94,6 @@ public function products(): HasManyThrough ->whereIn((new (config('rapidez.models.category_product')))->qualifyColumn('visibility'), [2, 4]); } - public function rewrites(): HasMany - { - return $this - ->hasMany(config('rapidez.models.rewrite'), 'entity_id', 'entity_id') - ->withoutGlobalScope('store') - ->where('entity_type', 'category'); - } - protected function parentcategories(): Attribute { return Attribute::make( diff --git a/src/Models/Product.php b/src/Models/Product.php index bc9fe76ea..8a4f35c32 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -10,11 +10,13 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; +use Rapidez\Core\Models\Traits\HasAlternatesThroughRewrites; use Rapidez\Core\Models\Traits\HasCustomAttributes; class Product extends Model { use HasCustomAttributes; + use HasAlternatesThroughRewrites; public const VISIBILITY_NOT_VISIBLE = 1; public const VISIBILITY_IN_CATALOG = 2; @@ -43,6 +45,11 @@ protected static function boot(): void static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereAttribute('status', static::STATUS_ENABLED)); } + protected static function getEntityType(): string + { + return 'product'; + } + public function gallery(): BelongsToMany { return $this->belongsToMany( @@ -82,6 +89,23 @@ public function stock(): BelongsTo ); } + public function options(): HasMany + { + return $this->hasMany( + config('rapidez.models.product_option'), + 'product_id', + ); + } + + public function categoryProducts(): HasMany + { + return $this + ->hasMany( + config('rapidez.models.category_product'), + 'product_id', + ); + } + public function superAttributes(): HasMany { return $this->hasMany( diff --git a/src/Models/ProductOld.php b/src/Models/ProductOld.php index 6af1583ee..52a951181 100644 --- a/src/Models/ProductOld.php +++ b/src/Models/ProductOld.php @@ -1,305 +1,305 @@ 'datetime', - self::CREATED_AT => 'datetime', - ]; - - protected $appends = ['url']; - - protected static function booting(): void - { - static::addGlobalScope(new WithProductAttributesScope); - static::addGlobalScope(new WithProductSuperAttributesScope); - static::addGlobalScope(new WithProductStockScope); - static::addGlobalScope(new WithProductCategoryInfoScope); - static::addGlobalScope(new WithProductRelationIdsScope); - static::addGlobalScope(new WithProductChildrenScope); - static::addGlobalScope(new WithProductGroupedScope); - static::addGlobalScope('defaults', function (Builder $builder) { - $builder - ->whereNotIn($builder->getQuery()->from . '.type_id', ['bundle']) - ->groupBy($builder->getQuery()->from . '.entity_id'); - }); - } - - public function getTable(): string - { - return 'catalog_product_flat_' . config('rapidez.store'); - } - - public function getCasts(): array - { - if (! isset($this->casts['name'])) { - $this->casts = array_merge( - parent::getCasts(), - [ - 'name' => DecodeHtmlEntities::class, - 'category_ids' => CommaSeparatedToIntegerArray::class, - 'category_paths' => CommaSeparatedToArray::class, - 'relation_ids' => CommaSeparatedToIntegerArray::class, - 'upsell_ids' => CommaSeparatedToIntegerArray::class, - 'children' => Children::class, - 'grouped' => Children::class, - 'qty_increments' => 'int', - 'min_sale_qty' => 'int', - 'max_sale_qty' => 'int', - ], - $this->getSuperAttributeCasts(), - $this->getMultiselectAttributeCasts(), - Eventy::filter(static::getModelName() . '.casts', []), - ); - } - - return $this->casts; - } - - public function gallery(): BelongsToMany - { - return $this->belongsToMany( - config('rapidez.models.product_image'), - 'catalog_product_entity_media_gallery_value_to_entity', - 'entity_id', - 'value_id', - ); - } - - public function views(): HasMany - { - return $this->hasMany( - config('rapidez.models.product_view'), - 'product_id', - ); - } - - public function options(): HasMany - { - return $this->hasMany( - config('rapidez.models.product_option'), - 'product_id', - ); - } - - public function categoryProducts(): HasMany - { - return $this - ->hasMany( - config('rapidez.models.category_product'), - 'product_id', - ); - } - - public function reviewSummary(): HasOne - { - return $this->hasOne( - config('rapidez.models.product_review_summary', \Rapidez\Core\Models\ProductReviewSummary::class), - 'entity_pk_value' - ); - } - - public function rewrites(): HasMany - { - return $this - ->hasMany(config('rapidez.models.rewrite'), 'entity_id') - ->withoutGlobalScope('store') - ->where('entity_type', 'product'); - } - - public function parent(): HasOneThrough - { - return $this->hasOneThrough( - config('rapidez.models.product'), - config('rapidez.models.product_link'), - 'product_id', 'entity_id', - 'entity_id', 'parent_id' - )->withoutGlobalScopes(); - } - - public function scopeByIds(Builder $query, array $productIds): Builder - { - return $query->whereIn($this->getQualifiedKeyName(), $productIds); - } - - protected function price(): Attribute - { - return Attribute::make( - get: function (?float $price): ?float { - if ($this->type_id == 'configurable') { - return collect($this->children)->min->price; - } - - if ($this->type_id == 'grouped') { - return collect($this->grouped)->min->price; - } - - return $price; - } - ); - } - - protected function specialPrice(): Attribute - { - return Attribute::make( - get: function (?float $specialPrice): ?float { - if (! in_array($this->type_id, ['configurable', 'grouped'])) { - if ($this->special_from_date && $this->special_from_date > now()->toDateTimeString()) { - return null; - } - - if ($this->special_to_date && $this->special_to_date < now()->toDateTimeString()) { - return null; - } - - return $specialPrice < $this->price ? $specialPrice : null; - } - - return collect($this->type_id == 'configurable' ? $this->children : $this->grouped)->filter(function ($child) { - if (! $child->special_price) { - return false; - } - - if (isset($child->special_from_date) && $child->special_from_date > now()->toDateTimeString()) { - return false; - } - - if (isset($child->special_to_date) && $child->special_to_date < now()->toDateTimeString()) { - return false; - } - - return true; - })->min->special_price; - } - ); - } - - protected function minSaleQty(): Attribute - { - return Attribute::make( - get: function (?int $minSaleQty): ?int { - if (! $this->qty_increments) { - return $minSaleQty; - } - $remainder = $minSaleQty % $this->qty_increments; - if ($remainder === 0) { - return $minSaleQty; - } - - return $minSaleQty - $remainder + $this->qty_increments; - } - ); - } - - protected function url(): Attribute - { - return Attribute::make( - get: fn (): string => '/' . ($this->url_key ? $this->url_key . Rapidez::config('catalog/seo/product_url_suffix') : 'catalog/product/view/id/' . $this->entity_id) - ); - } - - protected function images(): Attribute - { - return Attribute::make( - get: fn (): array => $this->gallery->sortBy('productImageValue.position')->pluck('value')->toArray() - )->shouldCache(); - } - - private function getImageFrom(?string $image): ?string - { - return $image !== 'no_selection' ? $image : null; - } - - protected function image(): Attribute - { - return Attribute::make( - get: $this->getImageFrom(...) - ); - } - - protected function smallImage(): Attribute - { - return Attribute::make( - get: $this->getImageFrom(...) - ); - } - - protected function thumbnail(): Attribute - { - return Attribute::make( - get: $this->getImageFrom(...) - ); - } - - protected function breadcrumbCategories(): Attribute - { - return Attribute::make( - get: function (): iterable { - if (! $path = session('latest_category_path')) { - return []; - } - - $categoryIds = explode('/', $path); - $categoryIds = array_slice($categoryIds, array_search(config('rapidez.root_category_id'), $categoryIds) + 1); - - if (! in_array(end($categoryIds), $this->category_ids)) { - return []; - } - - $categoryModel = config('rapidez.models.category'); - $categoryTable = (new $categoryModel)->getTable(); - - return Category::whereIn($categoryTable . '.entity_id', $categoryIds) - ->orderByRaw('FIELD(' . $categoryTable . '.entity_id,' . implode(',', $categoryIds) . ')') - ->get(); - }, - )->shouldCache(); - } - - public static function exist($productId): bool - { - return self::withoutGlobalScopes()->where('entity_id', $productId)->exists(); - } -} +// namespace Rapidez\Core\Models; + +// use Illuminate\Database\Eloquent\Builder; +// use Illuminate\Database\Eloquent\Casts\Attribute; +// use Illuminate\Database\Eloquent\Relations\BelongsToMany; +// use Illuminate\Database\Eloquent\Relations\HasMany; +// use Illuminate\Database\Eloquent\Relations\HasOne; +// use Illuminate\Database\Eloquent\Relations\HasOneThrough; +// use Rapidez\Core\Casts\Children; +// use Rapidez\Core\Casts\CommaSeparatedToArray; +// use Rapidez\Core\Casts\CommaSeparatedToIntegerArray; +// use Rapidez\Core\Casts\DecodeHtmlEntities; +// use Rapidez\Core\Facades\Rapidez; +// use Rapidez\Core\Models\Scopes\Product\WithProductAttributesScope; +// use Rapidez\Core\Models\Scopes\Product\WithProductCategoryInfoScope; +// use Rapidez\Core\Models\Scopes\Product\WithProductChildrenScope; +// use Rapidez\Core\Models\Scopes\Product\WithProductGroupedScope; +// use Rapidez\Core\Models\Scopes\Product\WithProductRelationIdsScope; +// use Rapidez\Core\Models\Scopes\Product\WithProductStockScope; +// use Rapidez\Core\Models\Scopes\Product\WithProductSuperAttributesScope; +// use Rapidez\Core\Models\Traits\HasAlternatesThroughRewrites; +// use Rapidez\Core\Models\Traits\Product\CastMultiselectAttributes; +// use Rapidez\Core\Models\Traits\Product\CastSuperAttributes; +// use Rapidez\Core\Models\Traits\Product\Searchable; +// use Rapidez\Core\Models\Traits\Product\SelectAttributeScopes; +// use TorMorten\Eventy\Facades\Eventy; + +// class Product extends Model +// { +// use CastMultiselectAttributes; +// use CastSuperAttributes; +// use HasAlternatesThroughRewrites; +// use Searchable; +// use SelectAttributeScopes; + +// public const VISIBILITY_NOT_VISIBLE = 1; +// public const VISIBILITY_IN_CATALOG = 2; +// public const VISIBILITY_IN_SEARCH = 3; +// public const VISIBILITY_BOTH = 4; + +// public array $attributesToSelect = []; + +// protected $primaryKey = 'entity_id'; + +// protected $casts = [ +// self::UPDATED_AT => 'datetime', +// self::CREATED_AT => 'datetime', +// ]; + +// protected $appends = ['url']; + +// protected static function booting(): void +// { +// static::addGlobalScope(new WithProductAttributesScope); +// static::addGlobalScope(new WithProductSuperAttributesScope); +// static::addGlobalScope(new WithProductStockScope); +// static::addGlobalScope(new WithProductCategoryInfoScope); +// static::addGlobalScope(new WithProductRelationIdsScope); +// static::addGlobalScope(new WithProductChildrenScope); +// static::addGlobalScope(new WithProductGroupedScope); +// static::addGlobalScope('defaults', function (Builder $builder) { +// $builder +// ->whereNotIn($builder->getQuery()->from . '.type_id', ['bundle']) +// ->groupBy($builder->getQuery()->from . '.entity_id'); +// }); +// } + +// public function getTable(): string +// { +// return 'catalog_product_flat_' . config('rapidez.store'); +// } + +// public function getCasts(): array +// { +// if (! isset($this->casts['name'])) { +// $this->casts = array_merge( +// parent::getCasts(), +// [ +// 'name' => DecodeHtmlEntities::class, +// 'category_ids' => CommaSeparatedToIntegerArray::class, +// 'category_paths' => CommaSeparatedToArray::class, +// 'relation_ids' => CommaSeparatedToIntegerArray::class, +// 'upsell_ids' => CommaSeparatedToIntegerArray::class, +// 'children' => Children::class, +// 'grouped' => Children::class, +// 'qty_increments' => 'int', +// 'min_sale_qty' => 'int', +// 'max_sale_qty' => 'int', +// ], +// $this->getSuperAttributeCasts(), +// $this->getMultiselectAttributeCasts(), +// Eventy::filter(static::getModelName() . '.casts', []), +// ); +// } + +// return $this->casts; +// } + +// public function gallery(): BelongsToMany +// { +// return $this->belongsToMany( +// config('rapidez.models.product_image'), +// 'catalog_product_entity_media_gallery_value_to_entity', +// 'entity_id', +// 'value_id', +// ); +// } + +// public function views(): HasMany +// { +// return $this->hasMany( +// config('rapidez.models.product_view'), +// 'product_id', +// ); +// } + +// public function options(): HasMany +// { +// return $this->hasMany( +// config('rapidez.models.product_option'), +// 'product_id', +// ); +// } + +// public function categoryProducts(): HasMany +// { +// return $this +// ->hasMany( +// config('rapidez.models.category_product'), +// 'product_id', +// ); +// } + +// public function reviewSummary(): HasOne +// { +// return $this->hasOne( +// config('rapidez.models.product_review_summary', \Rapidez\Core\Models\ProductReviewSummary::class), +// 'entity_pk_value' +// ); +// } + +// public function rewrites(): HasMany +// { +// return $this +// ->hasMany(config('rapidez.models.rewrite'), 'entity_id') +// ->withoutGlobalScope('store') +// ->where('entity_type', 'product'); +// } + +// public function parent(): HasOneThrough +// { +// return $this->hasOneThrough( +// config('rapidez.models.product'), +// config('rapidez.models.product_link'), +// 'product_id', 'entity_id', +// 'entity_id', 'parent_id' +// )->withoutGlobalScopes(); +// } + +// public function scopeByIds(Builder $query, array $productIds): Builder +// { +// return $query->whereIn($this->getQualifiedKeyName(), $productIds); +// } + +// protected function price(): Attribute +// { +// return Attribute::make( +// get: function (?float $price): ?float { +// if ($this->type_id == 'configurable') { +// return collect($this->children)->min->price; +// } + +// if ($this->type_id == 'grouped') { +// return collect($this->grouped)->min->price; +// } + +// return $price; +// } +// ); +// } + +// protected function specialPrice(): Attribute +// { +// return Attribute::make( +// get: function (?float $specialPrice): ?float { +// if (! in_array($this->type_id, ['configurable', 'grouped'])) { +// if ($this->special_from_date && $this->special_from_date > now()->toDateTimeString()) { +// return null; +// } + +// if ($this->special_to_date && $this->special_to_date < now()->toDateTimeString()) { +// return null; +// } + +// return $specialPrice < $this->price ? $specialPrice : null; +// } + +// return collect($this->type_id == 'configurable' ? $this->children : $this->grouped)->filter(function ($child) { +// if (! $child->special_price) { +// return false; +// } + +// if (isset($child->special_from_date) && $child->special_from_date > now()->toDateTimeString()) { +// return false; +// } + +// if (isset($child->special_to_date) && $child->special_to_date < now()->toDateTimeString()) { +// return false; +// } + +// return true; +// })->min->special_price; +// } +// ); +// } + +// protected function minSaleQty(): Attribute +// { +// return Attribute::make( +// get: function (?int $minSaleQty): ?int { +// if (! $this->qty_increments) { +// return $minSaleQty; +// } +// $remainder = $minSaleQty % $this->qty_increments; +// if ($remainder === 0) { +// return $minSaleQty; +// } + +// return $minSaleQty - $remainder + $this->qty_increments; +// } +// ); +// } + +// protected function url(): Attribute +// { +// return Attribute::make( +// get: fn (): string => '/' . ($this->url_key ? $this->url_key . Rapidez::config('catalog/seo/product_url_suffix') : 'catalog/product/view/id/' . $this->entity_id) +// ); +// } + +// protected function images(): Attribute +// { +// return Attribute::make( +// get: fn (): array => $this->gallery->sortBy('productImageValue.position')->pluck('value')->toArray() +// )->shouldCache(); +// } + +// private function getImageFrom(?string $image): ?string +// { +// return $image !== 'no_selection' ? $image : null; +// } + +// protected function image(): Attribute +// { +// return Attribute::make( +// get: $this->getImageFrom(...) +// ); +// } + +// protected function smallImage(): Attribute +// { +// return Attribute::make( +// get: $this->getImageFrom(...) +// ); +// } + +// protected function thumbnail(): Attribute +// { +// return Attribute::make( +// get: $this->getImageFrom(...) +// ); +// } + +// protected function breadcrumbCategories(): Attribute +// { +// return Attribute::make( +// get: function (): iterable { +// if (! $path = session('latest_category_path')) { +// return []; +// } + +// $categoryIds = explode('/', $path); +// $categoryIds = array_slice($categoryIds, array_search(config('rapidez.root_category_id'), $categoryIds) + 1); + +// if (! in_array(end($categoryIds), $this->category_ids)) { +// return []; +// } + +// $categoryModel = config('rapidez.models.category'); +// $categoryTable = (new $categoryModel)->getTable(); + +// return Category::whereIn($categoryTable . '.entity_id', $categoryIds) +// ->orderByRaw('FIELD(' . $categoryTable . '.entity_id,' . implode(',', $categoryIds) . ')') +// ->get(); +// }, +// )->shouldCache(); +// } + +// public static function exist($productId): bool +// { +// return self::withoutGlobalScopes()->where('entity_id', $productId)->exists(); +// } +// } diff --git a/src/Models/Scopes/Product/WithProductAttributesScope.php b/src/Models/Scopes/Product/WithProductAttributesScope.php deleted file mode 100644 index 9ecb17389..000000000 --- a/src/Models/Scopes/Product/WithProductAttributesScope.php +++ /dev/null @@ -1,93 +0,0 @@ -addSelect([ - $model->getQualifiedKeyName(), - $model->qualifyColumn('sku'), - $model->qualifyColumn('visibility'), - $model->qualifyColumn('type_id'), - $model->getQualifiedCreatedAtColumn(), - $model->getQualifiedUpdatedAtColumn(), - ]); - - if (empty($model->attributesToSelect)) { - return; - } - - $attributeModel = config('rapidez.models.attribute'); - $attributes = $attributeModel::getCachedWhere(function ($attribute) use ($model) { - return in_array($attribute['code'], $model->attributesToSelect) - && ! in_array($attribute['code'], ['visibility', 'sku', 'type_id', $model->getKeyName()]); - }); - - $attributes = array_filter($attributes, fn ($a) => $a['type'] !== 'static'); - - $grammar = $builder->getQuery()->getGrammar(); - foreach ($attributes as $attribute) { - $attribute = (object) $attribute; - - if ($attribute->flat) { - if ($attribute->input === 'select' && $attribute->type === 'int' && ! in_array($attribute->source_model, [ - 'Magento\Tax\Model\TaxClass\Source\Product', - 'Magento\Eav\Model\Entity\Attribute\Source\Boolean', - ])) { - $builder->addSelect($builder->getQuery()->from . '.' . $attribute->code . '_value AS ' . $attribute->code); - } else { - $builder->addSelect($builder->getQuery()->from . '.' . $attribute->code . ' AS ' . $attribute->code); - } - } else { - if ($attribute->input === 'select') { - $builder - ->selectRaw('COALESCE(ANY_VALUE(' . $grammar->wrap($attribute->code . '_option_value_' . config('rapidez.store') . '.value') . '), ANY_VALUE(' . $grammar->wrap($attribute->code . '_option_value_0.value') . ')) AS ' . $grammar->wrap($attribute->code)) - ->leftJoin( - 'catalog_product_entity_' . $attribute->type . ' AS ' . $attribute->code, - function ($join) use ($builder, $attribute) { - $join->on($attribute->code . '.entity_id', '=', $builder->getQuery()->from . '.entity_id') - ->where($attribute->code . '.attribute_id', $attribute->id) - ->where($attribute->code . '.store_id', 0); - } - )->leftJoin( - 'eav_attribute_option_value AS ' . $attribute->code . '_option_value_' . config('rapidez.store'), - function ($join) use ($attribute) { - $join->on($attribute->code . '_option_value_' . config('rapidez.store') . '.option_id', '=', $attribute->code . '.value') - ->where($attribute->code . '_option_value_' . config('rapidez.store') . '.store_id', config('rapidez.store')); - } - )->leftJoin( - 'eav_attribute_option_value AS ' . $attribute->code . '_option_value_0', - function ($join) use ($attribute) { - $join->on($attribute->code . '_option_value_0.option_id', '=', $attribute->code . '.value') - ->where($attribute->code . '_option_value_0.store_id', 0); - } - ); - } else { - $builder - ->selectRaw('COALESCE(ANY_VALUE(' . $grammar->wrap($attribute->code . '_' . config('rapidez.store') . '.value') . '), ANY_VALUE(' . $grammar->wrap($attribute->code . '_0.value') . ')) AS ' . $grammar->wrap($attribute->code)) - ->leftJoin( - 'catalog_product_entity_' . $attribute->type . ' AS ' . $attribute->code . '_' . config('rapidez.store'), - function ($join) use ($builder, $attribute) { - $join->on($attribute->code . '_' . config('rapidez.store') . '.entity_id', '=', $builder->getQuery()->from . '.entity_id') - ->where($attribute->code . '_' . config('rapidez.store') . '.attribute_id', $attribute->id) - ->where($attribute->code . '_' . config('rapidez.store') . '.store_id', config('rapidez.store')); - } - )->leftJoin( - 'catalog_product_entity_' . $attribute->type . ' AS ' . $attribute->code . '_0', - function ($join) use ($builder, $attribute) { - $join->on($attribute->code . '_0.entity_id', '=', $builder->getQuery()->from . '.entity_id') - ->where($attribute->code . '_0.attribute_id', $attribute->id) - ->where($attribute->code . '_0.store_id', 0); - } - ); - } - } - } - } -} diff --git a/src/Models/Scopes/Product/WithProductCategoryInfoScope.php b/src/Models/Scopes/Product/WithProductCategoryInfoScope.php deleted file mode 100644 index c74749bd5..000000000 --- a/src/Models/Scopes/Product/WithProductCategoryInfoScope.php +++ /dev/null @@ -1,24 +0,0 @@ -selectRaw('GROUP_CONCAT(DISTINCT(category_id)) as category_ids') - ->selectRaw('GROUP_CONCAT(DISTINCT(catalog_category_flat_store_' . config('rapidez.store') . '.path)) as category_paths') - ->leftJoin('catalog_category_product_index_store' . config('rapidez.store'), 'catalog_category_product_index_store' . config('rapidez.store') . '.product_id', '=', $model->getTable() . '.entity_id') - ->leftJoin('catalog_category_flat_store_' . config('rapidez.store'), 'catalog_category_flat_store_' . config('rapidez.store') . '.entity_id', '=', 'catalog_category_product_index_store' . config('rapidez.store') . '.category_id'); - } -} diff --git a/src/Models/Scopes/Product/WithProductChildrenScope.php b/src/Models/Scopes/Product/WithProductChildrenScope.php deleted file mode 100644 index 69efda0a8..000000000 --- a/src/Models/Scopes/Product/WithProductChildrenScope.php +++ /dev/null @@ -1,62 +0,0 @@ -getQuery()->from; - $attributeModel = config('rapidez.models.attribute'); - - $superAttributes = Arr::pluck($attributeModel::getCachedWhere(function ($attribute) { - return $attribute['super'] && $attribute['flat']; - }), 'code'); - - $grammar = $builder->getQuery()->getGrammar(); - $superAttributesSelect = ''; - foreach ($superAttributes as $superAttribute) { - $superAttributesSelect .= '"' . $superAttribute . '", ' . $grammar->wrap('children.' . $superAttribute) . ','; - } - - $store = config('rapidez.store', 0); - $stockQty = config('rapidez.system.expose_stock') ? '"qty", children_stock.qty,' : ''; - - $builder - ->selectRaw('JSON_REMOVE(JSON_OBJECTAGG(IFNULL(children.entity_id, "null__"), JSON_OBJECT( - ' . Eventy::filter('product.children.select', <<leftJoin('catalog_product_super_link', 'catalog_product_super_link.parent_id', '=', $flat . '.entity_id') - ->leftJoin($flat . ' AS children', 'children.entity_id', '=', 'catalog_product_super_link.product_id') - ->leftJoin('cataloginventory_stock_item AS children_stock', 'children.entity_id', '=', 'children_stock.product_id'); - } -} diff --git a/src/Models/Scopes/Product/WithProductGroupedScope.php b/src/Models/Scopes/Product/WithProductGroupedScope.php deleted file mode 100644 index 84d5d19f0..000000000 --- a/src/Models/Scopes/Product/WithProductGroupedScope.php +++ /dev/null @@ -1,47 +0,0 @@ -selectRaw('JSON_REMOVE(JSON_OBJECTAGG(IFNULL(grouped.entity_id, "null__"), JSON_OBJECT( - ' . Eventy::filter('product.grouped.select', <<leftJoin('catalog_product_link AS grouped_link', function ($join) use ($model) { - $join->on('grouped_link.product_id', '=', $model->getTable() . '.entity_id') - ->where('grouped_link.link_type_id', 3); - }) - ->leftJoin($model->getTable() . ' as grouped', 'grouped_link.linked_product_id', '=', 'grouped.entity_id') - ->leftJoin('cataloginventory_stock_item AS grouped_stock', 'grouped.entity_id', '=', 'grouped_stock.product_id'); - } -} diff --git a/src/Models/Scopes/Product/WithProductRelationIdsScope.php b/src/Models/Scopes/Product/WithProductRelationIdsScope.php deleted file mode 100644 index 909488815..000000000 --- a/src/Models/Scopes/Product/WithProductRelationIdsScope.php +++ /dev/null @@ -1,23 +0,0 @@ - 'relation', 4 => 'upsell'] as $linkTypeId => $linkCode) { - $query = DB::table('catalog_product_link') - ->selectRaw('GROUP_CONCAT(linked_product_id)') - ->where('link_type_id', $linkTypeId) - ->whereColumn('product_id', $model->getTable() . '.entity_id'); - - $builder->selectSub($query, $linkCode . '_ids'); - } - } -} diff --git a/src/Models/Scopes/Product/WithProductStockScope.php b/src/Models/Scopes/Product/WithProductStockScope.php deleted file mode 100644 index d807394c8..000000000 --- a/src/Models/Scopes/Product/WithProductStockScope.php +++ /dev/null @@ -1,35 +0,0 @@ -selectRaw('ANY_VALUE(cataloginventory_stock_item.qty) AS qty'); - } - - $configBackorder = Rapidez::config('cataloginventory/item_options/backorders'); - - // TODO: These values should listen to: - // - use_config_min_sale_qty - // - use_config_max_sale_qty - // - use_config_enable_qty_inc - // - use_config_qty_increments - - $builder - ->selectRaw('ANY_VALUE(IF(cataloginventory_stock_item.use_config_backorders, ' . ($configBackorder ?: '0') . ', cataloginventory_stock_item.backorders)) as backorder_type') - ->selectRaw('ANY_VALUE(cataloginventory_stock_item.manage_stock) as manage_stock') - ->selectRaw('ANY_VALUE(cataloginventory_stock_item.min_sale_qty) as min_sale_qty') - ->selectRaw('ANY_VALUE(cataloginventory_stock_item.max_sale_qty) as max_sale_qty') - ->selectRaw('ANY_VALUE(cataloginventory_stock_item.is_in_stock) AS in_stock') - ->selectRaw('IF(ANY_VALUE(cataloginventory_stock_item.enable_qty_increments), ANY_VALUE(cataloginventory_stock_item.qty_increments), 1) AS qty_increments') - ->leftJoin('cataloginventory_stock_item', $model->getTable() . '.entity_id', '=', 'cataloginventory_stock_item.product_id'); - } -} diff --git a/src/Models/Scopes/Product/WithProductSuperAttributesScope.php b/src/Models/Scopes/Product/WithProductSuperAttributesScope.php deleted file mode 100644 index ebf371006..000000000 --- a/src/Models/Scopes/Product/WithProductSuperAttributesScope.php +++ /dev/null @@ -1,60 +0,0 @@ -getQuery()->getGrammar(); - foreach ($superAttributes as $superAttributeId => $superAttribute) { - $query = DB::table('catalog_product_super_link') - ->selectRaw('JSON_OBJECTAGG(' . $grammar->wrap($superAttribute) . ', JSON_OBJECT( - "sort_order", `option`.`sort_order`, - "label", ' . $grammar->wrap($superAttribute . '_value') . ', - "value", ' . $grammar->wrap($superAttribute) . ' - )) AS ' . $grammar->wrap($superAttribute)) - ->join($model->getTable() . ' AS children', 'children.entity_id', '=', 'catalog_product_super_link.product_id') - ->join('catalog_product_super_attribute', function ($join) use ($superAttributeId) { - $join->on('catalog_product_super_attribute.product_id', '=', 'catalog_product_super_link.parent_id') - ->where('attribute_id', $superAttributeId); - }) - ->join('eav_attribute_option AS option', 'option.option_id', '=', $superAttribute) - ->whereColumn('parent_id', $model->getTable() . '.entity_id') - ->whereNotNull($superAttribute); - - $builder->selectSub($query, 'super_' . $superAttribute); - } - - $query = DB::table('catalog_product_super_attribute') - ->selectRaw('JSON_OBJECTAGG(eav_attribute.attribute_id, JSON_OBJECT( - "code", `attribute_code`, - "label", COALESCE(NULLIF(`value`, ""), `frontend_label`), - "text_swatch", JSON_UNQUOTE(JSON_EXTRACT(IF(JSON_VALID(additional_data), additional_data, null), "$.swatch_input_type")) = "text", - "visual_swatch", JSON_UNQUOTE(JSON_EXTRACT(IF(JSON_VALID(additional_data), additional_data, null), "$.swatch_input_type")) = "visual", - "update_image", JSON_UNQUOTE(JSON_EXTRACT(IF(JSON_VALID(additional_data), additional_data, null), "$.update_product_preview_image")) = 1 - )) AS `super_attributes`') - ->join('eav_attribute', 'eav_attribute.attribute_id', '=', 'catalog_product_super_attribute.attribute_id') - ->join('catalog_eav_attribute', 'catalog_eav_attribute.attribute_id', '=', 'catalog_product_super_attribute.attribute_id') - ->leftJoin('catalog_product_super_attribute_label', function ($join) { - $join - ->on('catalog_product_super_attribute_label.product_super_attribute_id', '=', 'catalog_product_super_attribute.product_super_attribute_id') - ->where('catalog_product_super_attribute_label.store_id', config('rapidez.store')); - }) - ->whereColumn('product_id', $model->getTable() . '.entity_id') - ->orderBy('catalog_product_super_attribute.position'); - - $builder->selectSub($query, 'super_attributes'); - } -} diff --git a/src/Models/Traits/HasAlternatesThroughRewrites.php b/src/Models/Traits/HasAlternatesThroughRewrites.php index 3ca53aaac..32604bd7a 100644 --- a/src/Models/Traits/HasAlternatesThroughRewrites.php +++ b/src/Models/Traits/HasAlternatesThroughRewrites.php @@ -3,10 +3,21 @@ namespace Rapidez\Core\Models\Traits; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Relations\HasMany; use Rapidez\Core\Facades\Rapidez; trait HasAlternatesThroughRewrites { + abstract protected static function getEntityType(): string; + + public function rewrites(): HasMany + { + return $this + ->hasMany(config('rapidez.models.rewrite'), 'entity_id') + ->withoutGlobalScope('store') + ->where('entity_type', static::getEntityType()); + } + protected function alternates(): Attribute { return Attribute::make( diff --git a/src/Models/Traits/Product/CastMultiselectAttributes.php b/src/Models/Traits/Product/CastMultiselectAttributes.php deleted file mode 100644 index cee2604d1..000000000 --- a/src/Models/Traits/Product/CastMultiselectAttributes.php +++ /dev/null @@ -1,23 +0,0 @@ -attributesToSelect = $attributes; - - return $query; - } - - 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'], [ - 'name', - 'meta_title', - 'meta_description', - 'price', - 'special_price', - 'special_from_date', - 'special_to_date', - 'description', - 'url_key', - ]); - }), 'code'); - - return $query; - } - - public function scopeSelectOnlyComparable(Builder $query): Builder - { - $attributeModel = config('rapidez.models.attribute'); - $this->attributesToSelect = Arr::pluck($attributeModel::getCachedWhere(function ($attribute) { - return $attribute['compare'] || in_array($attribute['code'], ['name']); - }), 'code'); - - return $query; - } - - public function scopeSelectOnlyIndexable(Builder $query): Builder - { - $attributeModel = config('rapidez.models.attribute'); - $this->attributesToSelect = Arr::pluck($attributeModel::getCachedWhere(function ($attribute) { - if (in_array($attribute['code'], ['msrp_display_actual_price_type', 'price_view', 'shipment_type', 'status'])) { - return false; - } - - if ($attribute['listing'] || $attribute['filter'] || $attribute['search'] || $attribute['sorting']) { - return true; - } - - $alwaysInFlat = array_merge(['sku'], Eventy::filter('index.' . static::getModelName() . '.attributes', [])); - if (in_array($attribute['code'], $alwaysInFlat)) { - return true; - } - - return false; - }), 'code'); - - return $query; - } -} From 3f25db8ca493175b59b838a1808d175584c39fc5 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Wed, 6 Aug 2025 09:21:43 +0000 Subject: [PATCH 25/45] Apply fixes from Duster --- 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 8a4f35c32..5053e1a30 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -15,8 +15,8 @@ class Product extends Model { - use HasCustomAttributes; use HasAlternatesThroughRewrites; + use HasCustomAttributes; public const VISIBILITY_NOT_VISIBLE = 1; public const VISIBILITY_IN_CATALOG = 2; From 98aacc5b661ebcdf83a43c276c626554dcc4329b Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 6 Aug 2025 11:42:08 +0200 Subject: [PATCH 26/45] Remove unnecessary casts --- src/Models/Product.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index 5053e1a30..df8049906 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -29,11 +29,6 @@ class Product extends Model protected $table = 'catalog_product_entity'; protected $primaryKey = 'entity_id'; - protected $casts = [ - self::UPDATED_AT => 'datetime', - self::CREATED_AT => 'datetime', - ]; - protected $with = ['stock', 'superAttributes']; protected static function boot(): void From 052ba64a83d66b1dc254c9e6b08f7cbfac2be30e Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 6 Aug 2025 13:01:52 +0200 Subject: [PATCH 27/45] Add URL, images, price for configurable/grouped products, move stuff around --- config/rapidez/models.php | 2 + src/Models/Product.php | 88 +++++++++++++------ src/Models/Traits/HasCustomAttributes.php | 14 +++ .../Traits/Product/HasSuperAttributes.php | 34 +++++++ 4 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 src/Models/Traits/Product/HasSuperAttributes.php diff --git a/config/rapidez/models.php b/config/rapidez/models.php index 864efc62c..79cdd20b3 100644 --- a/config/rapidez/models.php +++ b/config/rapidez/models.php @@ -24,6 +24,7 @@ 'product_option_type_price' => Rapidez\Core\Models\ProductOptionTypePrice::class, 'product_option_type_value' => Rapidez\Core\Models\ProductOptionTypeValue::class, 'product_review_summary' => Rapidez\Core\Models\ProductReviewSummary::class, + 'product_stock' => Rapidez\Core\Models\ProductStock::class, 'quote' => Rapidez\Core\Models\Quote::class, 'quote_id_mask' => Rapidez\Core\Models\QuoteIdMask::class, 'quote_item' => Rapidez\Core\Models\QuoteItem::class, @@ -39,4 +40,5 @@ 'sales_order_payment' => Rapidez\Core\Models\SalesOrderPayment::class, 'search_query' => Rapidez\Core\Models\SearchQuery::class, 'search_synonym' => Rapidez\Core\Models\SearchSynonym::class, + 'super_attribute' => Rapidez\Core\Models\SuperAttribute::class, ]; diff --git a/src/Models/Product.php b/src/Models/Product.php index df8049906..86ae05ee8 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -9,14 +9,19 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasOneThrough; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; +use Rapidez\Core\Facades\Rapidez; use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; use Rapidez\Core\Models\Traits\HasAlternatesThroughRewrites; use Rapidez\Core\Models\Traits\HasCustomAttributes; +use Rapidez\Core\Models\Traits\Product\HasSuperAttributes; class Product extends Model { use HasAlternatesThroughRewrites; use HasCustomAttributes; + use HasSuperAttributes; public const VISIBILITY_NOT_VISIBLE = 1; public const VISIBILITY_IN_CATALOG = 2; @@ -55,6 +60,13 @@ public function gallery(): BelongsToMany ); } + protected function images(): Attribute + { + return Attribute::get( + fn (): array => $this->gallery->sortBy('productImageValue.position')->pluck('value')->toArray() + )->shouldCache(); + } + public function parent(): HasOneThrough { return $this->hasOneThrough( @@ -78,7 +90,7 @@ public function children(): HasManyThrough public function stock(): BelongsTo { return $this->belongsTo( - ProductStock::class, + config('rapidez.models.product_stock'), 'entity_id', 'product_id', ); @@ -101,42 +113,60 @@ public function categoryProducts(): HasMany ); } - public function superAttributes(): HasMany + private function getImageFrom(?string $image): ?string { - return $this->hasMany( - SuperAttribute::class, - 'product_id', - )->orderBy('position'); + return $image !== 'no_selection' ? $image : null; + } + + protected function image(): Attribute + { + return Attribute::get($this->getImageFrom(...)); + } + + protected function smallImage(): Attribute + { + return Attribute::get($this->getImageFrom(...)); + } + + protected function thumbnail(): Attribute + { + return Attribute::get($this->getImageFrom(...)); + } + + protected function url(): Attribute + { + return Attribute::make( + get: fn (): string => '/' . ($this->url_key ? $this->url_key . Rapidez::config('catalog/seo/product_url_suffix') : 'catalog/product/view/id/' . $this->entity_id) + ); } - public function superAttributeValues(): Attribute + protected function price(): Attribute { - return Attribute::get(function () { - return $this->superAttributes - ->sortBy('position') - ->mapWithKeys(fn ($attribute) => [ - $attribute->attribute_code => $this->children->mapWithKeys(function ($child) use ($attribute) { - return [$child->entity_id => [ - 'label' => $child->{$attribute->attribute_code}, - 'value' => $child->customAttributes[$attribute->attribute_code]->value_id, - 'sort_order' => $child->customAttributes[$attribute->attribute_code]->sort_order, - ]]; - }), - ]); - }); + return Attribute::get(function (?float $price): ?float { + if ($this->type_id == 'configurable') { + return collect($this->children)->min->price; + } + + if ($this->type_id == 'grouped') { + return collect($this->grouped)->min->price; + } + + return $price; + })->shouldCache(); } - public function getAttribute($key) + protected function prices(): Attribute { - if (! $key) { - return; - } + return Attribute::get(function (): ?Collection { + if ($this->type_id == 'configurable') { + return collect($this->children)->pluck('price'); + } - $value = parent::getAttribute($key); - if ($value !== null) { - return $value; - } + if ($this->type_id == 'grouped') { + return collect($this->grouped)->pluck('price'); + } - return $this->getCustomAttribute($key)?->value; + return null; + })->shouldCache(); } } diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 7d807203c..5ba83822f 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -135,4 +135,18 @@ public function getCustomAttribute($key) { return $this->customAttributes[$key] ?? null; } + + public function getAttribute($key) + { + if (! $key) { + return; + } + + $value = parent::getAttribute($key); + if ($value !== null) { + return $value; + } + + return $this->getCustomAttribute($key)?->value; + } } diff --git a/src/Models/Traits/Product/HasSuperAttributes.php b/src/Models/Traits/Product/HasSuperAttributes.php new file mode 100644 index 000000000..4e47ec7e6 --- /dev/null +++ b/src/Models/Traits/Product/HasSuperAttributes.php @@ -0,0 +1,34 @@ +hasMany( + config('rapidez.models.super_attribute'), + 'product_id', + )->orderBy('position'); + } + + public function superAttributeValues(): Attribute + { + return Attribute::get(function () { + return $this->superAttributes + ->sortBy('position') + ->mapWithKeys(fn ($attribute) => [ + $attribute->attribute_code => $this->children->mapWithKeys(function ($child) use ($attribute) { + return [$child->entity_id => [ + 'label' => $child->{$attribute->attribute_code}, + 'value' => $child->customAttributes[$attribute->attribute_code]->value_id, + 'sort_order' => $child->customAttributes[$attribute->attribute_code]->sort_order, + ]]; + }), + ]); + }); + } +} From 7e934b7853fca3d31318b12cec73c87bfd8ba2b5 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Wed, 6 Aug 2025 11:02:13 +0000 Subject: [PATCH 28/45] Apply fixes from Duster --- src/Models/Product.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index 86ae05ee8..128e90d06 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasOneThrough; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Rapidez\Core\Facades\Rapidez; use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; @@ -157,7 +156,7 @@ protected function price(): Attribute protected function prices(): Attribute { - return Attribute::get(function (): ?Collection { + return Attribute::get(function (): ?Collection { if ($this->type_id == 'configurable') { return collect($this->children)->pluck('price'); } From f310cc72c9bfec289d1e004d15f30fc08dc8ea19 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Wed, 6 Aug 2025 13:45:04 +0200 Subject: [PATCH 29/45] Simplify price & add specialPrice --- src/Models/Product.php | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index 86ae05ee8..521149387 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasOneThrough; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Rapidez\Core\Facades\Rapidez; use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; @@ -142,31 +141,37 @@ protected function url(): Attribute protected function price(): Attribute { - return Attribute::get(function (?float $price): ?float { - if ($this->type_id == 'configurable') { - return collect($this->children)->min->price; - } + return Attribute::get(fn (?float $price): ?float => $this->prices?->min() ?? $price)->shouldCache(); + } - if ($this->type_id == 'grouped') { - return collect($this->grouped)->min->price; + protected function prices(): Attribute + { + return Attribute::get(function (): ?Collection { + if (! in_array($this->type_id, ['configurable', 'grouped'])) { + return null; } - return $price; + return collect($this->type_id == 'configurable' ? $this->children : $this->grouped)->pluck('price'); })->shouldCache(); } - protected function prices(): Attribute + protected function specialPrice(): Attribute { - return Attribute::get(function (): ?Collection { - if ($this->type_id == 'configurable') { - return collect($this->children)->pluck('price'); - } + return Attribute::get(function (?float $specialPrice): ?float { + if (! in_array($this->type_id, ['configurable', 'grouped'])) { + if (!now()->isBetween( + $this->special_from_date ?? now()->subHour(), + $this->special_to_date ?? now()->addHour(), + )) { + return null; + } - if ($this->type_id == 'grouped') { - return collect($this->grouped)->pluck('price'); + return $specialPrice < $this->price ? $specialPrice : null; } - return null; - })->shouldCache(); + return collect($this->type_id == 'configurable' ? $this->children : $this->grouped) + ->filter(fn ($child) => $child->specialPrice !== null) + ->min->special_price; + }); } } From e42bdc576332b5a60e829544ef44320b548dc619 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Wed, 6 Aug 2025 11:46:40 +0000 Subject: [PATCH 30/45] Apply fixes from Duster --- src/Models/Product.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index 521149387..2ebe6628e 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -146,7 +146,7 @@ protected function price(): Attribute protected function prices(): Attribute { - return Attribute::get(function (): ?Collection { + return Attribute::get(function (): ?Collection { if (! in_array($this->type_id, ['configurable', 'grouped'])) { return null; } @@ -159,7 +159,7 @@ protected function specialPrice(): Attribute { return Attribute::get(function (?float $specialPrice): ?float { if (! in_array($this->type_id, ['configurable', 'grouped'])) { - if (!now()->isBetween( + if (! now()->isBetween( $this->special_from_date ?? now()->subHour(), $this->special_to_date ?? now()->addHour(), )) { From c7c4da287e4f8a301e49e1a61ca5bf35de0b2df0 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 7 Aug 2025 13:00:08 +0200 Subject: [PATCH 31/45] Make custom attributes trait slightly more abstract, add product link for relations --- config/rapidez/models.php | 15 ++-- src/Models/AbstractAttribute.php | 2 +- src/Models/Product.php | 23 +++++- src/Models/ProductLink.php | 36 +++++++++- src/Models/ProductSuperLink.php | 15 ++++ src/Models/Traits/HasCustomAttributes.php | 85 +++++++++++++---------- 6 files changed, 127 insertions(+), 49 deletions(-) create mode 100644 src/Models/ProductSuperLink.php diff --git a/config/rapidez/models.php b/config/rapidez/models.php index 79cdd20b3..836318c67 100644 --- a/config/rapidez/models.php +++ b/config/rapidez/models.php @@ -2,21 +2,21 @@ // The fully qualified class names of the models. return [ - 'page' => Rapidez\Core\Models\Page::class, 'attribute' => Rapidez\Core\Models\Attribute::class, - 'product' => Rapidez\Core\Models\Product::class, + 'block' => Rapidez\Core\Models\Block::class, 'category' => Rapidez\Core\Models\Category::class, - 'customer_group' => Rapidez\Core\Models\CustomerGroup::class, 'category_product' => Rapidez\Core\Models\CategoryProduct::class, 'customer' => Rapidez\Core\Models\Customer::class, + 'customer_group' => Rapidez\Core\Models\CustomerGroup::class, 'config' => Rapidez\Core\Models\Config::class, 'oauth_token' => Rapidez\Core\Models\OauthToken::class, 'option_swatch' => Rapidez\Core\Models\OptionSwatch::class, 'option_value' => Rapidez\Core\Models\OptionValue::class, + 'page' => Rapidez\Core\Models\Page::class, + 'product' => Rapidez\Core\Models\Product::class, 'product_image' => Rapidez\Core\Models\ProductImage::class, 'product_image_value' => Rapidez\Core\Models\ProductImageValue::class, 'product_link' => Rapidez\Core\Models\ProductLink::class, - 'product_view' => Rapidez\Core\Models\ProductView::class, 'product_option' => Rapidez\Core\Models\ProductOption::class, 'product_option_title' => Rapidez\Core\Models\ProductOptionTitle::class, 'product_option_price' => Rapidez\Core\Models\ProductOptionPrice::class, @@ -25,20 +25,21 @@ 'product_option_type_value' => Rapidez\Core\Models\ProductOptionTypeValue::class, 'product_review_summary' => Rapidez\Core\Models\ProductReviewSummary::class, 'product_stock' => Rapidez\Core\Models\ProductStock::class, + 'product_super_link' => Rapidez\Core\Models\ProductSuperLink::class, + 'product_view' => Rapidez\Core\Models\ProductView::class, 'quote' => Rapidez\Core\Models\Quote::class, 'quote_id_mask' => Rapidez\Core\Models\QuoteIdMask::class, 'quote_item' => Rapidez\Core\Models\QuoteItem::class, 'quote_item_option' => Rapidez\Core\Models\QuoteItemOption::class, 'report_event' => Rapidez\Core\Models\ReportEvent::class, 'rewrite' => Rapidez\Core\Models\Rewrite::class, - 'store' => Rapidez\Core\Models\Store::class, - 'widget' => Rapidez\Core\Models\Widget::class, - 'block' => Rapidez\Core\Models\Block::class, 'sales_order' => Rapidez\Core\Models\SalesOrder::class, 'sales_order_address' => Rapidez\Core\Models\SalesOrderAddress::class, 'sales_order_item' => Rapidez\Core\Models\SalesOrderItem::class, 'sales_order_payment' => Rapidez\Core\Models\SalesOrderPayment::class, 'search_query' => Rapidez\Core\Models\SearchQuery::class, 'search_synonym' => Rapidez\Core\Models\SearchSynonym::class, + 'store' => Rapidez\Core\Models\Store::class, 'super_attribute' => Rapidez\Core\Models\SuperAttribute::class, + 'widget' => Rapidez\Core\Models\Widget::class, ]; diff --git a/src/Models/AbstractAttribute.php b/src/Models/AbstractAttribute.php index 5bbbcad26..d8a320a20 100644 --- a/src/Models/AbstractAttribute.php +++ b/src/Models/AbstractAttribute.php @@ -12,7 +12,7 @@ protected static function boot(): void { parent::boot(); - static::addGlobalScope(new ForCurrentStoreWithoutLimitScope(['attribute_id', 'entity_id'])); + static::addGlobalScope('store', new ForCurrentStoreWithoutLimitScope(['attribute_id', 'entity_id'])); static::addGlobalScope('attribute', function (Builder $builder) { $builder->leftJoin('eav_attribute', $builder->qualifyColumn('attribute_id'), '=', 'eav_attribute.attribute_id'); diff --git a/src/Models/Product.php b/src/Models/Product.php index 2ebe6628e..74b4119b1 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -40,7 +40,7 @@ protected static function boot(): void parent::boot(); static::addGlobalScope(ForCurrentWebsiteScope::class); - static::addGlobalScope('customAttributes', fn (Builder $builder) => $builder->withCustomAttributes()); + static::withCustomAttributes(); static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereAttribute('status', static::STATUS_ENABLED)); } @@ -70,7 +70,7 @@ public function parent(): HasOneThrough { return $this->hasOneThrough( config('rapidez.models.product'), - config('rapidez.models.product_link'), + config('rapidez.models.product_super_link'), 'product_id', 'entity_id', 'entity_id', 'parent_id' ); @@ -80,12 +80,29 @@ public function children(): HasManyThrough { return $this->hasManyThrough( config('rapidez.models.product'), - config('rapidez.models.product_link'), + config('rapidez.models.product_super_link'), 'parent_id', 'entity_id', 'entity_id', 'product_id' ); } + public function links(): HasMany + { + return $this->hasMany( + ProductLink::class, + 'product_id', 'entity_id', + ); + } + + public function getLinkedProducts(string $type): Collection + { + return $this->links() + ->with('linkedProduct') + ->where('code', $type) + ->get() + ->pluck('linkedProduct'); + } + public function stock(): BelongsTo { return $this->belongsTo( diff --git a/src/Models/ProductLink.php b/src/Models/ProductLink.php index 258638dd5..fffe9f04a 100644 --- a/src/Models/ProductLink.php +++ b/src/Models/ProductLink.php @@ -2,14 +2,48 @@ namespace Rapidez\Core\Models; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; +use Rapidez\Core\Models\Traits\HasCustomAttributes; + class ProductLink extends Model { + use HasCustomAttributes; + const CREATED_AT = null; const UPDATED_AT = null; - protected $table = 'catalog_product_super_link'; + protected $table = 'catalog_product_link'; + + protected $attributeTypes = ['int', 'decimal', 'varchar']; + protected $attributeTablePrefix = 'catalog_product_link_attribute'; + protected $attributeCode = 'product_link_attribute_code'; protected $primaryKey = 'link_id'; protected $guarded = []; + + protected static function boot() + { + parent::boot(); + + static::withCustomAttributes(); + static::addGlobalScope('withLinkType', fn(Builder $builder) => $builder->join('catalog_product_link_type', 'catalog_product_link_type.link_type_id', '=', 'catalog_product_link.link_type_id')); + } + + protected function modifyRelation(HasMany $relation): HasMany + { + $relation->withoutGlobalScopes(['store', 'attribute']); + $relation->withGlobalScope('productLinkAttribute', function (Builder $builder) { + $builder->leftJoin('catalog_product_link_attribute', $builder->qualifyColumn('product_link_attribute_id'), '=', 'catalog_product_link_attribute.product_link_attribute_id'); + }); + + return $relation; + } + + public function linkedProduct(): HasOne + { + return $this->hasOne(Product::class, 'entity_id', 'linked_product_id'); + } } diff --git a/src/Models/ProductSuperLink.php b/src/Models/ProductSuperLink.php new file mode 100644 index 000000000..c8659c54c --- /dev/null +++ b/src/Models/ProductSuperLink.php @@ -0,0 +1,15 @@ +attributeTypes ?? ['datetime', 'decimal', 'int', 'text', 'varchar']; + } + + protected function getCustomAttributeCode(): string + { + return $this->attributeCode ?? 'attribute_code'; + } + + protected static function withCustomAttributes() + { + static::addGlobalScope('customAttributes', fn (Builder $builder) => $builder->withCustomAttributes()); + } + public function scopeWithCustomAttributes(Builder $builder, ?callable $callback = null) { + $relations = Arr::map($this->getCustomAttributeTypes(), fn($type) => 'attribute' . ucfirst($type)); if ($callback) { - $builder->with([ - 'attributeDatetime' => $callback, - 'attributeDecimal' => $callback, - 'attributeInt' => $callback, - 'attributeText' => $callback, - 'attributeVarchar' => $callback, - ]); - } else { - $builder->with([ - 'attributeDatetime', - 'attributeDecimal', - 'attributeInt', - 'attributeText', - 'attributeVarchar', - ]); + $relations = Arr::mapWithKeys($relations, fn($relation) => [$relation => $callback]); } + + $builder->with($relations); } public function scopeAttributeHas(Builder $builder, string $attributeCode, callable $callback) @@ -49,7 +54,7 @@ public function scopeWhereAttribute(Builder $builder, string $attributeCode, $op { return $builder->attributeHas( $attributeCode, - fn ($query) => $query->where('value', $operator, $value)->where('attribute_code', $attributeCode) + fn ($query) => $query->where('value', $operator, $value)->where($this->getCustomAttributeCode(), $attributeCode) ); } @@ -58,8 +63,8 @@ public function attributeDatetime(): HasMany return $this->hasManyWithAttributeTypeTable( AttributeDatetime::class, 'datetime', - 'entity_id', - 'entity_id', + $this->primaryKey, + $this->primaryKey, ); } @@ -68,8 +73,8 @@ public function attributeDecimal(): HasMany return $this->hasManyWithAttributeTypeTable( AttributeDecimal::class, 'decimal', - 'entity_id', - 'entity_id', + $this->primaryKey, + $this->primaryKey, ); } @@ -78,8 +83,8 @@ public function attributeInt(): HasMany return $this->hasManyWithAttributeTypeTable( AttributeInt::class, 'int', - 'entity_id', - 'entity_id', + $this->primaryKey, + $this->primaryKey, ); } @@ -88,8 +93,8 @@ public function attributeText(): HasMany return $this->hasManyWithAttributeTypeTable( AttributeText::class, 'text', - 'entity_id', - 'entity_id', + $this->primaryKey, + $this->primaryKey, ); } @@ -98,36 +103,42 @@ public function attributeVarchar(): HasMany return $this->hasManyWithAttributeTypeTable( AttributeVarchar::class, 'varchar', - 'entity_id', - 'entity_id', + $this->primaryKey, + $this->primaryKey, ); } - public function hasManyWithAttributeTypeTable($class, $type, $foreignKey = null, $localKey = null) + public function hasManyWithAttributeTypeTable($class, $type, $foreignKey = null, $localKey = null): HasMany { - $table = ($this->attributesTablePrefix ?? $this->table) . '_' . $type; + $table = ($this->attributeTablePrefix ?? $this->table) . '_' . $type; + // Set the relation with the custom table $relation = $this->hasMany($class, $foreignKey, $localKey); $relation->getModel()->setTable($table); $relation->getQuery()->from($table); + return $this->modifyRelation($relation); + } + + protected function modifyRelation(HasMany $relation): HasMany + { return $relation; } public function customAttributes(): AttributeCast { return AttributeCast::get(function () { - if (! $this->relationLoaded('attributeDatetime')) { - return collect(); + $data = collect(); + + foreach ($this->getCustomAttributeTypes() as $type) { + $values = $this->{'attribute' . ucfirst($type)} ?? null; + + if ($values) { + $data->push(...$values); + } } - return collect() - ->concat($this->attributeDatetime) - ->concat($this->attributeDecimal) - ->concat($this->attributeInt) - ->concat($this->attributeText) - ->concat($this->attributeVarchar) - ->keyBy('attribute_code'); + return $data->keyBy($this->getCustomAttributeCode()); }); } From 7faf8d3e674c3ff0523d964913d336d436072a37 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Thu, 7 Aug 2025 11:00:36 +0000 Subject: [PATCH 32/45] Apply fixes from Duster --- src/Models/ProductLink.php | 2 +- src/Models/Traits/HasCustomAttributes.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Models/ProductLink.php b/src/Models/ProductLink.php index fffe9f04a..64af557a5 100644 --- a/src/Models/ProductLink.php +++ b/src/Models/ProductLink.php @@ -29,7 +29,7 @@ protected static function boot() parent::boot(); static::withCustomAttributes(); - static::addGlobalScope('withLinkType', fn(Builder $builder) => $builder->join('catalog_product_link_type', 'catalog_product_link_type.link_type_id', '=', 'catalog_product_link.link_type_id')); + static::addGlobalScope('withLinkType', fn (Builder $builder) => $builder->join('catalog_product_link_type', 'catalog_product_link_type.link_type_id', '=', 'catalog_product_link.link_type_id')); } protected function modifyRelation(HasMany $relation): HasMany diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 009411069..1c1032b5a 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -32,9 +32,9 @@ protected static function withCustomAttributes() public function scopeWithCustomAttributes(Builder $builder, ?callable $callback = null) { - $relations = Arr::map($this->getCustomAttributeTypes(), fn($type) => 'attribute' . ucfirst($type)); + $relations = Arr::map($this->getCustomAttributeTypes(), fn ($type) => 'attribute' . ucfirst($type)); if ($callback) { - $relations = Arr::mapWithKeys($relations, fn($relation) => [$relation => $callback]); + $relations = Arr::mapWithKeys($relations, fn ($relation) => [$relation => $callback]); } $builder->with($relations); From 0830d958fed8620dc05cb30c0351f6e911c4f43e Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 7 Aug 2025 13:14:10 +0200 Subject: [PATCH 33/45] Add inverted product links --- src/Models/Product.php | 21 +++++++++++++++++++-- src/Models/ProductLink.php | 5 +++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Models/Product.php b/src/Models/Product.php index 74b4119b1..b9c5dd3e5 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -86,7 +86,7 @@ public function children(): HasManyThrough ); } - public function links(): HasMany + public function productLinks(): HasMany { return $this->hasMany( ProductLink::class, @@ -94,15 +94,32 @@ public function links(): HasMany ); } + public function productLinkParents(): HasMany + { + return $this->hasMany( + ProductLink::class, + 'linked_product_id', 'entity_id', + ); + } + public function getLinkedProducts(string $type): Collection { - return $this->links() + return $this->productLinks() ->with('linkedProduct') ->where('code', $type) ->get() ->pluck('linkedProduct'); } + public function getLinkedParents(string $type): Collection + { + return $this->productLinkParents() + ->with('linkedParent') + ->where('code', $type) + ->get() + ->pluck('linkedParent'); + } + public function stock(): BelongsTo { return $this->belongsTo( diff --git a/src/Models/ProductLink.php b/src/Models/ProductLink.php index 64af557a5..66a7a2a29 100644 --- a/src/Models/ProductLink.php +++ b/src/Models/ProductLink.php @@ -46,4 +46,9 @@ public function linkedProduct(): HasOne { return $this->hasOne(Product::class, 'entity_id', 'linked_product_id'); } + + public function linkedParent(): HasOne + { + return $this->hasOne(Product::class, 'entity_id', 'product_id'); + } } From c13f98c5f952d19449dc0505fc6f090f3bb6fbfa Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 7 Aug 2025 13:31:38 +0200 Subject: [PATCH 34/45] Add category info & use config models --- config/rapidez/models.php | 1 + src/Models/CategoryProduct.php | 7 +++++++ src/Models/Product.php | 20 ++++++++++---------- src/Models/ProductLink.php | 4 ++-- src/Models/Traits/HasAttributeOptions.php | 3 +-- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/config/rapidez/models.php b/config/rapidez/models.php index 836318c67..101128bdc 100644 --- a/config/rapidez/models.php +++ b/config/rapidez/models.php @@ -3,6 +3,7 @@ // The fully qualified class names of the models. return [ 'attribute' => Rapidez\Core\Models\Attribute::class, + 'attribute_option' => Rapidez\Core\Models\AttributeOption::class, 'block' => Rapidez\Core\Models\Block::class, 'category' => Rapidez\Core\Models\Category::class, 'category_product' => Rapidez\Core\Models\CategoryProduct::class, diff --git a/src/Models/CategoryProduct.php b/src/Models/CategoryProduct.php index 780b73a85..8477e981b 100644 --- a/src/Models/CategoryProduct.php +++ b/src/Models/CategoryProduct.php @@ -2,6 +2,8 @@ namespace Rapidez\Core\Models; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + class CategoryProduct extends Model { protected $primaryKey = 'entity_id'; @@ -10,4 +12,9 @@ public function getTable() { return 'catalog_category_product_index_store' . config('rapidez.store'); } + + public function category(): BelongsTo + { + return $this->belongsTo(config('rapidez.models.category'), 'category_id', 'entity_id'); + } } diff --git a/src/Models/Product.php b/src/Models/Product.php index b9c5dd3e5..6a4a41d5f 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -33,7 +33,7 @@ class Product extends Model protected $table = 'catalog_product_entity'; protected $primaryKey = 'entity_id'; - protected $with = ['stock', 'superAttributes']; + protected $with = ['stock', 'superAttributes', 'categoryProducts.category']; protected static function boot(): void { @@ -120,6 +120,15 @@ public function getLinkedParents(string $type): Collection ->pluck('linkedParent'); } + public function categoryProducts(): HasMany + { + return $this + ->hasMany( + config('rapidez.models.category_product'), + 'product_id', + ); + } + public function stock(): BelongsTo { return $this->belongsTo( @@ -137,15 +146,6 @@ public function options(): HasMany ); } - public function categoryProducts(): HasMany - { - return $this - ->hasMany( - config('rapidez.models.category_product'), - 'product_id', - ); - } - private function getImageFrom(?string $image): ?string { return $image !== 'no_selection' ? $image : null; diff --git a/src/Models/ProductLink.php b/src/Models/ProductLink.php index 66a7a2a29..96550c347 100644 --- a/src/Models/ProductLink.php +++ b/src/Models/ProductLink.php @@ -44,11 +44,11 @@ protected function modifyRelation(HasMany $relation): HasMany public function linkedProduct(): HasOne { - return $this->hasOne(Product::class, 'entity_id', 'linked_product_id'); + return $this->hasOne(config('rapidez.models.product'), 'entity_id', 'linked_product_id'); } public function linkedParent(): HasOne { - return $this->hasOne(Product::class, 'entity_id', 'product_id'); + return $this->hasOne(config('rapidez.models.product'), 'entity_id', 'product_id'); } } diff --git a/src/Models/Traits/HasAttributeOptions.php b/src/Models/Traits/HasAttributeOptions.php index dec409b47..ade7b02d5 100644 --- a/src/Models/Traits/HasAttributeOptions.php +++ b/src/Models/Traits/HasAttributeOptions.php @@ -4,7 +4,6 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; -use Rapidez\Core\Models\AttributeOption; trait HasAttributeOptions { @@ -21,6 +20,6 @@ protected function options(): Attribute public function attributeOptions(): HasMany { - return $this->hasMany(AttributeOption::class, 'attribute_id', 'attribute_id'); + return $this->hasMany(config('rapidez.models.attribute_option'), 'attribute_id', 'attribute_id'); } } From f3e8018487b5977c840b36bf8df574e9d7200b42 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 7 Aug 2025 13:37:32 +0200 Subject: [PATCH 35/45] Add minSaleQty --- src/Models/Product.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Models/Product.php b/src/Models/Product.php index 6a4a41d5f..2216b29ae 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -208,4 +208,15 @@ protected function specialPrice(): Attribute ->min->special_price; }); } + + protected function minSaleQty(): Attribute + { + return Attribute::get(function (?int $minSaleQty): ?int { + if (! $this->qty_increments) { + return $minSaleQty; + } + + return ceil($minSaleQty / $this->qty_increments) * $this->qty_increments; + }); + } } From 9c0f1048f333522ca9a40d24efe2a4fcd1ec6cc7 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 7 Aug 2025 13:47:18 +0200 Subject: [PATCH 36/45] Add breadcrumb categories --- src/Models/Product.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Models/Product.php b/src/Models/Product.php index 2216b29ae..8f93a4168 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -219,4 +219,9 @@ protected function minSaleQty(): Attribute return ceil($minSaleQty / $this->qty_increments) * $this->qty_increments; }); } + + protected function breadcrumbCategories(): Attribute + { + return Attribute::get(fn (): Collection => $this->categoryProducts->where('category_id', '!=', config('rapidez.root_category_id'))->pluck('category')); + } } From c8499d0d303ef71bf3d47cbcc5e9811a77caac73 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 7 Aug 2025 13:49:07 +0200 Subject: [PATCH 37/45] Add reviewSummary --- config/rapidez/models.php | 4 ++-- src/Models/Product.php | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/config/rapidez/models.php b/config/rapidez/models.php index 101128bdc..42deb7d38 100644 --- a/config/rapidez/models.php +++ b/config/rapidez/models.php @@ -19,10 +19,10 @@ 'product_image_value' => Rapidez\Core\Models\ProductImageValue::class, 'product_link' => Rapidez\Core\Models\ProductLink::class, 'product_option' => Rapidez\Core\Models\ProductOption::class, - 'product_option_title' => Rapidez\Core\Models\ProductOptionTitle::class, 'product_option_price' => Rapidez\Core\Models\ProductOptionPrice::class, - 'product_option_type_title' => Rapidez\Core\Models\ProductOptionTypeTitle::class, + 'product_option_title' => Rapidez\Core\Models\ProductOptionTitle::class, 'product_option_type_price' => Rapidez\Core\Models\ProductOptionTypePrice::class, + 'product_option_type_title' => Rapidez\Core\Models\ProductOptionTypeTitle::class, 'product_option_type_value' => Rapidez\Core\Models\ProductOptionTypeValue::class, 'product_review_summary' => Rapidez\Core\Models\ProductReviewSummary::class, 'product_stock' => Rapidez\Core\Models\ProductStock::class, diff --git a/src/Models/Product.php b/src/Models/Product.php index 8f93a4168..72b2b9c8c 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Support\Collection; use Rapidez\Core\Facades\Rapidez; @@ -146,6 +147,14 @@ public function options(): HasMany ); } + public function reviewSummary(): HasOne + { + return $this->hasOne( + config('rapidez.models.product_review_summary'), + 'entity_pk_value' + ); + } + private function getImageFrom(?string $image): ?string { return $image !== 'no_selection' ? $image : null; From b50ce36d8f9dc97f8d3fc373f7fe9c9af2a07aef Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Thu, 7 Aug 2025 15:34:00 +0200 Subject: [PATCH 38/45] Fix bug & add toArray for indexing --- src/Models/AttributeModels/ArrayBackend.php | 6 +++-- src/Models/Traits/HasCustomAttributes.php | 30 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Models/AttributeModels/ArrayBackend.php b/src/Models/AttributeModels/ArrayBackend.php index 8f6544ce0..7f890c761 100644 --- a/src/Models/AttributeModels/ArrayBackend.php +++ b/src/Models/AttributeModels/ArrayBackend.php @@ -6,8 +6,10 @@ class ArrayBackend implements AttributeModel { public static function value($value, $attribute) { - $values = explode(',', $value); + if (!$value) { + return collect(); + } - return collect($values)->map(fn ($value) => $attribute->options[$value]?->value ?? $value); + return collect(explode(',', $value))->map(fn ($value) => $attribute->options[$value]?->value ?? $value); } } diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 1c1032b5a..eaf15b6fd 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -15,6 +15,21 @@ trait HasCustomAttributes { + protected $exceptAttributes = null; + protected $onlyAttributes = null; + + public function exceptAttributes($attributes): static + { + $this->exceptAttributes = $attributes; + return $this; + } + + public function onlyAttributes($attributes): static + { + $this->onlyAttributes = $attributes; + return $this; + } + protected function getCustomAttributeTypes(): array { return $this->attributeTypes ?? ['datetime', 'decimal', 'int', 'text', 'varchar']; @@ -160,4 +175,19 @@ public function getAttribute($key) return $this->getCustomAttribute($key)?->value; } + + public function toCollection() + { + return collect(parent::toArray()) + ->except(Arr::map($this->getCustomAttributeTypes(), fn($type) => 'attribute_' . $type)) + ->except(['category_products', 'stock']) + ->merge($this->customAttributes->pluck('value', $this->getCustomAttributeCode())) + ->only($this->onlyAttributes) + ->except($this->exceptAttributes); + } + + public function toArray() + { + return $this->toCollection()->toArray(); + } } From d265943f22db44e523a0c9ad2dac04f9d7de4e97 Mon Sep 17 00:00:00 2001 From: Jade Geels Date: Fri, 8 Aug 2025 13:36:13 +0200 Subject: [PATCH 39/45] Allow indexing (but slow) --- src/Models/EavAttribute.php | 41 ++++++++++++++++ src/Models/Product.php | 12 ++++- src/Models/Traits/HasCustomAttributes.php | 38 +++------------ src/Models/Traits/Product/Searchable.php | 57 ++++++++++++----------- 4 files changed, 89 insertions(+), 59 deletions(-) create mode 100644 src/Models/EavAttribute.php diff --git a/src/Models/EavAttribute.php b/src/Models/EavAttribute.php new file mode 100644 index 000000000..583131882 --- /dev/null +++ b/src/Models/EavAttribute.php @@ -0,0 +1,41 @@ +leftJoin('catalog_eav_attribute', 'catalog_eav_attribute.attribute_id', '=', 'eav_attribute.attribute_id') + ->get(); + }); + } + + public static function getCachedCustomer() + { + return Cache::rememberForever('customer_eav_attributes', function () { + return EavAttribute::query() + ->leftJoin('customer_eav_attribute', 'customer_eav_attribute.attribute_id', '=', 'eav_attribute.attribute_id') + ->get(); + }); + } + + public static function getCachedIndexable() + { + return static::getCachedCatalog()->where(fn ($attribute) => + $attribute->backend_type === 'static' + || $attribute->is_used_for_promo_rules + || $attribute->used_in_product_listing + || $attribute->used_for_sort_by + || in_array($attribute->attribute_code, ['status', 'required_options', 'tax_class_id', 'weight']) + ); + } +} diff --git a/src/Models/Product.php b/src/Models/Product.php index 72b2b9c8c..f790437d5 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -11,17 +11,20 @@ use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Cache; use Rapidez\Core\Facades\Rapidez; use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; use Rapidez\Core\Models\Traits\HasAlternatesThroughRewrites; use Rapidez\Core\Models\Traits\HasCustomAttributes; use Rapidez\Core\Models\Traits\Product\HasSuperAttributes; +use Rapidez\Core\Models\Traits\Product\Searchable; class Product extends Model { use HasAlternatesThroughRewrites; use HasCustomAttributes; use HasSuperAttributes; + use Searchable; public const VISIBILITY_NOT_VISIBLE = 1; public const VISIBILITY_IN_CATALOG = 2; @@ -45,6 +48,11 @@ protected static function boot(): void static::addGlobalScope('onlyEnabled', fn (Builder $builder) => $builder->whereAttribute('status', static::STATUS_ENABLED)); } + protected function modifyRelation(HasMany $relation): HasMany + { + return $relation->leftJoin('catalog_eav_attribute', 'catalog_eav_attribute.attribute_id', '=', $relation->qualifyColumn('attribute_id')); + } + protected static function getEntityType(): string { return 'product'; @@ -231,6 +239,8 @@ protected function minSaleQty(): Attribute protected function breadcrumbCategories(): Attribute { - return Attribute::get(fn (): Collection => $this->categoryProducts->where('category_id', '!=', config('rapidez.root_category_id'))->pluck('category')); + return Attribute::get(fn (): Collection => + $this->categoryProducts->where('category_id', '!=', config('rapidez.root_category_id'))->pluck('category') + )->shouldCache(); } } diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index eaf15b6fd..451eef5ff 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -6,30 +6,17 @@ use Illuminate\Database\Eloquent\Casts\Attribute as AttributeCast; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Cache; use Rapidez\Core\Models\Attribute; use Rapidez\Core\Models\AttributeDatetime; use Rapidez\Core\Models\AttributeDecimal; use Rapidez\Core\Models\AttributeInt; use Rapidez\Core\Models\AttributeText; use Rapidez\Core\Models\AttributeVarchar; +use Rapidez\Core\Models\EavAttribute; trait HasCustomAttributes { - protected $exceptAttributes = null; - protected $onlyAttributes = null; - - public function exceptAttributes($attributes): static - { - $this->exceptAttributes = $attributes; - return $this; - } - - public function onlyAttributes($attributes): static - { - $this->onlyAttributes = $attributes; - return $this; - } - protected function getCustomAttributeTypes(): array { return $this->attributeTypes ?? ['datetime', 'decimal', 'int', 'text', 'varchar']; @@ -42,7 +29,9 @@ protected function getCustomAttributeCode(): string protected static function withCustomAttributes() { - static::addGlobalScope('customAttributes', fn (Builder $builder) => $builder->withCustomAttributes()); + static::addGlobalScope('customAttributes', fn (Builder $builder) => + $builder->withCustomAttributes() + ); } public function scopeWithCustomAttributes(Builder $builder, ?callable $callback = null) @@ -154,7 +143,7 @@ public function customAttributes(): AttributeCast } return $data->keyBy($this->getCustomAttributeCode()); - }); + })->shouldCache(); } public function getCustomAttribute($key) @@ -175,19 +164,4 @@ public function getAttribute($key) return $this->getCustomAttribute($key)?->value; } - - public function toCollection() - { - return collect(parent::toArray()) - ->except(Arr::map($this->getCustomAttributeTypes(), fn($type) => 'attribute_' . $type)) - ->except(['category_products', 'stock']) - ->merge($this->customAttributes->pluck('value', $this->getCustomAttributeCode())) - ->only($this->onlyAttributes) - ->except($this->exceptAttributes); - } - - public function toArray() - { - return $this->toCollection()->toArray(); - } } diff --git a/src/Models/Traits/Product/Searchable.php b/src/Models/Traits/Product/Searchable.php index 7bebe297a..168190617 100644 --- a/src/Models/Traits/Product/Searchable.php +++ b/src/Models/Traits/Product/Searchable.php @@ -8,6 +8,7 @@ use Rapidez\Core\Facades\Rapidez; use Rapidez\Core\Models\Category; use Rapidez\Core\Models\CategoryProduct; +use Rapidez\Core\Models\EavAttribute; use Rapidez\Core\Models\Product; use Rapidez\Core\Models\Traits\Searchable as ParentSearchable; use TorMorten\Eventy\Facades\Eventy; @@ -21,8 +22,8 @@ trait Searchable */ protected function makeAllSearchableUsing(Builder $query) { - return $query->selectOnlyIndexable() - ->with(['categoryProducts', 'reviewSummary']) + return $query + ->with(['reviewSummary']) ->withEventyGlobalScopes('index.' . static::getModelName() . '.scopes'); } @@ -52,9 +53,20 @@ public function shouldBeSearchable(): bool */ public function toSearchableArray(): array { - $data = $this->toArray(); + $indexable = Cache::driver('array')->rememberForever('indexable_attribute_codes', fn() => + EavAttribute::getCachedIndexable()->pluck($this->getCustomAttributeCode()) + ); + $keys = $this->customAttributes->keys()->intersect($indexable)->toArray(); + + $data = [ + 'entity_id' => $this->entity_id, + 'sku' => $this->sku, + ...Arr::mapWithKeys($keys, fn($attribute) => [$attribute => $this->getCustomAttribute($attribute)?->value]), + ]; + $data['url'] = $this->url; $data['store'] = config('rapidez.store'); + $data['super_attributes'] = $this->superAttributes->keyBy('attribute_id'); $maxPositions = Cache::driver('array')->rememberForever('max-positions-' . config('rapidez.store'), function () { return CategoryProduct::query() @@ -64,12 +76,6 @@ public function toSearchableArray(): array ->pluck('position', 'category_id'); }); - foreach ($this->super_attributes ?: [] as $superAttribute) { - $data['super_' . $superAttribute->code] = $superAttribute->text_swatch || $superAttribute->visual_swatch - ? array_keys((array) $this->{'super_' . $superAttribute->code}) - : Arr::pluck($this->{'super_' . $superAttribute->code} ?: [], 'label'); - } - $data = $this->withCategories($data); $data['positions'] = $this->categoryProducts @@ -85,29 +91,28 @@ public function toSearchableArray(): array */ public function withCategories(array $data): array { - $categories = Cache::driver('array')->rememberForever('categories-' . config('rapidez.store'), function () { - return Category::withEventyGlobalScopes('index.' . config('rapidez.models.category')::getModelName() . '.scopes') - ->where('catalog_category_flat_store_' . config('rapidez.store') . '.entity_id', '<>', config('rapidez.root_category_id')) - ->pluck('name', 'entity_id'); + $categories = Cache::driver('array')->rememberForever('categories', function() { + return Category::all()->keyBy('entity_id'); }); - foreach ($data['category_paths'] as $categoryPath) { - $paths = explode('/', $categoryPath); - $paths = array_slice($paths, array_search(config('rapidez.root_category_id'), $paths) + 1); + foreach ($this->breadcrumbCategories as $category) { + if (!$category) { + continue; + } - $categoryHierarchy = []; - $currentPath = ''; + $path = array_slice(explode('/', $category->path), 2); + $level = count($path); - foreach ($paths as $categoryId) { - if (isset($categories[$categoryId])) { - $currentPath .= ($currentPath ? ' > ' : '') . $categories[$categoryId]; - $categoryHierarchy[] = $currentPath; - } + if ($level < 1) { + continue; } - foreach ($categoryHierarchy as $level => $category) { - $data['category_lvl' . ($level + 1)][] = $category; - } + $categories = collect($path) + ->map(fn($id) => $categories[$id]->name ?? null) + ->whereNotNull() + ->join(' > '); + + $data['category_lvl' . $level][] = $categories; } foreach ($data as $key => &$value) { From 5262dcf31998ac0e9ead68ddd8d371c53c45b1fd Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Fri, 8 Aug 2025 11:36:37 +0000 Subject: [PATCH 40/45] Apply fixes from Duster --- src/Models/AttributeModels/ArrayBackend.php | 2 +- src/Models/EavAttribute.php | 3 +-- src/Models/Product.php | 4 +--- src/Models/Traits/HasCustomAttributes.php | 5 +---- src/Models/Traits/Product/Searchable.php | 15 +++++++-------- 5 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/Models/AttributeModels/ArrayBackend.php b/src/Models/AttributeModels/ArrayBackend.php index 7f890c761..d983e01b3 100644 --- a/src/Models/AttributeModels/ArrayBackend.php +++ b/src/Models/AttributeModels/ArrayBackend.php @@ -6,7 +6,7 @@ class ArrayBackend implements AttributeModel { public static function value($value, $attribute) { - if (!$value) { + if (! $value) { return collect(); } diff --git a/src/Models/EavAttribute.php b/src/Models/EavAttribute.php index 583131882..721a1d82e 100644 --- a/src/Models/EavAttribute.php +++ b/src/Models/EavAttribute.php @@ -30,8 +30,7 @@ public static function getCachedCustomer() public static function getCachedIndexable() { - return static::getCachedCatalog()->where(fn ($attribute) => - $attribute->backend_type === 'static' + return static::getCachedCatalog()->where(fn ($attribute) => $attribute->backend_type === 'static' || $attribute->is_used_for_promo_rules || $attribute->used_in_product_listing || $attribute->used_for_sort_by diff --git a/src/Models/Product.php b/src/Models/Product.php index f790437d5..0a111efef 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -11,7 +11,6 @@ use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Cache; use Rapidez\Core\Facades\Rapidez; use Rapidez\Core\Models\Scopes\Product\ForCurrentWebsiteScope; use Rapidez\Core\Models\Traits\HasAlternatesThroughRewrites; @@ -239,8 +238,7 @@ protected function minSaleQty(): Attribute protected function breadcrumbCategories(): Attribute { - return Attribute::get(fn (): Collection => - $this->categoryProducts->where('category_id', '!=', config('rapidez.root_category_id'))->pluck('category') + return Attribute::get(fn (): Collection => $this->categoryProducts->where('category_id', '!=', config('rapidez.root_category_id'))->pluck('category') )->shouldCache(); } } diff --git a/src/Models/Traits/HasCustomAttributes.php b/src/Models/Traits/HasCustomAttributes.php index 451eef5ff..2bc92b2ee 100644 --- a/src/Models/Traits/HasCustomAttributes.php +++ b/src/Models/Traits/HasCustomAttributes.php @@ -6,14 +6,12 @@ use Illuminate\Database\Eloquent\Casts\Attribute as AttributeCast; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\Cache; use Rapidez\Core\Models\Attribute; use Rapidez\Core\Models\AttributeDatetime; use Rapidez\Core\Models\AttributeDecimal; use Rapidez\Core\Models\AttributeInt; use Rapidez\Core\Models\AttributeText; use Rapidez\Core\Models\AttributeVarchar; -use Rapidez\Core\Models\EavAttribute; trait HasCustomAttributes { @@ -29,8 +27,7 @@ protected function getCustomAttributeCode(): string protected static function withCustomAttributes() { - static::addGlobalScope('customAttributes', fn (Builder $builder) => - $builder->withCustomAttributes() + static::addGlobalScope('customAttributes', fn (Builder $builder) => $builder->withCustomAttributes() ); } diff --git a/src/Models/Traits/Product/Searchable.php b/src/Models/Traits/Product/Searchable.php index 168190617..e0d24858f 100644 --- a/src/Models/Traits/Product/Searchable.php +++ b/src/Models/Traits/Product/Searchable.php @@ -53,18 +53,17 @@ public function shouldBeSearchable(): bool */ public function toSearchableArray(): array { - $indexable = Cache::driver('array')->rememberForever('indexable_attribute_codes', fn() => - EavAttribute::getCachedIndexable()->pluck($this->getCustomAttributeCode()) + $indexable = Cache::driver('array')->rememberForever('indexable_attribute_codes', fn () => EavAttribute::getCachedIndexable()->pluck($this->getCustomAttributeCode()) ); $keys = $this->customAttributes->keys()->intersect($indexable)->toArray(); $data = [ 'entity_id' => $this->entity_id, - 'sku' => $this->sku, - ...Arr::mapWithKeys($keys, fn($attribute) => [$attribute => $this->getCustomAttribute($attribute)?->value]), + 'sku' => $this->sku, + ...Arr::mapWithKeys($keys, fn ($attribute) => [$attribute => $this->getCustomAttribute($attribute)?->value]), ]; - $data['url'] = $this->url; + $data['url'] = $this->url; $data['store'] = config('rapidez.store'); $data['super_attributes'] = $this->superAttributes->keyBy('attribute_id'); @@ -91,12 +90,12 @@ public function toSearchableArray(): array */ public function withCategories(array $data): array { - $categories = Cache::driver('array')->rememberForever('categories', function() { + $categories = Cache::driver('array')->rememberForever('categories', function () { return Category::all()->keyBy('entity_id'); }); foreach ($this->breadcrumbCategories as $category) { - if (!$category) { + if (! $category) { continue; } @@ -108,7 +107,7 @@ public function withCategories(array $data): array } $categories = collect($path) - ->map(fn($id) => $categories[$id]->name ?? null) + ->map(fn ($id) => $categories[$id]->name ?? null) ->whereNotNull() ->join(' > '); From 2240d05fca380fb38fd747ffaec1089a62b92c8c Mon Sep 17 00:00:00 2001 From: Jade Date: Fri, 8 Aug 2025 15:52:09 +0200 Subject: [PATCH 41/45] Formatting --- src/Models/Traits/Product/Searchable.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Models/Traits/Product/Searchable.php b/src/Models/Traits/Product/Searchable.php index e0d24858f..1330047ce 100644 --- a/src/Models/Traits/Product/Searchable.php +++ b/src/Models/Traits/Product/Searchable.php @@ -53,8 +53,7 @@ public function shouldBeSearchable(): bool */ public function toSearchableArray(): array { - $indexable = Cache::driver('array')->rememberForever('indexable_attribute_codes', fn () => EavAttribute::getCachedIndexable()->pluck($this->getCustomAttributeCode()) - ); + $indexable = Cache::driver('array')->rememberForever('indexable_attribute_codes', fn () => EavAttribute::getCachedIndexable()->pluck($this->getCustomAttributeCode())); $keys = $this->customAttributes->keys()->intersect($indexable)->toArray(); $data = [ From 3947219ebfec3110dea577c05c4a35c91fadd1de Mon Sep 17 00:00:00 2001 From: Jade Date: Tue, 26 Aug 2025 13:22:31 +0200 Subject: [PATCH 42/45] Add tier pricing --- src/Models/Product.php | 30 ++++++++++++++++++++++++++++++ src/Models/ProductTierPrice.php | 20 ++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/Models/ProductTierPrice.php diff --git a/src/Models/Product.php b/src/Models/Product.php index 0a111efef..4e1d2ad3d 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -162,6 +162,36 @@ public function reviewSummary(): HasOne ); } + public function tierPrices(): HasMany + { + return $this->hasMany( + ProductTierPrice::class, + 'entity_id', + 'entity_id' + )->whereIn('website_id', [0, config('rapidez.website')]); + } + + public function getUnitPrice(int $quantity = 1, int $customerGroup = 0) + { + $tierPrice = $this->tierPrices() + ->where(function($query) use ($customerGroup) { + $query->where('customer_group_id', $customerGroup) + ->orWhere('all_groups', '1'); + }) + ->where('qty', '<=', $quantity) + ->orderBy('value') + ->first()?->value ?? null; + // NOTE: We always need the option with the lowest matching value, *not* the one with the highest matching qty! + // It wouldn't make sense to select a tier with a higher qty if the price is higher. + + return $tierPrice ?? $this->price; + } + + public function getPrice(int $quantity = 1, int $customerGroup = 0) + { + return $this->getUnitPrice($quantity, $customerGroup) * $quantity; + } + private function getImageFrom(?string $image): ?string { return $image !== 'no_selection' ? $image : null; diff --git a/src/Models/ProductTierPrice.php b/src/Models/ProductTierPrice.php new file mode 100644 index 000000000..00f4e2525 --- /dev/null +++ b/src/Models/ProductTierPrice.php @@ -0,0 +1,20 @@ +belongsTo(Product::class, 'entity_id'); + } +} From 1097b17e5d8e55fd8ab7060a5ff60803c5264318 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Tue, 26 Aug 2025 11:23:04 +0000 Subject: [PATCH 43/45] Apply fixes from Duster --- 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 4e1d2ad3d..35a64bc5c 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -174,7 +174,7 @@ public function tierPrices(): HasMany public function getUnitPrice(int $quantity = 1, int $customerGroup = 0) { $tierPrice = $this->tierPrices() - ->where(function($query) use ($customerGroup) { + ->where(function ($query) use ($customerGroup) { $query->where('customer_group_id', $customerGroup) ->orWhere('all_groups', '1'); }) From 9602fe3339c64d018068a8455231d89310e83747 Mon Sep 17 00:00:00 2001 From: Jade Date: Mon, 1 Sep 2025 13:55:55 +0200 Subject: [PATCH 44/45] Fix performance --- src/Models/Traits/HasAttributeOptions.php | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Models/Traits/HasAttributeOptions.php b/src/Models/Traits/HasAttributeOptions.php index ade7b02d5..1716138f2 100644 --- a/src/Models/Traits/HasAttributeOptions.php +++ b/src/Models/Traits/HasAttributeOptions.php @@ -8,18 +8,29 @@ trait HasAttributeOptions { protected static $hasAttributeOptions = true; + protected static $attributeCache = []; protected function options(): Attribute { - // Sort by store_id first to always get the higher store id if there are two. - return Attribute::get(fn () => $this->attributeOptions - ->sortBy('store_id') - ->keyBy('option_id') - ); + return Attribute::get(function() { + $store = config('rapidez.store'); + $key = "store_$store.$this->attribute_code"; + + $value = data_get(static::$attributeCache, $key); + if ($value) { + return $value; + } + + $value = $this->attributeOptions->keyBy('option_id'); + data_set(static::$attributeCache, $key, $value); + + return $value; + }); } public function attributeOptions(): HasMany { - return $this->hasMany(config('rapidez.models.attribute_option'), 'attribute_id', 'attribute_id'); + // Sort by store_id first to always get the higher store id if there are two. + return $this->hasMany(config('rapidez.models.attribute_option'), 'attribute_id', 'attribute_id')->orderBy('store_id'); } } From cdb4c9b27783e11ad1357eeff127b9c9d333c979 Mon Sep 17 00:00:00 2001 From: Jade-GG Date: Mon, 1 Sep 2025 11:56:18 +0000 Subject: [PATCH 45/45] Apply fixes from Duster --- src/Models/Traits/HasAttributeOptions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Models/Traits/HasAttributeOptions.php b/src/Models/Traits/HasAttributeOptions.php index 1716138f2..6eee83cd7 100644 --- a/src/Models/Traits/HasAttributeOptions.php +++ b/src/Models/Traits/HasAttributeOptions.php @@ -12,9 +12,9 @@ trait HasAttributeOptions protected function options(): Attribute { - return Attribute::get(function() { + return Attribute::get(function () { $store = config('rapidez.store'); - $key = "store_$store.$this->attribute_code"; + $key = "store_{$store}.{$this->attribute_code}"; $value = data_get(static::$attributeCache, $key); if ($value) { @@ -23,7 +23,7 @@ protected function options(): Attribute $value = $this->attributeOptions->keyBy('option_id'); data_set(static::$attributeCache, $key, $value); - + return $value; }); }