POST /users
{
"email": "john.doe@email.com",
"firstname": "John",
"lastname": "Doe"
}
#[AsController]
#[Route(path: '/users', name: 'create_user', methods: [Request::METHOD_POST])]
final readonly class CreateUserController
{
public function __construct(private CreateUser $createUser)
{
}
public function __invoke(
#[MapRequestPayload] CreateUserRequest $createUserRequest,
): JsonResponse {
($this->createUser)(
$createUserRequest->email,
$createUserRequest->firstname,
$createUserRequest->lastname,
);
return new JsonResponse(status: Response::HTTP_CREATED);
}
}
final readonly class CreateUser
{
public function __construct(private UserRepository $userRepository)
{
}
public function __invoke(string $email, string $firstname, string $lastname): void
{
$user = new User($email, $firstname, $lastname);
$this->userRepository->save($user);
}
}
final class CreateUserTest extends KernelTestCase
{
private CreateUser $createUser;
private UserRepository $userRepository;
protected function setUp(): void
{
parent::setUp();
$this->createUser = self::getService(CreateUser::class);
$this->userRepository = self::getService(UserRepository::class);
}
public function testItCreatesAUser(): void
{
($this->createUser)('john.doe@email.com', 'John', 'Doe');
$retrievedUser = $this->userRepository->getByEmail($userEmail);
self::assertSame('john.doe@email.com', $retrievedUser->email);
self::assertSame('John', $retrievedUser->firstname);
self::assertSame('Doe', $retrievedUser->lastname);
}
}
public function testItCreatesAUser(): void
{
$userRepository = $this->createMock(UserRepository::class);
$createUser = new CreateUser($userRepository);
$userRepository
->expects($this->once())
->method('save')
->with(self::callback(
static fn (User $user): bool => $user->email === 'john.doe@email.com'
&& $user->firstname === 'John'
&& $user->lastname === 'Doe',
));
($createUser)($userEmail, 'John', 'Doe');
}
final readonly class CreateUser
{
public function __invoke(string $email, string $firstname, string $lastname): void
{
if ($this->userAlreadyExists($email)) {
throw new UserAlreadyExists($email);
}
$this->userRepository->save(new User($email, $firstname, $lastname));
}
private function userAlreadyExists(string $email): bool
{
try {
$this->userRepository->getByEmail($email);
} catch (UserNotFound) {
return false;
}
return true;
}
}
public function testItThrowsAnExceptionIfUserWithSameEmailAlreadyExists(): void
{
($this->createUser)('jane.doe@email.com', 'Jane', 'Doe');
try {
($this->createUser)('jane.doe@email.com', 'John', 'Doe');
} catch (\Throwable $exception) {
self::assertInstanceOf(UserAlreadyExists::class, $exception);
self::assertSame(
'User with email "jane.doe@email.com" already exist.',
$exception->getMessage(),
);
$existingUser = $this->userRepository->getByEmail('jane.doe@email.com');
self::assertSame('Jane', $existingUser->firstname);
return;
}
self::fail('An exception should have been thrown.');
}
public function testItThrowsAnExceptionIfUserWithSameEmailAlreadyExists(): void
{
$userRepository = $this->createMock(UserRepository::class);
$createUser = new CreateUser($userRepository);
$userRepository->expects($this->never())->method('save');
$userRepository
->expects($this->once())
->method('getByEmail')
->with('jane.doe@email.com')
->willReturn(new User('jane.doe@email.com', 'Jane', 'Doe'));
$this->expectException(UserAlreadyExists::class);
$this->expectExceptionMessage('User with email "jane.doe@email.com" already exist.');
($createUser)('jane.doe@email.com', 'John', 'Doe');
}
public function testItCreatesAUser(): void
{
$userRepository = $this->createMock(UserRepository::class);
$createUser = new CreateUser($userRepository);
$userRepository
->expects($this->once())
->method('getByEmail')
->with($userEmail)
->willThrowException(new UserNotFound($userEmail));
$userRepository
->expects($this->once())
->method('save')
->with(self::callback(
static fn (User $user): bool => $user->email === 'john.doe@email.com'
&& $user->firstname === 'John'
&& $user->lastname === 'Doe',
));
($createUser)($userEmail, 'John', 'Doe');
}
final class DatabaseUserRepository extends ServiceEntityRepository implements UserRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
public function save(User ...$users): void
{
foreach ($users as $user) {
$this->getEntityManager()->persist($user);
}
$this->getEntityManager()->flush();
}
}
final class InMemoryUserRepository implements UserRepository
{
/** @var User[] */
private array $users = [];
public function save(User ...$users): void
{
foreach ($users as $user) {
$this->users[] = clone $user;
}
}
}
final class UserRepositoryTest extends AbstractIntegrationTestCase
{
private readonly UserRepository $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = self::getService(UserRepository::class);
}
#[Group('with-in-memory-adapters')]
#[Group('with-production-adapters')]
public function testItSavesAUser(): void
{
$user = new User('john.doe@email.com', 'John', 'Doe');
$this->repository->save($user);
$this->assertUserIsStored($user);
}
}
abstract class AbstractIntegrationTestCase extends KernelTestCase
{
protected static function bootKernel(array $options = []): KernelInterface
{
$testEnvironment = self::getCurrentTestEnvironment();
if (!\in_array($testEnvironment, ['memory', 'test'], true)) {
throw new \RuntimeException(
"Invalid TEST_ENV environment variable value \"$testEnvironment\".,
);
}
$options = array_merge($options, ['environment' => $testEnvironment]);
return parent::bootKernel($options);
}
}
# config/services.yaml
when@memory:
services:
App\Domain\Repository\UserRepository:
class: App\Infrastructure\Persistence\InMemory\InMemoryUserRepository
public function testItCreatesAUser(): void
{
$userRepository = new InMemoryUserRepository();
$createUser = new CreateUser($userRepository);
($createUser)('john.doe@email.com', 'John', 'Doe');
$retrievedUser = $userRepository->getByEmail('john.doe@email.com');
self::assertSame($userEmail, $retrievedUser->email);
self::assertSame('John', $retrievedUser->firstname);
self::assertSame('Doe', $retrievedUser->lastname);
}
final class CreateUserTest extends AbstractAcceptanceTestCase
{
private CreateUser $createUser;
private UserRepository $userRepository;
protected function setUp(): void
{
parent::setUp();
$this->createUser = self::getService(CreateUser::class);
$this->userRepository = self::getService(UserRepository::class);
}
public function testItCreatesAUser(): void
{
($this->createUser)('john.doe@email.com', 'John', 'Doe');
$retrievedUser = $this->userRepository->getByEmail($userEmail);
self::assertSame('john.doe@email.com', $retrievedUser->email);
self::assertSame('John', $retrievedUser->firstname);
self::assertSame('Doe', $retrievedUser->lastname);
}
}
abstract class AbstractAcceptanceTestCase extends KernelTestCase
{
protected static function bootKernel(array $options = []): KernelInterface
{
$options = array_merge($options, ['environment' => 'memory']);
return parent::bootKernel($options);
}
}