feat(IncoSheets): introduce IncoShortSheet entity and IncoShortSheetDirectory with SQL select optimization and search pagination

This commit is contained in:
Jérémy Munsch 2026-08-14 16:15:49 +02:00
parent cce81c1a85
commit bfe0b0010c
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
13 changed files with 302 additions and 24 deletions

View file

@ -5,10 +5,12 @@ declare(strict_types=1);
namespace Application\Usecase\IncoSheets\FindAll;
use Domain\Models\Entities\Inco\IncoSheet;
use Domain\Models\Entities\Inco\IncoShortSheet;
use Domain\Models\ValueObjects\Inco\IncoSheetFilter as DirectoryIncoSheetFilter;
use Domain\Models\ValueObjects\Inco\IncoSheetSortColumn as DirectoryIncoSheetSortColumn;
use Domain\Models\ValueObjects\Inco\IncoSheetSorter as DirectoryIncoSheetSorter;
use Domain\Ports\Directories\IncoSheetDirectory;
use Domain\Ports\Directories\IncoShortSheetDirectory;
use Shared\DDD\Usecase\Input as BaseInput;
use Shared\DDD\Usecase\Usecase;
use Shared\Paginated;
@ -18,7 +20,8 @@ use Webmozart\Assert\Assert;
final readonly class FindAllIncoSheets implements Usecase
{
public function __construct(
private IncoSheetDirectory $directory
private IncoSheetDirectory $directory,
private IncoShortSheetDirectory $shortDirectory,
) {
}
@ -36,26 +39,54 @@ final readonly class FindAllIncoSheets implements Usecase
direction: $input->sorter->direction,
);
if ($input->short) {
$shortPage = $this->shortDirectory->findAll(
pagination: $input->pagination,
sorter: $sorter,
filter: $filter
);
return $this->toShortOutput($shortPage);
}
$page = $this->directory->findAll(
pagination: $input->pagination,
sorter: $sorter,
filter: $filter
);
return $this->toOutput($page, $input->short);
return $this->toFullOutput($page);
}
/**
* @param Paginated<IncoSheet> $resultPage
*/
private function toOutput(Paginated $resultPage, bool $short): Output
private function toFullOutput(Paginated $resultPage): Output
{
$pagination = $resultPage->getPagination();
$items = array_map(
static fn (IncoSheet $sheet): PaginatedIncoSheet | PaginatedShortIncoSheet => $short
? PaginatedShortIncoSheet::fromDomain($sheet)
: PaginatedIncoSheet::fromDomain($sheet),
static fn (IncoSheet $sheet) => PaginatedIncoSheet::fromDomain($sheet),
$resultPage->getItems()
);
return new Output(
incoSheets: $items,
total: $resultPage->getTotal(),
page: $pagination->pageNumber(),
perPage: $pagination->pageSize(),
);
}
/**
* @param Paginated<IncoShortSheet> $resultPage
*/
private function toShortOutput(Paginated $resultPage): Output
{
$pagination = $resultPage->getPagination();
$items = array_map(
static fn (IncoShortSheet $sheet) => PaginatedShortIncoSheet::fromDomain($sheet),
$resultPage->getItems()
);

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Application\Usecase\IncoSheets\FindAll;
use Domain\Models\Entities\Inco\IncoSheet;
use Domain\Models\Entities\Inco\IncoShortSheet;
use Domain\Models\ValueObjects\Inco\IncoId;
use Shared\DDD\Usecase\Output as UsecaseOutput;
@ -19,7 +19,7 @@ final readonly class PaginatedShortIncoSheet implements UsecaseOutput
) {
}
public static function fromDomain(IncoSheet $incoSheet): self
public static function fromDomain(IncoShortSheet $incoSheet): self
{
return new self(
id: $incoSheet->id,

View file

@ -6,6 +6,7 @@ namespace Application\Usecase\IncoSheets\Search;
use Domain\Models\ValueObjects\Inco\IncoIdQuery;
use Shared\DDD\Usecase\Input as UsecaseInput;
use Shared\Pagination;
final readonly class Input implements UsecaseInput
{
@ -15,6 +16,7 @@ final readonly class Input implements UsecaseInput
public function __construct(
public IncoIdQuery $query,
public bool $short = false,
public ?Pagination $pagination = null,
string ...$brands,
) {
$this->brands = $brands;

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Application\Usecase\IncoSheets\Search;
use Domain\Models\Entities\Inco\IncoSheet;
use Domain\Models\Entities\Inco\IncoShortSheet;
use Domain\Models\ValueObjects\Inco\IncoId;
use Shared\DDD\Usecase\Output as UsecaseOutput;
@ -19,7 +19,7 @@ final readonly class PaginatedShortIncoSheet implements UsecaseOutput
) {
}
public static function fromDomain(IncoSheet $incoSheet): self
public static function fromDomain(IncoShortSheet $incoSheet): self
{
return new self(
id: $incoSheet->id,

View file

@ -5,17 +5,21 @@ declare(strict_types=1);
namespace Application\Usecase\IncoSheets\Search;
use Domain\Models\Entities\Inco\IncoSheet;
use Domain\Models\Entities\Inco\IncoShortSheet;
use Domain\Ports\Directories\IncoSheetDirectory;
use Domain\Ports\Directories\IncoShortSheetDirectory;
use Shared\DDD\Usecase\Input as BaseInput;
use Shared\DDD\Usecase\Usecase;
use Shared\Paginated;
use Shared\Pagination;
use Webmozart\Assert\Assert;
/** @implements Usecase<\Shared\DDD\Usecase\Input, \Shared\DDD\Usecase\Output> */
final readonly class SearchIncoSheets implements Usecase
{
public function __construct(
private IncoSheetDirectory $directory
private IncoSheetDirectory $directory,
private IncoShortSheetDirectory $shortDirectory,
) {
}
@ -24,25 +28,56 @@ final readonly class SearchIncoSheets implements Usecase
{
Assert::isInstanceOf($input, Input::class);
$pagination = $input->pagination ?? Pagination::firstPage();
if ($input->short) {
$shortPage = $this->shortDirectory->search(
$input->query,
$pagination,
...$input->brands
);
return $this->toShortOutput($shortPage);
}
$page = $this->directory->search(
$input->query,
$pagination,
...$input->brands
);
return $this->toOutput($page, $input->short);
return $this->toFullOutput($page);
}
/**
* @param Paginated<IncoSheet> $resultPage
*/
private function toOutput(Paginated $resultPage, bool $short): Output
private function toFullOutput(Paginated $resultPage): Output
{
$pagination = $resultPage->getPagination();
$items = array_map(
static fn (IncoSheet $sheet): PaginatedIncoSheet | PaginatedShortIncoSheet => $short
? PaginatedShortIncoSheet::fromDomain($sheet)
: PaginatedIncoSheet::fromDomain($sheet),
static fn (IncoSheet $sheet) => PaginatedIncoSheet::fromDomain($sheet),
$resultPage->getItems()
);
return new Output(
incoSheets: $items,
total: $resultPage->getTotal(),
page: $pagination->pageNumber(),
perPage: $pagination->pageSize(),
);
}
/**
* @param Paginated<IncoShortSheet> $resultPage
*/
private function toShortOutput(Paginated $resultPage): Output
{
$pagination = $resultPage->getPagination();
$items = array_map(
static fn (IncoShortSheet $sheet) => PaginatedShortIncoSheet::fromDomain($sheet),
$resultPage->getItems()
);

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Domain\Models\Entities\Inco;
use Domain\Models\ValueObjects\Inco\IncoId;
use Shared\DDD\Entity;
/** @implements Entity<IncoId> */
final readonly class IncoShortSheet implements Entity
{
public function __construct(
public IncoId $id,
public string $name,
public string $label,
public ?\DateTimeInterface $createdAt,
public ?\DateTimeInterface $updatedAt,
) {
}
#[\Override]
public function id(): IncoId
{
return $this->id;
}
}

View file

@ -26,6 +26,7 @@ interface IncoSheetDirectory extends Directory
/** @return Paginated<IncoSheet> */
public function search(
IncoIdQuery $query,
Pagination $pagination,
string ...$brands
): Paginated;

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Domain\Ports\Directories;
use Domain\Models\Entities\Inco\IncoShortSheet;
use Domain\Models\ValueObjects\Inco\IncoId;
use Domain\Models\ValueObjects\Inco\IncoIdQuery;
use Domain\Models\ValueObjects\Inco\IncoSheetFilter;
use Domain\Models\ValueObjects\Inco\IncoSheetSorter;
use Shared\DDD\Directory;
use Shared\Paginated;
use Shared\Pagination;
/** @extends Directory<IncoId, IncoShortSheet> */
interface IncoShortSheetDirectory extends Directory
{
/** @return Paginated<IncoShortSheet> */
public function findAll(
Pagination $pagination,
IncoSheetSorter $sorter,
?IncoSheetFilter $filter = null
): Paginated;
/** @return Paginated<IncoShortSheet> */
public function search(
IncoIdQuery $query,
Pagination $pagination,
string ...$brands
): Paginated;
public function find(IncoId $id): ?IncoShortSheet;
}

View file

@ -72,6 +72,7 @@ final class EloquentIncoSheetDirectory implements IncoSheetDirectory
#[\Override]
public function search(
IncoIdQuery $query,
Pagination $pagination,
string ...$brands
): Paginated {
$dbQuery = IncoSheetModel::query();
@ -84,7 +85,10 @@ final class EloquentIncoSheetDirectory implements IncoSheetDirectory
$total = (int) (clone $dbQuery)->count();
$rows = $dbQuery->get();
$rows = $dbQuery
->offset($pagination->from())
->limit($pagination->pageSize())
->get();
$items = [];
@ -92,8 +96,6 @@ final class EloquentIncoSheetDirectory implements IncoSheetDirectory
$items[] = $this->toDomain($row);
}
$pagination = Pagination::ofPage(1, max(1, $total));
/** @var IncoSheet[] $items */
return new Paginated($pagination, $items, $total);
}

View file

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Infra\Adapters\Directories;
use Domain\Models\Entities\Inco\IncoShortSheet;
use Domain\Models\ValueObjects\Inco\IncoId;
use Domain\Models\ValueObjects\Inco\IncoIdQuery;
use Domain\Models\ValueObjects\Inco\IncoSheetFilter;
use Domain\Models\ValueObjects\Inco\IncoSheetSortColumn;
use Domain\Models\ValueObjects\Inco\IncoSheetSorter;
use Domain\Ports\Directories\IncoShortSheetDirectory;
use Infra\Database\Models\IncoSheet as IncoSheetModel;
use Shared\Helpers\DatesHelper;
use Shared\Paginated;
use Shared\Pagination;
final class EloquentIncoShortSheetDirectory implements IncoShortSheetDirectory
{
private const SHORT_FIELDS = ['id', 'name', 'label', 'created_at', 'updated_at'];
#[\Override]
public function findAll(
Pagination $pagination,
IncoSheetSorter $sorter,
?IncoSheetFilter $filter = null
): Paginated {
$query = IncoSheetModel::query()->select(self::SHORT_FIELDS);
if ($filter !== null) {
if ($filter->since !== null) {
$query->where(static function ($q) use ($filter): void {
$q->where('updated_at', '>=', $filter->since)
->orWhere('created_at', '>=', $filter->since);
});
}
if (! empty($filter->brands)) {
$query->whereIn('brand->code', $filter->brands);
}
}
$total = (int) (clone $query)->count();
$rows = (clone $query)
->orderBy(
$this->toSqlColumn($sorter->sortBy),
$sorter->direction
)
->offset($pagination->from())
->limit($pagination->pageSize())
->get();
$items = [];
foreach ($rows as $row) {
$items[] = $this->toDomain($row);
}
/** @var IncoShortSheet[] $items */
return new Paginated($pagination, $items, $total);
}
#[\Override]
public function search(
IncoIdQuery $query,
Pagination $pagination,
string ...$brands
): Paginated {
$dbQuery = IncoSheetModel::query()->select(self::SHORT_FIELDS);
$dbQuery->where('id', 'LIKE', $query->value);
if (! empty($brands)) {
$dbQuery->whereIn('brand->code', $brands);
}
$total = (int) (clone $dbQuery)->count();
$rows = $dbQuery
->offset($pagination->from())
->limit($pagination->pageSize())
->get();
$items = [];
foreach ($rows as $row) {
$items[] = $this->toDomain($row);
}
/** @var IncoShortSheet[] $items */
return new Paginated($pagination, $items, $total);
}
#[\Override]
public function find(IncoId $id): ?IncoShortSheet
{
$model = IncoSheetModel::query()
->select(self::SHORT_FIELDS)
->where('id', $id->value)
->first();
if ($model === null) {
return null;
}
return $this->toDomain($model);
}
private function toDomain(IncoSheetModel $model): IncoShortSheet
{
$rawId = $model->getAttribute('id');
$rawName = $model->getAttribute('name');
$rawLabel = $model->getAttribute('label');
$rawCreatedAt = $model->getAttribute('created_at');
$rawUpdatedAt = $model->getAttribute('updated_at');
$id = is_string($rawId) || is_numeric($rawId) ? (string) $rawId : '';
$name = is_string($rawName) ? $rawName : '';
$label = is_string($rawLabel) ? $rawLabel : '';
$createdAt = is_string($rawCreatedAt) ? DatesHelper::parseDateTime($rawCreatedAt) : null;
$updatedAt = is_string($rawUpdatedAt) ? DatesHelper::parseDateTime($rawUpdatedAt) : null;
return new IncoShortSheet(
id: new IncoId($id),
name: $name,
label: $label,
createdAt: $createdAt,
updatedAt: $updatedAt,
);
}
private function toSqlColumn(IncoSheetSortColumn $column): string
{
return match ($column) {
IncoSheetSortColumn::incoSheetId => 'id',
IncoSheetSortColumn::createdAt => 'created_at',
IncoSheetSortColumn::updatedAt => 'updated_at',
};
}
}

View file

@ -7,10 +7,12 @@ namespace Infra\Providers;
use Domain\Ports\Directories\EmployeeDirectory;
use Domain\Ports\Directories\IncoCompositionDirectory;
use Domain\Ports\Directories\IncoSheetDirectory;
use Domain\Ports\Directories\IncoShortSheetDirectory;
use Illuminate\Support\ServiceProvider;
use Infra\Adapters\Directories\EloquentEmployeeDirectory;
use Infra\Adapters\Directories\EloquentIncoCompositionDirectory;
use Infra\Adapters\Directories\EloquentIncoSheetDirectory;
use Infra\Adapters\Directories\EloquentIncoShortSheetDirectory;
use Infra\Usecase\LaravelExecutor;
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
@ -21,6 +23,7 @@ class DomainServiceProvider extends ServiceProvider
{
$this->app->bind(EmployeeDirectory::class, EloquentEmployeeDirectory::class);
$this->app->bind(IncoSheetDirectory::class, EloquentIncoSheetDirectory::class);
$this->app->bind(IncoShortSheetDirectory::class, EloquentIncoShortSheetDirectory::class);
$this->app->bind(IncoCompositionDirectory::class, EloquentIncoCompositionDirectory::class);
$this->app->singleton(UsecaseExecutor::class, LaravelExecutor::class);
}

View file

@ -43,6 +43,7 @@ final readonly class IncoSheetsSearchController
return new SearchInput(
$request->incoIdSearch(),
$request->isShort(),
$request->pagination(),
...$request->brands()
);
}

View file

@ -17,6 +17,7 @@ use Domain\Models\ValueObjects\Inco\IncoSheetSortColumn as DirectoryIncoSheetSor
use Domain\Models\ValueObjects\Inco\IncoSheetSorter as DirectoryIncoSheetSorter;
use Domain\Models\ValueObjects\Inco\Ingredients;
use Domain\Ports\Directories\IncoSheetDirectory;
use Domain\Ports\Directories\IncoShortSheetDirectory;
use Shared\Paginated;
use Shared\Pagination;
@ -25,11 +26,12 @@ covers(FindAllIncoSheets::class);
describe('FindAllIncoSheets Usecase Unit Tests', function (): void {
it('executes successfully and returns paginated output DTO', function (): void {
// GIVEN
$pagination = Pagination::ofPage(1, 10);
$sorter = new Sorter(SortColumn::incoSheetId, \SortDirection::Ascending);
$input = new Input($pagination, $sorter);
$incoDirectory = \Mockery::mock(IncoSheetDirectory::class);
$usecase = new FindAllIncoSheets($incoDirectory);
$pagination = Pagination::ofPage(1, 10);
$sorter = new Sorter(SortColumn::incoSheetId, \SortDirection::Ascending);
$input = new Input($pagination, $sorter);
$incoDirectory = \Mockery::mock(IncoSheetDirectory::class);
$shortIncoDirectory = \Mockery::mock(IncoShortSheetDirectory::class);
$usecase = new FindAllIncoSheets($incoDirectory, $shortIncoDirectory);
$incoId = new IncoId('12345');
$incoSheet = new IncoSheet(