feat(brand): add dedicated BrandCache service, event invalidation, and persist brand name locally
This commit is contained in:
parent
dc83df4105
commit
cc368c7102
14 changed files with 550 additions and 169 deletions
|
|
@ -24,11 +24,13 @@ final class BrandFactory extends Factory
|
|||
#[\Override]
|
||||
public function definition(): array
|
||||
{
|
||||
$name = fake()->company();
|
||||
$generator = new BrandCodeGenerator();
|
||||
$code = $generator->generate(new BrandName(fake()->company()));
|
||||
$code = $generator->generate(new BrandName($name));
|
||||
|
||||
return [
|
||||
'id' => Uuid::uuid7()->toString(),
|
||||
'name' => $name,
|
||||
'arcadia_id' => ArcadiaBrand::factory(),
|
||||
'uwil_public_id' => $code->value,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ return new class () extends Migration
|
|||
{
|
||||
Schema::create('brands', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
|
||||
$table->string('name')->comment('The human readable brand name');
|
||||
$table->string('uwil_public_id')->unique()
|
||||
->comment('A unique public id that is generated spefically for the UWIL eco system');
|
||||
$table->bigInteger('arcadia_id')->unique()
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ final class BrandSeeder extends Seeder
|
|||
['arcadia_id' => $brandData['code']],
|
||||
[
|
||||
'id' => Uuid::uuid7()->toString(),
|
||||
'name' => $brandData['libelle'],
|
||||
'uwil_public_id' => $brandData['uwil_public_id'],
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,34 +11,33 @@ use Domain\Models\ValueObjects\Brand\BrandCode;
|
|||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Infra\Cache\BrandCache;
|
||||
use Infra\Database\Models\Brand as BrandModel;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
final class EloquentBrandRepository implements BrandRepository
|
||||
final readonly class EloquentBrandRepository implements BrandRepository
|
||||
{
|
||||
private const int CACHE_TTL_SECONDS = 86400;
|
||||
public function __construct(
|
||||
private BrandCache $brandCache,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function find(BrandId $id): ?Brand
|
||||
{
|
||||
$key = "brand:id:{$id->value}";
|
||||
$brand = $this->fromCacheData(Cache::get($key));
|
||||
|
||||
if ($brand !== null) {
|
||||
return $brand;
|
||||
$cached = $this->brandCache->getById($id);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$model = BrandModel::query()
|
||||
->with('arcadiaBrand')
|
||||
->find($id->value);
|
||||
$model = BrandModel::query()->find($id->value);
|
||||
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$brand = $this->toDomain($model);
|
||||
Cache::put($key, $this->toCacheData($brand), self::CACHE_TTL_SECONDS);
|
||||
$this->brandCache->put($brand);
|
||||
|
||||
return $brand;
|
||||
}
|
||||
|
|
@ -46,15 +45,12 @@ final class EloquentBrandRepository implements BrandRepository
|
|||
#[\Override]
|
||||
public function findByCode(BrandCode $code): ?Brand
|
||||
{
|
||||
$key = "brand:code:{$code->value}";
|
||||
$brand = $this->fromCacheData(Cache::get($key));
|
||||
|
||||
if ($brand !== null) {
|
||||
return $brand;
|
||||
$cached = $this->brandCache->getByCode($code);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$model = BrandModel::query()
|
||||
->with('arcadiaBrand')
|
||||
->where('uwil_public_id', $code->value)
|
||||
->first();
|
||||
|
||||
|
|
@ -63,7 +59,7 @@ final class EloquentBrandRepository implements BrandRepository
|
|||
}
|
||||
|
||||
$brand = $this->toDomain($model);
|
||||
Cache::put($key, $this->toCacheData($brand), self::CACHE_TTL_SECONDS);
|
||||
$this->brandCache->put($brand);
|
||||
|
||||
return $brand;
|
||||
}
|
||||
|
|
@ -71,15 +67,12 @@ final class EloquentBrandRepository implements BrandRepository
|
|||
#[\Override]
|
||||
public function findByArcadiaId(ArcadiaId $arcadiaId): ?Brand
|
||||
{
|
||||
$key = "brand:arcadia:{$arcadiaId->value}";
|
||||
$brand = $this->fromCacheData(Cache::get($key));
|
||||
|
||||
if ($brand !== null) {
|
||||
return $brand;
|
||||
$cached = $this->brandCache->getByArcadiaId($arcadiaId);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$model = BrandModel::query()
|
||||
->with('arcadiaBrand')
|
||||
->where('arcadia_id', $arcadiaId->value)
|
||||
->first();
|
||||
|
||||
|
|
@ -88,7 +81,7 @@ final class EloquentBrandRepository implements BrandRepository
|
|||
}
|
||||
|
||||
$brand = $this->toDomain($model);
|
||||
Cache::put($key, $this->toCacheData($brand), self::CACHE_TTL_SECONDS);
|
||||
$this->brandCache->put($brand);
|
||||
|
||||
return $brand;
|
||||
}
|
||||
|
|
@ -96,32 +89,19 @@ final class EloquentBrandRepository implements BrandRepository
|
|||
#[\Override]
|
||||
public function findAll(): array
|
||||
{
|
||||
$key = 'brand:all';
|
||||
$data = Cache::get($key);
|
||||
|
||||
if (is_array($data)) {
|
||||
$brands = [];
|
||||
foreach ($data as $item) {
|
||||
$brand = $this->fromCacheData($item);
|
||||
if ($brand !== null) {
|
||||
$brands[] = $brand;
|
||||
}
|
||||
}
|
||||
if (! empty($brands) && count($brands) === count($data)) {
|
||||
return $brands;
|
||||
}
|
||||
$cached = $this->brandCache->getAll();
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$brands = array_values(
|
||||
BrandModel::query()
|
||||
->with('arcadiaBrand')
|
||||
->get()
|
||||
->map(fn (BrandModel $model): Brand => $this->toDomain($model))
|
||||
->all()
|
||||
);
|
||||
|
||||
$cachedList = array_map(fn (Brand $brand): array => $this->toCacheData($brand), $brands);
|
||||
Cache::put($key, $cachedList, self::CACHE_TTL_SECONDS);
|
||||
$this->brandCache->putAll(...$brands);
|
||||
|
||||
return $brands;
|
||||
}
|
||||
|
|
@ -132,12 +112,13 @@ final class EloquentBrandRepository implements BrandRepository
|
|||
BrandModel::query()->updateOrCreate(
|
||||
['id' => $entity->id()->value],
|
||||
[
|
||||
'name' => $entity->name->value,
|
||||
'arcadia_id' => $entity->arcadiaId,
|
||||
'uwil_public_id' => $entity->code,
|
||||
]
|
||||
);
|
||||
|
||||
$this->invalidateCache($entity);
|
||||
$this->brandCache->invalidate($entity);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
|
|
@ -151,62 +132,14 @@ final class EloquentBrandRepository implements BrandRepository
|
|||
throw BrandNotFoundException::forBrand($entity->id());
|
||||
}
|
||||
|
||||
$this->invalidateCache($entity);
|
||||
}
|
||||
|
||||
private function invalidateCache(Brand $entity): void
|
||||
{
|
||||
Cache::forget('brand:all');
|
||||
Cache::forget("brand:id:{$entity->id()->value}");
|
||||
Cache::forget("brand:code:{$entity->code->value}");
|
||||
Cache::forget("brand:arcadia:{$entity->arcadiaId->value}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, name: string, code: string, arcadiaId: int}
|
||||
*/
|
||||
private function toCacheData(Brand $brand): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $brand->id()->value,
|
||||
'name' => $brand->name->value,
|
||||
'code' => $brand->code->value,
|
||||
'arcadiaId' => $brand->arcadiaId->value,
|
||||
];
|
||||
}
|
||||
|
||||
private function fromCacheData(mixed $data): ?Brand
|
||||
{
|
||||
if (
|
||||
! is_array($data)
|
||||
|| ! isset($data['id'], $data['name'], $data['code'], $data['arcadiaId'])
|
||||
|| ! is_string($data['id'])
|
||||
|| ! is_string($data['name'])
|
||||
|| ! is_string($data['code'])
|
||||
|| ! is_int($data['arcadiaId'])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Brand::hydrate(
|
||||
id: new BrandId(Uuid::fromString($data['id'])),
|
||||
name: new BrandName($data['name']),
|
||||
code: new BrandCode($data['code']),
|
||||
arcadiaId: new ArcadiaId($data['arcadiaId']),
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
$this->brandCache->invalidate($entity);
|
||||
}
|
||||
|
||||
private function toDomain(BrandModel $model): Brand
|
||||
{
|
||||
$name = $model->arcadiaBrand !== null ? $model->arcadiaBrand->libelle : '';
|
||||
|
||||
return Brand::hydrate(
|
||||
id: new BrandId(Uuid::fromString($model->id)),
|
||||
name: new BrandName($name),
|
||||
name: new BrandName($model->name),
|
||||
code: $model->uwil_public_id,
|
||||
arcadiaId: $model->arcadia_id,
|
||||
);
|
||||
|
|
|
|||
160
src/Infra/Cache/BrandCache.php
Normal file
160
src/Infra/Cache/BrandCache.php
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Infra\Cache;
|
||||
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
final readonly class BrandCache
|
||||
{
|
||||
private const int CACHE_TTL_SECONDS = 86400;
|
||||
|
||||
private const string KEY_ALL = 'brand:all';
|
||||
|
||||
public function __construct(
|
||||
private CacheInterface $cacheStore,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getById(BrandId $id): ?Brand
|
||||
{
|
||||
return $this->fromCacheData($this->cacheStore->get($this->idKey($id)));
|
||||
}
|
||||
|
||||
public function getByCode(BrandCode $code): ?Brand
|
||||
{
|
||||
return $this->fromCacheData($this->cacheStore->get($this->codeKey($code)));
|
||||
}
|
||||
|
||||
public function getByArcadiaId(ArcadiaId $arcadiaId): ?Brand
|
||||
{
|
||||
return $this->fromCacheData($this->cacheStore->get($this->arcadiaKey($arcadiaId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Brand>|null
|
||||
*/
|
||||
public function getAll(): ?array
|
||||
{
|
||||
$data = $this->cacheStore->get(self::KEY_ALL);
|
||||
|
||||
if (! is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$brands = [];
|
||||
foreach ($data as $item) {
|
||||
$brand = $this->fromCacheData($item);
|
||||
if ($brand !== null) {
|
||||
$brands[] = $brand;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($brands) !== count($data) || empty($brands)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $brands;
|
||||
}
|
||||
|
||||
public function put(Brand $brand): void
|
||||
{
|
||||
$data = $this->toCacheData($brand);
|
||||
|
||||
$this->cacheStore->set($this->idKey($brand->id()), $data, self::CACHE_TTL_SECONDS);
|
||||
$this->cacheStore->set($this->codeKey($brand->code), $data, self::CACHE_TTL_SECONDS);
|
||||
$this->cacheStore->set($this->arcadiaKey($brand->arcadiaId), $data, self::CACHE_TTL_SECONDS);
|
||||
}
|
||||
|
||||
public function putAll(Brand ...$brands): void
|
||||
{
|
||||
$cachedList = array_map(fn (Brand $brand): array => $this->toCacheData($brand), $brands);
|
||||
$this->cacheStore->set(self::KEY_ALL, $cachedList, self::CACHE_TTL_SECONDS);
|
||||
|
||||
foreach ($brands as $brand) {
|
||||
$this->put($brand);
|
||||
}
|
||||
}
|
||||
|
||||
public function invalidate(Brand $brand): void
|
||||
{
|
||||
$this->cacheStore->delete(self::KEY_ALL);
|
||||
$this->cacheStore->delete($this->idKey($brand->id()));
|
||||
$this->cacheStore->delete($this->codeKey($brand->code));
|
||||
$this->cacheStore->delete($this->arcadiaKey($brand->arcadiaId));
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->cacheStore->delete(self::KEY_ALL);
|
||||
|
||||
$all = $this->getAll();
|
||||
if ($all !== null) {
|
||||
foreach ($all as $brand) {
|
||||
$this->cacheStore->delete($this->idKey($brand->id()));
|
||||
$this->cacheStore->delete($this->codeKey($brand->code));
|
||||
$this->cacheStore->delete($this->arcadiaKey($brand->arcadiaId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function idKey(BrandId $id): string
|
||||
{
|
||||
return "brand:id:{$id->value}";
|
||||
}
|
||||
|
||||
private function codeKey(BrandCode $code): string
|
||||
{
|
||||
return "brand:code:{$code->value}";
|
||||
}
|
||||
|
||||
private function arcadiaKey(ArcadiaId $arcadiaId): string
|
||||
{
|
||||
return "brand:arcadia:{$arcadiaId->value}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, name: string, code: string, arcadiaId: int}
|
||||
*/
|
||||
private function toCacheData(Brand $brand): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $brand->id()->value,
|
||||
'name' => $brand->name->value,
|
||||
'code' => $brand->code->value,
|
||||
'arcadiaId' => $brand->arcadiaId->value,
|
||||
];
|
||||
}
|
||||
|
||||
private function fromCacheData(mixed $data): ?Brand
|
||||
{
|
||||
if (
|
||||
! is_array($data)
|
||||
|| ! isset($data['id'], $data['name'], $data['code'], $data['arcadiaId'])
|
||||
|| ! is_string($data['id'])
|
||||
|| ! is_string($data['name'])
|
||||
|| ! is_string($data['code'])
|
||||
|| ! is_int($data['arcadiaId'])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Brand::hydrate(
|
||||
id: new BrandId(Uuid::fromString($data['id'])),
|
||||
name: new BrandName($data['name']),
|
||||
code: new BrandCode($data['code']),
|
||||
arcadiaId: new ArcadiaId($data['arcadiaId']),
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,18 +8,16 @@ use Database\Factories\BrandFactory;
|
|||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Infra\Casts\ArcadiaIdCast;
|
||||
use Infra\Casts\BrandCodeCast;
|
||||
use Infra\Database\Relations\ArcadiaBrandBelongsTo;
|
||||
|
||||
/**
|
||||
* @phpcs:disable Generic.Files.LineLength.TooLong
|
||||
*
|
||||
* @property string $id
|
||||
* @property string $name The human readable brand name
|
||||
* @property \Domain\Models\ValueObjects\Brand\BrandCode $uwil_public_id A unique public id that is generated spefically for the UWIL eco system
|
||||
* @property \Domain\Models\ValueObjects\Brand\ArcadiaId $arcadia_id An unique external autoincrement id for the brand, it may be exposed but should not be used
|
||||
* @property-read \Infra\Database\Models\ArcadiaBrand|null $arcadiaBrand
|
||||
*
|
||||
* @method static \Database\Factories\BrandFactory factory($count = null, $state = [])
|
||||
*
|
||||
|
|
@ -44,6 +42,7 @@ final class Brand extends Model
|
|||
/** @var list<string> */
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'name',
|
||||
'arcadia_id',
|
||||
'uwil_public_id',
|
||||
];
|
||||
|
|
@ -57,20 +56,4 @@ final class Brand extends Model
|
|||
'uwil_public_id' => BrandCodeCast::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArcadiaBrandBelongsTo
|
||||
*/
|
||||
public function arcadiaBrand(): BelongsTo
|
||||
{
|
||||
$instance = $this->newRelatedInstance(ArcadiaBrand::class);
|
||||
|
||||
return new ArcadiaBrandBelongsTo(
|
||||
$instance->newQuery(),
|
||||
$this,
|
||||
'arcadia_id',
|
||||
'code',
|
||||
'arcadiaBrand'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Infra\Database\Relations;
|
||||
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @extends BelongsTo<\Infra\Database\Models\ArcadiaBrand, \Infra\Database\Models\Brand>
|
||||
*/
|
||||
final class ArcadiaBrandBelongsTo extends BelongsTo
|
||||
{
|
||||
#[\Override]
|
||||
protected function getForeignKeyFrom(Model $model): mixed
|
||||
{
|
||||
$foreignKey = parent::getForeignKeyFrom($model);
|
||||
|
||||
if ($foreignKey instanceof ArcadiaId) {
|
||||
return $foreignKey->value;
|
||||
}
|
||||
|
||||
return $foreignKey;
|
||||
}
|
||||
}
|
||||
21
src/Infra/Listeners/InvalidateBrandCache.php
Normal file
21
src/Infra/Listeners/InvalidateBrandCache.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Infra\Listeners;
|
||||
|
||||
use Domain\Events\BrandCreated;
|
||||
use Infra\Cache\BrandCache;
|
||||
|
||||
final readonly class InvalidateBrandCache
|
||||
{
|
||||
public function __construct(
|
||||
private BrandCache $brandCache,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(BrandCreated $_): void
|
||||
{
|
||||
$this->brandCache->reset();
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ use Domain\Ports\Directories\IncoCompositionDirectory;
|
|||
use Domain\Ports\Directories\IncoSheetDirectory;
|
||||
use Domain\Ports\Directories\IncoShortSheetDirectory;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Infra\Adapters\Directories\EloquentArcadiaBrandDirectory;
|
||||
use Infra\Adapters\Directories\EloquentEmployeeDirectory;
|
||||
|
|
@ -18,6 +20,7 @@ use Infra\Adapters\Directories\EloquentIncoSheetDirectory;
|
|||
use Infra\Adapters\Directories\EloquentIncoShortSheetDirectory;
|
||||
use Infra\Adapters\Events\LaravelTransactionalDomainEventDispatcher;
|
||||
use Infra\Adapters\Repositories\EloquentBrandRepository;
|
||||
use Infra\Cache\BrandCache;
|
||||
use Infra\Usecase\LaravelExecutor;
|
||||
use Shared\DDD\Event\DomainEventDispatcher;
|
||||
use Shared\DDD\Event\TransactionalDomainEventDispatcher;
|
||||
|
|
@ -35,6 +38,14 @@ class DomainServiceProvider extends ServiceProvider
|
|||
$this->app->bind(ArcadiaBrandDirectory::class, EloquentArcadiaBrandDirectory::class);
|
||||
$this->app->bind(BrandRepository::class, EloquentBrandRepository::class);
|
||||
|
||||
$this->app->bind(BrandCache::class, static function (): BrandCache {
|
||||
$storeName = Config::string('cache.arcadia_store');
|
||||
/** @var \Psr\SimpleCache\CacheInterface $cache */
|
||||
$cache = Cache::store($storeName);
|
||||
|
||||
return new BrandCache($cache);
|
||||
});
|
||||
|
||||
$this->app->scoped(LaravelTransactionalDomainEventDispatcher::class);
|
||||
$this->app->bind(DomainEventDispatcher::class, LaravelTransactionalDomainEventDispatcher::class);
|
||||
$this->app->bind(TransactionalDomainEventDispatcher::class, LaravelTransactionalDomainEventDispatcher::class);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use Application\Security\Events\ClientStatusToggled;
|
|||
use Domain\Events\BrandCreated;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Infra\Listeners\InvalidateBrandCache;
|
||||
use Infra\Listeners\LogBrandCreated;
|
||||
use Infra\Listeners\LogClientSecretRotated;
|
||||
use Infra\Listeners\LogClientStatusChanged;
|
||||
|
|
@ -23,6 +24,11 @@ class EventServiceProvider extends ServiceProvider
|
|||
LogBrandCreated::class,
|
||||
);
|
||||
|
||||
Event::listen(
|
||||
BrandCreated::class,
|
||||
InvalidateBrandCache::class,
|
||||
);
|
||||
|
||||
Event::listen(
|
||||
ClientSecretGenerated::class,
|
||||
LogClientSecretRotated::class,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ describe('BrandSeeder Integration Test', function (): void {
|
|||
$brand = Brand::query()->where('uwil_public_id', 'LGL01')->first();
|
||||
expect($brand)->not->toBeNull()
|
||||
->and($brand?->arcadia_id->value)->toBe(1)
|
||||
->and($brand?->arcadiaBrand)->not->toBeNull()
|
||||
->and($brand?->arcadiaBrand?->libelle)->toBe('Le Gaulois');
|
||||
->and($brand?->name)->toBe('Le Gaulois');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use Domain\Models\ValueObjects\Brand\BrandName;
|
|||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Infra\Adapters\Repositories\EloquentBrandRepository;
|
||||
use Infra\Cache\BrandCache;
|
||||
use Infra\Database\Models\ArcadiaBrand;
|
||||
use Infra\Database\Models\Brand as BrandModel;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
|
@ -30,18 +31,13 @@ beforeEach(function (): void {
|
|||
describe('EloquentBrandRepository Integration Tests', function (): void {
|
||||
it('persists a new brand into database and invalidates cache', function (): void {
|
||||
// GIVEN
|
||||
$repository = new EloquentBrandRepository();
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$repository = new EloquentBrandRepository($brandCache);
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
$brandCode = new BrandCode('LGL01');
|
||||
$arcadiaId = new ArcadiaId(1);
|
||||
$name = new BrandName('Le Gaulois');
|
||||
|
||||
ArcadiaBrand::query()->firstOrCreate([
|
||||
'code' => 1,
|
||||
], [
|
||||
'libelle' => 'Le Gaulois',
|
||||
]);
|
||||
|
||||
$brand = Brand::hydrate(
|
||||
id: $brandId,
|
||||
name: $name,
|
||||
|
|
@ -63,12 +59,13 @@ describe('EloquentBrandRepository Integration Tests', function (): void {
|
|||
|
||||
it('finds a brand by BrandId and serves subsequent calls from cache', function (): void {
|
||||
// GIVEN
|
||||
$repository = new EloquentBrandRepository();
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$repository = new EloquentBrandRepository($brandCache);
|
||||
$rawId = Uuid::uuid7()->toString();
|
||||
|
||||
ArcadiaBrand::query()->firstOrCreate(['code' => 10], ['libelle' => 'Maître CoQ']);
|
||||
BrandModel::query()->create([
|
||||
'id' => $rawId,
|
||||
'name' => 'Maître CoQ',
|
||||
'arcadia_id' => 10,
|
||||
'uwil_public_id' => 'MCQ01',
|
||||
]);
|
||||
|
|
@ -91,17 +88,19 @@ describe('EloquentBrandRepository Integration Tests', function (): void {
|
|||
expect($firstResult)->not->toBeNull()
|
||||
->and($secondResult)->not->toBeNull()
|
||||
->and($secondResult->id()->value)->toBe($brandId->value)
|
||||
->and($secondResult->code->value)->toBe('MCQ01');
|
||||
->and($secondResult->code->value)->toBe('MCQ01')
|
||||
->and($secondResult->name->value)->toBe('Maître CoQ');
|
||||
});
|
||||
|
||||
it('finds a brand by BrandCode and serves subsequent calls from cache', function (): void {
|
||||
// GIVEN
|
||||
$repository = new EloquentBrandRepository();
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$repository = new EloquentBrandRepository($brandCache);
|
||||
$rawId = Uuid::uuid7()->toString();
|
||||
|
||||
ArcadiaBrand::query()->firstOrCreate(['code' => 20], ['libelle' => 'Doux']);
|
||||
BrandModel::query()->create([
|
||||
'id' => $rawId,
|
||||
'name' => 'Doux',
|
||||
'arcadia_id' => 20,
|
||||
'uwil_public_id' => 'DOX01',
|
||||
]);
|
||||
|
|
@ -118,17 +117,19 @@ describe('EloquentBrandRepository Integration Tests', function (): void {
|
|||
// THEN
|
||||
expect($firstResult)->not->toBeNull()
|
||||
->and($secondResult)->not->toBeNull()
|
||||
->and($secondResult->code->value)->toBe('DOX01');
|
||||
->and($secondResult->code->value)->toBe('DOX01')
|
||||
->and($secondResult->name->value)->toBe('Doux');
|
||||
});
|
||||
|
||||
it('finds a brand by ArcadiaId and serves subsequent calls from cache', function (): void {
|
||||
// GIVEN
|
||||
$repository = new EloquentBrandRepository();
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$repository = new EloquentBrandRepository($brandCache);
|
||||
$rawId = Uuid::uuid7()->toString();
|
||||
|
||||
ArcadiaBrand::query()->firstOrCreate(['code' => 30], ['libelle' => 'Père Dodu']);
|
||||
BrandModel::query()->create([
|
||||
'id' => $rawId,
|
||||
'name' => 'Père Dodu',
|
||||
'arcadia_id' => 30,
|
||||
'uwil_public_id' => 'PRD01',
|
||||
]);
|
||||
|
|
@ -145,23 +146,24 @@ describe('EloquentBrandRepository Integration Tests', function (): void {
|
|||
// THEN
|
||||
expect($firstResult)->not->toBeNull()
|
||||
->and($secondResult)->not->toBeNull()
|
||||
->and($secondResult->arcadiaId->value)->toBe(30);
|
||||
->and($secondResult->arcadiaId->value)->toBe(30)
|
||||
->and($secondResult->name->value)->toBe('Père Dodu');
|
||||
});
|
||||
|
||||
it('finds all brands and caches the list', function (): void {
|
||||
// GIVEN
|
||||
$repository = new EloquentBrandRepository();
|
||||
|
||||
ArcadiaBrand::query()->firstOrCreate(['code' => 1], ['libelle' => 'Brand 1']);
|
||||
ArcadiaBrand::query()->firstOrCreate(['code' => 2], ['libelle' => 'Brand 2']);
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$repository = new EloquentBrandRepository($brandCache);
|
||||
|
||||
BrandModel::query()->create([
|
||||
'id' => Uuid::uuid7()->toString(),
|
||||
'name' => 'Brand 1',
|
||||
'arcadia_id' => 1,
|
||||
'uwil_public_id' => 'BRD01',
|
||||
]);
|
||||
BrandModel::query()->create([
|
||||
'id' => Uuid::uuid7()->toString(),
|
||||
'name' => 'Brand 2',
|
||||
'arcadia_id' => 2,
|
||||
'uwil_public_id' => 'BRD02',
|
||||
]);
|
||||
|
|
@ -176,12 +178,13 @@ describe('EloquentBrandRepository Integration Tests', function (): void {
|
|||
|
||||
it('gracefully recovers when cache contains corrupted or non-array data', function (): void {
|
||||
// GIVEN
|
||||
$repository = new EloquentBrandRepository();
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$repository = new EloquentBrandRepository($brandCache);
|
||||
$rawId = Uuid::uuid7()->toString();
|
||||
|
||||
ArcadiaBrand::query()->firstOrCreate(['code' => 40], ['libelle' => 'Fermiers d\'Ancenis']);
|
||||
BrandModel::query()->create([
|
||||
'id' => $rawId,
|
||||
'name' => "Fermiers d'Ancenis",
|
||||
'arcadia_id' => 40,
|
||||
'uwil_public_id' => 'FDA01',
|
||||
]);
|
||||
|
|
@ -211,12 +214,13 @@ describe('EloquentBrandRepository Integration Tests', function (): void {
|
|||
|
||||
it('deletes an existing brand and invalidates cache', function (): void {
|
||||
// GIVEN
|
||||
$repository = new EloquentBrandRepository();
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$repository = new EloquentBrandRepository($brandCache);
|
||||
$rawId = Uuid::uuid7()->toString();
|
||||
|
||||
ArcadiaBrand::query()->firstOrCreate(['code' => 50], ['libelle' => 'To Delete']);
|
||||
BrandModel::query()->create([
|
||||
'id' => $rawId,
|
||||
'name' => 'To Delete',
|
||||
'arcadia_id' => 50,
|
||||
'uwil_public_id' => 'DEL01',
|
||||
]);
|
||||
|
|
@ -244,7 +248,8 @@ describe('EloquentBrandRepository Integration Tests', function (): void {
|
|||
|
||||
it('throws BrandNotFoundException when deleting non-existent brand', function (): void {
|
||||
// GIVEN
|
||||
$repository = new EloquentBrandRepository();
|
||||
$brandCache = resolve(BrandCache::class);
|
||||
$repository = new EloquentBrandRepository($brandCache);
|
||||
$brand = Brand::hydrate(
|
||||
id: new BrandId(Uuid::uuid7()),
|
||||
name: new BrandName('Ghost'),
|
||||
|
|
|
|||
244
tests/Unit/Infra/Cache/BrandCacheTest.php
Normal file
244
tests/Unit/Infra/Cache/BrandCacheTest.php
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Infra\Cache;
|
||||
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Infra\Cache\BrandCache;
|
||||
use Mockery;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(BrandCache::class);
|
||||
|
||||
describe('BrandCache Unit Tests', function (): void {
|
||||
it('returns brand from cache when found by id', function (): void {
|
||||
// GIVEN
|
||||
$uuid = Uuid::uuid7()->toString();
|
||||
$brandId = new BrandId(Uuid::fromString($uuid));
|
||||
|
||||
$cacheData = [
|
||||
'id' => $uuid,
|
||||
'name' => 'Le Gaulois',
|
||||
'code' => 'LGL01',
|
||||
'arcadiaId' => 1,
|
||||
];
|
||||
|
||||
/** @var CacheInterface&Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('get')
|
||||
->once()
|
||||
->with("brand:id:{$uuid}")
|
||||
->andReturn($cacheData);
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
|
||||
// WHEN
|
||||
$result = $brandCache->getById($brandId);
|
||||
|
||||
// THEN
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result->id()->value)->toBe($brandId->value)
|
||||
->and($result->name->value)->toBe('Le Gaulois')
|
||||
->and($result->code->value)->toBe('LGL01')
|
||||
->and($result->arcadiaId->value)->toBe(1);
|
||||
});
|
||||
|
||||
it('returns brand from cache when found by code', function (): void {
|
||||
// GIVEN
|
||||
$uuid = Uuid::uuid7()->toString();
|
||||
$brandCode = new BrandCode('LGL01');
|
||||
|
||||
$cacheData = [
|
||||
'id' => $uuid,
|
||||
'name' => 'Le Gaulois',
|
||||
'code' => 'LGL01',
|
||||
'arcadiaId' => 1,
|
||||
];
|
||||
|
||||
/** @var CacheInterface&Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('get')
|
||||
->once()
|
||||
->with('brand:code:LGL01')
|
||||
->andReturn($cacheData);
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
|
||||
// WHEN
|
||||
$result = $brandCache->getByCode($brandCode);
|
||||
|
||||
// THEN
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result->code->value)->toBe('LGL01')
|
||||
->and($result->name->value)->toBe('Le Gaulois');
|
||||
});
|
||||
|
||||
it('returns brand from cache when found by arcadia id', function (): void {
|
||||
// GIVEN
|
||||
$uuid = Uuid::uuid7()->toString();
|
||||
$arcadiaId = new ArcadiaId(10);
|
||||
|
||||
$cacheData = [
|
||||
'id' => $uuid,
|
||||
'name' => 'Maître CoQ',
|
||||
'code' => 'MCQ01',
|
||||
'arcadiaId' => 10,
|
||||
];
|
||||
|
||||
/** @var CacheInterface&Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('get')
|
||||
->once()
|
||||
->with('brand:arcadia:10')
|
||||
->andReturn($cacheData);
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
|
||||
// WHEN
|
||||
$result = $brandCache->getByArcadiaId($arcadiaId);
|
||||
|
||||
// THEN
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result->arcadiaId->value)->toBe(10)
|
||||
->and($result->name->value)->toBe('Maître CoQ');
|
||||
});
|
||||
|
||||
it('returns null and recovers gracefully when cache data is corrupted', function (): void {
|
||||
// GIVEN
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
|
||||
/** @var CacheInterface&Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('get')
|
||||
->once()
|
||||
->with("brand:id:{$brandId->value}")
|
||||
->andReturn('corrupted_string');
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
|
||||
// WHEN
|
||||
$result = $brandCache->getById($brandId);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
it('stores brand in cache across all key variations', function (): void {
|
||||
// GIVEN
|
||||
$uuid = Uuid::uuid7()->toString();
|
||||
$brand = Brand::hydrate(
|
||||
id: new BrandId(Uuid::fromString($uuid)),
|
||||
name: new BrandName('Doux'),
|
||||
code: new BrandCode('DOX01'),
|
||||
arcadiaId: new ArcadiaId(20),
|
||||
);
|
||||
|
||||
$expectedData = [
|
||||
'id' => $uuid,
|
||||
'name' => 'Doux',
|
||||
'code' => 'DOX01',
|
||||
'arcadiaId' => 20,
|
||||
];
|
||||
|
||||
/** @var CacheInterface&Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('set')
|
||||
->once()
|
||||
->with("brand:id:{$uuid}", $expectedData, 86400);
|
||||
$cacheMock->shouldReceive('set')
|
||||
->once()
|
||||
->with('brand:code:DOX01', $expectedData, 86400);
|
||||
$cacheMock->shouldReceive('set')
|
||||
->once()
|
||||
->with('brand:arcadia:20', $expectedData, 86400);
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
|
||||
// WHEN
|
||||
$brandCache->put($brand);
|
||||
|
||||
// THEN
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('invalidates specific brand keys and collection key', function (): void {
|
||||
// GIVEN
|
||||
$uuid = Uuid::uuid7()->toString();
|
||||
$brand = Brand::hydrate(
|
||||
id: new BrandId(Uuid::fromString($uuid)),
|
||||
name: new BrandName('Père Dodu'),
|
||||
code: new BrandCode('PRD01'),
|
||||
arcadiaId: new ArcadiaId(30),
|
||||
);
|
||||
|
||||
/** @var CacheInterface&Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('delete')
|
||||
->once()
|
||||
->with('brand:all');
|
||||
$cacheMock->shouldReceive('delete')
|
||||
->once()
|
||||
->with("brand:id:{$uuid}");
|
||||
$cacheMock->shouldReceive('delete')
|
||||
->once()
|
||||
->with('brand:code:PRD01');
|
||||
$cacheMock->shouldReceive('delete')
|
||||
->once()
|
||||
->with('brand:arcadia:30');
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
|
||||
// WHEN
|
||||
$brandCache->invalidate($brand);
|
||||
|
||||
// THEN
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('resets all brand caches when reset is called', function (): void {
|
||||
// GIVEN
|
||||
$uuid = Uuid::uuid7()->toString();
|
||||
$cacheData = [
|
||||
[
|
||||
'id' => $uuid,
|
||||
'name' => 'Brand 1',
|
||||
'code' => 'BRD01',
|
||||
'arcadiaId' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
/** @var CacheInterface&Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('get')
|
||||
->once()
|
||||
->with('brand:all')
|
||||
->andReturn($cacheData);
|
||||
|
||||
$cacheMock->shouldReceive('delete')
|
||||
->once()
|
||||
->with('brand:all');
|
||||
$cacheMock->shouldReceive('delete')
|
||||
->once()
|
||||
->with("brand:id:{$uuid}");
|
||||
$cacheMock->shouldReceive('delete')
|
||||
->once()
|
||||
->with('brand:code:BRD01');
|
||||
$cacheMock->shouldReceive('delete')
|
||||
->once()
|
||||
->with('brand:arcadia:1');
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
|
||||
// WHEN
|
||||
$brandCache->reset();
|
||||
|
||||
// THEN
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
});
|
||||
43
tests/Unit/Infra/Listeners/InvalidateBrandCacheTest.php
Normal file
43
tests/Unit/Infra/Listeners/InvalidateBrandCacheTest.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Infra\Listeners;
|
||||
|
||||
use Domain\Events\BrandCreated;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandCode;
|
||||
use Domain\Models\ValueObjects\Brand\BrandId;
|
||||
use Domain\Models\ValueObjects\Brand\BrandName;
|
||||
use Infra\Cache\BrandCache;
|
||||
use Infra\Listeners\InvalidateBrandCache;
|
||||
use Mockery;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(InvalidateBrandCache::class);
|
||||
|
||||
describe('InvalidateBrandCache Listener Unit Tests', function (): void {
|
||||
it('resets brand cache when BrandCreated event is handled', function (): void {
|
||||
// GIVEN
|
||||
$event = new BrandCreated(
|
||||
brandId: new BrandId(Uuid::uuid7()),
|
||||
brandName: new BrandName('Le Gaulois'),
|
||||
brandCode: new BrandCode('LGL01'),
|
||||
arcadiaId: new ArcadiaId(1),
|
||||
occurredAt: new \DateTimeImmutable(),
|
||||
);
|
||||
|
||||
/** @var BrandCache&Mockery\MockInterface $brandCacheMock */
|
||||
$brandCacheMock = Mockery::mock(BrandCache::class);
|
||||
$brandCacheMock->shouldReceive('reset')
|
||||
->once();
|
||||
|
||||
$listener = new InvalidateBrandCache($brandCacheMock);
|
||||
|
||||
// WHEN
|
||||
$listener->handle($event);
|
||||
|
||||
// THEN
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue