perf(brands): paginate ArcadiaBrandDirectory and BrandRepository with SQL limit/offset and cache warming
This commit is contained in:
parent
97a73a5e05
commit
742da323aa
10 changed files with 96 additions and 47 deletions
|
|
@ -24,22 +24,17 @@ final readonly class FindAllBrands implements Usecase
|
|||
{
|
||||
Assert::isInstanceOf($input, Input::class);
|
||||
|
||||
$arcadiaBrands = $this->arcadiaBrandDirectory->findAll();
|
||||
$brandItems = [];
|
||||
$paginated = $this->arcadiaBrandDirectory->findAll($input->pagination);
|
||||
$brandItems = [];
|
||||
|
||||
foreach ($arcadiaBrands as $arcadiaBrand) {
|
||||
foreach ($paginated->getItems() as $arcadiaBrand) {
|
||||
$brand = $this->brandResolver->resolve($arcadiaBrand->id);
|
||||
$brandItems[] = BrandItem::fromDomain($brand);
|
||||
}
|
||||
|
||||
$total = count($brandItems);
|
||||
$offset = $input->pagination->from();
|
||||
$limit = $input->pagination->pageSize();
|
||||
$paged = array_values(array_slice($brandItems, $offset, $limit));
|
||||
|
||||
return new Output(
|
||||
brands: $paged,
|
||||
total: $total,
|
||||
brands: $brandItems,
|
||||
total: $paginated->getTotal(),
|
||||
page: $input->pagination->pageNumber(),
|
||||
perPage: $input->pagination->pageSize(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ namespace Domain\Ports\Directories;
|
|||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Shared\DDD\Port\Directory;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
/** @extends Directory<ArcadiaId, ArcadiaBrand> */
|
||||
interface ArcadiaBrandDirectory extends Directory
|
||||
{
|
||||
/** @return list<ArcadiaBrand> */
|
||||
public function findAll(): array;
|
||||
/** @return Paginated<ArcadiaBrand> */
|
||||
public function findAll(Pagination $pagination): Paginated;
|
||||
|
||||
public function find(ArcadiaId $id): ?ArcadiaBrand;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
|||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Shared\DDD\Port\Repository;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
/** @extends Repository<BrandId, Brand> */
|
||||
interface BrandRepository extends Repository
|
||||
|
|
@ -19,8 +21,8 @@ interface BrandRepository extends Repository
|
|||
|
||||
public function findByArcadiaId(ArcadiaId $arcadiaId): ?Brand;
|
||||
|
||||
/** @return list<Brand> */
|
||||
public function findAll(): array;
|
||||
/** @return Paginated<Brand> */
|
||||
public function findAll(Pagination $pagination): Paginated;
|
||||
|
||||
public function persist(Brand $entity): void;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ use Domain\Ports\Directories\ArcadiaBrandDirectory as ArcadiaBrandDirectoryPort;
|
|||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Infra\Adapters\Mappers\ArcadiaBrandMapper;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
final class ArcadiaMarqueBrandDirectory implements ArcadiaBrandDirectoryPort
|
||||
{
|
||||
|
|
@ -23,9 +25,9 @@ final class ArcadiaMarqueBrandDirectory implements ArcadiaBrandDirectoryPort
|
|||
}
|
||||
|
||||
#[\Override]
|
||||
public function findAll(): array
|
||||
public function findAll(Pagination $pagination): Paginated
|
||||
{
|
||||
$rows = $this->connection
|
||||
$query = $this->connection
|
||||
->table(MarqueTable::table($this->connection))
|
||||
->where('COD_MARQ', '>', 0)
|
||||
->where('LIB_MARQ', 'NOT LIKE', 'Z -%')
|
||||
|
|
@ -34,7 +36,14 @@ final class ArcadiaMarqueBrandDirectory implements ArcadiaBrandDirectoryPort
|
|||
})
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNull('TYPE_MARQUE')->orWhere('TYPE_MARQUE', '!=', 99);
|
||||
})
|
||||
});
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
|
||||
$rows = (clone $query)
|
||||
->orderBy('COD_MARQ', 'asc')
|
||||
->offset($pagination->from())
|
||||
->limit($pagination->pageSize())
|
||||
->get();
|
||||
|
||||
$items = [];
|
||||
|
|
@ -45,7 +54,7 @@ final class ArcadiaMarqueBrandDirectory implements ArcadiaBrandDirectoryPort
|
|||
$items[] = $domain;
|
||||
}
|
||||
|
||||
return $items;
|
||||
return new Paginated($pagination, $items, $total);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ namespace Infra\Adapters\Directories;
|
|||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Ports\Directories\ArcadiaBrandDirectory;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
final class InMemoryArcadiaBrandDirectory implements ArcadiaBrandDirectory
|
||||
{
|
||||
|
|
@ -28,12 +30,17 @@ final class InMemoryArcadiaBrandDirectory implements ArcadiaBrandDirectory
|
|||
}
|
||||
|
||||
#[\Override]
|
||||
public function findAll(): array
|
||||
public function findAll(Pagination $pagination): Paginated
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$filtered = array_values(array_filter(
|
||||
$this->brands,
|
||||
static fn (ArcadiaBrand $b): bool => ! str_starts_with($b->name->value, 'Z -')
|
||||
));
|
||||
|
||||
$total = count($filtered);
|
||||
$paged = array_slice($filtered, $pagination->from(), $pagination->pageSize());
|
||||
|
||||
return new Paginated($pagination, $paged, $total);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ use Domain\Ports\Repositories\BrandRepository;
|
|||
use Infra\Cache\BrandCache;
|
||||
use Infra\Database\Models\Brand as BrandModel;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
final readonly class EloquentBrandRepository implements BrandRepository
|
||||
{
|
||||
|
|
@ -86,23 +88,24 @@ final readonly class EloquentBrandRepository implements BrandRepository
|
|||
}
|
||||
|
||||
#[\Override]
|
||||
public function findAll(): array
|
||||
public function findAll(Pagination $pagination): Paginated
|
||||
{
|
||||
$cached = $this->brandCache->getAll();
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
$query = BrandModel::query()->orderBy('name', 'asc');
|
||||
$total = $query->count();
|
||||
|
||||
$brands = array_values(
|
||||
BrandModel::query()
|
||||
->get()
|
||||
->map(fn (BrandModel $model): Brand => $this->toDomain($model))
|
||||
->all()
|
||||
);
|
||||
$brands = $query
|
||||
->offset($pagination->from())
|
||||
->limit($pagination->pageSize())
|
||||
->get()
|
||||
->map(function (BrandModel $model): Brand {
|
||||
$brand = $this->toDomain($model);
|
||||
$this->brandCache->put($brand);
|
||||
|
||||
$this->brandCache->putAll(...$brands);
|
||||
return $brand;
|
||||
})
|
||||
->all();
|
||||
|
||||
return $brands;
|
||||
return new Paginated($pagination, array_values($brands), $total);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
|||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
|
||||
final class InMemoryBrandRepository implements BrandRepository
|
||||
{
|
||||
|
|
@ -60,9 +62,13 @@ final class InMemoryBrandRepository implements BrandRepository
|
|||
}
|
||||
|
||||
#[\Override]
|
||||
public function findAll(): array
|
||||
public function findAll(Pagination $pagination): Paginated
|
||||
{
|
||||
return array_values($this->brands);
|
||||
$all = array_values($this->brands);
|
||||
$total = count($all);
|
||||
$paged = array_slice($all, $pagination->from(), $pagination->pageSize());
|
||||
|
||||
return new Paginated($pagination, $paged, $total);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use Infra\Adapters\Repositories\EloquentBrandRepository;
|
|||
use Infra\Cache\BrandCache;
|
||||
use Infra\Database\Models\Brand as BrandModel;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Pagination;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
|
@ -147,30 +148,34 @@ describe('EloquentBrandRepository Integration Tests', function (): void {
|
|||
->and($secondResult->name->value)->toBe('Père Dodu');
|
||||
});
|
||||
|
||||
it('finds all brands and caches the list', function (): void {
|
||||
it('finds brands with pagination and warms cache', function (): void {
|
||||
// GIVEN
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$repository = new EloquentBrandRepository($brandCache);
|
||||
|
||||
$id1 = Uuid::uuid7()->toString();
|
||||
$id2 = Uuid::uuid7()->toString();
|
||||
BrandModel::query()->create([
|
||||
'id' => Uuid::uuid7()->toString(),
|
||||
'id' => $id1,
|
||||
'name' => 'Brand 1',
|
||||
'arcadia_id' => 1,
|
||||
'uwil_public_id' => 'BRD01',
|
||||
]);
|
||||
BrandModel::query()->create([
|
||||
'id' => Uuid::uuid7()->toString(),
|
||||
'id' => $id2,
|
||||
'name' => 'Brand 2',
|
||||
'arcadia_id' => 2,
|
||||
'uwil_public_id' => 'BRD02',
|
||||
]);
|
||||
|
||||
// WHEN
|
||||
$all = $repository->findAll();
|
||||
$all = $repository->findAll(Pagination::ofPage(1, 25));
|
||||
|
||||
// THEN
|
||||
expect($all)->toHaveCount(2)
|
||||
->and(Cache::has('brand:all'))->toBeTrue();
|
||||
expect($all->getItems())->toHaveCount(2)
|
||||
->and($all->getTotal())->toBe(2)
|
||||
->and(Cache::has("brand:id:{$id1}"))->toBeTrue()
|
||||
->and(Cache::has("brand:id:{$id2}"))->toBeTrue();
|
||||
});
|
||||
|
||||
it('gracefully recovers when cache contains corrupted or non-array data', function (): void {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
|||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Infra\Adapters\Directories\InMemoryArcadiaBrandDirectory;
|
||||
use Shared\Pagination;
|
||||
|
||||
covers(InMemoryArcadiaBrandDirectory::class);
|
||||
|
||||
|
|
@ -38,16 +39,17 @@ describe('InMemoryArcadiaBrandDirectory Unit Tests', function (): void {
|
|||
$directory->add($b1, $b2);
|
||||
|
||||
// WHEN
|
||||
$all = $directory->findAll();
|
||||
$all = $directory->findAll(Pagination::ofPage(1, 25));
|
||||
|
||||
// THEN
|
||||
expect($all)->toHaveCount(2);
|
||||
expect($all->getItems())->toHaveCount(2)
|
||||
->and($all->getTotal())->toBe(2);
|
||||
|
||||
// WHEN
|
||||
$directory->clear();
|
||||
|
||||
// THEN
|
||||
expect($directory->findAll())->toBeEmpty();
|
||||
expect($directory->findAll(Pagination::ofPage(1, 25))->getItems())->toBeEmpty();
|
||||
});
|
||||
|
||||
it('filters out comment and obsolete brands starting with Z -', function (): void {
|
||||
|
|
@ -58,12 +60,13 @@ describe('InMemoryArcadiaBrandDirectory Unit Tests', function (): void {
|
|||
$directory->add($valid, $comment);
|
||||
|
||||
// WHEN
|
||||
$all = $directory->findAll();
|
||||
$all = $directory->findAll(Pagination::ofPage(1, 25));
|
||||
$found = $directory->find(new ArcadiaId('2'));
|
||||
|
||||
// THEN
|
||||
expect($all)->toHaveCount(1)
|
||||
->and($all[0]->id->value)->toBe('1')
|
||||
expect($all->getItems())->toHaveCount(1)
|
||||
->and($all->getItems()[0]->id->value)->toBe('1')
|
||||
->and($all->getTotal())->toBe(1)
|
||||
->and($found)->toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use Domain\Models\ValueObjects\Brand\BrandId;
|
|||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Infra\Adapters\Repositories\InMemoryBrandRepository;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Pagination;
|
||||
|
||||
covers(InMemoryBrandRepository::class);
|
||||
|
||||
|
|
@ -44,6 +45,22 @@ describe('InMemoryBrandRepository Unit Tests', function (): void {
|
|||
->and($missingByCode)->toBeNull();
|
||||
});
|
||||
|
||||
it('finds brands with pagination', function (): void {
|
||||
// GIVEN
|
||||
$repository = new InMemoryBrandRepository();
|
||||
$b1 = createBrand('LGL01', 'Le Gaulois', '1');
|
||||
$b2 = createBrand('MCQ02', 'Maître CoQ', '2');
|
||||
$repository->add($b1, $b2);
|
||||
|
||||
// WHEN
|
||||
$paginated = $repository->findAll(Pagination::ofPage(1, 1));
|
||||
|
||||
// THEN
|
||||
expect($paginated->getItems())->toHaveCount(1)
|
||||
->and($paginated->getTotal())->toBe(2)
|
||||
->and($paginated->getItems()[0])->toEqual($b1);
|
||||
});
|
||||
|
||||
it('persists and deletes a brand', function (): void {
|
||||
// GIVEN
|
||||
$repository = new InMemoryBrandRepository();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue