refactor(application): align UpdateBrand on FindBrand polymorphic output pattern

This commit is contained in:
Jérémy Munsch 2026-09-20 17:17:33 +02:00
parent 3951d7fc95
commit c85379b50e
Signed by: forge-joniko
GPG key ID: D7EFE2F5CB6CC0E4
6 changed files with 73 additions and 37 deletions

View file

@ -6,9 +6,9 @@ namespace Application\UseCases\Brand\Update;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Shared\DDD\UseCases\Input as UseCaseInput;
use Shared\Models\ValueObjects\Email;
use Shared\Models\ValueObjects\PicturePath;
use Shared\Models\ValueObjects\Rcs;
use Shared\ValueObjects\Email;
use Domain\Models\ValueObjects\PicturePath;
use Shared\ValueObjects\Rcs;
use Webmozart\Assert\Assert;
final readonly class Input implements UseCaseInput
@ -21,8 +21,8 @@ final readonly class Input implements UseCaseInput
) {
Assert::true(
$this->logo instanceof PicturePath
|| $this->rcs instanceof Rcs
|| $this->contact instanceof Email,
|| $this->rcs instanceof Rcs
|| $this->contact instanceof Email,
'At least one field must be provided to update the brand.',
);
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Application\UseCases\Brand\Update\Output;
final readonly class BrandNotFound implements Output
{
}

View file

@ -2,16 +2,15 @@
declare(strict_types=1);
namespace Application\UseCases\Brand\Update;
namespace Application\UseCases\Brand\Update\Output;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Brand\BrandName;
use Shared\DDD\UseCases\Output as UseCaseOutput;
use Shared\Models\ValueObjects\Email;
use Shared\Models\ValueObjects\PicturePath;
use Shared\Models\ValueObjects\Rcs;
use Domain\Models\ValueObjects\PicturePath;
use Shared\ValueObjects\Email;
use Shared\ValueObjects\Rcs;
final readonly class Output implements UseCaseOutput
final readonly class BrandUpdated implements Output
{
public function __construct(
public BrandName $name,
@ -20,6 +19,7 @@ final readonly class Output implements UseCaseOutput
public Rcs $rcs,
public Email $contact,
public \DateTimeInterface $createdAt,
public ?\DateTimeInterface $updatedAt = null,
) {
}
}

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Application\UseCases\Brand\Update\Output;
use Shared\DDD\UseCases\Output as UseCaseOutput;
interface Output extends UseCaseOutput
{
}

View file

@ -4,18 +4,21 @@ declare(strict_types=1);
namespace Application\UseCases\Brand\Update;
use Application\UseCases\Brand\Update\Output\BrandNotFound;
use Application\UseCases\Brand\Update\Output\BrandUpdated;
use Application\UseCases\Brand\Update\Output\Output;
use Domain\Models\Entities\Brand;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Ports\Repositories\BrandRepository;
use Shared\DDD\Exceptions\NotFoundException;
use Shared\DDD\UseCases\Input as BaseInput;
use Shared\DDD\UseCases\Transactional;
use Shared\DDD\UseCases\UseCase;
use Shared\Models\ValueObjects\Email;
use Shared\Models\ValueObjects\PicturePath;
use Shared\Models\ValueObjects\Rcs;
use Shared\ValueObjects\Email;
use Shared\ValueObjects\Rcs;
use Webmozart\Assert\Assert;
/** @implements UseCase<\Application\UseCases\Brand\Update\Input, \Application\UseCases\Brand\Update\Output> */
final readonly class UpdateBrand implements UseCase
/** @implements UseCase<\Application\UseCases\Brand\Update\Input, \Application\UseCases\Brand\Update\Output\Output> */
final readonly class UpdateBrand implements UseCase, Transactional
{
public function __construct(
private BrandRepository $repository,
@ -30,7 +33,7 @@ final readonly class UpdateBrand implements UseCase
$brand = $this->repository->findByCode($input->code);
if (! $brand instanceof Brand) {
throw new NotFoundException();
return new BrandNotFound();
}
if ($input->logo instanceof PicturePath) {
@ -47,13 +50,14 @@ final readonly class UpdateBrand implements UseCase
$this->repository->save($brand);
return new Output(
return new BrandUpdated(
name: $brand->name,
code: $brand->code,
logo: $brand->logo,
rcs: $brand->rcs,
contact: $brand->contact,
createdAt: $brand->createdAt(),
updatedAt: $brand->updatedAt(),
);
}
}

View file

@ -5,20 +5,21 @@ declare(strict_types=1);
namespace Tests\Unit\Application\UseCases\Brand\Update;
use Application\UseCases\Brand\Update\Input;
use Application\UseCases\Brand\Update\Output;
use Application\UseCases\Brand\Update\Output\BrandNotFound;
use Application\UseCases\Brand\Update\Output\BrandUpdated;
use Application\UseCases\Brand\Update\Output\Output;
use Application\UseCases\Brand\Update\UpdateBrand;
use Domain\Models\Entities\Brand;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Brand\BrandId;
use Domain\Models\ValueObjects\Brand\BrandName;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Ports\Repositories\BrandRepository;
use Shared\DDD\Exceptions\NotFoundException;
use Shared\DDD\UseCases\Input as BaseInput;
use Shared\Models\ValueObjects\Email;
use Shared\Models\ValueObjects\PicturePath;
use Shared\Models\ValueObjects\Rcs;
use Shared\ValueObjects\Email;
use Shared\ValueObjects\Rcs;
covers(UpdateBrand::class);
covers(UpdateBrand::class, Input::class, BrandUpdated::class, BrandNotFound::class);
$existingBrand = static fn (BrandCode $code): Brand => Brand::hydrate(
BrandId::generate(),
@ -46,7 +47,8 @@ describe('update brand use case', function () use ($existingBrand): void {
&& $saved->rcs === $newRcs
&& $saved->contact === $newContact
&& $saved->code === $brand->code
&& $saved->name === $brand->name,
&& $saved->name === $brand->name
&& $saved->updatedAt !== null,
));
$useCase = new UpdateBrand($repository);
@ -55,8 +57,9 @@ describe('update brand use case', function () use ($existingBrand): void {
$result = $useCase->execute($input);
// THEN
expect($result)->toBeInstanceOf(Output::class)
->and($result->code)->toBe($brand->code);
expect($result)->toBeInstanceOf(BrandUpdated::class)
->and($result->code)->toBe($brand->code)
->and($result->updatedAt)->not->toBeNull();
});
it('updates only the contact and leaves logo/rcs untouched', function () use ($existingBrand): void {
@ -73,19 +76,24 @@ describe('update brand use case', function () use ($existingBrand): void {
&& $saved->logo === $brand->logo
&& $saved->rcs === $brand->rcs
&& $saved->code === $brand->code
&& $saved->name === $brand->name,
&& $saved->name === $brand->name
&& $saved->updatedAt !== null,
));
$useCase = new UpdateBrand($repository);
// WHEN
$useCase->execute($input);
$result = $useCase->execute($input);
// THEN handled via the save() expectation above
expect(true)->toBeTrue();
// THEN
expect($result)->toBeInstanceOf(BrandUpdated::class)
->and($result->code)->toBe($brand->code)
->and($result->updatedAt)->not->toBeNull();
});
});
it('throws when the brand does not exist', function (): void {
describe('update brand use case validation rules', function (): void {
it('returns BrandNotFound when the brand does not exist', function (): void {
// GIVEN
$code = new BrandCode('AAA01');
$input = new Input(code: $code, contact: new Email('contact-new@example.com'));
@ -96,11 +104,15 @@ describe('update brand use case', function () use ($existingBrand): void {
$useCase = new UpdateBrand($repository);
// WHEN / THEN
expect(fn (): Output => $useCase->execute($input))
->toThrow(NotFoundException::class);
});
// WHEN
$result = $useCase->execute($input);
// THEN
expect($result)->toBeInstanceOf(BrandNotFound::class);
});
});
describe('update brand use case assertions', function (): void {
it('rejects an input of the wrong type', function (): void {
// GIVEN
$repository = \Mockery::mock(BrandRepository::class);