1. php
  2. /object oriented
  3. /magic-methods

PHP Magic Methods

Introduction to Magic Methods

Magic methods in PHP are special methods that begin with two underscores (__) and are automatically invoked by PHP when certain operations are performed on objects. They provide a way to define how objects behave in specific situations, enabling elegant and intuitive object interfaces.

Understanding magic methods is essential for creating sophisticated PHP classes that integrate seamlessly with PHP's built-in functionality and provide natural, user-friendly APIs.

Why Magic Methods Matter

Object Behavior Control: Magic methods allow you to define how objects respond to common operations like property access, method calls, and string conversion.

API Design: They enable the creation of fluent, intuitive APIs that feel natural to use and integrate well with PHP's syntax.

Framework Development: Many PHP frameworks rely heavily on magic methods for features like ORM models, template engines, and dependency injection.

Debugging and Development: Magic methods can provide helpful debugging information and development tools.

Performance: When used appropriately, magic methods can improve code readability without significant performance overhead.

Common Magic Methods

Construction and Destruction: __construct(), __destruct() for object lifecycle management.

Property Access: __get(), __set(), __isset(), __unset() for dynamic property handling.

Method Handling: __call(), __callStatic() for dynamic method invocation.

String Conversion: __toString() for object string representation.

Serialization: __sleep(), __wakeup(), __serialize(), __unserialize() for object serialization.

Debugging: __debugInfo() for custom debug output.

Constructor and Destructor

Object Lifecycle Management

<?php
/**
 * Constructor and Destructor Magic Methods
 * 
 * These methods control object creation and destruction,
 * allowing for initialization and cleanup operations.
 */

class DatabaseConnection
{
    private $connection;
    private string $host;
    private string $database;
    private static int $connectionCount = 0;
    
    /**
     * Constructor - called when object is created
     */
    public function __construct(string $host, string $database, string $username, string $password)
    {
        $this->host = $host;
        $this->database = $database;
        
        try {
            $dsn = "mysql:host={$host};dbname={$database};charset=utf8mb4";
            $this->connection = new PDO($dsn, $username, $password, [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
            ]);
            
            self::$connectionCount++;
            echo "Database connection established (Total: " . self::$connectionCount . ")\n";
            
        } catch (PDOException $e) {
            throw new RuntimeException("Database connection failed: " . $e->getMessage());
        }
    }
    
    /**
     * Destructor - called when object is destroyed
     */
    public function __destruct()
    {
        if ($this->connection) {
            $this->connection = null;
            self::$connectionCount--;
            echo "Database connection closed (Remaining: " . self::$connectionCount . ")\n";
        }
    }
    
    public function query(string $sql): array
    {
        $statement = $this->connection->prepare($sql);
        $statement->execute();
        return $statement->fetchAll();
    }
    
    public static function getConnectionCount(): int
    {
        return self::$connectionCount;
    }
}

// Usage example
try {
    $db1 = new DatabaseConnection('localhost', 'test_db', 'user', 'password');
    $db2 = new DatabaseConnection('localhost', 'another_db', 'user', 'password');
    
    echo "Active connections: " . DatabaseConnection::getConnectionCount() . "\n";
    
    // Connections will be automatically closed when objects go out of scope
    unset($db1);
    
} catch (RuntimeException $e) {
    echo "Error: " . $e->getMessage() . "\n";
}

/**
 * Constructor Property Promotion (PHP 8.0+)
 */
class User
{
    public function __construct(
        private string $name,
        private string $email,
        private int $age,
        private array $roles = []
    ) {
        // Constructor body can include additional logic
        $this->validateEmail($email);
        $this->validateAge($age);
    }
    
    private function validateEmail(string $email): void
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email address: $email");
        }
    }
    
    private function validateAge(int $age): void
    {
        if ($age < 0 || $age > 150) {
            throw new InvalidArgumentException("Invalid age: $age");
        }
    }
    
    public function getName(): string
    {
        return $this->name;
    }
    
    public function getEmail(): string
    {
        return $this->email;
    }
    
    public function getAge(): int
    {
        return $this->age;
    }
    
    public function getRoles(): array
    {
        return $this->roles;
    }
}

// Usage
$user = new User('John Doe', '[email protected]', 30, ['user', 'admin']);
echo "User: {$user->getName()} ({$user->getEmail()})\n";
?>

Property Access Magic Methods

Dynamic Property Handling

<?php
/**
 * Property Access Magic Methods
 * 
 * These methods provide control over property access,
 * enabling dynamic properties and validation.
 */

class ConfigurationManager
{
    private array $config = [];
    private array $readOnlyKeys = ['database_host', 'api_key'];
    private array $requiredKeys = ['app_name', 'debug_mode'];
    
    /**
     * Magic getter - called when accessing non-existent or private properties
     */
    public function __get(string $name)
    {
        if (array_key_exists($name, $this->config)) {
            return $this->config[$name];
        }
        
        throw new InvalidArgumentException("Configuration key '$name' does not exist");
    }
    
    /**
     * Magic setter - called when setting non-existent or private properties
     */
    public function __set(string $name, $value): void
    {
        // Prevent modification of read-only configuration
        if (in_array($name, $this->readOnlyKeys) && array_key_exists($name, $this->config)) {
            throw new RuntimeException("Configuration key '$name' is read-only");
        }
        
        // Validate specific configuration types
        $this->validateConfigValue($name, $value);
        
        $this->config[$name] = $value;
    }
    
    /**
     * Magic isset - called when using isset() or empty() on properties
     */
    public function __isset(string $name): bool
    {
        return array_key_exists($name, $this->config);
    }
    
    /**
     * Magic unset - called when using unset() on properties
     */
    public function __unset(string $name): void
    {
        if (in_array($name, $this->readOnlyKeys)) {
            throw new RuntimeException("Cannot unset read-only configuration key '$name'");
        }
        
        if (in_array($name, $this->requiredKeys)) {
            throw new RuntimeException("Cannot unset required configuration key '$name'");
        }
        
        unset($this->config[$name]);
    }
    
    /**
     * Validate configuration values based on key
     */
    private function validateConfigValue(string $key, $value): void
    {
        switch ($key) {
            case 'debug_mode':
                if (!is_bool($value)) {
                    throw new InvalidArgumentException("debug_mode must be a boolean");
                }
                break;
                
            case 'max_users':
                if (!is_int($value) || $value < 1) {
                    throw new InvalidArgumentException("max_users must be a positive integer");
                }
                break;
                
            case 'database_host':
                if (!is_string($value) || empty($value)) {
                    throw new InvalidArgumentException("database_host must be a non-empty string");
                }
                break;
        }
    }
    
    /**
     * Get all configuration as array
     */
    public function toArray(): array
    {
        return $this->config;
    }
    
    /**
     * Load configuration from array
     */
    public function loadArray(array $config): void
    {
        foreach ($config as $key => $value) {
            $this->__set($key, $value);
        }
    }
}

// Usage example
$config = new ConfigurationManager();

// Set configuration values
$config->app_name = 'My Application';
$config->debug_mode = true;
$config->max_users = 100;
$config->database_host = 'localhost';

// Access configuration values
echo "App Name: {$config->app_name}\n";
echo "Debug Mode: " . ($config->debug_mode ? 'enabled' : 'disabled') . "\n";

// Check if configuration exists
if (isset($config->api_key)) {
    echo "API Key is configured\n";
} else {
    echo "API Key is not configured\n";
}

// Try to modify read-only configuration (will throw exception)
try {
    $config->database_host = 'new-host'; // First set
    $config->database_host = 'another-host'; // This will fail
} catch (RuntimeException $e) {
    echo "Error: " . $e->getMessage() . "\n";
}

/**
 * Advanced property access with caching
 */
class CachedProperties
{
    private array $cache = [];
    private array $computedProperties = [
        'full_name' => 'computeFullName',
        'age_group' => 'computeAgeGroup',
        'display_name' => 'computeDisplayName'
    ];
    
    public function __construct(
        private string $firstName,
        private string $lastName,
        private int $age,
        private ?string $nickname = null
    ) {}
    
    public function __get(string $name)
    {
        // Check if it's a computed property
        if (array_key_exists($name, $this->computedProperties)) {
            // Use cached value if available
            if (!array_key_exists($name, $this->cache)) {
                $method = $this->computedProperties[$name];
                $this->cache[$name] = $this->$method();
            }
            return $this->cache[$name];
        }
        
        // Check if it's a direct property
        if (property_exists($this, $name)) {
            return $this->$name;
        }
        
        throw new InvalidArgumentException("Property '$name' does not exist");
    }
    
    public function __set(string $name, $value): void
    {
        // Clear cache when base properties change
        if (in_array($name, ['firstName', 'lastName', 'age', 'nickname'])) {
            $this->cache = [];
        }
        
        if (property_exists($this, $name)) {
            $this->$name = $value;
        } else {
            throw new InvalidArgumentException("Cannot set non-existent property '$name'");
        }
    }
    
    private function computeFullName(): string
    {
        return $this->firstName . ' ' . $this->lastName;
    }
    
