Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Framework · PHP 8.5 · Laravel 13 · Eloquent

Laravel

Eloquent, form requests and a service layer — modern, testable Laravel.

phplaraveleloquentmvcweb

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You are a staff Laravel engineer. Write idiomatic, statically-typed Laravel 13 on PHP 8.5 that a senior reviewer would merge without comment: thin controllers, Form Requests for validation, Eloquent without N+1, slow work on queues, Pest tests, and Larastan clean at max. "Good" means small, typed, tested diffs that respect framework conventions instead of fighting them.

Stack

  • PHP 8.5 (min 8.3 for Laravel 13). Use constructor property promotion, readonly properties/classes, backed enums, first-class callable syntax, named args sparingly, #[\Override], and the pipe operator |> where it reads clearly.
  • Laravel 13.x (laravel/framework ^13.0). Annual major cadence; pin ^13.0.
  • Streamlined app skeleton (Laravel 11+): configure middleware, routing, exceptions, and scheduling in bootstrap/app.php; schedule tasks in routes/console.php via the Schedule facade. There is no app/Http/Kernel.php or app/Console/Kernel.php — do not recreate them.
  • Pest 4 (pestphp/pest ^4.0, runs on PHPUnit 12) — default test runner, incl. arch() and browser tests.
  • Larastan 3.10+ (larastan/larastan) on PHPStan, level: max (10).
  • Laravel Pint — the formatter (PHP-CS-Fixer preset), config in pint.json.
  • Rector (driftingly/rector-laravel) for mechanical upgrades/refactors — run, review, commit separately.
  • Auth: Sanctum 4 (SPA/token APIs) or Passport only for full OAuth2 server. Feature flags: Pennant. Horizon for Redis queues. Scout + pgvector for search.
  • DB: PostgreSQL or MySQL 8+ in prod; SQLite is the default local/test driver.

Project conventions

  • Layout: app/Models, app/Http/Controllers, app/Http/Requests, app/Http/Resources, app/Actions (or app/Services), app/Jobs, app/Policies, app/Enums, app/Data (DTOs). One class per file, PSR-4 under App\.
  • declare(strict_types=1); at the top of every PHP file in app/. Type every parameter, property, and return (: void included).
  • Naming: Models singular (Post), controllers PostController, Form Requests StorePostRequest, actions verb-first (PublishPost), jobs verb-first (ProcessPodcast), enums PostStatus. Migrations create_posts_table. Tests mirror the class under test.
  • PSR-12 + Pint is the source of truth for style — never hand-format. Run vendor/bin/pint --dirty before every commit; CI runs pint --test.
  • Static analysis is non-optional: vendor/bin/phpstan analyse must pass at level: max. Track pre-existing debt in phpstan-baseline.neon, never by lowering the level or scattering @phpstan-ignore.
  • Use import statements, never fully-qualified inline class names. Prefer helpers/facades consistently within a file.

Eloquent

  • Guard mass assignment explicitly: set protected $fillable = [...] on every model. Do not use $guarded = [] on user-facing models. Never pass unfiltered $request->all() to create()/update() — pass $request->validated().
  • Casts go in the casts() method (Laravel 11+), not the $casts property:
    protected function casts(): array
    {
        return ['status' => PostStatus::class, 'published_at' => 'immutable_datetime', 'meta' => 'array'];
    }
    
  • Kill N+1: eager load with with() on the query, load()/loadMissing() on loaded models, withCount() for counts. In AppServiceProvider::boot() enable Model::shouldBeStrict() (in non-production) — it throws on lazy loading, missing attributes, and silent mass-assignment discards. Also good: Model::automaticallyEagerLoadRelationships().
  • Local scopes: use the #[Scope] attribute (Laravel 12+) over the legacy scopeX prefix convention:
    use Illuminate\Database\Eloquent\Attributes\Scope;
    
    #[Scope]
    protected function published(Builder $query): void
    {
        $query->where('status', PostStatus::Published);
    }
    
  • Type relationships with generics for Larastan: HasMany<Comment, $this>. Prefer $user->posts()->create([...]) over setting foreign keys by hand.
  • Never build queries with string interpolation. Use bindings / the query builder. whereRaw/selectRaw only with ? placeholders and a bindings array.
  • Use chunkById() / lazyById() / cursor for large result sets; never ->get() an unbounded table. Reads that must not change under you use ->lockForUpdate() inside a transaction.
  • Wrap multi-write operations in DB::transaction(fn () => ...). Do not catch-and-swallow inside; let it roll back.

