PHP

PHP Coding Standards

A guide to writing clean, maintainable PHP code following PSR coding standards, covering type declarations, OOP principles, documentation, and testing.

On this page
  1. Table of Contents
  2. Core Principles
  3. PHP Code Style
  4. Type Declarations
  5. Classes and Object-Oriented Code
  6. Control Structures
  7. Documentation and Comments
  8. Testing Standards
  9. Common Anti-Patterns to Avoid
  10. Conclusion

This document serves as a comprehensive guide to writing clean, maintainable PHP code. It follows industry standards and PSR guidelines established by the PHP community. These standards are framework-agnostic and apply to any PHP project.

Table of Contents

  1. Core Principles
  2. PHP Code Style
  3. Type Declarations
  4. Classes and Object-Oriented Code
  5. Control Structures
  6. Documentation and Comments
  7. Testing Standards
  8. Common Anti-Patterns to Avoid

Core Principles

Write Expressive Code

Code should be self-documenting. If you need a comment to explain what code does, consider refactoring:

// ✅ Good: Method name explains the intent
public function calculateMonthlyInterest(): float
{
    return $this->principal * $this->rate / 12;
}

// ❌ Bad: Requires comment to understand
public function calculate(): float
{
    // Calculate monthly interest
    return $this->principal * $this->rate / 12;
}

PHP Code Style

PSR Standards Compliance

Follow PSR-1, PSR-2, and PSR-12 standards. Use camelCase for everything that is not public-facing:

// ✅ Good
class UserRegistrationService
{
    private string $apiKey;

    public function registerUser(array $userData): User
    {
        // Implementation
    }
}

// ❌ Bad
class user_registration_service
{
    private $api_key;

    public function register_user($user_data)
    {
        // Implementation
    }
}

String Handling

Prefer string interpolation over concatenation:

// ✅ Good
$greeting = "Hello, {$user->name}! Welcome to {$application->name}.";

// ❌ Bad
$greeting = 'Hello, ' . $user->name . '! Welcome to ' . $application->name . '.';

Whitespace for Readability

Statements should be allowed to breathe. Add blank lines between logical sections:

// ✅ Good
public function processOrder(Order $order): bool
{
    $paymentResult = $this->paymentService->charge($order);

    if (!$paymentResult->isSuccessful()) {
        $this->logger->error('Payment failed', ['order_id' => $order->id]);
        return false;
    }

    $order->markAsPaid();
    $this->emailService->sendConfirmation($order);

    return true;
}

// ❌ Bad: Everything cramped together
public function processOrder(Order $order): bool
{
    $paymentResult = $this->paymentService->charge($order);
    if (!$paymentResult->isSuccessful()) {
        $this->logger->error('Payment failed', ['order_id' => $order->id]);
        return false;
    }
    $order->markAsPaid();
    $this->emailService->sendConfirmation($order);
    return true;
}

Type Declarations

Always Use Type Hints

Type everything wherever possible. Do not rely on docblocks for basic types:

// ✅ Good
class UserService
{
    public function findByEmail(string $email): ?User
    {
        return User::where('email', $email)->first();
    }
}

// ❌ Bad
class UserService
{
    /**
     * @param string $email
     * @return User|null
     */
    public function findByEmail($email)
    {
        return User::where('email', $email)->first();
    }
}

Nullable Types

Use short nullable notation:

// ✅ Good
public ?string $description;
public function getUser(): ?User

// ❌ Bad
public string|null $description;
public function getUser(): User|null

Void Return Types

If a method returns nothing, indicate it with void:

// ✅ Good
public function sendNotification(User $user): void
{
    Mail::to($user)->send(new WelcomeEmail());
}

// ❌ Bad
public function sendNotification(User $user)
{
    Mail::to($user)->send(new WelcomeEmail());
}

Constructor Property Promotion

Use constructor property promotion when all properties can be promoted:

// ✅ Good
class CreateUserAction
{
    public function __construct(
        private readonly UserRepository $userRepository,
        private readonly EmailService $emailService,
        private readonly Logger $logger,
    ) {}
}

// ❌ Bad
class CreateUserAction
{
    private UserRepository $userRepository;
    private EmailService $emailService;
    private Logger $logger;

    public function __construct(
        UserRepository $userRepository,
        EmailService $emailService,
        Logger $logger
    ) {
        $this->userRepository = $userRepository;
        $this->emailService = $emailService;
        $this->logger = $logger;
    }
}

Classes and Object-Oriented Code

Single Responsibility Principle

Each class should have one reason to change:

// ✅ Good: Separate concerns
class UserRegistrationService
{
    public function __construct(
        private UserRepository $userRepository,
        private EmailService $emailService,
    ) {}

    public function register(array $userData): User
    {
        $user = $this->userRepository->create($userData);
        $this->emailService->sendWelcomeEmail($user);

        return $user;
    }
}

class EmailService
{
    public function sendWelcomeEmail(User $user): void
    {
        // Use appropriate email service (mail(), PHPMailer, SwiftMailer, etc.)
        $this->mailer->send(new WelcomeEmail($user));
    }
}

// ❌ Bad: Mixed concerns
class UserRegistrationService
{
    public function register(array $userData): User
    {
        // Database logic
        $user = new User($userData);
        $user->save();

        // Email logic
        $emailContent = "Welcome {$user->name}...";
        mail($user->email, 'Welcome', $emailContent);

        // Logging logic
        file_put_contents('log.txt', "User {$user->id} registered\n", FILE_APPEND);

        return $user;
    }
}

Traits Usage

Each trait should go on its own line:

// ✅ Good
class User
{
    use TimestampsTrait;
    use ValidationTrait;
    use SerializableTrait;
}

// ❌ Bad
class User
{
    use TimestampsTrait, ValidationTrait, SerializableTrait;
}

Enums

Use PascalCase for enum values:

// ✅ Good
enum UserStatus: string
{
    case Active = 'active';
    case Inactive = 'inactive';
    case Suspended = 'suspended';
}

// ❌ Bad
enum UserStatus: string
{
    case active = 'active';
    case inactive = 'inactive';
    case suspended = 'suspended';
}

Control Structures

Happy Path Pattern

Structure conditionals so that the unhappy path comes first:

// ✅ Good: Happy path last and unindented
public function processPayment(Payment $payment): bool
{
    if (!$payment->isValid()) {
        $this->logger->error('Invalid payment data');
        return false;
    }

    if (!$this->hasInsufficientFunds($payment)) {
        $this->logger->error('Insufficient funds');
        return false;
    }

    // Happy path - main logic
    $this->chargeCard($payment);
    $this->updateBalance($payment);
    $this->sendConfirmation($payment);

    return true;
}

// ❌ Bad: Nested conditions
public function processPayment(Payment $payment): bool
{
    if ($payment->isValid()) {
        if ($this->hasInsufficientFunds($payment)) {
            $this->chargeCard($payment);
            $this->updateBalance($payment);
            $this->sendConfirmation($payment);
            return true;
        } else {
            $this->logger->error('Insufficient funds');
            return false;
        }
    } else {
        $this->logger->error('Invalid payment data');
        return false;
    }
}

Avoid Else Statements

Prefer early returns to else statements:

// ✅ Good
public function calculateDiscount(User $user): float
{
    if (!$user->isPremium()) {
        return 0.0;
    }

    if ($user->yearlyPurchases() > 10000) {
        return 0.15;
    }

    return 0.10;
}

// ❌ Bad
public function calculateDiscount(User $user): float
{
    if ($user->isPremium()) {
        if ($user->yearlyPurchases() > 10000) {
            return 0.15;
        } else {
            return 0.10;
        }
    } else {
        return 0.0;
    }
}

Separate Compound Conditions

Break complex conditions into separate if statements:

// ✅ Good
public function canAccessResource(User $user, Resource $resource): bool
{
    if (!$user->isActive()) {
        return false;
    }

    if (!$resource->isPublished()) {
        return false;
    }

    if (!$this->hasPermission($user, $resource)) {
        return false;
    }

    return true;
}

// ❌ Bad
public function canAccessResource(User $user, Resource $resource): bool
{
    return $user->isActive()
        && $resource->isPublished()
        && $this->hasPermission($user, $resource);
}

Documentation and Comments

When to Use Docblocks

Use docblocks only when they provide additional context beyond what the code already expresses:

// ✅ Good: Docblock adds value for complex return types
/**
 * @return array<int, User>
 */
public function getActiveUsers(): array
{
    return array_filter($this->users, fn($user) => $user->isActive());
}

// ✅ Good: No docblock needed - method signature is clear
public function createUser(string $email, string $name): User
{
    return new User($email, $name);
}

// ❌ Bad: Redundant docblock
/**
 * Create a user with email and name
 *
 * @param string $email
 * @param string $name
 * @return User
 */
public function createUser(string $email, string $name): User
{
    return new User($email, $name);
}

Comment Guidelines

Comments should explain why, not what:

