Skip to content

💎 PHP Standards

Professional PHP coding standards for Laravel applications. Following these guidelines ensures consistent, maintainable, and high-quality code across your entire team.

📋 PSR-12 Compliance

All PHP code must follow PSR-12 coding standards. This is non-negotiable and enforced through automated tools.

Core Formatting Rules

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\User;
use App\Repositories\UserRepositoryInterface;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;

class UserService
{
    public function __construct(
        private UserRepositoryInterface $userRepository
    ) {}

    public function createUser(array $userData): User
    {
        $userData['password'] = Hash::make($userData['password']);
        
        $user = $this->userRepository->create($userData);

        Log::info('User created successfully', [
            'user_id' => $user->id,
            'email' => $user->email,
        ]);

        return $user;
    }

    public function updateUser(int $id, array $userData): User
    {
        $user = $this->userRepository->find($id);

        if (isset($userData['password'])) {
            $userData['password'] = Hash::make($userData['password']);
        }

        $this->userRepository->update($id, $userData);

        return $user->refresh();
    }
}

❌ Bad Example

php
<?php
// Missing strict types declaration

namespace App\Services;

use App\Models\User;
// Missing other necessary imports

class UserService {
    // Missing visibility modifier
    function createUser($userData) { // Missing type hints
        $user = User::create([
            'name' => $userData['name'],
            'email' => $userData['email'],
            'password' => bcrypt($userData['password']), // Direct model access in service
        ]);
        
        // Missing logging
        return $user;
    }
}

🏷️ Naming Conventions

Consistent naming is crucial for code readability and maintainability.

Classes

TypeConventionExample
ControllersPascalCase + Controller suffixUserController, PaymentController
ModelsPascalCase, SingularUser, Product, OrderItem
ServicesPascalCase + Service suffixUserService, PaymentService
RepositoriesPascalCase + Repository suffixUserRepository, ProductRepository
ActionsPascalCase, Verb-NounCreateUser, ProcessPayment
JobsPascalCase, Verb-NounSendEmailNotification, ProcessOrder
EventsPascalCase, Past tenseUserCreated, OrderShipped
ListenersPascalCase, Verb-NounSendWelcomeEmail, UpdateInventory
TraitsPascalCase, AdjectiveSearchable, Sortable
InterfacesPascalCase + Interface suffixUserRepositoryInterface

Methods and Functions

✅ Good Example

php
<?php

class UserService
{
    // Action methods - use verbs
    public function createUser(array $data): User
    public function updateUser(int $id, array $data): User
    public function deleteUser(int $id): bool
    
    // Query methods - use get/find/search
    public function getUserById(int $id): ?User
    public function findUserByEmail(string $email): ?User
    public function searchUsers(string $query): Collection
    
    // Boolean methods - use is/has/can
    public function isUserActive(User $user): bool
    public function hasPermission(User $user, string $permission): bool
    public function canAccessResource(User $user, Resource $resource): bool
    
    // Calculation methods - use calculate/compute
    public function calculateTotalRevenue(Collection $orders): int
    public function computeDiscountAmount(Order $order): int
}

❌ Bad Example

php
<?php

class UserService
{
    // Inconsistent naming
    public function user_create(array $data): User  // snake_case
    public function UpdateUser(int $id, array $data): User  // PascalCase
    public function delete(int $id): bool  // Too generic
    
    // Unclear method names
    public function get(int $id): ?User  // What are we getting?
    public function check(User $user, string $permission): bool  // Check what?
    public function do(Collection $orders): int  // Do what?
}

Variables and Properties

✅ Good Example

php
<?php

class OrderController
{
    // Properties - descriptive camelCase
    private OrderService $orderService;
    private PaymentGateway $paymentGateway;
    private int $maxRetryAttempts = 3;
    
    public function store(Request $request): JsonResponse
    {
        // Local variables - descriptive camelCase
        $validatedData = $request->validated();
        $totalAmount = $this->calculateTotal($validatedData);
        $processingFee = $this->calculateProcessingFee($totalAmount);
        $finalAmount = $totalAmount + $processingFee;
        
        return response()->json([
            'total' => $finalAmount,
        ]);
    }
}

❌ Bad Example

php
<?php

class OrderController
{
    // Poor variable naming
    private $os;  // What is this?
    private $pg;  // Unclear abbreviation
    private $max = 3;  // Max what?
    