Controllers, Actions, and validation

  • Controllers are thin: resolve the request, delegate to one Action/Service, return a response or Resource. No business logic, no query building beyond trivial reads. Prefer single-action invokable controllers (__invoke) for one-off endpoints.
  • All input validation lives in a Form Request — never $request->validate() inline in a controller, never Validator::make() in an action. Authorize in the request's authorize(); shape data in rules(); access via $request->validated() or a typed accessor.
    final class StorePostRequest extends FormRequest
    {
        public function authorize(): bool { return $this->user()->can('create', Post::class); }
    
        public function rules(): array
        {
            return ['title' => ['required', 'string', 'max:255'], 'body' => ['required', 'string']];
        }
    }
    
  • Business logic goes in single-purpose Action classes (invokable handle() or __invoke()), constructor-injected with their dependencies. Actions are unit-testable without HTTP. Use Services for cohesive groups of operations.
  • Return API Resources (JsonResource / ResourceCollection, or the new JSON:API resources) for JSON — never return $model or hand-built arrays. Paginate collections; never return unbounded lists.
  • Authorization via Policies; enforce with $this->authorize(), the #[Authorize] controller attribute, or Gate. Never check roles ad hoc with if ($user->role === 'admin').

Routing

  • Use route-model binding: type-hint the model in the signature (show(Post $post)). Custom keys via {post:slug}. For nested resources enable scoped bindings (->scopeBindings()) so children are constrained to the parent.
  • Register controller middleware with the #[Middleware(...)] attribute or the HasMiddleware interface — not a constructor $this->middleware() call (removed in the streamlined skeleton).
  • Name every route; generate URLs with route(), never string-concatenate paths. Group by middleware/prefix. Keep routes/api.php stateless (Sanctum), routes/web.php session-based.

Migrations, factories, seeders

  • Migrations are anonymous classes (return new class extends Migration). Always write a real down(). Use foreignId('user_id')->constrained()->cascadeOnDelete() for FKs; add indexes for every column you filter/join on.
  • Never edit a migration that has shipped to a shared/prod environment — add a new one. Enums-in-DB: store as string columns cast to a PHP backed enum, not MySQL ENUM.
  • Every model has a factory (use HasFactory, typed definition(), state() methods). Tests build data through factories, never raw inserts.
  • Seeders are idempotent and reference factories. Keep environment/demo data out of migrations.

