9 cases: null/empty/malformed email -> BadRequest; null or
sub-8-char password -> BadRequest; duplicate email -> DomainException;
valid signup returns User with hashed password and isAdmin=false;
user is findable by email afterwards; EmailAddress vo lowercases
the domain. fails red - SignupUser class not yet defined.
Session: immutable holder of token, owning User, createdAt,
expiresAt. isExpired(now) compares >= expiresAt. SessionModel
keys on token (string primary, non-incrementing). migration adds
sessions table with foreign user_id (cascade on user delete) and
indexed expires_at for cleanup queries. EloquentSessionRepository
takes UserRepository to rehydrate the owning User on findByToken;
sessions for deleted users return null. FakeSessionRepository
mirrors with an in-memory map keyed by token, defensive copies on
read.
Clock + SystemClock (DateTimeImmutable in UTC), TokenGenerator +
RandomTokenGenerator (bin2hex(random_bytes(32)) -> 64-char hex),
PasswordHasher + BcryptPasswordHasher (password_hash with
PASSWORD_DEFAULT, password_verify). matching fakes:
FakeClock with mutable setTime, FakeTokenGenerator with a
pre-seeded queue (throws once exhausted), FakePasswordHasher
returns 'hashed:<plain>' for deterministic test assertions.
composer stan now passes --memory-limit=512M (default 128M
overflows once larastan loads more rules).
User holds email (EmailAddress vo), passwordHash, isAdmin - tide
keeps password and admin flag on the user row directly (no
separate profile entity like youngstartup). UserRepository
exposes find, findByEmail, create. CreateUserDto is readonly with
explicit isAdmin (per shared.md no-default-args rule).
immutable readonly. trims whitespace, splits on @, lowercases the
domain (local-part case preserved per RFC 5321), validates with
FILTER_VALIDATE_EMAIL after normalization. throws
InvalidArgumentException on empty / missing-@ / malformed input.
exposes value(), getDomain(), equals(), __toString(). all 7
EmailAddressTest cases green; 9 tests total pass.
7 cases: rejects spaces, double-@, empty input; trims whitespace;
lowercases domain only (preserving local-part case); equality by
normalized value; __toString and getDomain. fails red - class
App\\Shared\\ValueObject\\EmailAddress not yet defined.
BadRequestException, UnauthorizedException, ForbiddenException -
all extend DomainException. use cases throw these to signal HTTP
4xx categories; controllers translate to JsonResponse status
codes (400, 401, 403).
removed app/Models/User.php (laravel auth model - tide authors a
ddd User entity in app/User/), app/Http/Controllers/Controller.php
(controllers live flat in app/Controllers/ per youngstartup), and
all three 0001_01_01_* migrations (default users schema, cache,
jobs - tide writes its own users migration with is_admin and
password_hash). routes/api.php stripped of the sanctum-bound
/user demo route - left as an empty stub for incoming domains.