From 04f199657e7c4818a86355ed1d59b749dd2171af Mon Sep 17 00:00:00 2001 From: netzknecht Date: Fri, 1 Dec 2023 11:56:09 +0000 Subject: [PATCH] `HasPosition` trait --- .../Settings/Attributes/AttributeEdit.php | 3 - .../Attributes/AttributeGroupEdit.php | 4 - .../Settings/Product/Options/OptionEdit.php | 4 - .../Product/Options/OptionValueEdit.php | 4 - ..._unique_position_constraints_to_tables.php | 68 +++++++++++++++ packages/core/src/Base/Traits/HasPosition.php | 87 +++++++++++++++++++ packages/core/src/LunarServiceProvider.php | 18 ++++ packages/core/src/Models/Attribute.php | 13 +++ packages/core/src/Models/AttributeGroup.php | 12 +++ packages/core/src/Models/ProductOption.php | 4 +- .../core/src/Models/ProductOptionValue.php | 14 ++- 11 files changed, 214 insertions(+), 17 deletions(-) create mode 100644 packages/core/database/migrations/2023_12_01_000000_add_unique_position_constraints_to_tables.php create mode 100644 packages/core/src/Base/Traits/HasPosition.php diff --git a/packages/admin/src/Http/Livewire/Components/Settings/Attributes/AttributeEdit.php b/packages/admin/src/Http/Livewire/Components/Settings/Attributes/AttributeEdit.php index 861c52f953..46e57f32d9 100644 --- a/packages/admin/src/Http/Livewire/Components/Settings/Attributes/AttributeEdit.php +++ b/packages/admin/src/Http/Livewire/Components/Settings/Attributes/AttributeEdit.php @@ -181,9 +181,6 @@ public function save() if (! $this->attribute->id) { $this->attribute->attribute_type = $this->group->attributable_type; $this->attribute->attribute_group_id = $this->group->id; - $this->attribute->position = Attribute::whereAttributeGroupId( - $this->group->id - )->count() + 1; $this->attribute->save(); $this->notify( __('adminhub::notifications.attribute-edit.created') diff --git a/packages/admin/src/Http/Livewire/Components/Settings/Attributes/AttributeGroupEdit.php b/packages/admin/src/Http/Livewire/Components/Settings/Attributes/AttributeGroupEdit.php index 6c256b2fec..ddc723f88f 100644 --- a/packages/admin/src/Http/Livewire/Components/Settings/Attributes/AttributeGroupEdit.php +++ b/packages/admin/src/Http/Livewire/Components/Settings/Attributes/AttributeGroupEdit.php @@ -97,10 +97,6 @@ public function create() } $this->attributeGroup->attributable_type = $this->attributableType; - $this->attributeGroup->position = AttributeGroup::whereAttributableType( - $this->attributableType - )->count() + 1; - $this->attributeGroup->handle = $handle; $this->attributeGroup->save(); diff --git a/packages/admin/src/Http/Livewire/Components/Settings/Product/Options/OptionEdit.php b/packages/admin/src/Http/Livewire/Components/Settings/Product/Options/OptionEdit.php index 9cdf37e89b..fffc52b10a 100644 --- a/packages/admin/src/Http/Livewire/Components/Settings/Product/Options/OptionEdit.php +++ b/packages/admin/src/Http/Livewire/Components/Settings/Product/Options/OptionEdit.php @@ -198,10 +198,6 @@ public function save() return; } - if (! $this->productOption->position) { - $this->productOption->position = ProductOption::count() + 1; - } - $this->productOption->save(); $this->productOption = new ProductOption(); diff --git a/packages/admin/src/Http/Livewire/Components/Settings/Product/Options/OptionValueEdit.php b/packages/admin/src/Http/Livewire/Components/Settings/Product/Options/OptionValueEdit.php index ae1f5a9075..0e973415a8 100644 --- a/packages/admin/src/Http/Livewire/Components/Settings/Product/Options/OptionValueEdit.php +++ b/packages/admin/src/Http/Livewire/Components/Settings/Product/Options/OptionValueEdit.php @@ -64,10 +64,6 @@ public function save() $this->validate(); if (! $this->optionValue->id) { - $this->optionValue->position = ProductOptionValue::whereProductOptionId( - $this->option->id - )->count() + 1; - $this->optionValue->option()->associate($this->option); $this->optionValue->save(); $this->notify( diff --git a/packages/core/database/migrations/2023_12_01_000000_add_unique_position_constraints_to_tables.php b/packages/core/database/migrations/2023_12_01_000000_add_unique_position_constraints_to_tables.php new file mode 100644 index 0000000000..f3b9281684 --- /dev/null +++ b/packages/core/database/migrations/2023_12_01_000000_add_unique_position_constraints_to_tables.php @@ -0,0 +1,68 @@ +getTable(), function (Blueprint $table) use ($model) { + DB::table($model->getTable()) + ->select(array_merge( + [$model->getKeyName()], + $model->positionUniqueConstraints() + )) + ->orderBy('position') + ->orderBy('id') + ->get() + ->groupBy(fn (stdClass $row, int $key) => + collect($row) + ->only($model->positionUniqueConstraints()) + ->except('position') + ->join('-') + )->each + ->each(fn(stdClass $row, int $key) => $row->position = $key + 1); + }); + } + /** + * Add unique position index under consideration of + * the model's position constraints + */ + foreach ($models as $model) { + $model = app($model); + Schema::table($model->getTable(), function (Blueprint $table) use ($model) { + $schema = Schema::getConnection() + ->getDoctrineSchemaManager() + ->introspectTable($model->getTable()); + $uniqueIndex = $this->prefix . $table->getTable() . '_unique_position'; + $uniqueConstraints = array_merge($model->positionUniqueConstraints(), ['position']); + $table->unsignedBigInteger('position')->default(null)->index()->change(); + if (!$schema->hasIndex($uniqueIndex)) { + $table->unique($uniqueConstraints, $uniqueIndex); + } + }); + } + } +}; \ No newline at end of file diff --git a/packages/core/src/Base/Traits/HasPosition.php b/packages/core/src/Base/Traits/HasPosition.php new file mode 100644 index 0000000000..20046d4cda --- /dev/null +++ b/packages/core/src/Base/Traits/HasPosition.php @@ -0,0 +1,87 @@ +isDirty($model->positionUniqueConstraints()) + || !(intval($model->position) > 0) + || $model->query() + ->where($model->getKeyName(), '!=', $model->getKey()) + ->wherePosition( + $model->position, + $model->getAttributes() + ) + ->exists() + ) { + $model->position = $model->query() + ->where($model->getKeyName(), '!=', $model->getKey()) + ->wherePositionUniqueConstraints( + $model->getAttributes() + ) + ->max('position') + 1; + } + }); + } + + final public function positionUniqueConstraints(): array + { + $constraints = ['position']; + + if (!property_exists($this, 'positionUniqueConstraints') + || !is_array($this->positionUniqueConstraints)) + { + return $constraints; + } + + return array_merge($this->positionUniqueConstraints, $constraints); + } + + final public function scopeWherePosition(Builder $query, int $position, array|Collection $constraints = []): void + { + $query + ->where('position', $position) + ->wherePositionUniqueConstraints($constraints); + } + + final public function scopeWherePositionUniqueConstraints(Builder $query, array|Collection $constraints = []): void + { + $constraints = collect($constraints)->except('position'); + $modelConstraints = collect($this->positionUniqueConstraints())->reject('position'); + + if (count($modelConstraints) && !$constraints->hasAny($modelConstraints->toArray())) { + throw new \InvalidArgumentException( + sprintf( + 'Position constraints "%s" for "%s" not defined!', + $modelConstraints->diff($constraints)->join('", "', '" and "'), + get_class($this) + ) + ); + } + + $modelConstraints->each( + function ($attribute) use ($query, $constraints) { + if (method_exists($query, Str::camel('scope_' . $attribute))) { + $method = Str::camel($attribute); + } else { + $method = Str::camel('where_' . $attribute); + } + $query->{$method}($constraints[$attribute]); + } + ); + } + + public function scopePosition(Builder $query, int $position, ...$constraints): void + { + $query->wherePosition($position, $constraints); + } + +} \ No newline at end of file diff --git a/packages/core/src/LunarServiceProvider.php b/packages/core/src/LunarServiceProvider.php index e74400fca4..8125837c43 100644 --- a/packages/core/src/LunarServiceProvider.php +++ b/packages/core/src/LunarServiceProvider.php @@ -327,5 +327,23 @@ protected function registerBlueprintMacros(): void ); } }); + + Blueprint::macro('position', function (string|array $uniqueConstraints = []) { + /** @var Blueprint $this */ + if (is_string($uniqueConstraints)) { + $uniqueConstraints = app($uniqueConstraints)->positionUniqueConstraints(); + } + $this->unsignedBigInteger('position')->index(); + $this->unique( + array_merge($uniqueConstraints, ['position']), + $this->prefix . $this->table . '_unique_position' + ); + }); + + Blueprint::macro('dropPosition', function () { + /** @var Blueprint $this */ + $this->dropUnique($this->prefix . $this->table . '_unique_position'); + $this->dropColumn('position'); + }); } } diff --git a/packages/core/src/Models/Attribute.php b/packages/core/src/Models/Attribute.php index 1b0420eedf..6592b42abf 100644 --- a/packages/core/src/Models/Attribute.php +++ b/packages/core/src/Models/Attribute.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; use Lunar\Base\BaseModel; use Lunar\Base\Traits\HasMacros; +use Lunar\Base\Traits\HasPosition; use Lunar\Base\Traits\HasTranslations; use Lunar\Database\Factories\AttributeFactory; use Lunar\Facades\DB; @@ -37,6 +38,7 @@ class Attribute extends BaseModel { use HasFactory; use HasMacros; + use HasPosition; use HasTranslations; public static function boot() @@ -75,6 +77,17 @@ protected static function newFactory(): AttributeFactory 'configuration' => AsCollection::class, ]; + /** + * Define which attributes should be used + * to define the unique position constraint. + * + * @var array + */ + protected $positionUniqueConstraints = [ + 'attribute_type', + 'attribute_group_id', + ]; + /** * Return the attribuable relation. */ diff --git a/packages/core/src/Models/AttributeGroup.php b/packages/core/src/Models/AttributeGroup.php index 5dfdffdb62..35d6900e98 100644 --- a/packages/core/src/Models/AttributeGroup.php +++ b/packages/core/src/Models/AttributeGroup.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Lunar\Base\BaseModel; use Lunar\Base\Traits\HasMacros; +use Lunar\Base\Traits\HasPosition; use Lunar\Base\Traits\HasTranslations; use Lunar\Database\Factories\AttributeGroupFactory; @@ -23,6 +24,7 @@ class AttributeGroup extends BaseModel { use HasFactory; use HasMacros; + use HasPosition; use HasTranslations; /** @@ -50,6 +52,16 @@ protected static function newFactory(): AttributeGroupFactory 'name' => AsCollection::class, ]; + /** + * Define which attributes should be used + * to define the unique position constraint. + * + * @var array + */ + protected $positionUniqueConstraints = [ + 'attributable_type', + ]; + /** * Return the attributes relationship. */ diff --git a/packages/core/src/Models/ProductOption.php b/packages/core/src/Models/ProductOption.php index 8bb7402a66..6893c2606c 100644 --- a/packages/core/src/Models/ProductOption.php +++ b/packages/core/src/Models/ProductOption.php @@ -9,6 +9,7 @@ use Lunar\Base\BaseModel; use Lunar\Base\Traits\HasMacros; use Lunar\Base\Traits\HasMedia; +use Lunar\Base\Traits\HasPosition; use Lunar\Base\Traits\HasTranslations; use Lunar\Base\Traits\Searchable; use Lunar\Database\Factories\ProductOptionFactory; @@ -28,6 +29,7 @@ class ProductOption extends BaseModel implements SpatieHasMedia use HasFactory; use HasMacros; use HasMedia; + use HasPosition; use HasTranslations; use Searchable; @@ -49,7 +51,7 @@ protected static function newFactory(): ProductOptionFactory return ProductOptionFactory::new(); } - public function getNameAttribute(string $value): mixed + public function getNameAttribute(?string $value): mixed { return json_decode($value); } diff --git a/packages/core/src/Models/ProductOptionValue.php b/packages/core/src/Models/ProductOptionValue.php index 0f677b8920..03355dfb3f 100644 --- a/packages/core/src/Models/ProductOptionValue.php +++ b/packages/core/src/Models/ProductOptionValue.php @@ -9,6 +9,7 @@ use Lunar\Base\BaseModel; use Lunar\Base\Traits\HasMacros; use Lunar\Base\Traits\HasMedia; +use Lunar\Base\Traits\HasPosition; use Lunar\Base\Traits\HasTranslations; use Lunar\Database\Factories\ProductOptionValueFactory; use Spatie\MediaLibrary\HasMedia as SpatieHasMedia; @@ -26,6 +27,7 @@ class ProductOptionValue extends BaseModel implements SpatieHasMedia use HasFactory; use HasMacros; use HasMedia; + use HasPosition; use HasTranslations; /** @@ -53,7 +55,17 @@ protected static function newFactory(): ProductOptionValueFactory */ protected $guarded = []; - public function getNameAttribute(string $value): mixed + /** + * Define which attributes should be used + * to define the unique position constraint. + * + * @var array + */ + protected $positionUniqueConstraints = [ + 'product_option_id', + ]; + + public function getNameAttribute(?string $value): mixed { return json_decode($value); }