Object Oriented Programming (OOP) Lanjutan di PHP
Pelajari konsep OOP lanjutan di PHP termasuk design patterns, traits, dependency injection, dan best practices untuk aplikasi enterprise-level.
Traits - Code Reusability
Traits memungkinkan reuse kode di multiple class tanpa inheritance. Sangat berguna untuk mengatasi keterbatasan single inheritance di PHP.
Basic Traits
<?php
// Trait untuk logging
trait Loggable {
private $logFile = 'app.log';
public function log($message, $level = 'INFO') {
$timestamp = date('Y-m-d H:i:s');
$logEntry = "[$timestamp] [$level] " . get_class($this) . ": $message" . PHP_EOL;
file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX);
}
public function setLogFile($file) {
$this->logFile = $file;
}
public function getLogFile() {
return $this->logFile;
}
}
// Trait untuk timestamping
trait Timestampable {
private $createdAt;
private $updatedAt;
public function initTimestamps() {
$this->createdAt = time();
$this->updatedAt = time();
}
public function updateTimestamp() {
$this->updatedAt = time();
}
public function getCreatedAt() {
return date('Y-m-d H:i:s', $this->createdAt);
}
public function getUpdatedAt() {
return date('Y-m-d H:i:s', $this->updatedAt);
}
}
// Class menggunakan multiple traits
class User {
use Loggable, Timestampable;
private $id;
private $name;
private $email;
public function __construct($name, $email) {
$this->id = uniqid();
$this->name = $name;
$this->email = $email;
$this->initTimestamps();
$this->log("User created: $name");
}
public function updateProfile($name, $email) {
$oldName = $this->name;
$this->name = $name;
$this->email = $email;
$this->updateTimestamp();
$this->log("Profile updated from $oldName to $name");
}
public function getName() {
return $this->name;
}
}
class Product {
use Loggable, Timestampable;
private $id;
private $name;
private $price;
public function __construct($name, $price) {
$this->id = uniqid();
$this->name = $name;
$this->price = $price;
$this->initTimestamps();
$this->log("Product created: $name");
}
public function updatePrice($newPrice) {
$oldPrice = $this->price;
$this->price = $newPrice;
$this->updateTimestamp();
$this->log("Price updated from $oldPrice to $newPrice");
}
}
// Penggunaan
$user = new User("John Doe", "john@example.com");
echo $user->getCreatedAt();
$product = new Product("Laptop", 15000000);
$product->updatePrice(14000000);
echo $product->getUpdatedAt();
?>
Trait Conflicts dan Resolution
<?php
trait A {
public function smallTalk() {
echo "a";
}
public function bigTalk() {
echo "A";
}
}
trait B {
public function smallTalk() {
echo "b";
}
public function bigTalk() {
echo "B";
}
}
// Mengatasi conflict dengan insteadof dan as
class Talker {
use A, B {
B::smallTalk insteadof A; // Gunakan B::smallTalk
A::bigTalk insteadof B; // Gunakan A::bigTalk
B::bigTalk as speak; // Alias B::bigTalk sebagai speak()
}
}
// Trait dengan abstract methods
trait Cacheable {
private $cache = [];
abstract protected function getCacheKey($params);
public function getFromCache($params) {
$key = $this->getCacheKey($params);
return isset($this->cache[$key]) ? $this->cache[$key] : null;
}
public function setCache($params, $value) {
$key = $this->getCacheKey($params);
$this->cache[$key] = $value;
}
public function clearCache() {
$this->cache = [];
}
}
class UserRepository {
use Cacheable;
protected function getCacheKey($params) {
return 'user_' . md5(serialize($params));
}
public function findUser($id) {
$cacheKey = ['id' => $id];
// Check cache first
$cached = $this->getFromCache($cacheKey);
if ($cached) {
return $cached;
}
// Simulate database query
$user = ['id' => $id, 'name' => 'User ' . $id];
// Store in cache
$this->setCache($cacheKey, $user);
return $user;
}
}
$talker = new Talker();
$talker->smallTalk(); // b
$talker->bigTalk(); // A
$talker->speak(); // B
$userRepo = new UserRepository();
$user1 = $userRepo->findUser(1); // From database
$user2 = $userRepo->findUser(1); // From cache
?>
Trait Composition
<?php
// Trait yang menggunakan trait lain
trait DatabaseConnection {
private $connection;
protected function connect() {
$this->connection = new PDO('sqlite::memory:');
return $this->connection;
}
protected function disconnect() {
$this->connection = null;
}
}
trait QueryBuilder {
use DatabaseConnection;
private $table;
private $conditions = [];
private $limit;
public function table($table) {
$this->table = $table;
return $this;
}
public function where($column, $operator, $value) {
$this->conditions[] = "$column $operator '$value'";
return $this;
}
public function limit($limit) {
$this->limit = $limit;
return $this;
}
public function build() {
$sql = "SELECT * FROM {$this->table}";
if (!empty($this->conditions)) {
$sql .= " WHERE " . implode(' AND ', $this->conditions);
}
if ($this->limit) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
public function execute() {
if (!$this->connection) {
$this->connect();
}
$sql = $this->build();
return $this->connection->query($sql);
}
}
// Model class menggunakan trait composition
class UserModel {
use QueryBuilder;
public function findActiveUsers() {
return $this->table('users')
->where('status', '=', 'active')
->limit(10)
->execute();
}
public function findUserByEmail($email) {
return $this->table('users')
->where('email', '=', $email)
->limit(1)
->execute();
}
}
$userModel = new UserModel();
echo $userModel->table('users')->where('age', '>', '18')->build();
// Output: SELECT * FROM users WHERE age > '18'
?>
Tips Traits: Gunakan traits untuk horizontal code reuse, hindari trait yang terlalu kompleks, dan selalu dokumentasikan trait conflicts resolution.
Design Patterns
Design patterns adalah solusi reusable untuk masalah umum dalam software design. Mari pelajari beberapa pattern penting.
Singleton Pattern
<?php
class Database {
private static $instance = null;
private $connection;
private $host = 'localhost';
private $username = 'root';
private $password = '';
private $database = 'test';
// Prevent direct instantiation
private function __construct() {
try {
$this->connection = new PDO(
"mysql:host={$this->host};dbname={$this->database}",
$this->username,
$this->password,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
} catch (PDOException $e) {
throw new Exception("Database connection failed: " . $e->getMessage());
}
}
// Prevent cloning
private function __clone() {}
// Prevent unserialization
public function __wakeup() {
throw new Exception("Cannot unserialize singleton");
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
return $this->connection;
}
public function query($sql, $params = []) {
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
return $stmt;
}
}
// Configuration Singleton
class Config {
private static $instance = null;
private $config = [];
private function __construct() {
// Load configuration from file or database
$this->config = [
'app_name' => 'My Application',
'version' => '1.0.0',
'debug' => true,
'timezone' => 'Asia/Jakarta'
];
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function get($key, $default = null) {
return isset($this->config[$key]) ? $this->config[$key] : $default;
}
public function set($key, $value) {
$this->config[$key] = $value;
}
}
// Usage
$db1 = Database::getInstance();
$db2 = Database::getInstance();
var_dump($db1 === $db2); // true - same instance
$config = Config::getInstance();
echo $config->get('app_name'); // My Application
?>
Factory Pattern
<?php
// Abstract Product
abstract class Logger {
abstract public function log($message);
}
// Concrete Products
class FileLogger extends Logger {
private $filename;
public function __construct($filename) {
$this->filename = $filename;
}
public function log($message) {
$timestamp = date('Y-m-d H:i:s');
file_put_contents($this->filename, "[$timestamp] $message" . PHP_EOL, FILE_APPEND);
}
}
class DatabaseLogger extends Logger {
private $connection;
public function __construct($connection) {
$this->connection = $connection;
}
public function log($message) {
$stmt = $this->connection->prepare("INSERT INTO logs (message, created_at) VALUES (?, NOW())");
$stmt->execute([$message]);
}
}
class EmailLogger extends Logger {
private $email;
public function __construct($email) {
$this->email = $email;
}
public function log($message) {
mail($this->email, 'Application Log', $message);
}
}
// Factory
class LoggerFactory {
public static function create($type, $config = []) {
switch (strtolower($type)) {
case 'file':
return new FileLogger($config['filename'] ?? 'app.log');
case 'database':
if (!isset($config['connection'])) {
throw new InvalidArgumentException('Database connection required');
}
return new DatabaseLogger($config['connection']);
case 'email':
return new EmailLogger($config['email'] ?? 'admin@example.com');
default:
throw new InvalidArgumentException("Unknown logger type: $type");
}
}
}
// Abstract Factory untuk multiple product families
abstract class UIFactory {
abstract public function createButton();
abstract public function createInput();
}
class WebUIFactory extends UIFactory {
public function createButton() {
return new WebButton();
}
public function createInput() {
return new WebInput();
}
}
class MobileUIFactory extends UIFactory {
public function createButton() {
return new MobileButton();
}
public function createInput() {
return new MobileInput();
}
}
// Usage
$fileLogger = LoggerFactory::create('file', ['filename' => 'debug.log']);
$fileLogger->log('This is a test message');
$dbLogger = LoggerFactory::create('database', ['connection' => $pdo]);
$dbLogger->log('Database log message');
?>
Observer Pattern
<?php
// Subject interface
interface Subject {
public function attach(Observer $observer);
public function detach(Observer $observer);
public function notify();
}
// Observer interface
interface Observer {
public function update(Subject $subject);
}
// Concrete Subject
class User implements Subject {
private $observers = [];
private $name;
private $email;
private $state;
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
$this->state = 'created';
}
public function attach(Observer $observer) {
$this->observers[] = $observer;
}
public function detach(Observer $observer) {
$key = array_search($observer, $this->observers, true);
if ($key !== false) {
unset($this->observers[$key]);
}
}
public function notify() {
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function login() {
$this->state = 'logged_in';
$this->notify();
}
public function logout() {
$this->state = 'logged_out';
$this->notify();
}
public function updateProfile($name, $email) {
$this->name = $name;
$this->email = $email;
$this->state = 'profile_updated';
$this->notify();
}
// Getters
public function getName() { return $this->name; }
public function getEmail() { return $this->email; }
public function getState() { return $this->state; }
}
// Concrete Observers
class EmailNotificationObserver implements Observer {
public function update(Subject $subject) {
if ($subject instanceof User) {
switch ($subject->getState()) {
case 'logged_in':
$this->sendEmail($subject->getEmail(), 'Login Notification', 'You have logged in');
break;
case 'profile_updated':
$this->sendEmail($subject->getEmail(), 'Profile Updated', 'Your profile has been updated');
break;
}
}
}
private function sendEmail($to, $subject, $message) {
echo "Sending email to $to: $subject - $message\n";
}
}
class LoggerObserver implements Observer {
public function update(Subject $subject) {
if ($subject instanceof User) {
$message = "User {$subject->getName()} state changed to: {$subject->getState()}";
file_put_contents('user_activity.log', date('Y-m-d H:i:s') . " - $message\n", FILE_APPEND);
}
}
}
class CacheObserver implements Observer {
public function update(Subject $subject) {
if ($subject instanceof User) {
// Clear cache when user data changes
$cacheKey = "user_" . md5($subject->getEmail());
echo "Clearing cache for key: $cacheKey\n";
// Cache clearing logic here
}
}
}
// Usage
$user = new User('John Doe', 'john@example.com');
// Attach observers
$user->attach(new EmailNotificationObserver());
$user->attach(new LoggerObserver());
$user->attach(new CacheObserver());
// Trigger events
$user->login();
$user->updateProfile('John Smith', 'johnsmith@example.com');
$user->logout();
?>
Design Patterns: Gunakan patterns untuk menyelesaikan masalah spesifik, jangan memaksakan pattern jika tidak diperlukan, dan pahami trade-offs setiap pattern.
Dependency Injection
Dependency Injection adalah teknik untuk mengurangi coupling antar class dengan menyuntikkan dependency dari luar.
Constructor Injection
<?php
// Without Dependency Injection (Bad)
class OrderService {
private $emailService;
private $logger;
public function __construct() {
$this->emailService = new EmailService(); // Hard dependency
$this->logger = new FileLogger('orders.log'); // Hard dependency
}
public function processOrder($order) {
// Process order logic
$this->logger->log("Processing order: " . $order['id']);
$this->emailService->sendConfirmation($order['email']);
}
}
// With Dependency Injection (Good)
interface EmailServiceInterface {
public function sendConfirmation($email);
}
interface LoggerInterface {
public function log($message);
}
class EmailService implements EmailServiceInterface {
public function sendConfirmation($email) {
echo "Sending confirmation email to: $email\n";
}
}
class FileLogger implements LoggerInterface {
private $filename;
public function __construct($filename) {
$this->filename = $filename;
}
public function log($message) {
file_put_contents($this->filename, date('Y-m-d H:i:s') . " - $message\n", FILE_APPEND);
}
}
class DatabaseLogger implements LoggerInterface {
private $connection;
public function __construct($connection) {
$this->connection = $connection;
}
public function log($message) {
// Log to database
echo "Logging to database: $message\n";
}
}
class OrderService {
private $emailService;
private $logger;
// Constructor injection
public function __construct(EmailServiceInterface $emailService, LoggerInterface $logger) {
$this->emailService = $emailService;
$this->logger = $logger;
}
public function processOrder($order) {
$this->logger->log("Processing order: " . $order['id']);
// Process order logic here
$this->emailService->sendConfirmation($order['email']);
$this->logger->log("Order processed successfully: " . $order['id']);
}
}
// Usage
$emailService = new EmailService();
$logger = new FileLogger('orders.log');
$orderService = new OrderService($emailService, $logger);
$order = ['id' => 123, 'email' => 'customer@example.com'];
$orderService->processOrder($order);
?>
Dependency Injection Container
<?php
class Container {
private $bindings = [];
private $instances = [];
public function bind($abstract, $concrete) {
$this->bindings[$abstract] = $concrete;
}
public function singleton($abstract, $concrete) {
$this->bind($abstract, $concrete);
$this->instances[$abstract] = null;
}
public function resolve($abstract) {
// If it's a singleton and already instantiated
if (isset($this->instances[$abstract]) && $this->instances[$abstract] !== null) {
return $this->instances[$abstract];
}
// If we have a binding
if (isset($this->bindings[$abstract])) {
$concrete = $this->bindings[$abstract];
if (is_callable($concrete)) {
$instance = $concrete($this);
} else {
$instance = $this->build($concrete);
}
// Store singleton instance
if (isset($this->instances[$abstract])) {
$this->instances[$abstract] = $instance;
}
return $instance;
}
// Try to build the class directly
return $this->build($abstract);
}
private function build($concrete) {
$reflector = new ReflectionClass($concrete);
if (!$reflector->isInstantiable()) {
throw new Exception("Class $concrete is not instantiable");
}
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
return new $concrete;
}
$parameters = $constructor->getParameters();
$dependencies = $this->resolveDependencies($parameters);
return $reflector->newInstanceArgs($dependencies);
}
private function resolveDependencies($parameters) {
$dependencies = [];
foreach ($parameters as $parameter) {
$dependency = $parameter->getClass();
if ($dependency === null) {
if ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new Exception("Cannot resolve class dependency {$parameter->name}");
}
} else {
$dependencies[] = $this->resolve($dependency->name);
}
}
return $dependencies;
}
}
// Service classes
class DatabaseConnection {
public function __construct() {
echo "Database connection created\n";
}
}
class UserRepository {
private $db;
public function __construct(DatabaseConnection $db) {
$this->db = $db;
echo "UserRepository created with database\n";
}
public function findUser($id) {
return ['id' => $id, 'name' => 'User ' . $id];
}
}
class UserService {
private $userRepository;
private $logger;
public function __construct(UserRepository $userRepository, LoggerInterface $logger) {
$this->userRepository = $userRepository;
$this->logger = $logger;
echo "UserService created\n";
}
public function getUser($id) {
$this->logger->log("Getting user: $id");
return $this->userRepository->findUser($id);
}
}
// Setup container
$container = new Container();
// Bind interfaces to implementations
$container->bind(LoggerInterface::class, function($container) {
return new FileLogger('app.log');
});
// Bind as singleton
$container->singleton(DatabaseConnection::class, DatabaseConnection::class);
// Usage
$userService = $container->resolve(UserService::class);
$user = $userService->getUser(123);
print_r($user);
?>
Service Locator Pattern
<?php
class ServiceLocator {
private static $services = [];
public static function register($name, $service) {
self::$services[$name] = $service;
}
public static function get($name) {
if (!isset(self::$services[$name])) {
throw new Exception("Service '$name' not found");
}
$service = self::$services[$name];
if (is_callable($service)) {
return $service();
}
return $service;
}
public static function has($name) {
return isset(self::$services[$name]);
}
}
// Application class using Service Locator
class Application {
public function run() {
$logger = ServiceLocator::get('logger');
$database = ServiceLocator::get('database');
$emailService = ServiceLocator::get('email');
$logger->log('Application started');
// Application logic here
$logger->log('Application finished');
}
}
// Setup services
ServiceLocator::register('logger', function() {
return new FileLogger('app.log');
});
ServiceLocator::register('database', function() {
return new DatabaseConnection();
});
ServiceLocator::register('email', function() {
return new EmailService();
});
// Run application
$app = new Application();
$app->run();
?>
Note: Dependency Injection lebih baik daripada Service Locator karena dependencies lebih eksplisit dan mudah di-test. Gunakan DI Container untuk automatic resolution.
Advanced OOP Concepts
Konsep-konsep advanced lainnya yang penting dalam pengembangan aplikasi enterprise.
Late Static Binding
<?php
class Model {
protected static $table;
public static function getTable() {
return static::$table; // Late static binding
}
public static function find($id) {
$table = static::getTable();
echo "SELECT * FROM $table WHERE id = $id\n";
return new static(); // Return instance of called class
}
public static function create($data) {
$table = static::getTable();
echo "INSERT INTO $table ...\n";
return new static();
}
}
class User extends Model {
protected static $table = 'users';
public function getName() {
return "User from " . static::getTable();
}
}
class Product extends Model {
protected static $table = 'products';
public function getTitle() {
return "Product from " . static::getTable();
}
}
// Late static binding in action
echo User::getTable(); // users
echo Product::getTable(); // products
$user = User::find(1); // SELECT * FROM users WHERE id = 1
$product = Product::create(['name' => 'Laptop']); // INSERT INTO products ...
echo get_class($user); // User
echo get_class($product); // Product
?>
Anonymous Classes
<?php
// Anonymous class (PHP 7+)
$logger = new class('app.log') {
private $filename;
public function __construct($filename) {
$this->filename = $filename;
}
public function log($message) {
file_put_contents($this->filename, date('Y-m-d H:i:s') . " - $message\n", FILE_APPEND);
}
};
$logger->log('This is a test message');
// Anonymous class implementing interface
interface EventHandler {
public function handle($event);
}
function registerEventHandler(EventHandler $handler) {
$handler->handle('test_event');
}
// Register anonymous event handler
registerEventHandler(new class implements EventHandler {
public function handle($event) {
echo "Handling event: $event\n";
}
});
// Anonymous class extending parent class
class BaseController {
protected function response($data) {
return json_encode($data);
}
}
$apiController = new class extends BaseController {
public function getUsers() {
$users = ['John', 'Jane', 'Bob'];
return $this->response($users);
}
};
echo $apiController->getUsers(); // ["John","Jane","Bob"]
?>
Object Cloning dan References
<?php
class Person {
public $name;
public $address;
public function __construct($name, Address $address) {
$this->name = $name;
$this->address = $address;
}
// Custom clone behavior
public function __clone() {
// Deep clone the address object
$this->address = clone $this->address;
}
}
class Address {
public $street;
public $city;
public function __construct($street, $city) {
$this->street = $street;
$this->city = $city;
}
}
// Cloning demonstration
$address = new Address('123 Main St', 'New York');
$person1 = new Person('John', $address);
// Shallow copy (references same address object)
$person2 = $person1;
$person2->name = 'Jane';
$person2->address->city = 'Los Angeles';
echo $person1->name; // Jane (reference copy)
echo $person1->address->city; // Los Angeles (same object)
// Deep copy using clone
$person3 = clone $person1;
$person3->name = 'Bob';
$person3->address->city = 'Chicago';
echo $person1->name; // Jane
echo $person1->address->city; // Los Angeles (different object now)
echo $person3->address->city; // Chicago
// Serialization for deep cloning
function deepClone($object) {
return unserialize(serialize($object));
}
$person4 = deepClone($person1);
$person4->name = 'Alice';
$person4->address->city = 'Boston';
echo $person1->address->city; // Los Angeles (unchanged)
echo $person4->address->city; // Boston
?>
Reflection API
<?php
class DocumentationGenerator {
public static function generateClassDoc($className) {
$reflector = new ReflectionClass($className);
echo "Class: {$reflector->getName()}\n";
echo "Namespace: {$reflector->getNamespaceName()}\n";
echo "File: {$reflector->getFileName()}\n";
echo "Abstract: " . ($reflector->isAbstract() ? 'Yes' : 'No') . "\n";
echo "Final: " . ($reflector->isFinal() ? 'Yes' : 'No') . "\n";
// Parent class
$parent = $reflector->getParentClass();
if ($parent) {
echo "Parent: {$parent->getName()}\n";
}
// Interfaces
$interfaces = $reflector->getInterfaceNames();
if (!empty($interfaces)) {
echo "Interfaces: " . implode(', ', $interfaces) . "\n";
}
// Properties
echo "\nProperties:\n";
foreach ($reflector->getProperties() as $property) {
$visibility = '';
if ($property->isPublic()) $visibility = 'public';
if ($property->isProtected()) $visibility = 'protected';
if ($property->isPrivate()) $visibility = 'private';
if ($property->isStatic()) $visibility .= ' static';
echo " $visibility \${$property->getName()}\n";
}
// Methods
echo "\nMethods:\n";
foreach ($reflector->getMethods() as $method) {
$visibility = '';
if ($method->isPublic()) $visibility = 'public';
if ($method->isProtected()) $visibility = 'protected';
if ($method->isPrivate()) $visibility = 'private';
if ($method->isStatic()) $visibility .= ' static';
if ($method->isAbstract()) $visibility .= ' abstract';
$params = [];
foreach ($method->getParameters() as $param) {
$paramStr = '$' . $param->getName();
if ($param->hasType()) {
$paramStr = $param->getType() . ' ' . $paramStr;
}
if ($param->isOptional()) {
$paramStr .= ' = ' . var_export($param->getDefaultValue(), true);
}
$params[] = $paramStr;
}
echo " $visibility {$method->getName()}(" . implode(', ', $params) . ")\n";
}
}
public static function callMethod($object, $methodName, $args = []) {
$reflector = new ReflectionClass($object);
$method = $reflector->getMethod($methodName);
// Make private/protected methods accessible
$method->setAccessible(true);
return $method->invokeArgs($object, $args);
}
public static function getProperty($object, $propertyName) {
$reflector = new ReflectionClass($object);
$property = $reflector->getProperty($propertyName);
$property->setAccessible(true);
return $property->getValue($object);
}
}
// Example class for documentation
class ExampleClass {
private $privateProperty = 'private value';
protected $protectedProperty = 'protected value';
public $publicProperty = 'public value';
public function publicMethod($param1, $param2 = 'default') {
return "Public method called with: $param1, $param2";
}
private function privateMethod() {
return "Private method called";
}
}
// Generate documentation
DocumentationGenerator::generateClassDoc(ExampleClass::class);
// Access private members using reflection
$obj = new ExampleClass();
echo DocumentationGenerator::callMethod($obj, 'privateMethod'); // Private method called
echo DocumentationGenerator::getProperty($obj, 'privateProperty'); // private value
?>
Advanced Tips: Gunakan reflection untuk frameworks dan tools, late static binding untuk model inheritance, dan anonymous classes untuk one-time implementations.
Tutorial Saat Ini
Level: Lanjutan
Untuk yang sudah mahir PHP dan ingin mendalami