Best Practices
Overview
This document outlines best practices for Laravel backend development. Following these practices improves code quality, security, performance, and maintainability.
Architecture & Design Patterns
Repository Pattern
Use repositories for complex data access logic:
php
// app/Repositories/UserRepository.php
namespace App\Repositories;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
class UserRepository
{
public function findActive(): Collection
{
return User::where('is_active', true)
->orderBy('created_at', 'desc')
->get();
}
public function findByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}
public function create(array $data): User
{
return User::create($data);
}
}Service Layer Pattern
Encapsulate business logic in service classes:
php
// app/Services/User/UserService.php
namespace App\Services\User;
use App\Models\User;
use App\Repositories\UserRepository;
use App\Events\UserRegistered;
use Illuminate\Support\Facades\Hash;
class UserService
{
public function __construct(
protected UserRepository $repository
) {}
public function register(array $data): User
{
$user = $this->repository->create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
event(new UserRegistered($user));
return $user;
}
public function updateProfile(User $user, array $data): User
{
$user->update($data);
return $user->fresh();
}
}Action Pattern
For single-purpose operations, use action classes:
php
// app/Actions/User/CreateUserAction.php
namespace App\Actions\User;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class CreateUserAction
{
public function execute(array $data): User
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
}Database Best Practices
Query Optimization
Eager Loading
php
// Bad - N+1 query problem
$users = User::all();
foreach ($users as $user) {
echo $user->posts->count(); // Queries on each iteration
}
// Good - Eager load relationships
$users = User::with('posts')->get();
foreach ($users as $user) {
echo $user->posts->count(); // No additional queries
}
// Even better - Count without loading
$users = User::withCount('posts')->get();
foreach ($users as $user) {
echo $user->posts_count; // Uses aggregated count
}Query Scopes
php
// app/Models/User.php
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeVerified($query)
{
return $query->whereNotNull('email_verified_at');
}
public function scopeWithPosts($query)
{
return $query->has('posts');
}
// Usage
$users = User::active()->verified()->withPosts()->get();Select Only Needed Columns
php
// Bad - Fetches all columns
$users = User::all();
// Good - Fetch only needed columns
$users = User::select(['id', 'name', 'email'])->get();Chunking Large Datasets
php
// Bad - Loads all users into memory
User::all()->each(function ($user) {
// Process user
});
// Good - Process in chunks
User::chunk(100, function ($users) {
foreach ($users as $user) {
// Process user
}
});
// Even better - Use lazy loading
User::lazy()->each(function ($user) {
// Process user
});Database Indexing
php
// database/migrations/2024_01_01_create_users_table.php
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique(); // Indexed automatically
$table->string('phone')->nullable();
$table->timestamps();
// Add indexes for frequently queried columns
$table->index('phone');
$table->index(['created_at', 'is_active']); // Composite index
});Use Transactions
php
use Illuminate\Support\Facades\DB;
// Wrap related operations in a transaction
DB::transaction(function () use ($data) {
$user = User::create($data['user']);
$profile = $user->profile()->create($data['profile']);
$user->roles()->attach($data['roles']);
});
// With manual control
DB::beginTransaction();
try {
$user = User::create($data);
$order = $user->orders()->create($orderData);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}Soft Deletes
php
// app/Models/Post.php
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
}
// Usage
$post->delete(); // Soft delete
$post->forceDelete(); // Permanent delete
$post->restore(); // Restore soft deleted
// Query soft deleted records
Post::withTrashed()->get();
Post::onlyTrashed()->get();Security Best Practices
Input Validation
php
// Always validate user input
public function store(StoreUserRequest $request)
{
// Validation happens automatically in FormRequest
$validated = $request->validated();
$user = User::create($validated);
return new UserResource($user);
}Mass Assignment Protection
php
// app/Models/User.php
class User extends Model
{
// Option 1: Whitelist fillable fields
protected $fillable = [
'name',
'email',
'password',
];
// Option 2: Blacklist guarded fields
protected $guarded = [
'id',
'is_admin',
'role',
];
}SQL Injection Prevention
php
// Bad - Vulnerable to SQL injection
$users = DB::select("SELECT * FROM users WHERE email = '{$email}'");
// Good - Use parameter binding
$users = DB::select('SELECT * FROM users WHERE email = ?', [$email]);
// Best - Use Eloquent
$users = User::where('email', $email)->get();XSS Prevention
php
// In Blade templates, {{ }} automatically escapes output
{{ $user->name }} // Safe
{!! $user->bio !!} // Unsafe - only use for trusted content
// Manually escape if needed
{{ e($untrustedContent) }}CSRF Protection
php
// CSRF protection is automatic for POST, PUT, PATCH, DELETE
// Ensure VerifyCsrfToken middleware is active
// Exclude routes if needed (e.g., webhooks)
// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
'api/webhooks/*',
];Password Hashing
php
use Illuminate\Support\Facades\Hash;
// Hash passwords
$user->password = Hash::make($request->password);
// Verify passwords
if (Hash::check($request->password, $user->password)) {
// Password is correct
}
// Use Bcrypt or Argon2 (configured in config/hashing.php)Rate Limiting
php
// routes/api.php
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Apply to routes
Route::middleware(['throttle:api'])->group(function () {
Route::get('/users', [UserController::class, 'index']);
});
// Custom rate limits
Route::middleware(['throttle:10,1'])->group(function () {
Route::post('/login', [AuthController::class, 'login']);
});Performance Best Practices
Caching
php
use Illuminate\Support\Facades\Cache;
// Cache data
$users = Cache::remember('users.active', 3600, function () {
return User::where('is_active', true)->get();
});
// Cache with tags (Redis/Memcached only)
Cache::tags(['users', 'active'])->put('users.list', $users, 3600);
Cache::tags(['users'])->flush(); // Clear all user caches
// Cache individual models
$user = Cache::remember("user.{$id}", 3600, function () use ($id) {
return User::find($id);
});
// Clear cache
Cache::forget('users.active');Query Caching
php
// app/Models/User.php
use Illuminate\Database\Eloquent\Builder;
public function scopeCached(Builder $query, string $key, int $ttl = 3600)
{
return Cache::remember($key, $ttl, function () use ($query) {
return $query->get();
});
}
// Usage
$users = User::active()->cached('users.active', 1800);Queue Long-Running Tasks
php
// app/Jobs/ProcessReport.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessReport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Report $report
) {}
public function handle(): void
{
// Process report...
}
}
// Dispatch job
ProcessReport::dispatch($report);
// Dispatch with delay
ProcessReport::dispatch($report)->delay(now()->addMinutes(10));
// Dispatch to specific queue
ProcessReport::dispatch($report)->onQueue('reports');Lazy Collections
php
// For large datasets, use lazy collections
User::cursor()->each(function ($user) {
// Process one user at a time without loading all into memory
});
// Or use lazy()
User::lazy()->filter(function ($user) {
return $user->is_active;
})->each(function ($user) {
// Process filtered users
});Optimize Asset Loading
php
// Use CDN for static assets
// config/app.php
'asset_url' => env('ASSET_URL', null),
// .env
ASSET_URL=https://cdn.example.comAPI Best Practices
Versioning
php
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('users', V1\UserController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('users', V2\UserController::class);
});Resource Transformers
php
// app/Http/Resources/UserResource.php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toIso8601String(),
// Conditional fields
'role' => $this->when($request->user()->isAdmin(), $this->role),
// Relationships
'posts' => PostResource::collection($this->whenLoaded('posts')),
];
}
}Pagination
php
// Always paginate list endpoints
public function index()
{
$users = User::paginate(15);
return UserResource::collection($users);
}
// Custom pagination
$users = User::paginate($request->input('per_page', 15));
// Cursor pagination for better performance
$users = User::cursorPaginate(15);API Error Responses
php
// Use consistent error format
return response()->json([
'error' => [
'code' => 'USER_NOT_FOUND',
'message' => 'The requested user was not found.',
'details' => ['user_id' => $id]
]
], 404);Testing Best Practices
Feature Tests
php
// tests/Feature/UserTest.php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_be_created()
{
$response = $this->postJson('/api/v1/users', [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
$response->assertStatus(201)
->assertJsonStructure([
'data' => ['id', 'name', 'email']
]);
$this->assertDatabaseHas('users', [
'email' => 'john@example.com'
]);
}
}Unit Tests
php
// tests/Unit/UserServiceTest.php
namespace Tests\Unit;
use App\Services\UserService;
use App\Models\User;
use Tests\TestCase;
class UserServiceTest extends TestCase
{
public function test_creates_user_with_hashed_password()
{
$service = new UserService();
$user = $service->create([
'name' => 'John',
'email' => 'john@example.com',
'password' => 'password123'
]);
$this->assertInstanceOf(User::class, $user);
$this->assertNotEquals('password123', $user->password);
}
}Database Factories
php
// database/factories/UserFactory.php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => bcrypt('password'),
'is_active' => true,
];
}
public function inactive(): self
{
return $this->state(fn (array $attributes) => [
'is_active' => false,
]);
}
}
// Usage in tests
$user = User::factory()->create();
$users = User::factory()->count(10)->create();
$inactiveUser = User::factory()->inactive()->create();Code Quality Best Practices
Use Type Hints
php
// Always use type hints
public function createUser(string $name, string $email): User
{
return User::create([
'name' => $name,
'email' => $email,
]);
}Avoid Magic Numbers
php
// Bad
if ($user->login_attempts > 5) {
// Lock account
}
// Good
const MAX_LOGIN_ATTEMPTS = 5;
if ($user->login_attempts > self::MAX_LOGIN_ATTEMPTS) {
// Lock account
}Use Enums (PHP 8.1+)
php
// app/Enums/UserRole.php
namespace App\Enums;
enum UserRole: string
{
case ADMIN = 'admin';
case USER = 'user';
case MODERATOR = 'moderator';
public function label(): string
{
return match($this) {
self::ADMIN => 'Administrator',
self::USER => 'User',
self::MODERATOR => 'Moderator',
};
}
}
// Usage
$user->role = UserRole::ADMIN;
if ($user->role === UserRole::ADMIN) {
// Admin logic
}Use Named Arguments
php
// Makes code more readable
$user = User::create(
name: 'John Doe',
email: 'john@example.com',
password: Hash::make('password'),
is_active: true
);Early Returns
php
// Bad
public function processUser(User $user)
{
if ($user->isActive()) {
if ($user->isVerified()) {
// Process user
return true;
} else {
return false;
}
} else {
return false;
}
}
// Good - Early returns reduce nesting
public function processUser(User $user): bool
{
if (!$user->isActive()) {
return false;
}
if (!$user->isVerified()) {
return false;
}
// Process user
return true;
}Event-Driven Architecture
Events and Listeners
php
// app/Events/UserRegistered.php
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
class UserRegistered
{
use Dispatchable;
public function __construct(
public User $user
) {}
}
// app/Listeners/SendWelcomeEmail.php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
Mail::to($event->user)->send(new WelcomeEmail($event->user));
}
}
// Register in EventServiceProvider
protected $listen = [
UserRegistered::class => [
SendWelcomeEmail::class,
CreateUserProfile::class,
LogUserRegistration::class,
],
];
// Dispatch event
event(new UserRegistered($user));Model Observers
php
// app/Observers/UserObserver.php
namespace App\Observers;
use App\Models\User;
class UserObserver
{
public function creating(User $user): void
{
// Before creating
$user->uuid = Str::uuid();
}
public function created(User $user): void
{
// After created
event(new UserRegistered($user));
}
public function updated(User $user): void
{
// After updated
}
public function deleted(User $user): void
{
// After deleted
}
}
// Register in AppServiceProvider
public function boot(): void
{
User::observe(UserObserver::class);
}Environment Configuration
Environment Variables
php
// .env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://api.example.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=secret
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
SENTRY_LARAVEL_DSN=your-sentry-dsnConfiguration Files
php
// config/services.php
return [
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
'aws' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
];
// Access in code
$stripeKey = config('services.stripe.key');Dependency Injection
php
// Use constructor injection
class UserController extends Controller
{
public function __construct(
protected UserService $userService,
protected UserRepository $repository
) {}
public function index()
{
return $this->userService->getAll();
}
}
// Method injection for specific methods
public function store(StoreUserRequest $request, UserService $service)
{
return $service->create($request->validated());
}Logging Best Practices
php
use Illuminate\Support\Facades\Log;
// Use appropriate log levels
Log::debug('Debug information', ['data' => $data]);
Log::info('User logged in', ['user_id' => $user->id]);
Log::warning('API rate limit approaching', ['remaining' => 10]);
Log::error('Payment failed', ['error' => $e->getMessage()]);
Log::critical('Database connection lost');
// Add context
Log::withContext([
'user_id' => auth()->id(),
'request_id' => request()->header('X-Request-ID'),
]);
// Use channels
Log::channel('slack')->critical('System is down!');Related Documentation
Quick Checklist
- [ ] Use repositories for data access
- [ ] Encapsulate business logic in services
- [ ] Eager load relationships to avoid N+1
- [ ] Add database indexes for frequently queried columns
- [ ] Use transactions for related operations
- [ ] Validate all user input
- [ ] Protect against mass assignment
- [ ] Hash passwords with bcrypt/argon2
- [ ] Implement rate limiting on sensitive endpoints
- [ ] Cache frequently accessed data
- [ ] Queue long-running tasks
- [ ] Version your APIs
- [ ] Write tests for critical functionality
- [ ] Use type hints everywhere
- [ ] Handle errors gracefully
- [ ] Log important events
- [ ] Use dependency injection
- [ ] Keep controllers thin
- [ ] Follow SOLID principles