Skip to content

Last updated:

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/Models

Configuration (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 <?php tag
  • 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
CheckSubscription

Methods

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(): bool

Variables

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:
        - tests


Tools

bash
# Format code with Pint
./vendor/bin/pint

# Run PHPStan
./vendor/bin/phpstan analyse

# Run tests
php artisan test

CPR - Clinical Patient Records