Skip to main content

php OOP Best Practices

Trushi Jasani
EditReport

Writing clean, object-oriented php code requires following established principles and patterns. This guide covers the most important best practices for professional php development.

1. Follow the SOLID Principlesโ€‹

Single Responsibility Principle (SRP)โ€‹

Each class should have only one reason to change:

// Bad โ€” doing too much
class User {
public function save() { /* DB logic */ }
public function sendEmail() { /* Email logic */ }
public function generatePDF() { /* PDF logic */ }
}

// Good โ€” separated responsibilities
class User { /* properties and domain logic */ }
class UserRepository { public function save(User $user) { } }
class UserMailer { public function sendWelcome(User $user) { } }

Open/Closed Principle (OCP)โ€‹

Open for extension, closed for modification:

interface Discount {
public function apply(float $price): float;
}

class PercentDiscount implements Discount {
public function __construct(private float $percent) {}
public function apply(float $price): float {
return $price * (1 - $this->percent / 100);
}
}

class FlatDiscount implements Discount {
public function __construct(private float $amount) {}
public function apply(float $price): float {
return $price - $this->amount;
}
}

2. Use Constructor Promotion (php 8+)โ€‹

// Verbose (old style)
class Product {
private string $name;
private float $price;
public function __construct(string $name, float $price) {
$this->name = $name;
$this->price = $price;
}
}

// Clean (constructor promotion)
class Product {
public function __construct(
private string $name,
private float $price
) {}
}

3. Type Declare Everythingโ€‹

Use strict types and type hints for all methods:

<?php
declare(strict_types=1);

class OrderService {
public function calculateTotal(array $items, float $taxRate): float {
$subtotal = array_sum(array_column($items, 'price'));
return $subtotal * (1 + $taxRate);
}
}
?>

4. Prefer Composition Over Inheritanceโ€‹

Instead of deep inheritance chains, compose objects from smaller pieces:

// Bad: deep inheritance
class Animal {}
class Pet extends Animal {}
class Dog extends Pet {}
class GuideDog extends Dog {}

// Better: use composition + interfaces
class Dog {
public function __construct(
private Trainer $trainer,
private MedicalRecord $medicalRecord
) {}
}

5. Use Interfaces for Dependencies (Dependency Inversion)โ€‹

Depend on abstractions, not concrete implementations:

interface Logger {
public function log(string $message): void;
}

class FileLogger implements Logger {
public function log(string $message): void {
file_put_contents("app.log", $message . PHP_EOL, FILE_APPEND);
}
}

class UserService {
public function __construct(private Logger $logger) {}

public function createUser(string $name): void {
// ...
$this->logger->log("User created: $name");
}
}

// Easy to swap Logger implementations
$service = new UserService(new FileLogger());

6. Use Named Constructors (Static Factory Methods)โ€‹

class Temperature {
private function __construct(private float $celsius) {}

public static function fromCelsius(float $c): self {
return new self($c);
}

public static function fromFahrenheit(float $f): self {
return new self(($f - 32) * 5 / 9);
}

public function toCelsius(): float {
return $this->celsius;
}
}

$t1 = Temperature::fromCelsius(100);
$t2 = Temperature::fromFahrenheit(212);

7. Use Value Objects for Domain Conceptsโ€‹

class Email {
public function __construct(private string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: $value");
}
}

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

public function equals(Email $other): bool {
return $this->value === $other->value;
}
}

$email = new Email("alice@example.com");
echo $email->value();

8. Keep Methods Small and Focusedโ€‹

Each method should do one thing well:

// Bad: monolithic method
public function processOrder($data) {
// validate, save, send email, generate invoice โ€” all in one
}

// Good: small, named methods
public function processOrder(OrderData $data): Order {
$this->validateOrder($data);
$order = $this->saveOrder($data);
$this->notifyCustomer($order);
$this->generateInvoice($order);
return $order;
}

9. Use Enums (php 8.1+)โ€‹

enum OrderStatus {
case Pending;
case Processing;
case Shipped;
case Delivered;
case Cancelled;
}

class Order {
public OrderStatus $status = OrderStatus::Pending;
}

$order = new Order();
$order->status = OrderStatus::Shipped;

10. Autoload with Composer + PSR-4โ€‹

Use Composer for autoloading instead of manual require statements:

{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
<?php
require 'vendor/autoload.php';

use App\Services\UserService;
use App\Repositories\UserRepository;

$service = new UserService(new UserRepository());
?>

Quick Reference Checklistโ€‹

  • Use declare(strict_types=1) in every file
  • Type-hint all parameters and return types
  • Keep classes small with a single responsibility
  • Write interfaces before implementations
  • Avoid static methods and singletons where possible
  • Validate all inputs in constructors or setters
  • Use Composer and follow PSR-4 for autoloading
  • Write unit tests for every class
Finished reading? Mark this topic as complete.