trims title and body, rejects empty (post-trim) values with
BadRequest. supplies createdAt from injected Clock. persists
through PostRepository->create and returns the resulting Post.
44 tests pass.
7 cases: null + whitespace title -> BadRequest; null +
whitespace body -> BadRequest; valid request returns Post with
correct userId/title/body and createdAt = clock.now(); the post
is findable via the repo afterwards; title and body get trimmed
of leading/trailing whitespace. fails red - CreatePost class
absent.
PostModel maps posts table (id, user_id fk, title, body text,
created_at indexed). EloquentPostRepository: create, find,
findByUserId (desc by created_at), findRecent (limit, desc),
delete - chain via ::query() to keep larastan happy.
FakePostRepository sorts on read (defensive copy each return).
cascade-on-delete on user_id so removing a user nukes their
posts.
phpstan.neon suppresses staticMethod.dynamicCall under
app/*/Eloquent*Repository.php - phpstan-strict-rules flags
Eloquent's fluent builder idiom (Model::query()->orderBy())
because the static methods become instance calls mid-chain.
suppression scoped to repo files only so the rule still
applies elsewhere.
reads auth_token cookie (constant COOKIE_NAME for cross-layer
sharing with the AuthController). missing/empty cookie or
unknown token -> 401 json {error: unauthenticated}. expired
session is deleted then 401 returned. valid session attaches
the User entity to request attributes under 'user' so
downstream controllers can read it via request attributes. 37
tests pass.
generates token via injected TokenGenerator, asks Clock for now,
sets expiry to now+7d, persists through SessionRepository->create
and returns the resulting Session. all 31 tests pass.
4 cases: returns Session with the generated token + supplied
user; createdAt matches injected Clock now; expiresAt is now+7d;
session is findable via SessionRepository->findByToken. fails
red - CreateSession class missing.
input validation: email + password required. constructs
EmailAddress vo (BadRequest on bad format). looks up user; absent
or password-mismatch -> UnauthorizedException with constant
'invalid credentials' message (no enumeration leak). password
verified through PasswordHasher->verify against stored hash on
the User entity (no separate profile lookup -> tide keeps
password on the user row). returns the User entity for the
caller (typically CreateSession + AuthController). 27 tests
pass.
9 cases: null/empty/malformed email -> BadRequest; null/empty
password -> BadRequest; unknown email -> Unauthorized; wrong
password -> Unauthorized; valid creds return the User entity;
isAdmin flag survives the auth round-trip. fails red - the
AuthenticateUser class does not exist yet.
validates email present + format (wraps EmailAddress vo's
InvalidArgumentException as BadRequest), password present +
>= 8 chars, then ensures email not already registered. hashes
password through injected PasswordHasher and persists via
UserRepository->create with isAdmin=false (admins are seeder-
only per plan). throws DomainException on duplicate email so
the controller layer can map it to 409. all 18 tests pass.
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).
hot-fix: pgdata anchored to repo root, .postgres/.direnv ignored
at any nesting. resolves stray backend/.postgres tracking caught
during phase 1 merge.
shellHook now derives PGDATA from $(git rev-parse --show-toplevel)
instead of $PWD. nix develop / direnv from a subdir (e.g.
backend/) used to seed a duplicate cluster at backend/.postgres
that leaked into git tracking. .gitignore loses its leading slash
on .postgres/ + .direnv/ + .pc.* so any nested cluster also gets
ignored. fallback to pwd preserves behavior outside a git repo.
968 stray backend/.postgres/* blobs already pruned from history
via git-filter-repo before this commit.
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.