Added support for soft-deleted models in ChangeModelOrder and ChangeModelOrderPosition actions. This allows for proper order adjustments even when models are soft-deleted.
102 lines
3.2 KiB
PHP
102 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Actions\Common;
|
|
|
|
use App\Contracts\Action;
|
|
use App\Contracts\ActionInput;
|
|
use App\Interfaces\ModelWithOrder;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Webmozart\Assert\Assert;
|
|
|
|
/**
|
|
* @implements \App\Contracts\Action<\App\Actions\Common\ChangeModelOrderPositionInput, bool>
|
|
*/
|
|
final class ChangeModelOrderPosition implements Action
|
|
{
|
|
private const int MAX_ORDER_VALUE = 4294967295;
|
|
|
|
/**
|
|
* @param \App\Actions\Common\ChangeModelOrderPositionInput $input
|
|
*/
|
|
#[\Override]
|
|
public function __invoke(ActionInput $input): bool
|
|
{
|
|
Assert::isInstanceOf($input, ChangeModelOrderPositionInput::class);
|
|
|
|
$model = $input->model;
|
|
$currentOrder = $model->getOrder();
|
|
$targetOrder = $input->targetOrder;
|
|
|
|
if ($currentOrder === $targetOrder) {
|
|
return true;
|
|
}
|
|
|
|
$usesSoftDeletes = in_array(SoftDeletes::class, class_uses_recursive($model), true)
|
|
|| method_exists($model, 'trashed');
|
|
|
|
/** @var Builder<Model&ModelWithOrder> $baseQuery */
|
|
$baseQuery = $usesSoftDeletes
|
|
// @phpstan-ignore method.notFound (withTrashed)
|
|
? $model::query()->withTrashed()
|
|
: $model::query();
|
|
|
|
/** @var int|null $maxOrder */
|
|
$maxOrder = (clone $baseQuery)->max('order');
|
|
if ($maxOrder !== null && $targetOrder > $maxOrder) {
|
|
$targetOrder = (int) $maxOrder;
|
|
}
|
|
|
|
if ($currentOrder === $targetOrder) {
|
|
return true;
|
|
}
|
|
|
|
// Deactivate timestamps
|
|
$model->timestamps = false;
|
|
|
|
// Step 1: Temporarily park the moving model at MAX_ORDER_VALUE
|
|
$model->setOrder(self::MAX_ORDER_VALUE);
|
|
$model->saveQuietly();
|
|
|
|
if ($targetOrder < $currentOrder) {
|
|
// Moving UP
|
|
/** @var \Illuminate\Database\Eloquent\Collection<int, Model&ModelWithOrder> $itemsToShift */
|
|
$itemsToShift = (clone $baseQuery)
|
|
->where('order', '>=', $targetOrder)
|
|
->where('order', '<', $currentOrder)
|
|
->whereKeyNot($model->getKey())
|
|
->orderBy('order', 'desc')
|
|
->get();
|
|
|
|
foreach ($itemsToShift as $item) {
|
|
$item->timestamps = false;
|
|
$item->setOrder($item->getOrder() + 1);
|
|
$item->saveQuietly();
|
|
}
|
|
} else {
|
|
// Moving DOWN
|
|
/** @var \Illuminate\Database\Eloquent\Collection<int, Model&ModelWithOrder> $itemsToShift */
|
|
$itemsToShift = (clone $baseQuery)
|
|
->where('order', '>', $currentOrder)
|
|
->where('order', '<=', $targetOrder)
|
|
->whereKeyNot($model->getKey())
|
|
->orderBy('order', 'asc')
|
|
->get();
|
|
|
|
foreach ($itemsToShift as $item) {
|
|
$item->timestamps = false;
|
|
$item->setOrder($item->getOrder() - 1);
|
|
$item->saveQuietly();
|
|
}
|
|
}
|
|
|
|
// Step 2: Set the moving model to the target order
|
|
$model->setOrder($targetOrder);
|
|
$model->saveQuietly();
|
|
|
|
return true;
|
|
}
|
|
}
|