Laravel
Project context
This is a Laravel 11 project using Livewire 3 for interactive UI and Pest for tests. We follow Laravel conventions; resist the urge to import patterns from other ecosystems that don't fit.
Stack
- PHP 8.3+
- Laravel 11+
- Livewire 3 + Alpine.js for UI interactivity
- Tailwind CSS 4
- Pest for tests (not PHPUnit syntax — though Pest runs on PHPUnit)
- Eloquent for ORM
- Laravel Pint for code style
- Composer + npm/bun
Folder structure (Laravel defaults)
app/
Http/
Controllers/
Middleware/
Requests/ — FormRequest classes
Livewire/ — Livewire components
Models/
Policies/
Providers/
Services/ — application services
Jobs/
Events/
Listeners/
Notifications/
config/
database/
migrations/
factories/
seeders/
resources/
views/ — Blade templates
js/
css/
routes/
web.php
api.php
tests/
Feature/
Unit/
Models (Eloquent)
- One model per table; pluralized table name auto-derived
- Always set
$fillable(mass-assignment whitelist) — never$guarded = [] - Cast attributes:
protected $casts = ['published_at' => 'datetime', 'metadata' => 'array'] - Define relationships explicitly:
belongsTo,hasMany,belongsToMany - Scopes:
public function scopePublished($query) { $query->whereNotNull('published_at'); }
class Post extends Model
{
protected $fillable = ['title', 'body', 'author_id', 'published_at'];
protected $casts = ['published_at' => 'datetime'];
public function author() { return $this->belongsTo(User::class); }
public function comments() { return $this->hasMany(Comment::class); }
public function scopePublished($q) { $q->whereNotNull('published_at'); }
}
Controllers
- Resource controllers when you have full CRUD:
php artisan make:controller PostController --resource - Single-action controllers (
__invoke) for one-off endpoints - Use FormRequest classes for validation, never validate in the controller method body
- Return Eloquent resources or Inertia/Blade views — never bare arrays for API responses
class StorePostRequest extends FormRequest
{
public function authorize(): bool { return $this->user()->can('create', Post::class); }
public function rules(): array {
return [
'title' => ['required', 'string', 'max:200'],
'body' => ['required', 'string'],
];
}
}
Migrations
php artisan make:migration create_posts_table- Always set columns nullable / not nullable explicitly — be deliberate
- Add foreign keys with
->constrained()->cascadeOnDelete()where appropriate - Add indexes for any column used in
where() - Never edit a committed migration; create a new one
Livewire components
- One component per discrete piece of UI
- Mount data in
mount(); render viarender()method that returns a view - Use
#[Computed]attribute for derived properties - Use
#[Validate]attribute for property validation - Emit events with
$this->dispatch('event-name', payload)
class PostList extends Component
{
#[Computed]
public function posts() { return Post::published()->latest()->take(20)->get(); }
public function render() { return view('livewire.post-list'); }
}
Background jobs
php artisan make:job ProcessPost- Implement
ShouldQueueto dispatch async - Use
tries,backoff,timeoutproperties for retry behavior - Idempotent — jobs may run twice; design for it
Authorization
- Use Policies (
php artisan make:policy PostPolicy --model=Post) - Register in
AuthServiceProvider - Call via
$this->authorize('update', $post)in controllers,@can('update', $post)in Blade
Patterns to follow
- Eager-load to avoid N+1:
Post::with('author', 'comments')->get() - Use Pint for formatting; don't hand-format
- Use
Str::helpers (Str::slug,Str::random) over manual string manipulation - Use Carbon for dates — Laravel sets it up by default
- Use Mailables / Notifications for emails — never
mail()directly
Patterns to avoid
$guarded = []— always whitelist with$fillable- Querying inside Blade templates — pass eager-loaded data from the controller
- Hardcoded values that belong in
config/—config('services.foo.key') - Editing a committed migration — create a new one
DB::rawwithout parameter binding when params are user input — SQL injection- Validating in controllers — use FormRequest
Testing
- Pest, not PHPUnit class syntax:
it('does X', function () { ... }); - Feature tests hit the routes and assert responses
- Unit tests for service logic in isolation
- Use
RefreshDatabasetrait to reset between tests - Use factories liberally:
Post::factory()->count(5)->create()
it('lists posts', function () {
Post::factory()->count(3)->create();
$response = $this->get('/posts');
$response->assertOk();
$response->assertSeeText('Post 1');
});
Tooling
php artisan serve— dev server (orphp artisan octane:startfor prod-style)php artisan migrate— apply migrationsphp artisan tinker— REPLphp artisan test(orvendor/bin/pest) — testsvendor/bin/pint— formatternpm run dev(Vite) — frontend assets
AI behavioral rules
- Always whitelist with
$fillable, never$guarded = [] - Validate via FormRequest classes — never inline
$request->validate(...)in non-trivial controllers - Use eager loading (
with(...)) wherever a foreign key gets touched in a list view - Default to Pest syntax for new tests; only PHPUnit class style if the existing suite uses it
- Use Laravel's helpers (
Str::,Arr::,Carbon::) before reaching for raw PHP - Never modify a committed migration
- Don't bypass mass-assignment protection or skip validations without justifying why
- Run
pestandpintbefore declaring a task done