Framework conventions and best practices specific to Laravel applications.
env() outside configuration
files
// config/payment-gateway.php - ✅ kebab-case filename
return [
'api_key' => env('PAYMENT_API_KEY'), // ✅ snake_case keys
'webhook_secret' => env('PAYMENT_WEBHOOK_SECRET'),
'timeout_seconds' => 30,
'supported_currencies' => ['USD', 'EUR', 'GBP'],
];
// ✅ Use config() in application code
class PaymentService
{
public function __construct()
{
$this->apiKey = config('payment-gateway.api_key');
$this->timeout = config('payment-gateway.timeout_seconds');
}
}
// ❌ Avoid env() outside config files
class PaymentService
{
public function __construct()
{
// This won't work properly with config caching
$this->apiKey = env('PAYMENT_API_KEY');
}
}
// config/custom-service.php - ✅ More examples
return [
'default_provider' => env('CUSTOM_SERVICE_PROVIDER', 'local'),
'rate_limiting' => [
'max_requests' => env('CUSTOM_SERVICE_MAX_REQUESTS', 100),
'window_minutes' => env('CUSTOM_SERVICE_WINDOW', 60),
],
'features' => [
'advanced_reporting' => env('ENABLE_ADVANCED_REPORTING', false),
'beta_features' => env('ENABLE_BETA_FEATURES', false),
],
]; // routes/web.php - ✅ Preferred patterns
Route::get('/user-profile', [UserController::class, 'show'])
->name('userProfile');
Route::post('/password-reset', [PasswordController::class, 'reset'])
->name('passwordReset');
Route::get('/subscription-billing', [BillingController::class, 'index'])
->name('subscriptionBilling');
// ✅ Resource routes with kebab-case
Route::resource('blog-posts', BlogPostController::class);
// Results in: /blog-posts, /blog-posts/create, /blog-posts/{blog-post}, etc.
Route::resource('user-subscriptions', UserSubscriptionController::class);
// routes/api.php - ✅ API routes with HTTP verb first
Route::prefix('v1')->group(function () {
Route::get('/users/{user}', [ApiController::class, 'getUser']);
Route::post('/users', [ApiController::class, 'createUser']);
Route::put('/users/{user}', [ApiController::class, 'updateUser']);
Route::delete('/users/{user}', [ApiController::class, 'deleteUser']);
Route::get('/users/{user}/subscriptions', [ApiController::class, 'getUserSubscriptions']);
Route::post('/users/{user}/subscriptions', [ApiController::class, 'createSubscription']);
});
// ✅ Route model binding with kebab-case parameters
Route::get('/blog-posts/{blogPost}/comments/{comment}', [CommentController::class, 'show']);
// ❌ Avoid snake_case or camelCase in URLs
Route::get('/user_profile', [UserController::class, 'show']); // Wrong
Route::get('/userProfile', [UserController::class, 'show']); // Wrong
// ❌ Avoid closure routes in production
Route::get('/users', function () {
return User::all(); // Hard to test and maintain
}); // ✅ Preferred - plural resource names, standard CRUD
class UsersController extends Controller
{
public function index(): View
{
return view('users.index', [
'users' => User::paginate(20)
]);
}
public function create(): View
{
return view('users.create');
}
public function store(StoreUserRequest $request): RedirectResponse
{
$user = User::create($request->validated());
return redirect()
->route('users.show', $user)
->with('success', 'User created successfully.');
}
public function show(User $user): View
{
return view('users.show', compact('user'));
}
public function edit(User $user): View
{
return view('users.edit', compact('user'));
}
public function update(UpdateUserRequest $request, User $user): RedirectResponse
{
$user->update($request->validated());
return redirect()
->route('users.show', $user)
->with('success', 'User updated successfully.');
}
public function destroy(User $user): RedirectResponse
{
$user->delete();
return redirect()
->route('users.index')
->with('success', 'User deleted successfully.');
}
}
// ✅ Extract additional actions to separate controllers
class UserPasswordController extends Controller
{
public function edit(User $user): View
{
return view('users.passwords.edit', compact('user'));
}
public function update(UpdatePasswordRequest $request, User $user): RedirectResponse
{
$user->update([
'password' => Hash::make($request->password)
]);
return redirect()
->route('users.show', $user)
->with('success', 'Password updated successfully.');
}
}
class UserAvatarController extends Controller
{
public function store(StoreAvatarRequest $request, User $user): RedirectResponse
{
$path = $request->file('avatar')->store('avatars', 'public');
$user->update(['avatar_path' => $path]);
return back()->with('success', 'Avatar updated successfully.');
}
public function destroy(User $user): RedirectResponse
{
if ($user->avatar_path) {
Storage::disk('public')->delete($user->avatar_path);
$user->update(['avatar_path' => null]);
}
return back()->with('success', 'Avatar removed successfully.');
}
}
// ❌ Avoid - mixing additional actions in main controller
class UsersController extends Controller
{
// ... CRUD methods ...
public function updatePassword(User $user) { } // Should be separate
public function uploadAvatar(User $user) { } // Should be separate
public function sendWelcomeEmail(User $user) { } // Should be separate
public function exportToCsv() { } // Should be separate
} // ✅ Preferred - array notation in Form Requests
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'min:8', 'confirmed'],
'date_of_birth' => ['nullable', 'date', 'before:today'],
'phone' => ['nullable', 'string', 'regex:/^\+?[1-9]\d{1,14}$/'],
];
}
public function messages(): array
{
return [
'email.unique' => 'This email address is already registered.',
'phone.regex' => 'Please enter a valid phone number.',
];
}
}
// ✅ Custom validation rule - snake_case filename and class
// app/Rules/ValidDomainName.php
class ValidDomainName implements Rule
{
public function passes($attribute, $value): bool
{
return preg_match('/^[a-z0-9\-\.]+\.[a-z]{2,}$/i', $value);
}
public function message(): string
{
return 'The :attribute must be a valid domain name.';
}
}
// Usage in Form Request
class StoreWebsiteRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'url' => ['required', 'url'],
'domain' => ['required', new ValidDomainName],
];
}
}
// ✅ Simple validation in controller (when Form Request is overkill)
class SimpleController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
]);
// Process validated data...
}
}
// ❌ Avoid - pipe notation (less readable and flexible)
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
];
}
// ❌ Avoid - inline validation for complex rules
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $request->id,
'password' => 'required|min:8|confirmed',
'role' => 'required|in:admin,user,moderator',
'permissions.*' => 'exists:permissions,id',
'profile.bio' => 'nullable|string|max:1000',
'profile.website' => 'nullable|url',
]); // Too complex for inline validation
} // ✅ Preferred - UserPolicy with camelCase methods
class UserPolicy
{
public function viewAny(User $user): bool
{
return $user->hasRole('admin') || $user->hasRole('moderator');
}
public function view(User $user, User $model): bool
{
return $user->id === $model->id
|| $user->hasRole('admin')
|| ($user->hasRole('moderator') && !$model->hasRole('admin'));
}
public function create(User $user): bool
{
return $user->hasRole('admin');
}
public function update(User $user, User $model): bool
{
return $user->id === $model->id || $user->hasRole('admin');
}
public function delete(User $user, User $model): bool
{
return $user->hasRole('admin') && $user->id !== $model->id;
}
public function restore(User $user, User $model): bool
{
return $user->hasRole('admin');
}
public function forceDelete(User $user, User $model): bool
{
return $user->hasRole('admin') && $user->id !== $model->id;
}
}
// ✅ Usage in controllers
class UsersController extends Controller
{
public function index(): View
{
$this->authorize('viewAny', User::class);
return view('users.index', [
'users' => User::paginate(20)
]);
}
public function show(User $user): View
{
$this->authorize('view', $user);
return view('users.show', compact('user'));
}
public function edit(User $user): View
{
$this->authorize('update', $user);
return view('users.edit', compact('user'));
}
}
// ✅ Usage in Blade templates
@can('update', $user)
<a href="{{ route('users.edit', $user) }}" class="btn btn-primary">
Edit User
</a>
@endcan
@can('delete', $user)
<form method="POST" action="{{ route('users.destroy', $user) }}">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Delete</button>
</form>
@endcan
// ❌ Avoid - non-standard ability names
class UserPolicy
{
public function showUser(User $user, User $model): bool { } // Use 'view'
public function editUser(User $user, User $model): bool { } // Use 'update'
public function removeUser(User $user, User $model): bool { } // Use 'delete'
}