| name | laravel |
| description | [Applies to: **/*.php] Definitive guide for writing clean, performant, and secure Laravel applications, emphasizing modern best practices and common pitfalls. |
| source | cursor_mdc |
Laravel Best Practices (2025)
This guide outlines the definitive best practices for developing robust, scalable, and maintainable Laravel applications. Adhere to these principles to ensure your codebase is clean, performant, and secure, aligning with PSR-12, PHP The Right Way, and Laravel's conventions.
1. Code Organization & Structure
1.1 Thin Controllers, Fat Models/Services
Controllers orchestrate requests; they do not contain business logic. Delegate complex operations to dedicated Service classes or Model methods.
❌ BAD: Business logic in Controller
class OrderController extends Controller
{
public function store(Request $request)
{
$order = new Order();
$order->user_id = Auth::id();
$order->total = $request->input('price') * 1.20;
$order->status = 'pending';
$order->save();
}
}
✅ GOOD: Delegate to a Service
class OrderService
{
public function createOrder(array $data): Order
{
$order = new Order();
$order->user_id = $data['user_id'];
$order->total = $data['price'] * 1.20;
$order->status = 'pending';
$order->save();
return $order;
}
}
use App\Services\OrderService;
class OrderController extends Controller
{
public function __construct(private OrderService $orderService) {}
public function store(Request $request)
{
$order = $this->orderService->([
=> ::(),
=> ->(),
]);
()->(, );
}
}
1.2 Centralized Validation with Form Requests
Always use Form Request classes for validation. This keeps controllers clean and validation logic reusable.
❌ BAD: Validation in Controller
class PostController extends Controller
{
public function store(Request $request)
{
$request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
]);
}
}
✅ GOOD: Dedicated Form Request
class StorePostRequest extends FormRequest
{
public function authorize(): bool { return true; }
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
];
}
}
use App\Http\Requests\StorePostRequest;
class PostController extends Controller
{
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return response()->(, );
}
}
2. Common Patterns & Anti-patterns
2.1 Eloquent Over Raw Queries (and Eager Loading)
Prefer Eloquent ORM for database interactions. Always eager load relationships to prevent N+1 query problems.
❌ BAD: Raw queries & N+1 problem
$users = DB::select('SELECT * FROM users');
foreach ($users as $user) {
echo $user->profile->bio;
}
✅ GOOD: Eloquent with eager loading
$users = User::with('profile')->get();
@foreach ($users as $user)
{{ $user->profile->bio }}
@endforeach
2.2 Collections Over Arrays
Leverage Laravel's powerful Collection class for data manipulation.
❌ BAD: Manual array manipulation
$users = User::all()->toArray();
$activeUsers = [];
foreach ($users as $user) {
if ($user['is_active']) {
$activeUsers[] = $user;
}
}
✅ GOOD: Use Laravel Collections
$activeUsers = User::all()->filter(fn ($user) => $user->is_active);
$activeUserNames = User::all()
->filter(fn ($user) => $user->is_active)
->map(fn ($user) => $user->name);
2.3 Mass Assignment Protection
Always define $fillable or $guarded properties on your Eloquent models to prevent mass assignment vulnerabilities.
❌ BAD: No mass assignment protection
class User extends Model {}
User::create($request->all());
✅ GOOD: Define $fillable
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
}
User::create($request->validated());
2.4 No Database Queries in Blade Templates
Blade templates are for presentation. Pass all necessary data from the controller.
❌ BAD: Querying in Blade
<!-- resources/views/product.blade.php -->
<div>
<h1>{{ $product->name }}</h1>
@foreach (App\Models\Review::where('product_id', $product->id)->get() as $review)
<p>{{ $review->comment }}</p>
@endforeach
</div>
✅ GOOD: Pass pre-loaded data
class ProductController extends Controller
{
public function show(Product $product)
{
$product->load('reviews');
return view('product', compact('product'));
}
}
3. Performance Considerations
3.1 Chunking Large Datasets
When processing large numbers of records, use chunk() or chunkById() to avoid memory exhaustion.
❌ BAD: Loading all records into memory
User::where('active', true)->get()->each(function (User $user) {
});
✅ GOOD: Process in chunks
User::where('active', true)->chunk(1000, function (Collection $users) {
foreach ($users as $user) {
}
});
3.2 Caching Expensive Operations
Cache results of expensive queries or computations using Laravel's cache facade.
❌ BAD: Repeated expensive queries
$posts = Post::with('author')->get();
✅ GOOD: Cache with a TTL
$posts = Cache::remember('all_posts_with_authors', 60 * 60, function () {
return Post::with('author')->get();
});
3.3 Queue Background Jobs
Offload long-running tasks (e.g., sending emails, image processing, API calls) to queues.
❌ BAD: Blocking user requests
Mail::to($user->email)->send(new WelcomeEmail($user));
✅ GOOD: Dispatch to queue
Mail::to($user->email)->send(new WelcomeEmail($user))->onQueue('emails');
4. Security Best Practices
4.1 Enforce HTTPS + HSTS
Always deploy with HTTPS and enable HSTS (HTTP Strict Transport Security) to prevent downgrade attacks. Configure this at your web server (Nginx/Apache) or load balancer.
4.2 Authentication & Authorization
- Authentication: Use Sanctum for first-party SPAs and mobile apps. Use Passport if you need full OAuth2 support for third-party applications.
- Authorization: Implement Policies for model-specific authorization (e.g., "Can this user update this post?"). Use Gates for more general permissions (e.g., "Can this user access the admin dashboard?").
❌ BAD: Manual authorization checks
if (Auth::user()->role !== 'admin' || Auth::user()->id !== $post->user_id) {
abort(403);
}
✅ GOOD: Use Policies
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
class PostController extends Controller
{
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
}
}
4.3 Rate Limiting
Apply rate limiting to API endpoints and sensitive routes to prevent abuse and brute-force attacks.
❌ BAD: No rate limiting on API
Route::post('/api/register', [AuthController::class, 'register']);
✅ GOOD: Apply throttle middleware
Route::middleware('throttle:60,1')->group(function () {
Route::post('/api/register', [AuthController::class, 'register']);
});
4.4 Environment Configuration
Never leave APP_DEBUG=true in production. Store sensitive credentials in .env and access them via config().
❌ BAD: Direct env() access & debug in prod
$apiKey = env('STRIPE_KEY');
✅ GOOD: Use config() and secure .env
'stripe' => [
'key' => env('STRIPE_KEY'),
],
$apiKey = config('services.stripe.key');
5. API Design
5.1 URL-Based Versioning
Implement URL-based versioning for your APIs (e.g., /api/v1/users). This is explicit, cache-friendly, and easy for developers to understand.
❌ BAD: No versioning or header-based
Route::get('/api/users', [UserController::class, 'index']);
✅ GOOD: URL-based versioning
Route::prefix('v1')->group(function () {
Route::apiResource('users', UserController::class);
});
6. Testing Approaches
6.1 Prioritize Feature Tests
Write feature tests using PHPUnit or Pest to cover critical user flows and API interactions. This ensures your application behaves as expected from an end-to-end perspective.
class UserRegistrationTest extends TestCase
{
use RefreshDatabase;
public function a_user_can_register()
{
$response = $this->postJson('/api/v1/register', [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password',
'password_confirmation' => 'password',
]);
$response->assertStatus(201)
->assertJson(['message' => 'Registration successful']);
$this->assertDatabaseHas('users', ['email' => 'john@example.com']);
}
}