// ✅ Good: Explains the business reason
public function calculateShipping(): float
{
    // Free shipping for orders over $100 to encourage larger purchases
    if ($this->total > 100) {
        return 0;
    }

    return 5.99;
}

// ❌ Bad: Explains what the code does (obvious)
public function calculateShipping(): float
{
    // Check if total is greater than 100
    if ($this->total > 100) {
        // Return zero
        return 0;
    }

    // Return shipping cost
    return 5.99;
}

Testing Standards

Test Organization

Keep test classes focused and well-organized:

class UserServiceTest extends PHPUnit\Framework\TestCase
{
    private UserService $userService;

    protected function setUp(): void
    {
        parent::setUp();
        $this->userService = new UserService();
    }

    public function testCreatesUserWithValidData(): void
    {
        // Arrange
        $userData = [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123',
        ];

        // Act
        $user = $this->userService->createUser($userData);

        // Assert
        $this->assertInstanceOf(User::class, $user);
        $this->assertEquals('John Doe', $user->getName());
        $this->assertEquals('john@example.com', $user->getEmail());
    }

    public function testSendsWelcomeEmailAfterCreatingUser(): void
    {
        // Test implementation using mocks for email service
        $emailService = $this->createMock(EmailService::class);
        $emailService->expects($this->once())
            ->method('sendWelcomeEmail');

        $userService = new UserService($emailService);

        $userData = [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123',
        ];

        $userService->createUser($userData);
    }
}

Test Method Naming

Use descriptive method names that explain the scenario being tested:

// ✅ Good: Descriptive test names
public function testThrowsExceptionWhenEmailAlreadyExists(): void {}
public function testReturnsNullWhenUserNotFound(): void {}
public function testCalculatesDiscountCorrectlyForPremiumUsers(): void {}

// ❌ Bad: Unclear test names
public function testUserCreation(): void {}
public function testEmail(): void {}
public function testDiscount(): void {}

Common Anti-Patterns to Avoid

God Classes

Avoid classes that attempt to do everything:

// ❌ Bad: God class
class UserManager
{
    public function createUser($data) {}
    public function deleteUser($id) {}
    public function sendEmail($user) {}
    public function processPayment($user, $amount) {}
    public function generateReport($user) {}
    public function validateData($data) {}
    // ... 50 more methods
}

// ✅ Good: Separate responsibilities
class UserService
{
    public function createUser(array $data): User {}
    public function deleteUser(int $id): void {}
}

class EmailService
{
    public function sendWelcomeEmail(User $user): void {}
}

class PaymentService
{
    public function processPayment(User $user, float $amount): bool {}
}

Magic Numbers and Strings

Use constants or configuration for magic values:

// ✅ Good
class ShippingCalculator
{
    private const FREE_SHIPPING_THRESHOLD = 100.00;
    private const STANDARD_SHIPPING_RATE = 5.99;

    public function calculateShipping(float $orderTotal): float
    {
        if ($orderTotal >= self::FREE_SHIPPING_THRESHOLD) {
            return 0;
        }

        return self::STANDARD_SHIPPING_RATE;
    }
}

// ❌ Bad
class ShippingCalculator
{
    public function calculateShipping(float $orderTotal): float
    {
        if ($orderTotal >= 100.00) {
            return 0;
        }

        return 5.99;
    }
}

Primitive Obsession

Use value objects rather than primitive types to represent domain concepts:

// ✅ Good: Value objects
class Email
{
    public function __construct(private string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email format');
        }
    }

    public function getValue(): string
    {
        return $this->value;
    }

    public function getDomain(): string
    {
        return substr($this->value, strpos($this->value, '@') + 1);
    }
}

class User
{
    public function __construct(
        private string $name,
        private Email $email,
    ) {}
}

// ❌ Bad: Primitive obsession
class User
{
    public function __construct(
        private string $name,
        private string $email, // Just a string, no validation or behaviour
    ) {}
}

Conclusion

Writing clean PHP code is an ongoing practice that demands discipline and attention to detail. These guidelines provide a foundation for maintainable PHP applications across any framework or project. Keep the following principles in mind:

  1. Consistency is key - Follow the same patterns throughout your codebase
  2. Readability first - Code is read more often than it's written
  3. Follow PSR standards - Leverage established PHP community standards
  4. Test your code - Clean code is testable code
  5. Refactor regularly - Code quality requires continuous improvement

For further reading on these topics, explore the following resources:

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." -- Martin Fowler

← Older
PHPStan: Static Analysis for PHP

Newsletter

A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.