Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace App\Packages\Domains\Interface;

use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand;
use App\Packages\Domains\User\UserEntity;
use App\Packages\Features\QueryUseCases\Dto\User\UserDto;

interface UserRepositroyInterface
{
public function createUser(CreateUserCommand $command): UserDto;
public function createUser(UserEntity $entity): UserDto;

public function updateUser(UserEntity $entity): UserDto;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ public function test_build_returns_user_entity_with_correct_values(): void
$this->assertSame($data['email'], $entity->getEmail());
$this->assertSame(AgeRange::Teens, $entity->getAgeRange());
$this->assertTrue($entity->getSubscription()->isFree());
$this->assertNull($entity->getPasswordHash());
}

public function test_build_sets_password_hash_when_provided(): void
{
$faker = FakerFactory::create();

$entity = UserEntityFactory::build([
'id' => null,
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
'email' => $faker->unique()->safeEmail(),
'age_range' => '20s',
'subscription_tier' => 'free',
'subscription_expires_at' => null,
'password_hash' => 'hashed_value',
]);

$this->assertNull($entity->getId());
$this->assertSame('hashed_value', $entity->getPasswordHash());
}

public function test_build_passes_expires_at_to_subscription(): void
Expand Down
31 changes: 25 additions & 6 deletions src/app/Packages/Domains/Tests/User/UserEntityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ private function buildEntity(array $overrides = []): UserEntity
$subscription = new Subscription(SubscriptionTier::Free, null);

return new UserEntity(
$overrides['id'] ?? $faker->unique()->randomNumber(5),
$overrides['first_name'] ?? $faker->firstName(),
$overrides['last_name'] ?? $faker->lastName(),
$overrides['email'] ?? $faker->unique()->safeEmail(),
$overrides['age_range'] ?? AgeRange::Twenties,
$overrides['subscription'] ?? $subscription,
id: array_key_exists('id', $overrides) ? $overrides['id'] : $faker->unique()->randomNumber(5),
firstName: $overrides['first_name'] ?? $faker->firstName(),
lastName: $overrides['last_name'] ?? $faker->lastName(),
email: $overrides['email'] ?? $faker->unique()->safeEmail(),
ageRange: $overrides['age_range'] ?? AgeRange::Twenties,
subscription: $overrides['subscription'] ?? $subscription,
passwordHash: $overrides['password_hash'] ?? null,
);
}

Expand All @@ -32,6 +33,12 @@ public function test_getId_returns_correct_value(): void
$this->assertSame(42, $entity->getId());
}

public function test_getId_returns_null_when_not_yet_persisted(): void
{
$entity = $this->buildEntity(['id' => null]);
$this->assertNull($entity->getId());
}

public function test_getFirstName_returns_correct_value(): void
{
$entity = $this->buildEntity(['first_name' => 'John']);
Expand Down Expand Up @@ -68,4 +75,16 @@ public function test_getSubscription_returns_subscription_instance(): void
$entity = $this->buildEntity(['subscription' => $subscription]);
$this->assertSame($subscription, $entity->getSubscription());
}

public function test_getPasswordHash_returns_hashed_value(): void
{
$entity = $this->buildEntity(['password_hash' => 'hashed_secret']);
$this->assertSame('hashed_secret', $entity->getPasswordHash());
}

public function test_getPasswordHash_returns_null_when_not_set(): void
{
$entity = $this->buildEntity(['password_hash' => null]);
$this->assertNull($entity->getPasswordHash());
}
}
90 changes: 79 additions & 11 deletions src/app/Packages/Domains/Tests/UserRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@

use App\Models\User;
use App\Packages\Domains\UserRepository;
use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand;
use App\Packages\Domains\User\Factory\UserEntityFactory;
use App\Packages\Domains\User\UserEntity;
use App\Packages\Features\QueryUseCases\Dto\User\UserDto;
use Exception;
use Faker\Factory as FakerFactory;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
use PHPUnit\Framework\TestCase;

class UserRepositoryTest extends TestCase
{
Expand All @@ -20,26 +21,27 @@ protected function tearDown(): void
parent::tearDown();
}

