diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 8b714a0..7112e74 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -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, ]; diff --git a/src/Domain/Events/BrandCreated.php b/src/Domain/Events/BrandCreated.php new file mode 100644 index 0000000..5873b07 --- /dev/null +++ b/src/Domain/Events/BrandCreated.php @@ -0,0 +1,23 @@ +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; } diff --git a/src/Infra/Listeners/LogBrandCreated.php b/src/Infra/Listeners/LogBrandCreated.php new file mode 100644 index 0000000..97b0112 --- /dev/null +++ b/src/Infra/Listeners/LogBrandCreated.php @@ -0,0 +1,34 @@ +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, + ] + ); + } +} diff --git a/src/Infra/Providers/EventServiceProvider.php b/src/Infra/Providers/EventServiceProvider.php new file mode 100644 index 0000000..e3bd0f7 --- /dev/null +++ b/src/Infra/Providers/EventServiceProvider.php @@ -0,0 +1,21 @@ +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)); diff --git a/tests/Unit/Domain/Events/BrandCreatedTest.php b/tests/Unit/Domain/Events/BrandCreatedTest.php new file mode 100644 index 0000000..459c826 --- /dev/null +++ b/tests/Unit/Domain/Events/BrandCreatedTest.php @@ -0,0 +1,41 @@ +brandId)->toBe($brandId) + ->and($event->brandName)->toBe($brandName) + ->and($event->brandCode)->toBe($brandCode) + ->and($event->arcadiaId)->toBe($arcadiaId) + ->and($event->occurredAt)->toBe($now); + }); +}); diff --git a/tests/Unit/Domain/Services/BrandResolverTest.php b/tests/Unit/Domain/Services/BrandResolverTest.php index 10f022a..24a767c 100644 --- a/tests/Unit/Domain/Services/BrandResolverTest.php +++ b/tests/Unit/Domain/Services/BrandResolverTest.php @@ -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 */ + 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 */ + 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))) diff --git a/tests/Unit/Infra/Listeners/LogBrandCreatedTest.php b/tests/Unit/Infra/Listeners/LogBrandCreatedTest.php new file mode 100644 index 0000000..8c6ee35 --- /dev/null +++ b/tests/Unit/Infra/Listeners/LogBrandCreatedTest.php @@ -0,0 +1,56 @@ +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(); + }); +});