feat(infra): add Brand Eloquent models, migrations, casts, adapters and seeders

This commit is contained in:
Jérémy Munsch 2026-08-20 17:35:42 +02:00
parent a42a79861d
commit 20ec692160
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
22 changed files with 1095 additions and 2 deletions

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Infra\Database\Models\ArcadiaBrand;
/**
* @extends Factory<ArcadiaBrand>
*/
final class ArcadiaBrandFactory extends Factory
{
protected $model = ArcadiaBrand::class;
/**
* @return array<string, mixed>
*/
#[\Override]
public function definition(): array
{
return [
'code' => fake()->unique()->numberBetween(1, 999),
'libelle' => fake()->company(),
];
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use Domain\Models\ValueObjects\Brand\BrandName;
use Domain\Services\BrandCodeGenerator;
use Illuminate\Database\Eloquent\Factories\Factory;
use Infra\Database\Models\ArcadiaBrand;
use Infra\Database\Models\Brand;
use Ramsey\Uuid\Uuid;
/**
* @extends Factory<Brand>
*/
final class BrandFactory extends Factory
{
protected $model = Brand::class;
/**
* @return array<string, mixed>
*/
#[\Override]
public function definition(): array
{
$generator = new BrandCodeGenerator();
$code = $generator->generate(new BrandName(fake()->company()));
return [
'id' => Uuid::uuid7()->toString(),
'arcadia_id' => ArcadiaBrand::factory(),
'uwil_public_id' => $code->value,
];
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('brands', function (Blueprint $table) {
$table->uuid('id')->primary();
$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()
->comment('An unique external autoincrement id for the brand, it may be exposed but should not be used');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('brands');
}
};

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$schema = Schema::connection(Config::string('database.arcadia_meta_model'));
$schema->dropIfExists('arcadia_brands');
$schema->create('arcadia_brands', function (Blueprint $table) {
$table->integer('code')->unsigned()->primary();
$table->string('libelle');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$schema = Schema::connection(Config::string('database.arcadia_meta_model'));
$schema->dropIfExists('arcadia_brands');
}
};

View file

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Infra\Database\Models\ArcadiaBrand;
use Infra\Database\Models\Brand;
use Ramsey\Uuid\Uuid;
final class BrandSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
/** @var list<array{code: int, libelle: string, uwil_public_id: string}> $brands */
$brands = [
['code' => 1, 'libelle' => 'Le Gaulois', 'uwil_public_id' => 'LGL01'],
['code' => 2, 'libelle' => 'Maître CoQ', 'uwil_public_id' => 'MCQ02'],
['code' => 3, 'libelle' => 'Loué', 'uwil_public_id' => 'LOU03'],
['code' => 4, 'libelle' => 'Marie', 'uwil_public_id' => 'MAR04'],
['code' => 5, 'libelle' => "Tradition d'Asie", 'uwil_public_id' => 'TDA05'],
['code' => 6, 'libelle' => 'Réghalal', 'uwil_public_id' => 'REG06'],
];
foreach ($brands as $brandData) {
ArcadiaBrand::query()->updateOrCreate(
['code' => $brandData['code']],
['libelle' => $brandData['libelle']],
);
Brand::query()->firstOrCreate(
['arcadia_id' => $brandData['code']],
[
'id' => Uuid::uuid7()->toString(),
'uwil_public_id' => $brandData['uwil_public_id'],
],
);
}
}
}

View file

@ -17,6 +17,7 @@ final class DatabaseSeeder extends Seeder
EmployeeSeeder::class,
IncoSheetSeeder::class,
ApiClientSeeder::class,
BrandSeeder::class,
]);
}
}

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Infra\Adapters\Directories;
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
use Domain\Models\ValueObjects\Brand\ArcadiaId;
use Domain\Models\ValueObjects\Brand\BrandName;
use Domain\Ports\Directories\ArcadiaBrandDirectory;
use Infra\Database\Models\ArcadiaBrand as ArcadiaBrandModel;
final class EloquentArcadiaBrandDirectory implements ArcadiaBrandDirectory
{
#[\Override]
public function findAll(): array
{
return array_values(
ArcadiaBrandModel::query()
->get()
->map(fn (ArcadiaBrandModel $model): ArcadiaBrand => $this->toDomain($model))
->all()
);
}
#[\Override]
public function find(ArcadiaId $id): ?ArcadiaBrand
{
$model = ArcadiaBrandModel::query()->find($id->value);
if ($model === null) {
return null;
}
return $this->toDomain($model);
}
private function toDomain(ArcadiaBrandModel $model): ArcadiaBrand
{
return new ArcadiaBrand(
id: new ArcadiaId((int) $model->code),
name: new BrandName((string) $model->libelle),
);
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Infra\Adapters\Directories;
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
use Domain\Models\ValueObjects\Brand\ArcadiaId;
use Domain\Ports\Directories\ArcadiaBrandDirectory;
final class InMemoryArcadiaBrandDirectory implements ArcadiaBrandDirectory
{
/** @var array<int, ArcadiaBrand> */
private array $brands = [];
public function add(ArcadiaBrand ...$brands): self
{
foreach ($brands as $brand) {
$this->brands[$brand->id->value] = $brand;
}
return $this;
}
public function clear(): void
{
$this->brands = [];
}
#[\Override]
public function findAll(): array
{
return array_values($this->brands);
}
#[\Override]
public function find(ArcadiaId $id): ?ArcadiaBrand
{
return $this->brands[$id->value] ?? null;
}
}

View file

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Infra\Adapters\Repositories;
use Domain\Exceptions\BrandNotFoundException;
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 Domain\Ports\Repositories\BrandRepository;
use Infra\Database\Models\Brand as BrandModel;
use Ramsey\Uuid\Uuid;
final class EloquentBrandRepository implements BrandRepository
{
#[\Override]
public function find(BrandId $id): ?Brand
{
$model = BrandModel::query()
->with('arcadiaBrand')
->find($id->value);
if ($model === null) {
return null;
}
return $this->toDomain($model);
}
#[\Override]
public function findByCode(BrandCode $code): ?Brand
{
$model = BrandModel::query()
->with('arcadiaBrand')
->where('uwil_public_id', $code->value)
->first();
if ($model === null) {
return null;
}
return $this->toDomain($model);
}
#[\Override]
public function findByArcadiaId(ArcadiaId $arcadiaId): ?Brand
{
$model = BrandModel::query()
->with('arcadiaBrand')
->where('arcadia_id', $arcadiaId->value)
->first();
if ($model === null) {
return null;
}
return $this->toDomain($model);
}
#[\Override]
public function findAll(): array
{
return array_values(
BrandModel::query()
->with('arcadiaBrand')
->get()
->map(fn (BrandModel $model): Brand => $this->toDomain($model))
->all()
);
}
#[\Override]
public function persist(Brand $entity): void
{
BrandModel::query()->updateOrCreate(
['id' => $entity->id()->value],
[
'arcadia_id' => $entity->arcadiaId,
'uwil_public_id' => $entity->code,
]
);
}
#[\Override]
public function delete(Brand $entity): void
{
$deleted = BrandModel::query()
->where('id', $entity->id()->value)
->delete();
if ($deleted === 0) {
throw BrandNotFoundException::forBrand($entity->id());
}
}
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),
code: $model->uwil_public_id,
arcadiaId: $model->arcadia_id,
);
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Infra\Adapters\Repositories;
use Domain\Exceptions\BrandNotFoundException;
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\Ports\Repositories\BrandRepository;
final class InMemoryBrandRepository implements BrandRepository
{
/** @var array<string, Brand> */
private array $brands = [];
public function add(Brand ...$brands): self
{
foreach ($brands as $brand) {
$this->brands[$brand->id()->value] = $brand;
}
return $this;
}
public function clear(): void
{
$this->brands = [];
}
#[\Override]
public function find(BrandId $id): ?Brand
{
return $this->brands[$id->value] ?? null;
}
#[\Override]
public function findByCode(BrandCode $code): ?Brand
{
foreach ($this->brands as $brand) {
if ($brand->code->value === $code->value) {
return $brand;
}
}
return null;
}
#[\Override]
public function findByArcadiaId(ArcadiaId $arcadiaId): ?Brand
{
foreach ($this->brands as $brand) {
if ($brand->arcadiaId->value === $arcadiaId->value) {
return $brand;
}
}
return null;
}
#[\Override]
public function findAll(): array
{
return array_values($this->brands);
}
#[\Override]
public function persist(Brand $entity): void
{
$this->brands[$entity->id()->value] = $entity;
}
#[\Override]
public function delete(Brand $entity): void
{
if (! isset($this->brands[$entity->id()->value])) {
throw BrandNotFoundException::forBrand($entity->id());
}
unset($this->brands[$entity->id()->value]);
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Infra\Casts;
use Domain\Models\ValueObjects\Brand\ArcadiaId;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
/** @implements CastsAttributes<ArcadiaId, int|string|ArcadiaId> */
final class ArcadiaIdCast implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): ArcadiaId
{
if ($value === null) {
throw new \InvalidArgumentException('ArcadiaId value cannot be null');
}
if (! is_int($value) && ! (is_string($value) && ctype_digit($value))) {
throw new \InvalidArgumentException('Value must be an integer or numeric string');
}
return new ArcadiaId((int) $value);
}
public function set(Model $model, string $key, mixed $value, array $attributes): int
{
if ($value === null) {
throw new \InvalidArgumentException('ArcadiaId value cannot be null');
}
if ($value instanceof ArcadiaId) {
return $value->value;
}
if (is_int($value)) {
return (new ArcadiaId($value))->value;
}
if (is_string($value) && ctype_digit($value)) {
return (new ArcadiaId((int) $value))->value;
}
throw new \InvalidArgumentException('Value must be an integer, numeric string, or instance of ArcadiaId');
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Infra\Casts;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
/** @implements CastsAttributes<BrandCode, string|BrandCode> */
final class BrandCodeCast implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): BrandCode
{
if ($value === null) {
throw new \InvalidArgumentException('BrandCode value cannot be null');
}
if (! is_string($value)) {
throw new \InvalidArgumentException('Value must be a string');
}
return new BrandCode($value);
}
public function set(Model $model, string $key, mixed $value, array $attributes): string
{
if ($value === null) {
throw new \InvalidArgumentException('BrandCode value cannot be null');
}
if ($value instanceof BrandCode) {
return $value->value;
}
if (is_string($value)) {
return (new BrandCode($value))->value;
}
throw new \InvalidArgumentException('Value must be a string or instance of BrandCode');
}
}

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Infra\Database\Models;
use Database\Factories\ArcadiaBrandFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Config;
/**
* @property int $code
* @property string $libelle
*
* @method static \Database\Factories\ArcadiaBrandFactory factory($count = null, $state = [])
*
* @mixin \Eloquent
*/
final class ArcadiaBrand extends Model
{
/** @use HasFactory<ArcadiaBrandFactory> */
use HasFactory;
protected static function newFactory(): ArcadiaBrandFactory
{
return ArcadiaBrandFactory::new();
}
#[\Override]
public function getConnectionName(): string
{
return Config::string('database.arcadia_meta_model');
}
protected $table = 'arcadia_brands';
protected $primaryKey = 'code';
public $incrementing = false;
/** @var string */
protected $keyType = 'int';
public $timestamps = false;
/** @var list<string> */
protected $fillable = [
'code',
'libelle',
];
/** @return array<string, string> */
#[\Override]
protected function casts(): array
{
return [
'code' => 'integer',
];
}
}

View file

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Infra\Database\Models;
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 \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 = [])
*
* @mixin \Eloquent
*/
final class Brand extends Model
{
/** @use HasFactory<BrandFactory> */
use HasFactory;
use HasUuids;
protected static function newFactory(): BrandFactory
{
return BrandFactory::new();
}
protected $table = 'brands';
public $timestamps = false;
/** @var list<string> */
protected $fillable = [
'id',
'arcadia_id',
'uwil_public_id',
];
/** @return array<string, string> */
#[\Override]
protected function casts(): array
{
return [
'arcadia_id' => ArcadiaIdCast::class,
'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'
);
}
}

View file

@ -0,0 +1,27 @@
<?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;
}
}

View file

@ -4,16 +4,20 @@ declare(strict_types=1);
namespace Infra\Providers;
use Domain\Ports\Directories\ArcadiaBrandDirectory;
use Domain\Ports\Directories\EmployeeDirectory;
use Domain\Ports\Directories\IncoCompositionDirectory;
use Domain\Ports\Directories\IncoSheetDirectory;
use Domain\Ports\Directories\IncoShortSheetDirectory;
use Domain\Ports\Repositories\BrandRepository;
use Illuminate\Support\ServiceProvider;
use Infra\Adapters\Directories\EloquentArcadiaBrandDirectory;
use Infra\Adapters\Directories\EloquentEmployeeDirectory;
use Infra\Adapters\Directories\EloquentIncoCompositionDirectory;
use Infra\Adapters\Directories\EloquentIncoSheetDirectory;
use Infra\Adapters\Directories\EloquentIncoShortSheetDirectory;
use Infra\Adapters\Events\LaravelDomainEventDispatcher;
use Infra\Adapters\Repositories\EloquentBrandRepository;
use Infra\Usecase\LaravelExecutor;
use Shared\DDD\Event\DomainEventDispatcher;
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
@ -27,6 +31,8 @@ class DomainServiceProvider extends ServiceProvider
$this->app->bind(IncoSheetDirectory::class, EloquentIncoSheetDirectory::class);
$this->app->bind(IncoShortSheetDirectory::class, EloquentIncoShortSheetDirectory::class);
$this->app->bind(IncoCompositionDirectory::class, EloquentIncoCompositionDirectory::class);
$this->app->bind(ArcadiaBrandDirectory::class, EloquentArcadiaBrandDirectory::class);
$this->app->bind(BrandRepository::class, EloquentBrandRepository::class);
$this->app->bind(DomainEventDispatcher::class, LaravelDomainEventDispatcher::class);
$this->app->singleton(UsecaseExecutor::class, LaravelExecutor::class);
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Tests\Integration\Database\Seeders;
use Database\Seeders\BrandSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Infra\Database\Models\ArcadiaBrand;
use Infra\Database\Models\Brand;
use Tests\TestCase;
covers(Brand::class, ArcadiaBrand::class);
uses(TestCase::class, RefreshDatabase::class);
describe('BrandSeeder Integration Test', function (): void {
it('seeds default brand and arcadia brand records into the database', function (): void {
// GIVEN
$seeder = new BrandSeeder();
// WHEN
$seeder->run();
// THEN
expect(ArcadiaBrand::count())->toBe(6)
->and(Brand::count())->toBe(6);
$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');
});
});