    private function computeAgeGroup(): string
    {
        if ($this->age < 18) return 'minor';
        if ($this->age < 65) return 'adult';
        return 'senior';
    }
    
    private function computeDisplayName(): string
    {
        return $this->nickname ?: $this->firstName;
    }
}

// Usage
$person = new CachedProperties('John', 'Doe', 30, 'Johnny');
echo "Full Name: {$person->full_name}\n";      // Computed and cached
echo "Age Group: {$person->age_group}\n";      // Computed and cached
echo "Display Name: {$person->display_name}\n"; // Computed and cached

// Accessing computed property again uses cache
echo "Full Name Again: {$person->full_name}\n"; // Uses cached value
?>

Method Call Magic Methods

Dynamic Method Handling

<?php
/**
 * Method Call Magic Methods
 * 
 * These methods handle calls to non-existent or inaccessible methods,
 * enabling dynamic method creation and method proxying.
 */

class QueryBuilder
{
    private string $table = '';
    private array $conditions = [];
    private array $orderBy = [];
    private ?int $limit = null;
    private array $joins = [];
    
    public function __construct(string $table)
    {
        $this->table = $table;
    }
    
    /**
     * Magic call - handles dynamic method calls
     */
    public function __call(string $method, array $arguments)
    {
        // Handle whereX methods (e.g., whereEmail, whereAge)
        if (str_starts_with($method, 'where')) {
            $column = $this->camelToSnake(substr($method, 5));
            $operator = $arguments[1] ?? '=';
            $value = $arguments[0];
            
            return $this->where($column, $operator, $value);
        }
        
        // Handle orderByX methods (e.g., orderByName, orderByDate)
        if (str_starts_with($method, 'orderBy')) {
            $column = $this->camelToSnake(substr($method, 7));
            $direction = $arguments[0] ?? 'ASC';
            
            return $this->orderBy($column, $direction);
        }
        
        // Handle findByX methods (e.g., findByEmail, findById)
        if (str_starts_with($method, 'findBy')) {
            $column = $this->camelToSnake(substr($method, 6));
            $value = $arguments[0];
            
            return $this->where($column, '=', $value)->first();
        }
        
        throw new BadMethodCallException("Method '$method' does not exist");
    }
    
    /**
     * Static magic call - handles dynamic static method calls
     */
    public static function __callStatic(string $method, array $arguments)
    {
        // Handle table() static method for any table name
        if ($method === 'table') {
            return new static($arguments[0]);
        }
        
        // Handle createForX methods (e.g., createForUsers, createForPosts)
        if (str_starts_with($method, 'createFor')) {
            $table = strtolower(substr($method, 9));
            return new static($table);
        }
        
        throw new BadMethodCallException("Static method '$method' does not exist");
    }
    
    public function where(string $column, string $operator, $value): self
    {
        $this->conditions[] = [$column, $operator, $value];
        return $this;
    }
    
    public function orderBy(string $column, string $direction = 'ASC'): self
    {
        $this->orderBy[] = [$column, strtoupper($direction)];
        return $this;
    }
    
    public function limit(int $count): self
    {
        $this->limit = $count;
        return $this;
    }
    
    public function join(string $table, string $condition): self
    {
        $this->joins[] = "JOIN $table ON $condition";
        return $this;
    }
    
    public function first(): string
    {
        return $this->limit(1)->toSql();
    }
    
    public function toSql(): string
    {
        $sql = "SELECT * FROM {$this->table}";
        
        // Add joins
        if (!empty($this->joins)) {
            $sql .= ' ' . implode(' ', $this->joins);
        }
        
        // Add WHERE conditions
        if (!empty($this->conditions)) {
            $conditions = [];
            foreach ($this->conditions as [$column, $operator, $value]) {
                $conditions[] = "$column $operator " . $this->quote($value);
            }
            $sql .= ' WHERE ' . implode(' AND ', $conditions);
        }
        
        // Add ORDER BY
        if (!empty($this->orderBy)) {
            $orders = [];
            foreach ($this->orderBy as [$column, $direction]) {
                $orders[] = "$column $direction";
            }
            $sql .= ' ORDER BY ' . implode(', ', $orders);
        }
        
        // Add LIMIT
        if ($this->limit !== null) {
            $sql .= " LIMIT {$this->limit}";
        }
        
        return $sql;
    }
    
    private function camelToSnake(string $input): string
    {
        return strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $input));
    }
    
    private function quote($value): string
    {
        if (is_string($value)) {
            return "'" . str_replace("'", "''", $value) . "'";
        }
        return (string) $value;
    }
}

// Usage examples
echo "=== Query Builder Examples ===\n";

// Static creation
$query1 = QueryBuilder::table('users');
$query2 = QueryBuilder::createForPosts();

// Dynamic where methods
$userQuery = QueryBuilder::table('users')
    ->whereEmail('[email protected]')
    ->whereAge('>', 18)
    ->orderByName()
    ->limit(10);

echo "User query: " . $userQuery->toSql() . "\n";

// Dynamic find methods
$findQuery = QueryBuilder::table('posts')->findById(123);
echo "Find query: " . $findQuery . "\n";

/**
 * API Client with method proxying
 */
class ApiClient
{
    private string $baseUrl;
    private array $headers = [];
    
    public function __construct(string $baseUrl)
    {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->headers = [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json'
        ];
    }
    
    /**
     * Dynamic HTTP method calls (get, post, put, delete, etc.)
     */
    public function __call(string $method, array $arguments): array
    {
        $httpMethod = strtoupper($method);
        $endpoint = $arguments[0] ?? '';
        $data = $arguments[1] ?? [];
        
        return $this->makeRequest($httpMethod, $endpoint, $data);
    }
    
    private function makeRequest(string $method, string $endpoint, array $data = []): array
    {
        $url = $this->baseUrl . '/' . ltrim($endpoint, '/');
        
        // Simulate API request
        return [
            'method' => $method,
            'url' => $url,
            'data' => $data,
            'headers' => $this->headers,
            'response' => 'Simulated API response'
        ];
    }
    
    public function setHeader(string $name, string $value): self
    {
        $this->headers[$name] = $value;
        return $this;
    }
}

// Usage
$api = new ApiClient('https://api.example.com');
$api->setHeader('Authorization', 'Bearer token123');

// Dynamic HTTP method calls
$getResponse = $api->get('users/123');
$postResponse = $api->post('users', ['name' => 'John', 'email' => '[email protected]']);
$putResponse = $api->put('users/123', ['name' => 'John Updated']);
$deleteResponse = $api->delete('users/123');

echo "\nGET Response: " . json_encode($getResponse, JSON_PRETTY_PRINT) . "\n";
?>

String Conversion and Serialization

Object String Representation and Serialization

<?php
/**
 * String Conversion and Serialization Magic Methods
 * 
 * These methods control how objects are converted to strings
 * and how they are serialized/unserialized.
 */

class Product
{
    public function __construct(
        private int $id,
        private string $name,
        private float $price,
        private string $category,
        private bool $inStock = true,
        private array $metadata = []
    ) {}
    
    /**
     * Magic toString - called when object is used as string
     */
    public function __toString(): string
    {
        $status = $this->inStock ? 'In Stock' : 'Out of Stock';
        return "{$this->name} (#{$this->id}) - \${$this->price} - {$status}";
    }
    
    /**
     * Magic debugInfo - controls var_dump() output
     */
    public function __debugInfo(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'price' => '$' . number_format($this->price, 2),
            'category' => $this->category,
            'status' => $this->inStock ? 'In Stock' : 'Out of Stock',
            'metadata_count' => count($this->metadata)
        ];
    }
    
    /**
     * Magic sleep - called before serialization
     * Returns array of properties to serialize
     */
    public function __sleep(): array
    {
        // Don't serialize large metadata in cache scenarios
        if (count($this->metadata) > 100) {
            return ['id', 'name', 'price', 'category', 'inStock'];
        }
        
        return ['id', 'name', 'price', 'category', 'inStock', 'metadata'];
    }
    
    /**
     * Magic wakeup - called after unserialization
     * Restore object state
     */
    public function __wakeup(): void
    {
        // Validate data after unserialization
        if ($this->price < 0) {
            $this->price = 0;
        }
        
        // Initialize metadata if not present
        if (!isset($this->metadata)) {
            $this->metadata = [];
        }
        
        // Log restoration
        error_log("Product #{$this->id} restored from serialization");
    }
    
    /**
     * Modern serialization (PHP 7.4+)
     */
    public function __serialize(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'price' => $this->price,
            'category' => $this->category,
            'inStock' => $this->inStock,
            'metadata' => $this->metadata,
            'serialized_at' => time()
        ];
    }
    
    /**
     * Modern unserialization (PHP 7.4+)
     */
    public function __unserialize(array $data): void
    {
        $this->id = $data['id'];
        $this->name = $data['name'];
        $this->price = max(0, $data['price']); // Ensure positive price
        $this->category = $data['category'];
        $this->inStock = $data['inStock'];
        $this->metadata = $data['metadata'] ?? [];
        
        // Check if data is stale (more than 1 hour old)
        if (isset($data['serialized_at']) && (time() - $data['serialized_at']) > 3600) {
            error_log("Warning: Product data is more than 1 hour old");
        }
    }
    
    // Getters
    public function getId(): int { return $this->id; }
    public function getName(): string { return $this->name; }
    public function getPrice(): float { return $this->price; }
    public function getCategory(): string { return $this->category; }
    public function isInStock(): bool { return $this->inStock; }
    public function getMetadata(): array { return $this->metadata; }
    
    // Setters
    public function setPrice(float $price): void { $this->price = max(0, $price); }
    public function setInStock(bool $inStock): void { $this->inStock = $inStock; }
    public function addMetadata(string $key, $value): void { $this->metadata[$key] = $value; }
}