    public function store(Request $request): JsonResponse
    {
        $d = $request->validated();  // Single letter
        $t = $this->calculateTotal($d);  // Unclear
        $f = $this->calculateProcessingFee($t);  // Unclear
        $x = $t + $f;  // What is x?
        
        return response()->json(['total' => $x]);
    }
}

Constants

✅ Good Example

php
<?php

class PaymentService
{
    public const MAX_RETRY_ATTEMPTS = 3;
    public const DEFAULT_CURRENCY = 'USD';
    public const PAYMENT_STATUS_PENDING = 'pending';
    public const PAYMENT_STATUS_COMPLETED = 'completed';
    public const PAYMENT_STATUS_FAILED = 'failed';
    
    private const API_TIMEOUT_SECONDS = 30;
    private const MAX_TRANSACTION_AMOUNT = 1000000; // cents
}

❌ Bad Example

php
<?php

class PaymentService
{
    public const maxRetries = 3;  // Should be UPPER_SNAKE_CASE
    public const Default_Currency = 'USD';  // Inconsistent
    private const timeout = 30;  // Missing descriptive name
}

🎯 Type Declarations

Always use strict type declarations for all method parameters, return types, and property types.

Method Type Declarations

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\User;
use App\Models\Order;
use Illuminate\Support\Collection;

class OrderService
{
    public function createOrder(
        User $user,
        array $items,
        ?string $couponCode = null
    ): Order {
        // Implementation
    }

    public function calculateTotal(Order $order): int
    {
        return $order->items->sum('price');
    }

    public function getUserOrders(User $user, int $limit = 10): Collection
    {
        return $user->orders()->limit($limit)->get();
    }

    public function isOrderValid(Order $order): bool
    {
        return $order->items->isNotEmpty() && $order->total > 0;
    }
}

❌ Bad Example

php
<?php

// Missing strict types declaration

namespace App\Services;

class OrderService
{
    // Missing parameter types
    public function createOrder($user, $items, $couponCode = null)
    {
        // Implementation
    }

    // Missing return type
    public function calculateTotal($order)
    {
        return $order->items->sum('price');
    }

    // Inconsistent types
    public function getUserOrders($user, $limit = 10): Collection
    {
        return $user->orders()->limit($limit)->get();
    }
}

Property Type Declarations

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Repositories\UserRepositoryInterface;
use Illuminate\Support\Facades\Cache;

class UserService
{
    // Constructor property promotion with types
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private EmailService $emailService,
        private int $cacheTimeout = 3600
    ) {}

    // Typed properties
    private string $defaultRole = 'user';
    private bool $autoVerifyEmail = false;
    private array $allowedRoles = ['user', 'admin', 'moderator'];
}

❌ Bad Example

php
<?php

namespace App\Services;

class UserService
{
    // Missing types
    private $userRepository;
    private $emailService;
    private $cacheTimeout = 3600;
    
    // No constructor type hints
    public function __construct($userRepository, $emailService)
    {
        $this->userRepository = $userRepository;
        $this->emailService = $emailService;
    }
}

🎯 Named Arguments (PHP 8.0+)

Named arguments allow you to pass arguments to a function based on the parameter name, rather than parameter position. This is a powerful feature introduced in PHP 8.0 that significantly improves code readability and maintainability.

Why Use Named Arguments?

  1. Self-Documenting Code - Makes function calls immediately understandable
  2. Skip Optional Parameters - No need for null placeholders
  3. Order Independence - Arguments can be in any order
  4. Better IDE Support - Enhanced autocomplete and inline documentation
  5. Easier Refactoring - Less prone to errors when parameter order changes
  6. Code Review Friendly - Reviewers understand intentions without context

When to Use Named Arguments

Always use named arguments when:

  • Calling functions with 3+ parameters
  • Using optional parameters (skip defaults)
  • Working with boolean flags
  • Parameter names add clarity
  • Order might be confusing

Optional for:

  • Single parameter functions
  • Very obvious 2-parameter functions (e.g., max($a, $b))
  • When positional is clearer (rare cases)

Basic Usage

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\User;
use Illuminate\Support\Facades\Hash;

class UserService
{
    public function createUser(
        string $name,
        string $email,
        string $password,
        bool $isActive = true,
        ?string $role = null
    ): User {
        return User::create([
            'name' => $name,
            'email' => $email,
            'password' => Hash::make(value: $password),
            'is_active' => $isActive,
            'role' => $role ?? 'user',
        ]);
    }
}

// Usage with named arguments
$user = $userService->createUser(
    name: 'John Doe',
    email: '[email protected]',
    password: 'SecurePassword123!',
    isActive: true,
    role: 'admin'
);

// Skip optional parameters
$user = $userService->createUser(
    name: 'Jane Smith',
    email: '[email protected]',
    password: 'SecurePassword123!'
    // isActive defaults to true
    // role defaults to null
);

// Order-independent
$user = $userService->createUser(
    password: 'SecurePassword123!',
    name: 'Bob Johnson',
    email: '[email protected]'
);

❌ Bad Example

php
<?php

// Hard to understand what each parameter means
$user = $userService->createUser(
    'John Doe',
    '[email protected]',
    'SecurePassword123!',
    true,
    'admin'
);

// Need to pass null for optional parameters
$user = $userService->createUser(
    'Jane Smith',
    '[email protected]',
    'SecurePassword123!',
    true,
    null  // Have to pass null even though we want the default
);

Real-World Examples

Response Macros

php
// ✅ Self-documenting API responses
return response()->success(
    data: new UserResource($user),
    message: 'User created successfully',
    status: 201,
    meta: ['timestamp' => now()]
);

// ❌ Less clear
return response()->success(
    new UserResource($user),
    'User created successfully',
    201,
    ['timestamp' => now()]
);

Validation Rules

php
// ✅ Clear validation rules
$request->validate([
    'email' => [
        'required',
        'email',
        'unique:users,email',
        Rule::unique(table: 'users', column: 'email')->ignore(id: $userId),
    ],
    'password' => [
        'required',
        Password::min(length: 12)
            ->mixedCase()
            ->numbers()
            ->symbols()
            ->uncompromised(threshold: 3),
    ],
]);

Database Queries

php
// ✅ Clear query building
User::where(column: 'status', operator: '=', value: 'active')
    ->orderBy(column: 'created_at', direction: 'desc')
    ->take(value: 10)
    ->get();

// ✅ Cache operations
Cache::remember(
    key: 'users_active',
    ttl: 3600,
    callback: fn () => User::where('is_active', true)->get()
);

Model Creation

php
// ✅ Clear model attributes
User::create([
    'name' => $data['name'],
    'email' => $data['email'],
    'password' => Hash::make(value: $data['password']),
    'email_verified_at' => now(),
]);

// ✅ Model methods
$user->update(attributes: $validatedData);
$user->fill(attributes: $validatedData);

Service Calls

php
// ✅ Clear service method calls
$report = $reportService->generate(
    startDate: now()->subDays(days: 30),
    endDate: now(),
    format: 'pdf',
    includeCharts: true,
    recipientEmail: $user->email
);

// ✅ Notification dispatch
$user->notify(
    instance: new InvoicePaid(invoice: $invoice)
);

Event Dispatching

php
// ✅ Clear event dispatching
event(new UserRegistered(
    user: $user,
    ipAddress: $request->ip(),
    userAgent: $request->userAgent()
));

// ✅ Job dispatching
ProcessPodcast::dispatch(
    podcast: $podcast,
    queue: 'processing'
)->delay(delay: now()->addMinutes(minutes: 5));

Mixing Positional and Named Arguments

You can mix positional and named arguments, but positional arguments must come first:

php
// ✅ Good: Positional first, then named
$result = someFunction($required1, $required2, optional: $value);

// ❌ Bad: Cannot use positional after named
$result = someFunction(optional: $value, $required1, $required2);

Array Spreading with Named Arguments

php
<?php

$defaults = [
    'theme' => 'dark',
    'language' => 'en',
    'notifications' => true,
];

// ✅ Spread operator with named arguments
$settings = configureSettings(
    userId: $user->id,
    ...$defaults,
    theme: 'light'  // Override default
);

Best Practices for Named Arguments

1. Use for Clarity

php
// ✅ Good: Clear boolean flags
$notification->send(
    immediately: true,
    retryOnFailure: false
);

// ❌ Bad: What do true/false mean here?
$notification->send(true, false);

2. Skip Unnecessary Parameters

php
// ✅ Good: Skip middle optional parameters
createUser(
    name: 'John',
    email: '[email protected]',
    role: 'admin'
    // Skip optional $isActive, $phone, etc.
);

3. Complex Function Calls

php
// ✅ Good: Named arguments for complex calls
$report = generateReport(
    type: 'financial',
    startDate: '2024-01-01',
    endDate: '2024-12-31',
    includeGraphs: true,
    groupBy: 'month',
    currency: 'USD',
    format: 'pdf'
);

4. Constructor Calls

php
// ✅ Good: Clear constructor arguments
$service = new PaymentService(
    gateway: $stripeGateway,
    logger: $logger,
    cache: $cache,
    retryAttempts: 3,
    timeout: 30
);

Common Pitfalls

❌ Don't Overuse

php
// ❌ Bad: Overkill for simple calls
$max = max(num1: 5, num2: 10);

// ✅ Good: Use positional for obvious cases
$max = max(5, 10);

❌ Avoid Renaming Parameters

When you use named arguments, parameter names become part of your public API. Renaming them is a breaking change:

php
// Before
public function sendEmail(string $recipient, string $subject) {}

// After (BREAKING CHANGE if callers use named arguments)
public function sendEmail(string $to, string $title) {}

// Callers using named arguments will break:
sendEmail(recipient: '[email protected]', subject: 'Hello');  // ❌ Breaks!

IDE Configuration

Most modern IDEs support named arguments with autocomplete:

VS Code:

  • PHP Intelephense extension automatically supports named arguments
  • Shows parameter names in autocomplete suggestions

PHPStorm:

  • Built-in support for named arguments
  • Refactoring tools understand named arguments
  • Can convert between positional and named arguments

Migration Strategy

When updating existing code:

  1. Start with new code - Use named arguments in all new functions
  2. Update during refactoring - Convert to named arguments when touching old code
  3. Focus on clarity - Convert where it improves readability most
  4. Team consistency - Ensure team agreement on when to use them

Summary

Named arguments are a powerful PHP 8.0+ feature that makes your code more readable, maintainable, and less error-prone. Use them liberally, especially in Laravel applications where method calls often have many parameters.

php
// ✅ Adopt this pattern throughout your codebase
return response()->success(
    data: new UserResource($user),
    message: 'Operation successful',
    status: 200
);

$user = User::create([
    'name' => $data['name'],
    'email' => strtolower(string: $data['email']),
    'password' => Hash::make(value: $data['password']),
]);

Cache::remember(
    key: "user_{$userId}",
    ttl: 3600,
    callback: fn () => $this->userRepository->find(id: $userId)
);

📝 Documentation and Comments

DocBlocks

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\User;
use App\Models\Order;

/**
 * Service for managing user orders and order processing.
 * 
 * This service handles order creation, validation, processing,
 * and all business logic related to orders.
 */
class OrderService
{
    /**
     * Create a new order for the specified user.
     *
     * @param  User  $user  The user placing the order
     * @param  array  $items  Array of order items with quantities
     * @param  string|null  $couponCode  Optional discount coupon code
     * @return Order  The created order instance
     * 
     * @throws \InvalidArgumentException When items array is empty
     * @throws \App\Exceptions\InvalidCouponException When coupon is invalid
     */
    public function createOrder(User $user, array $items, ?string $couponCode = null): Order
    {
        // Implementation
    }
}

Inline Comments

✅ Good Example

php
<?php

public function processPayment(Order $order): PaymentResult
{
    // Validate order before processing payment
    if (!$this->isOrderValid($order)) {
        throw new InvalidOrderException('Order validation failed');
    }

    // Calculate total including taxes and fees
    $totalAmount = $this->calculateTotal($order);
    
    // Apply discount if coupon is present
    if ($order->coupon_code) {
        $totalAmount = $this->applyDiscount($totalAmount, $order->coupon_code);
    }

    // Process payment through gateway
    try {
        $result = $this->paymentGateway->charge($totalAmount, [
            'order_id' => $order->id,
            'customer_id' => $order->user_id,
        ]);
    } catch (PaymentException $e) {
        // Log payment failure for monitoring
        Log::error('Payment processing failed', [
            'order_id' => $order->id,
            'error' => $e->getMessage(),
        ]);
        throw $e;
    }

    return $result;
}

❌ Bad Example

php
<?php