View file

@ -6,11 +6,13 @@ namespace Tests\Integration\Database\Seeders;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Infra\Database\Models\ArcadiaBrand;
use Infra\Database\Models\Brand;
use Infra\Database\Models\Employee;
use Infra\Database\Models\IncoSheet;
use Tests\TestCase;
covers(Employee::class, IncoSheet::class);
covers(Employee::class, IncoSheet::class, Brand::class, ArcadiaBrand::class);
uses(TestCase::class, RefreshDatabase::class);
@ -24,6 +26,8 @@ describe('DatabaseSeeder Integration Test', function (): void {
// THEN
expect(Employee::count())->toBeGreaterThan(0)
->and(IncoSheet::count())->toBeGreaterThan(0);
->and(IncoSheet::count())->toBeGreaterThan(0)
->and(Brand::count())->toBeGreaterThan(0)
->and(ArcadiaBrand::count())->toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Infra\Adapters\Directories;
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
use Domain\Models\ValueObjects\Brand\ArcadiaId;
use Domain\Models\ValueObjects\Brand\BrandName;
use Infra\Adapters\Directories\InMemoryArcadiaBrandDirectory;
covers(InMemoryArcadiaBrandDirectory::class);
describe('InMemoryArcadiaBrandDirectory Unit Tests', function (): void {
it('finds an arcadia brand by id or returns null if not found', function (): void {
// GIVEN
$directory = new InMemoryArcadiaBrandDirectory();
$brand = new ArcadiaBrand(
id: new ArcadiaId(1),
name: new BrandName('Le Gaulois'),
);
$directory->add($brand);
// WHEN
$found = $directory->find(new ArcadiaId(1));
$missing = $directory->find(new ArcadiaId(999));
// THEN
expect($found)->toBe($brand)
->and($missing)->toBeNull();
});
it('returns all arcadia brands and clears them', function (): void {
// GIVEN
$directory = new InMemoryArcadiaBrandDirectory();
$b1 = new ArcadiaBrand(new ArcadiaId(1), new BrandName('Le Gaulois'));
$b2 = new ArcadiaBrand(new ArcadiaId(2), new BrandName('Maître CoQ'));
$directory->add($b1, $b2);
// WHEN
$all = $directory->findAll();
// THEN
expect($all)->toHaveCount(2);
// WHEN
$directory->clear();
// THEN
expect($directory->findAll())->toBeEmpty();
});
});

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Infra\Adapters\Repositories;
use Domain\Exceptions\BrandNotFoundException;
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\Adapters\Repositories\InMemoryBrandRepository;
use Ramsey\Uuid\Uuid;
covers(InMemoryBrandRepository::class);
describe('InMemoryBrandRepository Unit Tests', function (): void {
function createBrand(string $code, string $name, int $arcadiaId): Brand
{
return Brand::hydrate(
id: new BrandId(Uuid::uuid7()),
name: new BrandName($name),
code: new BrandCode($code),
arcadiaId: new ArcadiaId($arcadiaId),
);
}
it('finds a brand by id and by code', function (): void {
// GIVEN
$repository = new InMemoryBrandRepository();
$brand = createBrand('LGL01', 'Le Gaulois', 1);
$repository->add($brand);
// WHEN
$foundById = $repository->find($brand->id());
$foundByCode = $repository->findByCode(new BrandCode('LGL01'));
$missingById = $repository->find(new BrandId(Uuid::uuid7()));
$missingByCode = $repository->findByCode(new BrandCode('MCQ98'));
// THEN
expect($foundById)->toBe($brand)
->and($foundByCode)->toBe($brand)
->and($missingById)->toBeNull()
->and($missingByCode)->toBeNull();
});
it('persists and deletes a brand', function (): void {
// GIVEN
$repository = new InMemoryBrandRepository();
$brand = createBrand('MCQ02', 'Maître CoQ', 2);
// WHEN
$repository->persist($brand);
// THEN
expect($repository->find($brand->id()))->toBe($brand);
// WHEN
$repository->delete($brand);
// THEN
expect($repository->find($brand->id()))->toBeNull();
});
it('throws BrandNotFoundException when deleting non-existent brand', function (): void {
// GIVEN
$repository = new InMemoryBrandRepository();
$brand = createBrand('MCQ02', 'Maître CoQ', 2);
// WHEN & THEN
expect(fn () => $repository->delete($brand))
->toThrow(BrandNotFoundException::class);
});
it('clears all brands', function (): void {
// GIVEN
$repository = new InMemoryBrandRepository();
$brand = createBrand('MCQ02', 'Maître CoQ', 2);
$repository->add($brand);
// WHEN
$repository->clear();
// THEN
expect($repository->find($brand->id()))->toBeNull();
});
});

View file

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Infra\Casts;
use Domain\Models\ValueObjects\Brand\ArcadiaId;
use Infra\Casts\ArcadiaIdCast;
use Infra\Database\Models\Brand;
covers(ArcadiaIdCast::class);
describe('ArcadiaIdCast Unit Tests', function (): void {
$cast = new ArcadiaIdCast();
$model = new Brand();
it('transforms integer from database to ArcadiaId instance on get', function () use ($cast, $model): void {
// GIVEN
$dbValue = 123;
// WHEN
$result = $cast->get($model, 'arcadia_id', $dbValue, []);
// THEN
expect($result)->toBeInstanceOf(ArcadiaId::class)
->and($result->value)->toBe(123);
});
it('transforms numeric string from database to ArcadiaId instance on get', function () use ($cast, $model): void {
// GIVEN
$dbValue = '456';
// WHEN
$result = $cast->get($model, 'arcadia_id', $dbValue, []);
// THEN
expect($result)->toBeInstanceOf(ArcadiaId::class)
->and($result->value)->toBe(456);
});
it('throws InvalidArgumentException on get when value is null', function () use ($cast, $model): void {
// GIVEN & WHEN
expect(fn () => $cast->get($model, 'arcadia_id', null, []))
// THEN
->toThrow(\InvalidArgumentException::class, 'ArcadiaId value cannot be null');
});
it('throws InvalidArgumentException on get when value is invalid type', function () use ($cast, $model): void {
// GIVEN & WHEN
expect(fn () => $cast->get($model, 'arcadia_id', 'abc', []))
// THEN
->toThrow(\InvalidArgumentException::class, 'Value must be an integer or numeric string');
});
it('transforms ArcadiaId instance to integer on set', function () use ($cast, $model): void {
// GIVEN
$arcadiaId = new ArcadiaId(789);
// WHEN
$result = $cast->set($model, 'arcadia_id', $arcadiaId, []);
// THEN
expect($result)->toBe(789);
});
it('transforms raw integer to integer on set', function () use ($cast, $model): void {
// GIVEN
$value = 42;
// WHEN
$result = $cast->set($model, 'arcadia_id', $value, []);
// THEN
expect($result)->toBe(42);
});
it('transforms numeric string to integer on set', function () use ($cast, $model): void {
// GIVEN
$value = '99';
// WHEN
$result = $cast->set($model, 'arcadia_id', $value, []);
// THEN
expect($result)->toBe(99);
});
it('throws InvalidArgumentException on set when value is null', function () use ($cast, $model): void {
// GIVEN & WHEN
expect(fn () => $cast->set($model, 'arcadia_id', null, []))
// THEN
->toThrow(\InvalidArgumentException::class, 'ArcadiaId value cannot be null');
});
it('throws InvalidArgumentException on set when value is invalid type', function () use ($cast, $model): void {
// GIVEN
$invalid = ['array-not-supported'];
// WHEN
expect(fn () => $cast->set($model, 'arcadia_id', $invalid, []))
// THEN
->toThrow(\InvalidArgumentException::class, 'Value must be an integer, numeric string, or instance of ArcadiaId');
});
});

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Infra\Casts;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Infra\Casts\BrandCodeCast;
use Infra\Database\Models\Brand;
covers(BrandCodeCast::class);
describe('BrandCodeCast Unit Tests', function (): void {
$cast = new BrandCodeCast();
$model = new Brand();
it('transforms string from database to BrandCode instance on get', function () use ($cast, $model): void {
// GIVEN
$dbValue = 'LGL89';
// WHEN
$result = $cast->get($model, 'uwil_public_id', $dbValue, []);
// THEN
expect($result)->toBeInstanceOf(BrandCode::class)
->and($result->value)->toBe('LGL89');
});
it('throws InvalidArgumentException on get when value is null', function () use ($cast, $model): void {
// GIVEN & WHEN
expect(fn () => $cast->get($model, 'uwil_public_id', null, []))
// THEN
->toThrow(\InvalidArgumentException::class, 'BrandCode value cannot be null');
});
it('throws InvalidArgumentException on get when value is not a string', function () use ($cast, $model): void {
// GIVEN
/** @var mixed $invalid */
$invalid = 12345;
// WHEN
expect(fn () => $cast->get($model, 'uwil_public_id', $invalid, []))
// THEN
->toThrow(\InvalidArgumentException::class, 'Value must be a string');
});
it('transforms BrandCode instance to string on set', function () use ($cast, $model): void {
// GIVEN
$brandCode = new BrandCode('ABC12');
// WHEN
$result = $cast->set($model, 'uwil_public_id', $brandCode, []);
// THEN
expect($result)->toBe('ABC12');
});
it('transforms raw string to validated string on set', function () use ($cast, $model): void {
// GIVEN
$value = 'XYZ98';
// WHEN
$result = $cast->set($model, 'uwil_public_id', $value, []);
// THEN
expect($result)->toBe('XYZ98');
});
it('throws InvalidArgumentException on set when value is null', function () use ($cast, $model): void {
// GIVEN & WHEN
expect(fn () => $cast->set($model, 'uwil_public_id', null, []))
// THEN
->toThrow(\InvalidArgumentException::class, 'BrandCode value cannot be null');
});
it('throws InvalidArgumentException on set when value is not a string or BrandCode', function () use ($cast, $model): void {
// GIVEN
$invalid = 12345;
// WHEN
expect(fn () => $cast->set($model, 'uwil_public_id', $invalid, []))
// THEN
->toThrow(\InvalidArgumentException::class, 'Value must be a string or instance of BrandCode');
});
});