Files
AndyTranscribe/.claude/skills/infer-conventions/references/checklist.md
T

12 KiB

Detection Checklist

Every dimension here is a genuine fork: Laravel offers two or more valid approaches, the app's choice changes what the next agent writes, and no active project tool can pick for you. Left out on purpose: pure formatting (Pint owns it), any form an installed and enabled Rector rule rewrites to one canonical shape ($casts to casts(), $fillable to attributes, pipe-string rules to arrays, named to anonymous migrations, $signature to #[Signature]), and framework defaults any agent writes unprompted (ShouldQueue jobs, relation return types, HasFactory).

Each item gives the fork, then a hint (a grep or dir to spot which side the app takes). Hints are only a start. Read the matched files, never record on a raw count. Apply the ground rules to every verdict: a consistent choice that is a default or a tool's target form is not a pattern. Rows tagged (architecture) are the highest-signal, so record presence and deliberate absence.


A. Validation & HTTP input

  1. Validation entry point: inline $request->validate() vs Form Request classes vs Validator::make().
    • Hint: ls app/Http/Requests; grep ->validate( / Validator::make( in app/Http/Controllers.
  2. Custom rule location: invokable rule objects in app/Rules vs inline closures vs Validator::extend() in a provider. Rule objects are the default make:rule path, so record only if the app leans on closures or Validator::extend instead. "No rule objects" alone is just no-signal.
    • Hint: ls app/Rules; grep Validator::extend in app/Providers.
  3. Typed input retrieval: typed getters ($request->string(), ->integer(), ->enum(), ->date()) vs raw $request->input() / dynamic properties.
    • Hint: grep ->string( / ->integer( / ->enum( vs ->input( in app/Http.
  4. Custom messages/attributes: lang/*/validation.php vs Form Request messages() / attributes() methods.
    • Hint: ls lang; grep function messages, function attributes in app/Http/Requests.

B. Controllers & routing

  1. Controller shape: invokable single-action (__invoke) vs resource controllers vs plain multi-method.
    • Hint: grep __invoke in controllers; Route::resource / apiResource vs verb routes.
  2. Business-logic location (architecture): fat controllers vs delegated to Actions / Services / Jobs.
    • Hint: read a few controller methods; ls app/Actions app/Services.
  3. Route handler style: closures in routes/*.php vs controller classes.
    • Hint: count function () vs ::class in routes/web.php, routes/api.php.
  4. Middleware assignment: route/group ->middleware() vs controller HasMiddleware::middleware() vs #[Middleware] attribute.
    • Hint: grep implements HasMiddleware, #[Middleware( in controllers vs ->middleware( in routes.
  5. Route model binding: implicit (type-hinted models) vs explicit Route::bind vs manual findOrFail.
    • Hint: typed model params in signatures vs findOrFail( in controllers; grep Route::bind.
  6. Rate limiting: named RateLimiter::for() + throttle:name vs inline throttle:60,1.
    • Hint: grep RateLimiter::for in providers vs throttle: in route files.

C. Authorization

  1. Authorization home: Gates (Gate::define) vs Policy classes in app/Policies.
    • Hint: ls app/Policies; grep Gate::define in app/Providers.
  2. Authorization call site: $this->authorize() / Gate::authorize() vs $user->can() vs can middleware vs #[Authorize] vs @can in Blade.
    • Hint: grep authorize(, ->can(, middleware('can:, #[Authorize(, @can(.

D. Eloquent & models

  1. Mass assignment: $fillable allow-list vs $guarded block-list.
    • Hint: grep protected $fillable / protected $guarded in app/Models.
  2. Accessors/mutators: modern Attribute class vs legacy getXxxAttribute() / setXxxAttribute(). Record a legacy hold, it goes against the tool's grain.
    • Hint: grep : Attribute / Attribute::make vs function get[A-Z].*Attribute in app/Models.
  3. Primary keys: auto-increment vs HasUuids vs HasUlids.
    • Hint: grep HasUuids / HasUlids in app/Models; migration id() vs uuid('id').
  4. Custom casts: dedicated CastsAttributes classes (app/Casts) vs inline Attribute vs built-in cast strings.
    • Hint: ls app/Casts; grep Cast::class, AsStringable::class in models.
  5. Data/query layer (architecture): Eloquent directly in controllers vs repositories vs dedicated query objects (e.g. classes exposing builder(): Builder).
    • Hint: ls app/Repositories app/Queries; see where non-trivial queries are built.
  6. Query scopes: local scope/#[Scope] methods vs dedicated builder classes.
    • Hint: grep function scope / #[Scope] in models; ls app/*/Builders.
  7. Model events: observers (app/Observers, #[ObservedBy]) vs booted() closures vs event classes.
    • Hint: ls app/Observers; grep booted, ::observe, #[ObservedBy].
  8. Eager-load posture: explicit per-query ->with() vs model-level $with defaults. Treat preventLazyLoading() separately as a development guard because it can complement either posture.
    • Hint: grep protected $with, ->with(, and separately preventLazyLoading in app/.

E. Architecture & organization

  1. Action/Service structure (architecture): Action classes (invoked via handle / execute / __invoke) vs service objects vs neither. Cross-check the Step 0 app/ map: any Actions/Services/Pipelines/Jobs-as-actions folder is this pattern, so record how it is invoked.
    • Hint: ls app/ (the whole tree, not just Actions/Services); grep the invocation method in the folder you find.
  2. DTOs (architecture): spatie/laravel-data vs plain readonly classes vs arrays everywhere.
    • Hint: ls app/Data; grep extends Data, readonly class in app/.
  3. Dependency acquisition: constructor/method injection vs app() / resolve() / App::make() service location.
    • Hint: grep app( / resolve( / ::make( in app/ vs promoted constructor deps.
  4. Decoupling: events + listeners vs direct service calls.
    • Hint: ls app/Events app/Listeners; grep event(, ::dispatch(.
  5. Helper vs facade idiom: global helpers (config(), auth(), response()) vs facades (Config::, Auth::, Response::).
    • Hint: ratio of config( vs Config:: (etc.) across app/.
  6. Namespace layout (architecture): default app/ skeleton vs domain/module folders (app/Domain/**, modules).
    • Hint: ls app/, look for Domain/, Modules/, bounded-context folders.
  7. Enums: backed vs pure; case naming; where they live.
    • Hint: ls app/Enums; grep enum .*: string, enum .*: int.

F. Frontend & views

No Livewire/Inertia/Flux package is installed. This app may be API-only. Confirm from resources/views before sweeping, and treat the Livewire/Flux dimensions as not applicable.

  1. Frontend stack: Blade+Livewire vs Inertia (Vue/React/Svelte) vs Blade-only / API + separate SPA.
    • Hint: composer.json + package.json; ls resources/js/pages, resources/views.
  2. Blade composition: class <x-*> components vs anonymous components (@props) vs @include partials.
    • Hint: ls app/View/Components; grep <x-, @include in resources/views.
  3. Localization: short keys (lang/*/*.php + __('messages.welcome')) vs JSON string keys (lang/*.json + __('Full sentence')).
    • Hint: ls lang; grep dotted __(' vs sentence keys.

G. Database & migrations

  1. Foreign keys: foreignId()->constrained() vs foreignIdFor(Model::class) vs manual foreign()->references()->on().
    • Hint: grep foreignId(, foreignIdFor(, ->foreign( in database/migrations.
  2. down() methods: real reverse logic vs omitted / one-way migrations.
    • Hint: grep function down vs the migration count.
  3. Enum storage: DB enum() column vs string() + PHP-enum cast on the model.
    • Hint: grep ->enum( in migrations vs string columns cast to enums.
  4. Transactions: DB::transaction(fn ...) closure vs manual beginTransaction / commit / rollBack.
    • Hint: grep DB::transaction, beginTransaction in app/.
  5. Idempotent writes: upsert / updateOrCreate / firstOrCreate vs find-then-save.
    • Hint: grep upsert(, updateOrCreate(, firstOrCreate( in app/.

H. Testing

  1. Framework: Pest (it() / test() / expect()) vs PHPUnit classes.
    • Hint: ls tests/Pest.php; grep it( / test( vs extends TestCase.
  2. DB reset: RefreshDatabase vs DatabaseTruncation vs DatabaseMigrations.
    • Hint: grep those trait names in tests/.
  3. Fixtures: compare how equivalent test-owned records are created, such as factories vs manual inserts. Track seeders separately for shared reference data because $this->seed() commonly and legitimately coexists with factories.
    • Hint: grep ::factory( and direct inserts in tests/; separately inspect $this->seed( calls and what those seeders provide.
  4. Collaborator isolation: how the app doubles its own classes, Mockery mock() / spy() vs real integration. Ignore facade fakes like Mail::fake() here, they isolate framework services by default and are not a fork against Mockery.
    • Hint: grep ->mock(, ->spy(, Mockery:: in tests/.
  5. Endpoint assertions: array assertJson([...]) / assertJsonFragment vs fluent AssertableJson.
    • Hint: grep AssertableJson, assertJsonFragment in tests/.

I. Responses & API resources

  1. Response shape: API Resource classes vs response()->json() vs returning models/arrays directly.
    • Hint: ls app/Http/Resources; grep JsonResource, ->json( in controllers.
  2. Resource relationship inclusion: whenLoaded() guards vs unconditional relationship access. Do not count ordinary scalar attributes as rivals to conditional relationships, and evaluate general when() fields separately.
    • Hint: compare relationship fields using whenLoaded( with unconditional relationship property access in app/Http/Resources.
  3. Pagination contracts: within comparable endpoint categories, length-aware paginate() vs simplePaginate() vs cursorPaginate(). These have different totals, navigation, ordering, and performance contracts, so record only a stable path-scoped API policy, never a project-wide majority.
    • Hint: grep those in app/, then group matches by endpoint type and client contract before comparing them.
  4. Web redirects/URLs: route('name') vs url('/path') vs action([...]).
    • Hint: grep route(', url('/, action([ in app/Http and views.

J. Strings, collections & dates

  1. Iteration idiom: collect()->map()->filter() pipelines vs array_map / foreach.
    • Hint: grep collect(, ->map( vs array_map, foreach density in app/.
  2. String API: fluent Str::of()->... (Stringable) vs static Str:: vs native (trim, strtoupper).
    • Hint: grep Str::of( vs Str:: vs native string funcs.
  3. Dates: compare equivalent construction call styles (now() / today() helpers vs Carbon::) separately from the application's mutable/immutable date policy. Date::use(CarbonImmutable::class) can make helpers return immutable dates, so those signals are complementary rather than conflicting.
    • Hint: grep now( and Carbon:: for call style; separately inspect CarbonImmutable and Date::use for mutability policy.

Genuine forks only. Every row survived the "no tool can decide this, and it isn't the default" filter. Give each applicable dimension exactly one verdict: pattern, conflict, default, no-signal, tooling-owned, or already-recorded. The rows tagged (architecture) are where the highest-value rules come from.