Coding Standards
Overview
This document outlines the coding standards for the Laravel backend. Following these standards ensures consistency, maintainability, and readability across the codebase.
PHP Standards
PSR Compliance
Follow PHP-FIG (Framework Interoperability Group) standards:
- PSR-1: Basic Coding Standard
- PSR-12: Extended Coding Style Guide
- PSR-4: Autoloading Standard
PHP Version
- Minimum: PHP 8.2+
- Use modern PHP features (typed properties, constructor property promotion, enums, etc.)
Code Style Enforcement
Use Laravel Pint for automatic code formatting:
bash
# Run Pint
./vendor/bin/pint
# Check without fixing
./vendor/bin/pint --test
# Fix specific files
./vendor/bin/pint app/ModelsConfiguration (pint.json):
json
{
"preset": "laravel",
"rules": {
"array_syntax": {
"syntax": "short"
},
"binary_operator_spaces": {
"default": "single_space"
},
"blank_line_after_namespace": true,
"blank_line_after_opening_tag": true,
"concat_space": {
"spacing": "one"
},
"method_chaining_indentation": true,
"no_unused_imports": true,
"not_operator_with_successor_space": false,
"ordered_imports": {
"sort_algorithm": "alpha"
},
"trailing_comma_in_multiline": true
}
}File Structure
Namespace Declaration
php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Requests\User\StoreUserRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;Rules:
- One blank line after opening
<?phptag - Namespace declaration immediately after
- One blank line before imports
- Alphabetically ordered imports
- Group imports by vendor (Laravel first, then third-party, then app)
Class Structure Order
php
class UserController extends Controller
{
// 1. Traits
use AuthorizesRequests;
// 2. Constants
const STATUS_ACTIVE = 'active';
// 3. Properties (static first, then instance)
protected static array $cache = [];
protected UserService $userService;
// 4. Constructor
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
// 5. Static methods
public static function clearCache(): void
{
self::$cache = [];
}
// 6. Public methods (main functionality first)
public function index(): JsonResponse
{
// ...
}
// 7. Protected/private methods
protected function filterData(array $data): array
{
// ...
}
}Naming Conventions
Classes
php
// Controllers - Singular noun + Controller
UserController
PostController
OrderController
// Models - Singular noun
User
Post
Order
// Services - Singular noun + Service
UserService
PaymentService
// Resources - Singular noun + Resource
UserResource
PostResource
// Requests - Action + Noun + Request
StoreUserRequest
UpdatePostRequest
// Jobs - Verb phrase
ProcessPayment
SendWelcomeEmail
// Events - Past tense verb phrase
UserRegistered
OrderShipped
// Listeners - Verb phrase
SendWelcomeEmail
UpdateUserStatistics
// Middleware - Descriptive noun/verb
EnsureEmailIsVerified
CheckSubscriptionMethods
php
// Use camelCase
public function getUserById(int $id): User
{
return User::findOrFail($id);
}
// Action verbs for method names
public function createUser(array $data): User
public function updateUser(User $user, array $data): User
public function deleteUser(User $user): void
public function activateAccount(User $user): void
// Boolean methods - use 'is', 'has', 'can', 'should'
public function isActive(): bool
public function hasPermission(string $permission): bool
public function canDelete(): bool
public function shouldNotify(): boolVariables
php
// Use camelCase
$userId = 1;
$userEmail = 'john@example.com';
$orderTotal = 100.50;
// Descriptive names
// Bad
$d = '2024-01-01';
$arr = [];
// Good
$startDate = '2024-01-01';
$activeUsers = [];
// Collections/Arrays - plural
$users = User::all();
$posts = Post::where('status', 'published')->get();
// Single items - singular
$user = User::find(1);
$post = Post::first();Constants
php
// Use UPPER_SNAKE_CASE
const MAX_LOGIN_ATTEMPTS = 5;
const SESSION_LIFETIME = 120;
const DEFAULT_LOCALE = 'en';
// In config files
return [
'max_upload_size' => 10240,
'allowed_extensions' => ['jpg', 'png', 'pdf'],
];Type Declarations
Strict Types
Always declare strict types at the top of PHP files:
php
<?php
declare(strict_types=1);
namespace App\Services;Method Type Hints
php
// Always use type hints for parameters and return types
public function createUser(
string $name,
string $email,
?string $phone = null
): User {
// ...
}
// Void for methods that don't return
public function sendEmail(User $user): void
{
// ...
}
// Nullable types
public function findUser(int $id): ?User
{
return User::find($id);
}
// Union types (PHP 8.0+)
public function processValue(int|float $value): string
{
return (string) $value;
}
// Mixed type when necessary
public function getData(): mixed
{
return $this->data;
}Property Type Declarations
php
class User extends Model
{
// Typed properties
protected string $name;
protected ?string $email = null;
protected array $roles = [];
protected int $age;
protected bool $isActive = true;
// Constructor property promotion (PHP 8.0+)
public function __construct(
public string $firstName,
public string $lastName,
protected int $age,
) {}
}Code Organization
Single Responsibility Principle
Each class should have one reason to change:
php
// Bad - Controller doing too much
class UserController extends Controller
{
public function store(Request $request)
{
// Validation
$validated = $request->validate([...]);
// Business logic
$user = new User();
$user->name = $validated['name'];
$user->password = Hash::make($validated['password']);
$user->save();
// Send email
Mail::to($user)->send(new WelcomeEmail());
// Log activity
Log::info('User created', ['user_id' => $user->id]);
return response()->json($user);
}
}
// Good - Separated concerns
class UserController extends Controller
{
public function __construct(
protected UserService $userService
) {}
public function store(StoreUserRequest $request)
{
$user = $this->userService->create($request->validated());
return new UserResource($user);
}
}
// Service handles business logic
class UserService
{
public function create(array $data): User
{
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
event(new UserRegistered($user));
return $user;
}
}
// Event listener sends email
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
Mail::to($event->user)->send(new WelcomeEmail($event->user));
}
}DRY Principle (Don't Repeat Yourself)
php
// Bad - Repeated code
public function activateUser(User $user)
{
$user->status = 'active';
$user->activated_at = now();
$user->save();
activity()
->performedOn($user)
->causedBy(auth()->user())
->log('User activated');
}
public function deactivateUser(User $user)
{
$user->status = 'inactive';
$user->deactivated_at = now();
$user->save();
activity()
->performedOn($user)
->causedBy(auth()->user())
->log('User deactivated');
}
// Good - Extract common logic
public function activateUser(User $user): void
{
$this->updateUserStatus($user, 'active', 'activated_at');
}
public function deactivateUser(User $user): void
{
$this->updateUserStatus($user, 'inactive', 'deactivated_at');
}
protected function updateUserStatus(
User $user,
string $status,
string $timestampField
): void {
$user->update([
'status' => $status,
$timestampField => now(),
]);
activity()
->performedOn($user)
->causedBy(auth()->user())
->log("User {$status}");
}Laravel Specific Standards
Eloquent Models
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Model
{
use HasFactory, SoftDeletes;
// Table name (optional if following conventions)
protected $table = 'users';
// Primary key (optional if 'id')
protected $primaryKey = 'id';
// Mass assignable attributes
protected $fillable = [
'name',
'email',
'password',
];
// Hidden attributes
protected $hidden = [
'password',
'remember_token',
];
// Casted attributes
protected $casts = [
'email_verified_at' => 'datetime',
'is_active' => 'boolean',
'settings' => 'array',
'created_at' => 'datetime',
];
// Relationships
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
// Scopes
public function scopeActive($query)
{
return $query->where('is_active', true);
}
// Accessors (Laravel 9+)
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}",
);
}
// Mutators (Laravel 9+)
protected function password(): Attribute
{
return Attribute::make(
set: fn (string $value) => bcrypt($value),
);
}
}Controllers
php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\StoreUserRequest;
use App\Http\Requests\User\UpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use App\Services\User\UserService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class UserController extends Controller
{
public function __construct(
protected UserService $userService
) {}
public function index(): AnonymousResourceCollection
{
$users = User::paginate(15);
return UserResource::collection($users);
}
public function store(StoreUserRequest $request): JsonResponse
{
$user = $this->userService->create($request->validated());
return (new UserResource($user))
->response()
->setStatusCode(201);
}
public function show(User $user): UserResource
{
return new UserResource($user);
}
public function update(UpdateUserRequest $request, User $user): UserResource
{
$user = $this->userService->update($user, $request->validated());
return new UserResource($user);
}
public function destroy(User $user): JsonResponse
{
$this->userService->delete($user);
return response()->json(null, 204);
}
}Form Requests
php
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreUserRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', User::class);
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'role' => ['nullable', Rule::in(['user', 'admin', 'moderator'])],
];
}
public function messages(): array
{
return [
'name.required' => 'Please provide your name.',
'email.unique' => 'This email is already registered.',
];
}
public function attributes(): array
{
return [
'email' => 'email address',
];
}
}Resources
php
<?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,
'role' => $this->role,
'is_active' => $this->is_active,
'created_at' => $this->created_at?->toIso8601String(),
'updated_at' => $this->updated_at?->toIso8601String(),
// Conditional attributes
$this->mergeWhen($this->isAdmin(), [
'admin_level' => $this->admin_level,
]),
// Relationships
'posts' => PostResource::collection($this->whenLoaded('posts')),
];
}
}Comments and Documentation
PHPDoc Blocks
php
/**
* Create a new user in the system.
*
* @param array $data The user data
* @return User The created user instance
* @throws \App\Exceptions\ValidationException
*/
public function createUser(array $data): User
{
// Method implementation
}
/**
* Get all active users with their posts.
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getActiveUsersWithPosts()
{
return User::with('posts')
->where('is_active', true)
->get();
}Inline Comments
php
// Good - Explain WHY, not WHAT
// Calculate discount based on user tier instead of order total
// to comply with new pricing policy effective Q1 2024
$discount = $this->calculateTierDiscount($user);
// Bad - Explaining obvious code
// Loop through users
foreach ($users as $user) {
// Set active to true
$user->active = true;
}Complex Logic Comments
php
public function calculateShipping(Order $order): float
{
// Express shipping doubles the base rate
$rate = $order->is_express ? $this->baseRate * 2 : $this->baseRate;
/**
* Apply volume discount for bulk orders:
* - 10-50 items: 10% off
* - 51-100 items: 20% off
* - 100+ items: 30% off
*/
if ($order->item_count >= 100) {
$rate *= 0.7;
} elseif ($order->item_count >= 51) {
$rate *= 0.8;
} elseif ($order->item_count >= 10) {
$rate *= 0.9;
}
return $rate;
}Code Formatting
Indentation
- Use 4 spaces (no tabs)
- Indent method chaining
php
$users = User::where('is_active', true)
->whereHas('posts', function ($query) {
$query->where('published', true);
})
->with(['posts', 'comments'])
->orderBy('created_at', 'desc')
->paginate(15);Line Length
- Maximum 120 characters per line
- Break long method signatures across multiple lines
php
public function processPayment(
Order $order,
string $paymentMethod,
?string $promoCode = null,
bool $saveCard = false
): PaymentResult {
// Method implementation
}Blank Lines
php
class UserService
{
protected UserRepository $repository;
// One blank line between properties and methods
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
// One blank line between methods
public function createUser(array $data): User
{
$user = $this->repository->create($data);
// One blank line between logical blocks
event(new UserCreated($user));
return $user;
}
}Array Formatting
php
// Short arrays
$config = ['key' => 'value'];
// Multi-line arrays - trailing comma
$data = [
'name' => 'John Doe',
'email' => 'john@example.com',
'role' => 'admin',
];
// Nested arrays
$settings = [
'notifications' => [
'email' => true,
'sms' => false,
],
'privacy' => [
'profile_public' => true,
],
];Error Handling
Don't Suppress Errors
php
// Bad
@User::find($id);
// Good
try {
$user = User::findOrFail($id);
} catch (ModelNotFoundException $e) {
throw new UserNotFoundException($id);
}Use Specific Exceptions
php
// Bad
throw new \Exception('User not found');
// Good
throw new UserNotFoundException($userId);Static Analysis
Use PHPStan for static analysis:
bash
# Run PHPStan
./vendor/bin/phpstan analyse
# Configuration (phpstan.neon)
parameters:
level: 8
paths:
- app
excludePaths:
- testsRelated Documentation
Tools
bash
# Format code with Pint
./vendor/bin/pint
# Run PHPStan
./vendor/bin/phpstan analyse
# Run tests
php artisan test