Code Review
Why this exists
Review is where two things happen: the change gets a second pair of eyes for correctness, and the team's standards stay coherent over time. A review focused on style nits drowns out the architectural feedback that actually matters; a review that only catches missing tests misses the conventions that hold the codebase together.
This chapter is a checklist for human reviewers — what to look for, in what order, and how to give feedback that lands.
Read in this order
A useful pass goes top-down: start with the highest-impact concerns and stop early when they look fine.
- Architecture and DDD compliance — does the change respect domain boundaries? Are cross-domain calls going through Contracts? Are Services delegating to Actions rather than holding queries?
- Public API changes — is this PR adding methods to a Contract? Renaming an existing one? Those are breaking changes for every other domain and need a wider review.
- Security — every write goes through a FormRequest. Every state-changing endpoint is authorised. No
{!! $userInput !!}in Blade. No raw SQL with string interpolation. - Correctness — multi-statement writes are wrapped in
DB::transaction(). Eloquent queries that load relationships usewith()/withCount(). Resources guard related fields withwhenLoaded(). - Conventions — DTOs are named
*Data, properties aresnake_case. Actions are statichandle(). API controllers returnResponseBuilder. Migrations don't custom-name indexes. - Tests — the change has tests in
tests/Unit/for new Actions andtests/Feature/for new endpoints. Pest assertions go throughdata.*for API responses. - Style — Pint will catch most of this. Don't pile on style nits if the architecture is solid.
If the top of the list is clean, the rest is fast.
What to look for in each layer
Actions
final class X { public static function handle(...) }— no constructor, noexecute()/run().- Multi-statement writes wrapped in
DB::transaction(fn () => …). - Cross-domain dependencies resolved with
app(SomeContract::class), not via constructor injection. - Class name reads as a verb —
CreateOrder, notOrderCreatorAction.
See actions.
Services and Contracts
- Service body is delegation only — no
Model::query(), noModel::create(), noDB::transaction(). - Method signatures on one line.
- Contract methods generic (
getProvider(?int $id, ?FilterProviderData $filters)) rather than specific (getProviderByEmail). - New domain has its provider registered in
bootstrap/providers.php.
DTOs
- Class name ends with
Data. - Properties are
snake_caseandpublic readonly. - Uses the
Arrayabletrait. - Filter DTOs extend
FilterDataand have all-nullable properties. - Nested array properties have
@param ChildData[] $fieldPHPDoc.
See DTOs.
Controllers
- Thin: request → DTO → Contract → response shape.
- Constructor-injected Contracts only — no Services, Actions, or Models in the constructor.
- Every write endpoint has a typed FormRequest; reads that take filters use one too.
- API endpoints return
: ResponseBuilderand build responses viares(). - 201 on
store, 204 ondestroy. - SSR
store/update/destroyredirect with a flash message, never return a view.
See controllers.
Queries
- Filter logic lives in a
scopeFilterBytrait underModels/Scopes/. whereByTypefor value-or-array filters;whereLikeTextfor text search.- Eager loading via
with()/withCount()— no relationship access inside Blade or a Resource withoutwhenLoaded(). - Bulk processing uses
chunk()/chunkById()rather thanget().
See Eloquent and queries.
Migrations
- Anonymous class, modern column helpers.
- No speculative indexes; no extra
->index()on foreign keys. - No custom-named indexes or constraints.
- Column modifications include every previously-defined attribute.
See migrations.
How to write feedback
Cite file:line for every concrete finding. Pair every problem with a code example — show the offending code and the corrected version side by side. Suggesting the fix matters more than flagging the smell.
Prioritise architectural violations over style nits. Pint will catch style. The reviewer's job is to catch the structural problems Pint can't see.
Be specific in positive feedback too. "Looks good" doesn't help anyone learn what was good about it; "the way you extracted CalculateOrderTotal from CreateOrder makes this easier to test from the job, nice" gives the author and any onlooker something to repeat.
Don't pile on. Long lists of nits get skipped; a focused set of three or four substantive points gets read and acted on.
Human review checklist
Use this list when you open a PR to review. It's not exhaustive; it's the high-impact set worth scanning before you start commenting.
- Does the change respect domain boundaries? No
use Domain\X\Models\…from outsideDomain\X\? - Are Services free of direct Eloquent queries?
- Are multi-statement writes wrapped in
DB::transaction()? - Are DTOs named
*Datawithsnake_caseproperties? - Do write endpoints validate via a FormRequest?
- Do API controllers return
: ResponseBuilder? - Are 201 on create and 204 on destroy used correctly?
- Are Eloquent reads paired with the right
with()/withCount()? - Are migrations free of speculative indexes and custom-named constraints?
- Are new translation keys present in both
lang/en/andlang/ar/? - Are tests present for new Actions and new endpoints?
- Does the PR title follow Conventional Commits?
- Does the PR body reference the Linear issue (
Closes WIN-…)?
Referencing the anti-patterns list
The anti-patterns chapter lists nineteen specific items by stable number. When a finding matches a documented anti-pattern, cite the number: "this is anti-pattern #6 — Actions are static handle(), not instance execute()". This gives the author a fast jump to the canonical explanation instead of repeating it in the review.
What to read next
- Anti-patterns — the canonical list with examples for each.
- Git and commits — the commit and PR conventions you're enforcing in review.
- Refactoring legacy — when a review touches legacy code, this is what to apply.
Git and Commits
Branch names, Conventional Commits, PR titles, squash-merging, and the pre-push hook — the moving parts that keep our history readable and our Linear board in sync.
Job Batching
When a user action fans out into many independent jobs and the UI needs a live progress bar plus a Cancel-All button — how the project wraps Laravel's native batching through the `Core\JobBatch` sub-domain, and the things you must not build.