add persisted set ordering
This commit is contained in:
parent
fde12ad561
commit
dc034bf393
9 changed files with 340 additions and 6 deletions
|
|
@ -11,6 +11,8 @@ use App\Set\UseCases\CreateSetWithRoot\CreateSetWithRoot;
|
|||
use App\Set\UseCases\CreateSetWithRoot\CreateSetWithRootRequest;
|
||||
use App\Set\UseCases\DeleteSet\DeleteSet;
|
||||
use App\Set\UseCases\DeleteSet\DeleteSetRequest;
|
||||
use App\Set\UseCases\ReorderSets\ReorderSets;
|
||||
use App\Set\UseCases\ReorderSets\ReorderSetsRequest;
|
||||
use App\Set\UseCases\UpdateSet\UpdateSet;
|
||||
use App\Set\UseCases\UpdateSet\UpdateSetRequest;
|
||||
use App\Shared\Files\Filesystem;
|
||||
|
|
@ -26,19 +28,34 @@ class SetController
|
|||
private CreateSetWithRoot $createSetWithRoot,
|
||||
private UpdateSet $updateSet,
|
||||
private DeleteSet $deleteSet,
|
||||
private ReorderSets $reorderSets,
|
||||
private Filesystem $filesystem,
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$sets = [];
|
||||
foreach ($this->setRepository->getAll() as $set) {
|
||||
$sets[] = $this->buildSetPayload($set);
|
||||
return new JsonResponse([
|
||||
'sets' => $this->buildSetPayloads(
|
||||
$this->setRepository->getAll(),
|
||||
),
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function reorder(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$sets = $this->reorderSets->execute(new ReorderSetsRequest(
|
||||
setIds: $this->intArrayInput($request, 'setIds'),
|
||||
));
|
||||
} catch (BadRequestException $exception) {
|
||||
return new JsonResponse([
|
||||
'error' => $exception->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'sets' => $sets,
|
||||
'sets' => $this->buildSetPayloads($sets),
|
||||
], 200);
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +158,58 @@ class SetController
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DomainSet[] $sets
|
||||
* @return array<int, array{
|
||||
* id: int,
|
||||
* name: string,
|
||||
* description: string,
|
||||
* iconImageUrl: string,
|
||||
* rootElementId: int|null
|
||||
* }>
|
||||
*/
|
||||
private function buildSetPayloads(array $sets): array
|
||||
{
|
||||
$setPayloads = [];
|
||||
foreach ($sets as $set) {
|
||||
$setPayloads[] = $this->buildSetPayload($set);
|
||||
}
|
||||
|
||||
return $setPayloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]|null
|
||||
*/
|
||||
private function intArrayInput(Request $request, string $key): ?array
|
||||
{
|
||||
if (! $request->exists($key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $request->input($key);
|
||||
if (! is_array($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$integerValues = [];
|
||||
foreach ($value as $item) {
|
||||
if (is_int($item)) {
|
||||
$integerValues[] = $item;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($item) && ctype_digit($item)) {
|
||||
$integerValues[] = (int) $item;
|
||||
continue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $integerValues;
|
||||
}
|
||||
|
||||
private function stringInput(Request $request, string $key): ?string
|
||||
{
|
||||
if (! $request->exists($key)) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Set;
|
||||
|
||||
use DomainException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EloquentSetRepository implements SetRepository
|
||||
{
|
||||
|
|
@ -12,6 +13,7 @@ class EloquentSetRepository implements SetRepository
|
|||
'name' => $dto->name,
|
||||
'description' => $dto->description,
|
||||
'icon_image_url' => $dto->iconImageUrl,
|
||||
'sort_order' => $this->nextSortOrder(),
|
||||
]);
|
||||
|
||||
return $this->toDomain($model);
|
||||
|
|
@ -55,7 +57,7 @@ class EloquentSetRepository implements SetRepository
|
|||
|
||||
public function getAll(): array
|
||||
{
|
||||
$models = SetModel::orderBy('id')->get();
|
||||
$models = SetModel::orderBy('sort_order')->orderBy('id')->get();
|
||||
$sets = [];
|
||||
foreach ($models as $model) {
|
||||
$sets[] = $this->toDomain($model);
|
||||
|
|
@ -64,6 +66,30 @@ class EloquentSetRepository implements SetRepository
|
|||
return $sets;
|
||||
}
|
||||
|
||||
public function reorder(array $setIds): array
|
||||
{
|
||||
DB::transaction(function () use ($setIds): void {
|
||||
$sortOrder = 1;
|
||||
foreach ($setIds as $setId) {
|
||||
SetModel::where('id', $setId)
|
||||
->update(['sort_order' => $sortOrder]);
|
||||
$sortOrder++;
|
||||
}
|
||||
});
|
||||
|
||||
return $this->getAll();
|
||||
}
|
||||
|
||||
private function nextSortOrder(): int
|
||||
{
|
||||
$currentMaxSortOrder = SetModel::max('sort_order');
|
||||
if ($currentMaxSortOrder === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (int) $currentMaxSortOrder + 1;
|
||||
}
|
||||
|
||||
private function toDomain(SetModel $model): Set
|
||||
{
|
||||
return new Set(
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ use Illuminate\Database\Eloquent\Model;
|
|||
* @property string $name
|
||||
* @property string $description
|
||||
* @property string $icon_image_url
|
||||
* @property int $sort_order
|
||||
*
|
||||
* @method static Builder<static>|SetModel newModelQuery()
|
||||
* @method static Builder<static>|SetModel newQuery()
|
||||
* @method static Builder<static>|SetModel query()
|
||||
* @method static Builder<static>|SetModel whereId($value)
|
||||
* @method static Builder<static>|SetModel whereName($value)
|
||||
* @method static Builder<static>|SetModel whereSortOrder($value)
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
|
|
@ -25,5 +27,14 @@ class SetModel extends Model
|
|||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = ['name', 'description', 'icon_image_url'];
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'icon_image_url',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,4 +16,10 @@ interface SetRepository
|
|||
* @return Set[]
|
||||
*/
|
||||
public function getAll(): array;
|
||||
|
||||
/**
|
||||
* @param int[] $setIds
|
||||
* @return Set[]
|
||||
*/
|
||||
public function reorder(array $setIds): array;
|
||||
}
|
||||
|
|
|
|||
128
backend/app/Set/UseCases/ReorderSets/ReorderSets.php
Normal file
128
backend/app/Set/UseCases/ReorderSets/ReorderSets.php
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
namespace App\Set\UseCases\ReorderSets;
|
||||
|
||||
use App\Exceptions\BadRequestException;
|
||||
use App\Set\Set;
|
||||
use App\Set\SetRepository;
|
||||
|
||||
class ReorderSets
|
||||
{
|
||||
public function __construct(private SetRepository $setRepository)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Set[]
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
public function execute(ReorderSetsRequest $request): array
|
||||
{
|
||||
if ($request->setIds === null) {
|
||||
throw new BadRequestException('setIds is required');
|
||||
}
|
||||
|
||||
$setIds = $this->validatedSetIds($request->setIds);
|
||||
$existingSetIds = $this->setIds($this->setRepository->getAll());
|
||||
|
||||
$this->validateNoDuplicateIds($setIds);
|
||||
$this->validateAllIdsAreSets($setIds, $existingSetIds);
|
||||
$this->validateEverySetWasSubmitted($setIds, $existingSetIds);
|
||||
|
||||
return $this->setRepository->reorder($setIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $setIds
|
||||
* @return int[]
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
private function validatedSetIds(array $setIds): array
|
||||
{
|
||||
$validatedSetIds = [];
|
||||
foreach ($setIds as $setId) {
|
||||
if (! is_int($setId)) {
|
||||
throw new BadRequestException(
|
||||
'setIds must contain integers',
|
||||
);
|
||||
}
|
||||
|
||||
$validatedSetIds[] = $setId;
|
||||
}
|
||||
|
||||
return $validatedSetIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $setIds
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
private function validateNoDuplicateIds(array $setIds): void
|
||||
{
|
||||
$seenSetIds = [];
|
||||
foreach ($setIds as $setId) {
|
||||
if (isset($seenSetIds[$setId])) {
|
||||
throw new BadRequestException(
|
||||
'Set order contains duplicate ids',
|
||||
);
|
||||
}
|
||||
|
||||
$seenSetIds[$setId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $setIds
|
||||
* @param int[] $existingSetIds
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
private function validateAllIdsAreSets(
|
||||
array $setIds,
|
||||
array $existingSetIds,
|
||||
): void {
|
||||
$existingSetIdsById = [];
|
||||
foreach ($existingSetIds as $existingSetId) {
|
||||
$existingSetIdsById[$existingSetId] = true;
|
||||
}
|
||||
|
||||
foreach ($setIds as $setId) {
|
||||
if (! isset($existingSetIdsById[$setId])) {
|
||||
throw new BadRequestException(
|
||||
'Set order contains invalid set',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $setIds
|
||||
* @param int[] $existingSetIds
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
private function validateEverySetWasSubmitted(
|
||||
array $setIds,
|
||||
array $existingSetIds,
|
||||
): void {
|
||||
if (count($setIds) === count($existingSetIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
'Set order must include every set',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Set[] $sets
|
||||
* @return int[]
|
||||
*/
|
||||
private function setIds(array $sets): array
|
||||
{
|
||||
$setIds = [];
|
||||
foreach ($sets as $set) {
|
||||
$setIds[] = $set->getId();
|
||||
}
|
||||
|
||||
return $setIds;
|
||||
}
|
||||
}
|
||||
13
backend/app/Set/UseCases/ReorderSets/ReorderSetsRequest.php
Normal file
13
backend/app/Set/UseCases/ReorderSets/ReorderSetsRequest.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Set\UseCases\ReorderSets;
|
||||
|
||||
class ReorderSetsRequest
|
||||
{
|
||||
/**
|
||||
* @param mixed[]|null $setIds
|
||||
*/
|
||||
public function __construct(public ?array $setIds)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('sets', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('sort_order')->nullable();
|
||||
});
|
||||
|
||||
$setIds = DB::table('sets')->orderBy('id')->pluck('id');
|
||||
$sortOrder = 1;
|
||||
foreach ($setIds as $setId) {
|
||||
DB::table('sets')
|
||||
->where('id', $setId)
|
||||
->update(['sort_order' => $sortOrder]);
|
||||
$sortOrder++;
|
||||
}
|
||||
|
||||
Schema::table('sets', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('sort_order')
|
||||
->nullable(false)
|
||||
->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('sets', function (Blueprint $table): void {
|
||||
$table->dropColumn('sort_order');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -11,6 +11,8 @@ Route::post('/logout', [AuthController::class, 'logout']);
|
|||
Route::get('/me', [AuthController::class, 'me'])
|
||||
->middleware(AuthMiddleware::class);
|
||||
Route::get('/sets', [SetController::class, 'index']);
|
||||
Route::put('/sets/order', [SetController::class, 'reorder'])
|
||||
->middleware(AuthMiddleware::class);
|
||||
Route::post('/sets', [SetController::class, 'create'])
|
||||
->middleware(AuthMiddleware::class);
|
||||
Route::post('/sets/{id}/update', [SetController::class, 'update'])
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ class FakeSetRepository implements SetRepository
|
|||
*/
|
||||
private array $setsById = [];
|
||||
|
||||
/**
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private array $sortOrdersById = [];
|
||||
|
||||
public function create(CreateSetDto $dto): DomainSet
|
||||
{
|
||||
$id = count($this->setsById) + 1;
|
||||
|
|
@ -23,6 +28,7 @@ class FakeSetRepository implements SetRepository
|
|||
iconImageUrl: $dto->iconImageUrl,
|
||||
);
|
||||
$this->setsById[$id] = $set;
|
||||
$this->sortOrdersById[$id] = $this->nextSortOrder();
|
||||
|
||||
return $set;
|
||||
}
|
||||
|
|
@ -38,6 +44,7 @@ class FakeSetRepository implements SetRepository
|
|||
public function delete(DomainSet $set): void
|
||||
{
|
||||
unset($this->setsById[$set->getId()]);
|
||||
unset($this->sortOrdersById[$set->getId()]);
|
||||
}
|
||||
|
||||
public function find(int $id): ?DomainSet
|
||||
|
|
@ -58,10 +65,35 @@ class FakeSetRepository implements SetRepository
|
|||
foreach ($this->setsById as $set) {
|
||||
$sets[] = $this->cloneSet($set);
|
||||
}
|
||||
usort($sets, function (
|
||||
DomainSet $firstSet,
|
||||
DomainSet $secondSet,
|
||||
): int {
|
||||
$firstSortOrder = $this->sortOrdersById[$firstSet->getId()]
|
||||
?? $firstSet->getId();
|
||||
$secondSortOrder = $this->sortOrdersById[$secondSet->getId()]
|
||||
?? $secondSet->getId();
|
||||
if ($firstSortOrder === $secondSortOrder) {
|
||||
return $firstSet->getId() <=> $secondSet->getId();
|
||||
}
|
||||
|
||||
return $firstSortOrder <=> $secondSortOrder;
|
||||
});
|
||||
|
||||
return $sets;
|
||||
}
|
||||
|
||||
public function reorder(array $setIds): array
|
||||
{
|
||||
$sortOrder = 1;
|
||||
foreach ($setIds as $setId) {
|
||||
$this->sortOrdersById[$setId] = $sortOrder;
|
||||
$sortOrder++;
|
||||
}
|
||||
|
||||
return $this->getAll();
|
||||
}
|
||||
|
||||
private function cloneSet(DomainSet $set): DomainSet
|
||||
{
|
||||
return new DomainSet(
|
||||
|
|
@ -71,4 +103,13 @@ class FakeSetRepository implements SetRepository
|
|||
iconImageUrl: $set->getIconImageUrl(),
|
||||
);
|
||||
}
|
||||
|
||||
private function nextSortOrder(): int
|
||||
{
|
||||
if ($this->sortOrdersById === []) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return max($this->sortOrdersById) + 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue