Comprehensive guide to PHP OOP design patterns including dependency injection, repository pattern, factory pattern, SOLID principles, and professional software architecture with complete working code examples.
PHP Problem Solving: Object-Oriented Design Patterns and Best Practices
Object-oriented programming is fundamental to modern PHP development, enabling developers to build maintainable, scalable, and testable applications. Understanding design patterns and OOP best practices is essential for writing professional-quality code. This comprehensive tutorial explores essential design patterns, SOLID principles, dependency injection, and practical implementations for building robust PHP applications.
FOCUS AREA: This tutorial covers SOLID principles, dependency injection, factory patterns, repository pattern, service containers, and testing strategies with complete working code examples demonstrating professional PHP architecture.
Problem 1: Implementing Dependency Injection and Inversion of Control
Challenge: You need to design a flexible, testable application architecture where components depend on abstractions rather than concrete implementations, enabling easy testing and swapping of dependencies.
<?php
/**
* Dependency Injection and IoC Container Implementation
* Problem: Building flexible, testable application architecture
*/
// Define interfaces for abstraction
interface LoggerInterface {
public function log(string $level, string $message, array $context = []): void;
}
interface DatabaseInterface {
public function query(string $sql, array $params = []): array;
public function transaction(callable $callback): mixed;
}
interface CacheInterface {
public function get(string $key): mixed;
public function set(string $key, $value, int $ttl = 3600): bool;
public function delete(string $key): bool;
}
// Concrete implementations
class FileLogger implements LoggerInterface {
private string $logPath;
public function __construct(string $logPath) {
$this->logPath = $logPath;
}
public function log(string $level, string $message, array $context = []): void {
$entry = sprintf(
"[%s] %s: %s %s\n",
date('Y-m-d H:i:s'),
strtoupper($level),
$message,
json_encode($context)
);
file_put_contents($this->logPath, $entry, FILE_APPEND | LOCK_EX);
}
}
class NullLogger implements LoggerInterface {
public function log(string $level, string $message, array $context = []): void {
// No-op for testing
}
}
// Service class with dependency injection
class UserService {
private DatabaseInterface $db;
private CacheInterface $cache;
private LoggerInterface $logger;
public function __construct(
DatabaseInterface $db,
CacheInterface $cache,
LoggerInterface $logger
) {
$this->db = $db;
$this->cache = $cache;
$this->logger = $logger;
}
public function findUser(int $id): ?array {
$cacheKey = "user:{$id}";
// Try cache first
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
$this->logger->log('debug', 'User found in cache', ['id' => $id]);
return $cached;
}
// Query database
$users = $this->db->query(
"SELECT * FROM users WHERE id = :id",
['id' => $id]
);
if (empty($users)) {
$this->logger->log('warning', 'User not found', ['id' => $id]);
return null;
}
$user = $users[0];
// Store in cache
$this->cache->set($cacheKey, $user, 3600);
$this->logger->log('info', 'User loaded from database', ['id' => $id]);
return $user;
}
public function createUser(array $data): int {
return $this->db->transaction(function() use ($data) {
$this->db->query(
"INSERT INTO users (name, email) VALUES (:name, :email)",
$data
);
$id = $this->db->lastInsertId();
$this->logger->log('info', 'User created', ['id' => $id, 'email' => $data['email']]);
return $id;
});
}
}
// Simple DI Container
class Container {
private array $bindings = [];
private array $singletons = [];
private array $instances = [];
public function bind(string $abstract, $concrete, bool $singleton = false): void {
$this->bindings[$abstract] = $concrete;
if ($singleton) {
$this->singletons[$abstract] = true;
}
}
public function singleton(string $abstract, $concrete): void {
$this->bind($abstract, $concrete, true);
}
public function make(string $abstract) {
// Return existing singleton instance
if (isset($this->singletons[$abstract]) && isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
$concrete = $this->bindings[$abstract] ?? $abstract;
// If concrete is a closure, execute it
if ($concrete instanceof Closure) {
$instance = $concrete($this);
} elseif (is_string($concrete) && class_exists($concrete)) {
$instance = $this->build($concrete);
} else {
$instance = $concrete;
}
// Store singleton instance
if (isset($this->singletons[$abstract])) {
$this->instances[$abstract] = $instance;
}
return $instance;
}
private function build(string $class) {
$reflector = new ReflectionClass($class);
$constructor = $reflector->getConstructor();
if ($constructor === null) {
return new $class();
}
$dependencies = [];
foreach ($constructor->getParameters() as $param) {
$type = $param->getType();
if ($type && !$type->isBuiltin()) {
$dependencies[] = $this->make($type->getName());
} elseif ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
} else {
throw new Exception("Cannot resolve dependency {$param->getName()}");
}
}
return $reflector->newInstanceArgs($dependencies);
}
}
// Usage
$container = new Container();
// Configure bindings
$container->singleton(LoggerInterface::class, function() {
return new FileLogger('/var/log/app.log');
});
$container->singleton(DatabaseInterface::class, function() {
return new PdoDatabase('mysql:host=localhost;dbname=app', 'user', 'pass');
});
$container->singleton(CacheInterface::class, function() {
return new RedisCache(new Redis());
});
// Resolve UserService with all dependencies
$userService = $container->make(UserService::class);
$user = $userService->findUser(123);
?>
Problem 2: Repository Pattern and Data Access Abstraction
Challenge: You need to abstract database operations behind a clean interface, enabling easy testing, supporting multiple data sources, and keeping business logic separate from data access concerns.
<?php
/**
* Repository Pattern Implementation
* Problem: Abstracting data access with clean interfaces
*/
// Entity class
class User {
private ?int $id = null;
private string $name;
private string $email;
private DateTime $createdAt;
public function __construct(string $name, string $email) {
$this->name = $name;
$this->email = $email;
$this->createdAt = new DateTime();
}
// Getters and setters
public function getId(): ?int { return $this->id; }
public function setId(int $id): void { $this->id = $id; }
public function getName(): string { return $this->name; }
public function setName(string $name): void { $this->name = $name; }
public function getEmail(): string { return $this->email; }
public function setEmail(string $email): void { $this->email = $email; }
public function getCreatedAt(): DateTime { return $this->createdAt; }
}
// Repository interface
interface UserRepositoryInterface {
public function findById(int $id): ?User;
public function findByEmail(string $email): ?User;
public function findAll(array $criteria = []): array;
public function save(User $user): void;
public function delete(User $user): void;
}
// Concrete repository implementation
class PdoUserRepository implements UserRepositoryInterface {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function findById(int $id): ?User {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => $id]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
return $data ? $this->mapToUser($data) : null;
}
public function findByEmail(string $email): ?User {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute(['email' => $email]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
return $data ? $this->mapToUser($data) : null;
}
public function findAll(array $criteria = []): array {
$sql = "SELECT * FROM users WHERE 1=1";
$params = [];
if (isset($criteria['name'])) {
$sql .= " AND name LIKE :name";
$params['name'] = '%' . $criteria['name'] . '%';
}
if (isset($criteria['email'])) {
$sql .= " AND email LIKE :email";
$params['email'] = '%' . $criteria['email'] . '%';
}
$sql .= " ORDER BY created_at DESC";
if (isset($criteria['limit'])) {
$sql .= " LIMIT :limit";
$params['limit'] = (int)$criteria['limit'];
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$users = [];
while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
$users[] = $this->mapToUser($data);
}
return $users;
}
public function save(User $user): void {
if ($user->getId() === null) {
// Insert
$stmt = $this->pdo->prepare(
"INSERT INTO users (name, email, created_at) VALUES (:name, :email, :created_at)"
);
$stmt->execute([
'name' => $user->getName(),
'email' => $user->getEmail(),
'created_at' => $user->getCreatedAt()->format('Y-m-d H:i:s')
]);
$user->setId((int)$this->pdo->lastInsertId());
} else {
// Update
$stmt = $this->pdo->prepare(
"UPDATE users SET name = :name, email = :email WHERE id = :id"
);
$stmt->execute([
'id' => $user->getId(),
'name' => $user->getName(),
'email' => $user->getEmail()
]);
}
}
public function delete(User $user): void {
if ($user->getId() === null) {
throw new InvalidArgumentException("Cannot delete user without ID");
}
$stmt = $this->pdo->prepare("DELETE FROM users WHERE id = :id");
$stmt->execute(['id' => $user->getId()]);
}
private function mapToUser(array $data): User {
$user = new User($data['name'], $data['email']);
$user->setId((int)$data['id']);
return $user;
}
}
// Decorator for caching
class CachedUserRepository implements UserRepositoryInterface {
private UserRepositoryInterface $repository;
private CacheInterface $cache;
private int $ttl;
public function __construct(
UserRepositoryInterface $repository,
CacheInterface $cache,
int $ttl = 3600
) {
$this->repository = $repository;
$this->cache = $cache;
$this->ttl = $ttl;
}
public function findById(int $id): ?User {
$key = "user:id:{$id}";
$cached = $this->cache->get($key);
if ($cached !== null) {
return $cached;
}
$user = $this->repository->findById($id);
if ($user !== null) {
$this->cache->set($key, $user, $this->ttl);
}
return $user;
}
public function findByEmail(string $email): ?User {
// Skip cache for email lookups (could be added with proper invalidation)
return $this->repository->findByEmail($email);
}
public function findAll(array $criteria = []): array {
// Skip cache for complex queries
return $this->repository->findAll($criteria);
}
public function save(User $user): void {
$this->repository->save($user);
// Invalidate cache
if ($user->getId() !== null) {
$this->cache->delete("user:id:{$user->getId()}");
}
}
public function delete(User $user): void {
$this->repository->delete($user);
if ($user->getId() !== null) {
$this->cache->delete("user:id:{$user->getId()}");
}
}
}
?>
Problem 3: Factory Pattern and Object Creation
<?php
/**
* Factory Pattern for Flexible Object Creation
*/
interface PaymentGatewayInterface {
public function processPayment(float $amount, array $details): array;
public function refund(string $transactionId, float $amount): bool;
}
class StripeGateway implements PaymentGatewayInterface {
private string $apiKey;
public function __construct(string $apiKey) {
$this->apiKey = $apiKey;
}
public function processPayment(float $amount, array $details): array {
// Stripe-specific implementation
return ['transaction_id' => 'stripe_' . uniqid(), 'status' => 'success'];
}
public function refund(string $transactionId, float $amount): bool {
// Stripe refund implementation
return true;
}
}
class PayPalGateway implements PaymentGatewayInterface {
private string $clientId;
private string $clientSecret;
public function __construct(string $clientId, string $clientSecret) {
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
}
public function processPayment(float $amount, array $details): array {
// PayPal-specific implementation
return ['transaction_id' => 'paypal_' . uniqid(), 'status' => 'success'];
}
public function refund(string $transactionId, float $amount): bool {
// PayPal refund implementation
return true;
}
}
// Factory class
class PaymentGatewayFactory {
private array $config;
public function __construct(array $config) {
$this->config = $config;
}
public function create(string $gateway): PaymentGatewayInterface {
switch ($gateway) {
case 'stripe':
if (!isset($this->config['stripe']['api_key'])) {
throw new InvalidArgumentException("Stripe API key not configured");
}
return new StripeGateway($this->config['stripe']['api_key']);
case 'paypal':
if (!isset($this->config['paypal']['client_id'], $this->config['paypal']['client_secret'])) {
throw new InvalidArgumentException("PayPal credentials not configured");
}
return new PayPalGateway(
$this->config['paypal']['client_id'],
$this->config['paypal']['client_secret']
);
default:
throw new InvalidArgumentException("Unknown payment gateway: {$gateway}");
}
}
}
// Usage
$config = [
'stripe' => ['api_key' => 'sk_test_xxx'],
'paypal' => ['client_id' => 'client_xxx', 'client_secret' => 'secret_xxx']
];
$factory = new PaymentGatewayFactory($config);
$gateway = $factory->create('stripe');
$result = $gateway->processPayment(99.99, ['customer_id' => '123']);
?>
SOLID Principles Summary
Single Responsibility: Each class should have one reason to change
Open/Closed: Open for extension, closed for modification
Liskov Substitution: Subtypes must be substitutable for base types
Interface Segregation: Clients shouldn't depend on unused methods
Dependency Inversion: Depend on abstractions, not concretions
Design Patterns in Practice
The patterns demonstrated here form the foundation of maintainable PHP applications. Dependency injection enables testability and loose coupling. The repository pattern abstracts data access for flexibility. Factories encapsulate object creation logic. Combined with SOLID principles, these patterns produce code that is easier to test, maintain, and extend over time.
Professional PHP Architecture
Mastering OOP design patterns and best practices elevates PHP development from basic scripting to professional software engineering. The investment in learning these patterns pays dividends through reduced technical debt, improved testability, and faster feature development. Start applying these concepts incrementally, and your codebase will naturally evolve toward cleaner architecture.
Remember that patterns are tools, not rules. Apply them where they solve real problems, and don't force patterns where simple solutions suffice. The goal is maintainable, working software, not architectural purity. Let your application's requirements guide your design decisions, using patterns as proven solutions to common challenges.
Comments (0)
No comments yet. Be the first to share your thoughts!