add EmailConfirmationToken persistence: model, migration, eloquent + fake repo

This commit is contained in:
Yisroel Baum 2026-05-06 22:05:52 +03:00
parent 9747d07c31
commit e16cb45387
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
7 changed files with 288 additions and 0 deletions

View file

@ -0,0 +1,14 @@
<?php
namespace App\Email\EmailConfirmationToken;
use App\User\User;
use DateTimeImmutable;
readonly class CreateEmailConfirmationTokenDto
{
public function __construct(
public User $user,
public DateTimeImmutable $availableTo,
) {}
}

View file

@ -0,0 +1,73 @@
<?php
namespace App\Email\EmailConfirmationToken;
use App\User\User;
use App\User\UserRepository;
use DateTimeImmutable;
use DateTimeZone;
use DomainException;
class EloquentEmailConfirmationTokenRepository implements EmailConfirmationTokenRepository
{
public function __construct(
private UserRepository $userRepo,
) {}
public function create(
CreateEmailConfirmationTokenDto $dto,
): EmailConfirmationToken {
$model = EmailConfirmationTokenModel::create([
'user_id' => $dto->user->getId(),
'token' => bin2hex(random_bytes(32)),
'available_to' => $dto->availableTo,
]);
return $this->toDomain($model);
}
public function findByToken(string $token): ?EmailConfirmationToken
{
$model = EmailConfirmationTokenModel::where(
'token', $token,
)->first();
return $model === null ? null : $this->toDomain($model);
}
public function findByUser(User $user): ?EmailConfirmationToken
{
$model = EmailConfirmationTokenModel::where(
'user_id', $user->getId(),
)->first();
return $model === null ? null : $this->toDomain($model);
}
public function delete(int $id): void
{
EmailConfirmationTokenModel::query()->where('id', $id)->delete();
}
private function toDomain(
EmailConfirmationTokenModel $model,
): EmailConfirmationToken {
$user = $this->userRepo->find($model->user_id);
if ($user === null) {
throw new DomainException(
"User with id {$model->user_id} not found"
);
}
$availableTo = new DateTimeImmutable(
$model->available_to->toDateTimeString(),
new DateTimeZone('UTC'),
);
return new EmailConfirmationToken(
id: $model->id,
user: $user,
availableTo: $availableTo,
token: $model->token,
);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Email\EmailConfirmationToken;
use App\User\User;
use DateTimeImmutable;
class EmailConfirmationToken
{
public function __construct(
private int $id,
private User $user,
private DateTimeImmutable $availableTo,
private string $token,
) {}
public function getId(): int
{
return $this->id;
}
public function getUser(): User
{
return $this->user;
}
public function getAvailableTo(): DateTimeImmutable
{
return $this->availableTo;
}
public function getToken(): string
{
return $this->token;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Email\EmailConfirmationToken;
use DateTimeImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property int $user_id
* @property string $token
* @property DateTimeImmutable $available_to
*
* @method static Builder<static>|EmailConfirmationTokenModel newModelQuery()
* @method static Builder<static>|EmailConfirmationTokenModel newQuery()
* @method static Builder<static>|EmailConfirmationTokenModel query()
* @method static Builder<static>|EmailConfirmationTokenModel whereId($value)
* @method static Builder<static>|EmailConfirmationTokenModel whereUserId($value)
* @method static Builder<static>|EmailConfirmationTokenModel whereToken($value)
* @method static Builder<static>|EmailConfirmationTokenModel whereAvailableTo($value)
*
* @mixin \Eloquent
*/
class EmailConfirmationTokenModel extends Model
{
protected $table = 'email_confirmation_tokens';
public $timestamps = false;
protected $fillable = [
'user_id',
'token',
'available_to',
];
protected $casts = [
'available_to' => 'immutable_datetime',
];
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Email\EmailConfirmationToken;
use App\User\User;
interface EmailConfirmationTokenRepository
{
public function create(
CreateEmailConfirmationTokenDto $dto,
): EmailConfirmationToken;
public function findByToken(string $token): ?EmailConfirmationToken;
public function findByUser(User $user): ?EmailConfirmationToken;
public function delete(int $id): void;
}

View file

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('email_confirmation_tokens', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained('users')
->cascadeOnDelete();
$table->string('token')->unique();
$table->dateTime('available_to');
});
}
public function down(): void
{
Schema::dropIfExists('email_confirmation_tokens');
}
};

View file

@ -0,0 +1,82 @@
<?php
namespace Tests\Fakes;
use App\Email\EmailConfirmationToken\CreateEmailConfirmationTokenDto;
use App\Email\EmailConfirmationToken\EmailConfirmationToken;
use App\Email\EmailConfirmationToken\EmailConfirmationTokenRepository;
use App\User\User;
use App\User\UserRepository;
class FakeEmailConfirmationTokenRepository implements EmailConfirmationTokenRepository
{
/**
* @var EmailConfirmationToken[]
*/
private array $existingTokens = [];
public function __construct(
private UserRepository $userRepo,
) {}
public function create(
CreateEmailConfirmationTokenDto $dto,
): EmailConfirmationToken {
$id = $this->nextId();
$token = new EmailConfirmationToken(
id: $id,
user: $dto->user,
availableTo: $dto->availableTo,
token: bin2hex(random_bytes(32)),
);
$this->existingTokens[$id] = $token;
return $this->copy($token);
}
public function findByToken(string $token): ?EmailConfirmationToken
{
foreach ($this->existingTokens as $existing) {
if ($existing->getToken() === $token) {
return $this->copy($existing);
}
}
return null;
}
public function findByUser(User $user): ?EmailConfirmationToken
{
foreach ($this->existingTokens as $existing) {
if ($existing->getUser()->getId() === $user->getId()) {
return $this->copy($existing);
}
}
return null;
}
public function delete(int $id): void
{
unset($this->existingTokens[$id]);
}
private function copy(
EmailConfirmationToken $token,
): EmailConfirmationToken {
$user = $this->userRepo->find($token->getUser()->getId())
?? $token->getUser();
return new EmailConfirmationToken(
id: $token->getId(),
user: $user,
availableTo: $token->getAvailableTo(),
token: $token->getToken(),
);
}
private function nextId(): int
{
return count($this->existingTokens) + 1;
}
}