private function buildCommand(): CreateUserCommand
private function buildEntity(): UserEntity
{
$faker = FakerFactory::create();

return CreateUserCommand::fromArray([
return UserEntityFactory::build([
'id' => null,
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
'email' => $faker->unique()->safeEmail(),
'password' => $faker->password(8),
'age_range' => $faker->randomElement(['teens', '20s', '30s', '40s', '50s', '60plus']),
'subscription_tier' => $faker->randomElement(['free', 'premium']),
'subscription_expires_at' => null,
'password_hash' => 'hashed_password',
]);
}

private function buildUserModelMock(bool $wasRecentlyCreated): User
private function buildAttributes(): array
{
$faker = FakerFactory::create();

$attributes = [
return [
'id' => $faker->unique()->randomNumber(5),
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
Expand All @@ -48,6 +50,11 @@ private function buildUserModelMock(bool $wasRecentlyCreated): User
'subscription_tier' => 'free',
'subscription_expires_at' => null,
];
}

private function buildCreateModelMock(bool $wasRecentlyCreated): User
{
$attributes = $this->buildAttributes();

/** @var User|MockInterface $createdUser */
$createdUser = Mockery::mock(User::class);
Expand All @@ -61,21 +68,82 @@ private function buildUserModelMock(bool $wasRecentlyCreated): User
return $userModel;
}

private function buildUpdateModelMock(?int $foundId): User
{
$attributes = $this->buildAttributes();

/** @var User|MockInterface $userModel */
$userModel = Mockery::mock(User::class);

if ($foundId === null) {
$userModel->shouldReceive('find')->andReturn(null);
} else {
/** @var User|MockInterface $foundUser */
$foundUser = Mockery::mock(User::class)->makePartial();
$foundUser->shouldReceive('update')->andReturn(true);
$foundUser->shouldReceive('refresh')->andReturn(null);
$foundUser->shouldReceive('toArray')->andReturn($attributes);

$userModel->shouldReceive('find')->with($foundId)->andReturn($foundUser);
}

return $userModel;
}

public function test_createUser_returns_user_dto_on_success(): void
{
$repository = new UserRepository($this->buildUserModelMock(wasRecentlyCreated: true));
$result = $repository->createUser($this->buildCommand());
$repository = new UserRepository($this->buildCreateModelMock(wasRecentlyCreated: true));
$result = $repository->createUser($this->buildEntity());

$this->assertInstanceOf(UserDto::class, $result);
}

public function test_createUser_throws_exception_when_insert_fails(): void
{
$repository = new UserRepository($this->buildUserModelMock(wasRecentlyCreated: false));
$repository = new UserRepository($this->buildCreateModelMock(wasRecentlyCreated: false));

$this->expectException(Exception::class);
$this->expectExceptionMessage('Failed to create user.');

$repository->createUser($this->buildCommand());
$repository->createUser($this->buildEntity());
}

public function test_updateUser_returns_user_dto_on_success(): void
{
$existingId = 1;
$entity = UserEntityFactory::build([
'id' => $existingId,
'first_name' => 'Updated',
'last_name' => 'Name',
'email' => 'updated@example.com',
'age_range' => 'teens',
'subscription_tier' => 'free',
'subscription_expires_at' => null,
]);

$repository = new UserRepository($this->buildUpdateModelMock(foundId: $existingId));
$result = $repository->updateUser($entity);

$this->assertInstanceOf(UserDto::class, $result);
}

public function test_updateUser_throws_exception_when_user_not_found(): void
{
$entity = UserEntityFactory::build([
'id' => 999,
'first_name' => 'Ghost',
'last_name' => 'User',
'email' => 'ghost@example.com',
'age_range' => 'teens',
'subscription_tier' => 'free',
'subscription_expires_at' => null,
]);

$repository = new UserRepository($this->buildUpdateModelMock(foundId: null));

$this->expectException(Exception::class);
$this->expectExceptionMessage('User not found.');

$repository->updateUser($entity);
}
}
13 changes: 7 additions & 6 deletions src/app/Packages/Domains/User/Factory/UserEntityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ public static function build(array $data): UserEntity
]);

return new UserEntity(
$data['id'],
$data['first_name'],
$data['last_name'],
$data['email'],
AgeRange::from($data['age_range']),
$subscription,
id: isset($data['id']) ? (int) $data['id'] : null,
firstName: $data['first_name'],
lastName: $data['last_name'],
email: $data['email'],
ageRange: AgeRange::from($data['age_range']),
subscription: $subscription,
passwordHash: $data['password_hash'] ?? null,
);
}
}
10 changes: 8 additions & 2 deletions src/app/Packages/Domains/User/UserEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@
final readonly class UserEntity
{
public function __construct(
private readonly int $id,
private readonly ?int $id,
private readonly string $firstName,
private readonly string $lastName,
private readonly string $email,
private readonly AgeRange $ageRange,
private readonly Subscription $subscription,
private readonly ?string $passwordHash = null,
) {}

public function getId(): int
public function getId(): ?int
{
return $this->id;
}

public function getPasswordHash(): ?string
{
return $this->passwordHash;
}

public function getFirstName(): string
{
return $this->firstName;
Expand Down
40 changes: 31 additions & 9 deletions src/app/Packages/Domains/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use App\Models\User;
use App\Packages\Domains\Interface\UserRepositroyInterface;
use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand;
use App\Packages\Domains\User\UserEntity;
use App\Packages\Features\QueryUseCases\Dto\User\UserDto;
use App\Packages\Features\QueryUseCases\Factory\Dto\UserDtoFactory;
use Exception;
Expand All @@ -15,16 +15,16 @@ public function __construct(
private User $userModel
) {}

public function createUser(CreateUserCommand $command): UserDto
public function createUser(UserEntity $entity): UserDto
{
$insertUser = $this->userModel->create([
'email' => $command->email,
'first_name' => $command->firstName,
'last_name' => $command->lastName,
'password' => bcrypt($command->password),
'age_range' => $command->ageRange,
'subscription_tier' => $command->subscriptionTier,
'subscription_expires_at' => $command->subscriptionExpiresAt,
'email' => $entity->getEmail(),
'first_name' => $entity->getFirstName(),
'last_name' => $entity->getLastName(),
'password' => $entity->getPasswordHash(),
'age_range' => $entity->getAgeRange()->value,
'subscription_tier' => $entity->getSubscription()->getTier()->value,
'subscription_expires_at' => $entity->getSubscription()->getExpiresAt()?->format('Y-m-d H:i:s'),
]);

if (!$insertUser->wasRecentlyCreated) {
Expand All @@ -33,4 +33,26 @@ public function createUser(CreateUserCommand $command): UserDto

return UserDtoFactory::build($insertUser->toArray());
}

public function updateUser(UserEntity $entity): UserDto
{
$user = $this->userModel->find($entity->getId());

if ($user === null) {
throw new Exception('User not found.');
}

$user->update([
'first_name' => $entity->getFirstName(),
'last_name' => $entity->getLastName(),
'email' => $entity->getEmail(),
'age_range' => $entity->getAgeRange()->value,
'subscription_tier' => $entity->getSubscription()->getTier()->value,
'subscription_expires_at' => $entity->getSubscription()->getExpiresAt()?->format('Y-m-d H:i:s'),
]);

$user->refresh();

return UserDtoFactory::build($user->toArray());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Packages\Features\CommandUseCases\UseCase\User;

use App\Packages\Domains\Interface\UserRepositroyInterface;
use App\Packages\Domains\User\Factory\UserEntityFactory;
use App\Packages\Features\CommandUseCases\UseCommand\User\CreateUserCommand;
use App\Packages\Features\QueryUseCases\Dto\User\UserDto;

Expand All @@ -14,6 +15,17 @@ public function __construct(

public function handle(CreateUserCommand $command): UserDto
{
return $this->userRepository->createUser($command);
$entity = UserEntityFactory::build([
'id' => null,
'first_name' => $command->firstName,
'last_name' => $command->lastName,
'email' => $command->email,
'age_range' => $command->ageRange,
'subscription_tier' => $command->subscriptionTier,
'subscription_expires_at' => $command->subscriptionExpiresAt,
'password_hash' => bcrypt($command->password),
]);

return $this->userRepository->createUser($entity);
}
}
Loading
Loading