Skip to content

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