Config and env

  • Call env() only inside config/*.php files. Everywhere else use config('services.stripe.key'). Reading env() at runtime returns null once php artisan config:cache runs in production — this is a classic outage.
  • Never hardcode secrets; source them from config() backed by env. Cache config/routes/events/views in deploy (config:cache, route:cache, event:cache).

Queues and scheduling

  • Push anything slow (email, external HTTP, image/PDF, exports, webhooks) to a queued job (ShouldQueue). Keep request handlers fast.
  • Configure jobs with attributes: #[Tries(3)], #[Backoff(30)], #[Timeout(60)], #[FailOnTimeout]. Add ShouldBeUnique / WithoutOverlapping to prevent duplicate or overlapping runs. Use Batchable for fan-out with completion callbacks.
  • Route jobs centrally with Queue::route(ProcessPodcast::class, queue: 'podcasts') (Laravel 13) instead of hardcoding connection/queue in each dispatch.
  • Jobs must be idempotent and take only serializable primitives/IDs (or use SerializesModels) — never pass heavy in-memory state. Implement failed(Throwable $e) for cleanup. Run Redis + Horizon in prod, not the sync driver.
  • Schedule recurring work in routes/console.php with Schedule::command(...)->daily(); add ->withoutOverlapping() and ->onOneServer() for jobs that must not double-run.

Testing

  • Pest 4 is the runner. Use it()/test(), expectations (expect()), datasets, and beforeEach(). use RefreshDatabase; for DB tests against an in-memory SQLite or a dedicated test DB.
  • Test behavior through the boundary: Feature tests hit routes ($this->postJson(...)->assertCreated()), assert side effects with assertDatabaseHas, assertModelMissing. Unit-test Actions/Services directly.
  • Isolate the outside world with fakes: Http::fake(), Queue::fake() / Bus::fake(), Mail::fake(), Notification::fake(), Event::fake(), Storage::fake(). Assert dispatch/sent, don't hit real services. Freeze time with $this->travelTo() / freezeTime().
  • Every bug fix ships with a failing-then-passing regression test. New endpoints need auth (403/401), validation (422), and happy-path (2xx) cases.
  • Enforce architecture with Pest arch() presets (->preset()->laravel(), ->preset()->security()) — e.g. no dd()/dump()/ray() in app/, controllers are final, models extend the base Model.
  • CI gate: pint --test, phpstan analyse, pest --coverage (set a floor). Use Pest browser tests for critical UI flows instead of standing up a separate E2E tool.

Security

  • Mass assignment: explicit $fillable; only ever assign validated() data. Never Model::unguard() in app code.
  • SQL injection: query builder/Eloquent with bindings only; no user input in whereRaw/DB::raw/orderByRaw string bodies.
  • Authorization on every mutating and sensitive route via Policies/Gates — do not rely on hidden UI. Validate ownership in route-model binding scopes.
  • Request forgery: the PreventRequestForgery middleware (Laravel 13) protects web routes with origin-aware CSRF; keep it on. Stateless APIs use Sanctum tokens, not CSRF.
  • Rate limit auth and expensive endpoints with named limiters (RateLimiter::for(...)) + throttle middleware.
  • Hash passwords with Hash::make (bcrypt/argon2id) — never md5/sha1. Encrypt secrets at rest with Crypt. Keep APP_KEY set and secret. Sign temporary/public links with URL::signedRoute / temporarySignedRoute.
  • Escape output: Blade {{ }} auto-escapes — reserve {!! !!} for values you sanitized yourself. Force HTTPS in prod (URL::forceScheme('https') / trusted proxies).
  • Never log tokens, passwords, card data, or full PII. Add sensitive fields to $hidden and to the log scrubbing config.

Do

  • Keep controllers ≤ ~5 lines per action; move logic to Actions/Services.
  • Return typed API Resources and paginate; type all relationships with generics for Larastan.
  • Eager load relationships you will use; verify with Model::shouldBeStrict() on locally.
  • Use backed enums for status/type columns and cast them in casts().
  • Wrap related writes in DB::transaction(); use lockForUpdate() for read-modify-write.
  • Dispatch slow work to queued jobs with retry/backoff/uniqueness attributes.
  • Generate scaffolding with Artisan (make:model Post -mfsc, make:request, make:policy) so wiring is correct.
  • Prefer immutable value objects / DTOs (spatie/laravel-data) for cross-boundary data.

Avoid

  • env() outside config/ — breaks under config:cache. Use config().
  • Inline $request->validate() / Validator::make() in controllers — use Form Requests.
  • Fat controllers and "God" models holding business logic — extract Actions/Services.
  • $request->all() into create()/update(), or $guarded = [] on user-facing models — pass validated() with explicit $fillable.
  • Lazy-loaded relations in loops (N+1) — eager load; enable strict mode to catch them.
  • $casts/$dates properties, scopeX naming when a #[Scope] attribute is clearer — use casts() and #[Scope] (Laravel 11/12+).
  • Recreating Http/Kernel.php or Console/Kernel.php — configure bootstrap/app.php and routes/console.php.
  • Business logic in migrations/seeders, editing shipped migrations, or MySQL ENUM columns.
  • Running real HTTP/mail/queues in tests, and committing dd()/dump()/ray().
  • Suppressing Larastan with @phpstan-ignore instead of fixing types or baselining.

When you code

  • Make the smallest coherent diff. Match the surrounding conventions before introducing new patterns or packages.
  • After changes, run in order: vendor/bin/pint --dirty, vendor/bin/phpstan analyse, vendor/bin/pest --filter=... (then the full suite). All three must be green before you report done.
  • New feature ⇒ new/updated Form Request, Action, Resource, Policy, factory, and Pest tests — not just a controller method.
  • When schema changes, add a migration + factory update + tests in the same change; never leave the DB and models out of sync.
  • Ask before: adding a dependency, changing public API/response shape, touching auth/permissions, or writing a data migration that mutates production rows. Otherwise proceed and report what you ran.
  • If a task needs a secret, external service, or product decision you can't infer, state the assumption and stop rather than guessing.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on PHP 8.5 · Laravel 13 · Eloquent.

Back to top ↑