public function processPayment(Order $order): PaymentResult
{
    // do validation
    if (!$this->isOrderValid($order)) {
        throw new InvalidOrderException('Order validation failed');
    }

    // get total
    $totalAmount = $this->calculateTotal($order);
    
    // check coupon
    if ($order->coupon_code) {
        $totalAmount = $this->applyDiscount($totalAmount, $order->coupon_code);
    }

    // charge
    $result = $this->paymentGateway->charge($totalAmount, [
        'order_id' => $order->id,
        'customer_id' => $order->user_id,
    ]);

    return $result;
}

⚠️ Error Handling

✅ Good Example

php
<?php

declare(strict_types=1);

namespace App\Services;

use App\Exceptions\UserNotFoundException;
use App\Exceptions\InvalidUserDataException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class UserService
{
    public function updateUser(int $id, array $data): User
    {
        try {
            DB::beginTransaction();

            $user = $this->userRepository->find($id);
            
            if (!$user) {
                throw new UserNotFoundException("User with ID {$id} not found");
            }

            if (!$this->validateUserData($data)) {
                throw new InvalidUserDataException('Invalid user data provided');
            }

            $user = $this->userRepository->update($id, $data);

            DB::commit();

            Log::info('User updated successfully', [
                'user_id' => $id,
                'updated_fields' => array_keys($data),
            ]);

            return $user;

        } catch (UserNotFoundException | InvalidUserDataException $e) {
            DB::rollBack();
            throw $e;
        } catch (\Exception $e) {
            DB::rollBack();
            
            Log::error('Unexpected error updating user', [
                'user_id' => $id,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);

            throw new \RuntimeException('Failed to update user', 0, $e);
        }
    }
}

❌ Bad Example

php
<?php

class UserService
{
    public function updateUser(int $id, array $data): User
    {
        // No error handling
        $user = User::find($id);
        $user->update($data);
        return $user;  // What if user is null?
    }
}

📋 Best Practices Summary

✅ Do's

  • Use declare(strict_types=1) at the top of every PHP file
  • Type-hint everything - parameters, return types, properties
  • Follow PSR-12 for code formatting
  • Use descriptive names that clearly indicate purpose
  • Write self-documenting code with clear variable and method names
  • Add comments to explain complex logic or business rules
  • Handle errors properly with try-catch blocks
  • Use dependency injection instead of static calls
  • Keep methods short and focused on one task
  • Use early returns to reduce nesting

❌ Don'ts

  • Don't use short variable names like $d, $x, $tmp
  • Don't skip type declarations for parameters or returns
  • Don't use abbreviations unless universally understood
  • Don't write god classes that do too much
  • Don't ignore errors with empty catch blocks
  • Don't mix naming conventions in the same codebase
  • Don't use magic numbers - define constants instead
  • Don't write comments that repeat what code does
  • Don't skip strict types declaration

🔧 Code Quality Tools

Enforce these standards automatically with:

bash
# PHP CS Fixer - Auto-fix code style
./vendor/bin/php-cs-fixer fix

# Laravel Pint - Laravel-specific code style fixer
./vendor/bin/pint

# PHPStan - Static analysis
./vendor/bin/phpstan analyse

# Larastan - Laravel-aware static analysis
./vendor/bin/phpstan analyse --memory-limit=2G

Configuration Example

php
// .php-cs-fixer.php
<?php

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__)
    ->exclude(['bootstrap', 'storage', 'vendor'])
    ->name('*.php')
    ->notName('*.blade.php');

return (new PhpCsFixer\Config())
    ->setRules([
        '@PSR12' => true,
        'strict_param' => true,
        'array_syntax' => ['syntax' => 'short'],
        'ordered_imports' => ['sort_algorithm' => 'alpha'],
        'no_unused_imports' => true,
        'not_operator_with_successor_space' => true,
        'trailing_comma_in_multiline' => true,
        'phpdoc_scalar' => true,
        'unary_operator_spaces' => true,
        'binary_operator_spaces' => true,
        'blank_line_before_statement' => [
            'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
        ],
        'phpdoc_single_line_var_spacing' => true,
        'phpdoc_var_without_name' => true,
    ])
    ->setFinder($finder);

📝 Code Standards: These standards are enforced through automated tools in CI/CD. All pull requests must pass code style checks before merging.

Built with VitePress