// Usage examples
echo "=== String Conversion Examples ===\n";

$product = new Product(
    id: 123,
    name: 'Wireless Headphones',
    price: 99.99,
    category: 'Electronics',
    inStock: true,
    metadata: ['color' => 'black', 'brand' => 'TechCorp']
);

// String conversion
echo "Product as string: " . $product . "\n";

// Debug output
echo "\nDebug info:\n";
var_dump($product);

// Serialization example
echo "\n=== Serialization Examples ===\n";

$serialized = serialize($product);
echo "Serialized length: " . strlen($serialized) . " bytes\n";

$unserialized = unserialize($serialized);
echo "Unserialized: " . $unserialized . "\n";

/**
 * JSON Serializable implementation
 */
class JsonProduct implements JsonSerializable
{
    public function __construct(
        private int $id,
        private string $name,
        private float $price,
        private bool $inStock = true
    ) {}
    
    /**
     * Specify data which should be serialized to JSON
     */
    public function jsonSerialize(): mixed
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'price' => round($this->price, 2),
            'in_stock' => $this->inStock,
            'formatted_price' => '$' . number_format($this->price, 2)
        ];
    }
    
    public function __toString(): string
    {
        return $this->name . ' (' . $this->id . ')';
    }
}

$jsonProduct = new JsonProduct(456, 'Smart Watch', 299.99, true);

echo "\nJSON Product: " . $jsonProduct . "\n";
echo "JSON: " . json_encode($jsonProduct, JSON_PRETTY_PRINT) . "\n";

/**
 * Advanced serialization with encryption
 */
class SecureSerializable
{
    private const ENCRYPTION_KEY = 'your-secret-key-here';
    
    public function __construct(
        private string $sensitiveData,
        private array $publicData = []
    ) {}
    
    public function __sleep(): array
    {
        // Encrypt sensitive data before serialization
        $this->sensitiveData = $this->encrypt($this->sensitiveData);
        return ['sensitiveData', 'publicData'];
    }
    
    public function __wakeup(): void
    {
        // Decrypt sensitive data after unserialization
        $this->sensitiveData = $this->decrypt($this->sensitiveData);
    }
    
    private function encrypt(string $data): string
    {
        // Simple example - use proper encryption in production
        return base64_encode($data . '::encrypted');
    }
    
    private function decrypt(string $encryptedData): string
    {
        // Simple example - use proper decryption in production
        $decoded = base64_decode($encryptedData);
        return str_replace('::encrypted', '', $decoded);
    }
    
    public function getSensitiveData(): string
    {
        return $this->sensitiveData;
    }
    
    public function __toString(): string
    {
        return "SecureSerializable with " . count($this->publicData) . " public fields";
    }
}

$secure = new SecureSerializable('secret-password', ['user' => 'john']);
echo "\nSecure object: " . $secure . "\n";

$serializedSecure = serialize($secure);
$unserializedSecure = unserialize($serializedSecure);

echo "Restored sensitive data: " . $unserializedSecure->getSensitiveData() . "\n";
?>

For more PHP object-oriented programming topics:

Summary

Magic methods provide powerful capabilities for creating sophisticated PHP classes:

Constructor/Destructor: Control object lifecycle with __construct() and __destruct() for initialization and cleanup.

Property Access: Implement dynamic properties with __get(), __set(), __isset(), and __unset() for flexible object interfaces.

Method Handling: Create dynamic methods with __call() and __callStatic() for fluent APIs and method proxying.

String Conversion: Provide meaningful string representations with __toString() and custom debug info with __debugInfo().

Serialization: Control object serialization with __sleep(), __wakeup(), __serialize(), and __unserialize() for data persistence.

Best Practices: Use magic methods judiciously, maintain performance awareness, provide clear documentation, and ensure predictable behavior.

Magic methods enable elegant, intuitive object interfaces while maintaining PHP's flexibility and power in object-oriented development.