« back to all posts

Sane Laravel rules for Cursor and other AI assistants

This week, I tried out Cursor. The code it originally wrote was not up to my standards, which led me to making a set of custom LLM rules.

Also check out spatie/laravel-stubs for stubs that favor types as opposed to bloated PHPDoc comments.

The .cursor/rules/laravel.mdc file:

---
alwaysApply: true
---

- Do not use `auth()` helper or `Auth` facade, instead always retrieve the user either from the Request (`$request->user()`) or via `#[CurrentUser] User $user` attribute (`use Illuminate\Container\Attributes\CurrentUser;`). Prop-drill it down the methods instead of calling `Auth` or `auth`.

- Do not add comments because code is already self-explanatory.

- Do not assign ids manually like `Post::create(['user_id' => Auth::id()`, instead call relationship method on the parental model like `$user->posts()->create()`.

- Do not query ids manually like `where('user_id', $user->id)`, always prefer methods such as  `whereBelongsTo($user)`, or `whereAttachedTo($role)`.

- Always prefer to write less code and start with `artisan make:` commands. For instance, you can get a model, resource controller and a migration all at once via `make:model -mcr`.

- Always use Policy classes as opposed to controller checks or unnecessary `authorize` in form requests. The best way is to attach a policy on the route itself via `Route::post('/post', ...)->can('create', Post::class);` or `->can('update', 'post')`.

- Put logic in the right place from the start by avoiding `private`/`protected` methods in controllers. Prefer dedicated methods in FormRequests, fat domain models and action classes.

- Always use singular entity names for controllers, ie FormController but not FormsController.

- Always use `$request->validated()` instead of `$request->all()` to avoid unexpected data injections because there is usually a `protected $guarded = [];` but never overrwrite `validated` - it already returns all keys defined in `rules` of the `FormReqiest` class.

- Never manually close modal windows that have Inertia `<Form` inside - the page will reload and re-render automatically. This assumes avoiding `onSuccess` for all modals that have `<Form` component. 

- Do not list out all the fields in controller like `StoreFormRequest $request) Model::create(['field' => $request->input('field')]);` if we already had them listed in a custom FormRequest class, instead always pass all via `$request->validated()`.

- Do not be overly clever by trying to overwrite or redefine built-in framework methods such as `validated` or `all` in `FormRequest`. In this specific case use `protected prepareForValidation(): void` and `protected passedValidation(): void` or introduce new methods instead but not overwrite Laravel ones.

- When manipulating strings, try always prefer Laravel's `Str::` and `Str::of('source')->->->` methods for nice fluent chained readable string manipulations. Popular methods include `chopEnd`, `chopStart`, `beforeLast`, `trim`, `replace`, `contains`, `snake`, `slug`, `match`.

- Prefer `to_route(` as opposed to `redirect()->route(` - it returns same `RedirectResponse`.

- Do not check `$this->has` or `$request->has` or `if($this->input('field'))` for required validation fields because they have the `'required'` Laravel rule on them as part of `public function rules(): array` in `FormRequest`.

- PHP 8.5 introduced `new \Uri\Rfc3986\Uri('https://example.com')` with methods such as `getHost`, `getSchema`, `getPort`, `getQuery`, `getPath`.

- PHP 8.5 introduced pipe opeartor which accepts any callable `$result = "Hello World" |> my_function(...) |> 'strtoupper' |> str_shuffle(...) |> fn($x) => trim($x) |> function(string $x): string {return strtolower($x);} |> new MyClass() |> [MyClass::class, 'myStaticMethod'] |> new MyClass()->myInstanceMethod(...);`

- PHP 8.5 introduced `array_first` and `array_last`; `#[\Override]` attribute can now be applied to properties. With `#[\NoDiscard]` PHP will check whether the returned value is consumed and emit a warning if it is not.

Peace ✌️