9 min read
PHP Code Standards
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
- Core Principles
- PHP Code Style
- Type Declarations
- Classes and Object-Oriented Code
- Control Structures
- Documentation and Comments
- Testing Standards
- 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' => '[email protected]', 'password' => 'password123', ]; // Act $user = $this->userService->createUser($userData); // Assert $this->assertInstanceOf(User::class, $user); $this->assertEquals('John Doe', $user->getName()); $this->assertEquals('[email protected]', $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' => '[email protected]', '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:
- Consistency is key - Follow the same patterns throughout your codebase
- Readability first - Code is read more often than it's written
- Follow PSR standards - Leverage established PHP community standards
- Test your code - Clean code is testable code
- 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