feat(event): implement BrandCreated event and listeners
- Added BrandCreated event to encapsulate brand creation details. - Created LogBrandCreated listener to log brand creation events. - Updated BrandResolver to dispatch BrandCreated event upon new brand creation. - Introduced EventServiceProvider to register event listeners. - Added unit tests for BrandCreated event and LogBrandCreated listener.
This commit is contained in:
parent
9c2c109d51
commit
abe60df95f
9 changed files with 252 additions and 37 deletions
|
|
@ -4,10 +4,12 @@ declare(strict_types=1);
|
|||
|
||||
use Infra\Providers\AppServiceProvider;
|
||||
use Infra\Providers\DomainServiceProvider;
|
||||
use Infra\Providers\EventServiceProvider;
|
||||
use Infra\Providers\SecurityServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
DomainServiceProvider::class,
|
||||
EventServiceProvider::class,
|
||||
SecurityServiceProvider::class,
|
||||
];
|
||||
|
|
|
|||
23
src/Domain/Events/BrandCreated.php
Normal file
23
src/Domain/Events/BrandCreated.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Events;
|
||||
|
||||
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 Shared\DDD\Event\Event;
|
||||
|
||||
final readonly class BrandCreated implements Event
|
||||
{
|
||||
public function __construct(
|
||||
public BrandId $brandId,
|
||||
public BrandName $brandName,
|
||||
public BrandCode $brandCode,
|
||||
public ArcadiaId $arcadiaId,
|
||||
public \DateTimeImmutable $occurredAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -4,25 +4,22 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Services;
|
||||
|
||||
use Domain\Events\BrandCreated;
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaId;
|
||||
use Domain\Ports\Directories\ArcadiaBrandDirectory;
|
||||
use Domain\Ports\Repositories\BrandRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shared\DDD\Event\DomainEventDispatcher;
|
||||
|
||||
final readonly class BrandResolver
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(
|
||||
private ArcadiaBrandDirectory $arcadiaBrandDirectory,
|
||||
private BrandRepository $brandRepository,
|
||||
private BrandCodeGenerator $brandCodeGenerator,
|
||||
?LoggerInterface $logger = null,
|
||||
private DomainEventDispatcher $dispatcher,
|
||||
) {
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -56,20 +53,13 @@ final readonly class BrandResolver
|
|||
|
||||
$this->brandRepository->persist($brand);
|
||||
|
||||
$this->logger->info(
|
||||
sprintf(
|
||||
'Created new brand code "%s" for brand "%s" (Arcadia ID %d)',
|
||||
$code->value,
|
||||
$arcadiaBrand->name->value,
|
||||
$arcadiaBrand->id->value
|
||||
),
|
||||
[
|
||||
'brand_id' => $brand->id()->value,
|
||||
'code' => $code->value,
|
||||
'name' => $arcadiaBrand->name->value,
|
||||
'arcadia_id' => $arcadiaBrand->id->value,
|
||||
]
|
||||
);
|
||||
$this->dispatcher->dispatch(new BrandCreated(
|
||||
brandId: $brand->id,
|
||||
brandName: $brand->name,
|
||||
brandCode: $brand->code,
|
||||
arcadiaId: $brand->arcadiaId,
|
||||
occurredAt: new \DateTimeImmutable(),
|
||||
));
|
||||
|
||||
return $brand;
|
||||
}
|
||||
|
|
|
|||
34
src/Infra/Listeners/LogBrandCreated.php
Normal file
34
src/Infra/Listeners/LogBrandCreated.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Infra\Listeners;
|
||||
|
||||
use Domain\Events\BrandCreated;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final readonly class LogBrandCreated
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(BrandCreated $event): void
|
||||
{
|
||||
$this->logger->info(
|
||||
sprintf(
|
||||
'Created new brand code "%s" for brand "%s" (Arcadia ID %d)',
|
||||
$event->brandCode->value,
|
||||
$event->brandName->value,
|
||||
$event->arcadiaId->value
|
||||
),
|
||||
[
|
||||
'brand_id' => $event->brandId->value,
|
||||
'code' => $event->brandCode->value,
|
||||
'name' => $event->brandName->value,
|
||||
'arcadia_id' => $event->arcadiaId->value,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
21
src/Infra/Providers/EventServiceProvider.php
Normal file
21
src/Infra/Providers/EventServiceProvider.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Infra\Providers;
|
||||
|
||||
use Domain\Events\BrandCreated;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Infra\Listeners\LogBrandCreated;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
Event::listen(
|
||||
BrandCreated::class,
|
||||
LogBrandCreated::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,9 @@ use Domain\Services\BrandCodeGenerator;
|
|||
use Domain\Services\BrandResolver;
|
||||
use Infra\Adapters\Directories\InMemoryArcadiaBrandDirectory;
|
||||
use Infra\Adapters\Repositories\InMemoryBrandRepository;
|
||||
use Psr\Log\NullLogger;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Event\DomainEventDispatcher;
|
||||
use Shared\DDD\Event\Event;
|
||||
use Shared\Pagination;
|
||||
|
||||
covers(
|
||||
|
|
@ -33,14 +34,19 @@ describe('FindAllBrands Usecase Unit Tests', function (): void {
|
|||
$arcadiaDirectory = new InMemoryArcadiaBrandDirectory();
|
||||
$brandRepository = new InMemoryBrandRepository();
|
||||
$codeGenerator = new BrandCodeGenerator();
|
||||
$logger = new NullLogger();
|
||||
$dispatcher = new class () implements DomainEventDispatcher
|
||||
{
|
||||
public function dispatch(Event $event): void
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function () use ($arcadiaDirectory, $brandRepository): void {
|
||||
$arcadiaDirectory->clear();
|
||||
$brandRepository->clear();
|
||||
});
|
||||
|
||||
it('returns existing brands and generates missing ones automatically', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $logger): void {
|
||||
it('returns existing brands and generates missing ones automatically', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher): void {
|
||||
// GIVEN
|
||||
$arcadia1 = new ArcadiaBrand(new ArcadiaId(1), new BrandName('Le Gaulois'));
|
||||
$arcadia2 = new ArcadiaBrand(new ArcadiaId(2), new BrandName('Maître CoQ'));
|
||||
|
|
@ -55,7 +61,7 @@ describe('FindAllBrands Usecase Unit Tests', function (): void {
|
|||
);
|
||||
$brandRepository->add($existingBrand);
|
||||
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $logger);
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
|
||||
$usecase = new FindAllBrands($arcadiaDirectory, $resolver);
|
||||
$input = new Input(Pagination::ofPage(1, 25));
|
||||
|
||||
|
|
@ -83,14 +89,14 @@ describe('FindAllBrands Usecase Unit Tests', function (): void {
|
|||
expect($brandRepository->findByArcadiaId(new ArcadiaId(2)))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('slices brands according to pagination', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $logger): void {
|
||||
it('slices brands according to pagination', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher): void {
|
||||
// GIVEN
|
||||
$arcadia1 = new ArcadiaBrand(new ArcadiaId(1), new BrandName('Le Gaulois'));
|
||||
$arcadia2 = new ArcadiaBrand(new ArcadiaId(2), new BrandName('Maître CoQ'));
|
||||
$arcadia3 = new ArcadiaBrand(new ArcadiaId(3), new BrandName('Loué'));
|
||||
$arcadiaDirectory->add($arcadia1, $arcadia2, $arcadia3);
|
||||
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $logger);
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
|
||||
$usecase = new FindAllBrands($arcadiaDirectory, $resolver);
|
||||
$input = new Input(Pagination::ofPage(2, 2));
|
||||
|
||||
|
|
@ -105,9 +111,9 @@ describe('FindAllBrands Usecase Unit Tests', function (): void {
|
|||
->and($result->perPage)->toBe(2);
|
||||
});
|
||||
|
||||
it('returns an empty list when there are no arcadia brands', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $logger): void {
|
||||
it('returns an empty list when there are no arcadia brands', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher): void {
|
||||
// GIVEN
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $logger);
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
|
||||
$usecase = new FindAllBrands($arcadiaDirectory, $resolver);
|
||||
$input = new Input(Pagination::ofPage(1, 25));
|
||||
|
||||
|
|
|
|||
41
tests/Unit/Domain/Events/BrandCreatedTest.php
Normal file
41
tests/Unit/Domain/Events/BrandCreatedTest.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Events;
|
||||
|
||||
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 Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(BrandCreated::class);
|
||||
|
||||
describe('BrandCreated Domain Event Unit Tests', function (): void {
|
||||
it('holds correct event data upon instantiation', function (): void {
|
||||
// GIVEN
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
$brandName = new BrandName('Duc');
|
||||
$brandCode = new BrandCode('DUC01');
|
||||
$arcadiaId = new ArcadiaId(42);
|
||||
$now = new \DateTimeImmutable();
|
||||
|
||||
// WHEN
|
||||
$event = new BrandCreated(
|
||||
brandId: $brandId,
|
||||
brandName: $brandName,
|
||||
brandCode: $brandCode,
|
||||
arcadiaId: $arcadiaId,
|
||||
occurredAt: $now,
|
||||
);
|
||||
|
||||
// THEN
|
||||
expect($event->brandId)->toBe($brandId)
|
||||
->and($event->brandName)->toBe($brandName)
|
||||
->and($event->brandCode)->toBe($brandCode)
|
||||
->and($event->arcadiaId)->toBe($arcadiaId)
|
||||
->and($event->occurredAt)->toBe($now);
|
||||
});
|
||||
});
|
||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Tests\Unit\Domain\Services;
|
||||
|
||||
use Domain\Events\BrandCreated;
|
||||
use Domain\Exceptions\BrandNotFoundException;
|
||||
use Domain\Models\Entities\Brand;
|
||||
use Domain\Models\ValueObjects\Brand\ArcadiaBrand;
|
||||
|
|
@ -15,8 +16,9 @@ use Domain\Services\BrandCodeGenerator;
|
|||
use Domain\Services\BrandResolver;
|
||||
use Infra\Adapters\Directories\InMemoryArcadiaBrandDirectory;
|
||||
use Infra\Adapters\Repositories\InMemoryBrandRepository;
|
||||
use Psr\Log\NullLogger;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\DDD\Event\DomainEventDispatcher;
|
||||
use Shared\DDD\Event\Event;
|
||||
|
||||
covers(BrandResolver::class);
|
||||
|
||||
|
|
@ -24,14 +26,13 @@ describe('BrandResolver Unit Tests', function (): void {
|
|||
$arcadiaDirectory = new InMemoryArcadiaBrandDirectory();
|
||||
$brandRepository = new InMemoryBrandRepository();
|
||||
$codeGenerator = new BrandCodeGenerator();
|
||||
$logger = new NullLogger();
|
||||
|
||||
beforeEach(function () use ($arcadiaDirectory, $brandRepository): void {
|
||||
$arcadiaDirectory->clear();
|
||||
$brandRepository->clear();
|
||||
});
|
||||
|
||||
it('returns existing brand if already present in repository', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $logger): void {
|
||||
it('returns existing brand if already present in repository without dispatching event', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
|
||||
// GIVEN
|
||||
$existing = Brand::hydrate(
|
||||
id: new BrandId(Uuid::uuid7()),
|
||||
|
|
@ -41,19 +42,43 @@ describe('BrandResolver Unit Tests', function (): void {
|
|||
);
|
||||
$brandRepository->add($existing);
|
||||
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $logger);
|
||||
$dispatcher = new class () implements DomainEventDispatcher
|
||||
{
|
||||
/** @var list<Event> */
|
||||
public array $dispatched = [];
|
||||
|
||||
public function dispatch(Event $event): void
|
||||
{
|
||||
$this->dispatched[] = $event;
|
||||
}
|
||||
};
|
||||
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
|
||||
|
||||
// WHEN
|
||||
$resolved = $resolver->resolve(new ArcadiaId(1));
|
||||
|
||||
// THEN
|
||||
expect($resolved)->toBe($existing);
|
||||
expect($resolved)->toBe($existing)
|
||||
->and($dispatcher->dispatched)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('generates, persists and logs a new brand if not in repository but in Arcadia directory', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $logger): void {
|
||||
it('generates, persists and dispatches BrandCreated event when brand is not in repository but in Arcadia directory', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
|
||||
// GIVEN
|
||||
$arcadiaDirectory->add(new ArcadiaBrand(new ArcadiaId(2), new BrandName('Maître CoQ')));
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $logger);
|
||||
|
||||
$dispatcher = new class () implements DomainEventDispatcher
|
||||
{
|
||||
/** @var list<Event> */
|
||||
public array $dispatched = [];
|
||||
|
||||
public function dispatch(Event $event): void
|
||||
{
|
||||
$this->dispatched[] = $event;
|
||||
}
|
||||
};
|
||||
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
|
||||
|
||||
// WHEN
|
||||
$resolved = $resolver->resolve(new ArcadiaId(2));
|
||||
|
|
@ -64,11 +89,28 @@ describe('BrandResolver Unit Tests', function (): void {
|
|||
->and(str_starts_with($resolved->code->value, 'MCQ'))->toBeTrue();
|
||||
|
||||
expect($brandRepository->findByArcadiaId(new ArcadiaId(2)))->not->toBeNull();
|
||||
|
||||
expect($dispatcher->dispatched)->toHaveCount(1)
|
||||
->and($dispatcher->dispatched[0])->toBeInstanceOf(BrandCreated::class);
|
||||
|
||||
/** @var BrandCreated $event */
|
||||
$event = $dispatcher->dispatched[0];
|
||||
expect($event->arcadiaId->value)->toBe(2)
|
||||
->and($event->brandName->value)->toBe('Maître CoQ')
|
||||
->and($event->brandCode->value)->toBe($resolved->code->value)
|
||||
->and($event->brandId->value)->toBe($resolved->id->value);
|
||||
});
|
||||
|
||||
it('throws BrandNotFoundException when resolving an unknown ArcadiaId', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator, $logger): void {
|
||||
it('throws BrandNotFoundException when resolving an unknown ArcadiaId', function () use ($arcadiaDirectory, $brandRepository, $codeGenerator): void {
|
||||
// GIVEN
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $logger);
|
||||
$dispatcher = new class () implements DomainEventDispatcher
|
||||
{
|
||||
public function dispatch(Event $event): void
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$resolver = new BrandResolver($arcadiaDirectory, $brandRepository, $codeGenerator, $dispatcher);
|
||||
|
||||
// WHEN & THEN
|
||||
expect(fn () => $resolver->resolve(new ArcadiaId(999)))
|
||||
|
|
|
|||
56
tests/Unit/Infra/Listeners/LogBrandCreatedTest.php
Normal file
56
tests/Unit/Infra/Listeners/LogBrandCreatedTest.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?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\Listeners\LogBrandCreated;
|
||||
use Mockery;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
covers(LogBrandCreated::class);
|
||||
|
||||
describe('LogBrandCreated Listener Unit Tests', function (): void {
|
||||
it('logs informative message with event details when brand is created', function (): void {
|
||||
// GIVEN
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
$brandName = new BrandName('Le Gaulois');
|
||||
$brandCode = new BrandCode('LGL01');
|
||||
$arcadiaId = new ArcadiaId(1);
|
||||
$event = new BrandCreated(
|
||||
brandId: $brandId,
|
||||
brandName: $brandName,
|
||||
brandCode: $brandCode,
|
||||
arcadiaId: $arcadiaId,
|
||||
occurredAt: new \DateTimeImmutable(),
|
||||
);
|
||||
|
||||
/** @var LoggerInterface&Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('info')
|
||||
->once()
|
||||
->with(
|
||||
'Created new brand code "LGL01" for brand "Le Gaulois" (Arcadia ID 1)',
|
||||
[
|
||||
'brand_id' => $brandId->value,
|
||||
'code' => 'LGL01',
|
||||
'name' => 'Le Gaulois',
|
||||
'arcadia_id' => 1,
|
||||
]
|
||||
);
|
||||
|
||||
$listener = new LogBrandCreated($loggerMock);
|
||||
|
||||
// WHEN
|
||||
$listener->handle($event);
|
||||
|
||||
// THEN
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue