Menü schliessen
Created: January 21st 2026
Categories: Php
Author: Aleksandar Pantic

Magic Methods in PHP: A Complete Guide with Practical Examples

PHP Magic Methods are special methods that start with a double underscore (__) and are automatically triggered by PHP in specific situations. They give you the power to control how your objects behave in ways that would otherwise be impossible. This blog post will explain the most important magic methods, when to use them, and provide practical examples that you can apply in your projects.

What Are Magic Methods?

Magic methods are predefined methods in PHP that allow you to hook into certain actions performed on objects. They're called "magic" because PHP calls them automatically behind the scenes - you don't invoke them directly.

For example, when you create a new object with new ClassName(), PHP automatically calls the __construct() method. When you try to access a property that doesn't exist, PHP calls __get().

Why Use Magic Methods?

Magic methods provide several powerful capabilities:

  • Object Initialization: Set up your object's initial state when it's created.
  • Property Overloading: Control access to undefined or inaccessible properties.
  • Method Overloading: Handle calls to undefined methods gracefully.
  • Object Conversion: Define how your object converts to string or array.
  • Serialization Control: Customize how objects are serialized and unserialized.

The Most Important Magic Methods

Let's explore each magic method with practical examples.

__construct() - Object Initialization

The constructor is called automatically when you create a new instance of a class. It's perfect for setting up initial values and dependencies.

<?php

class User
{
    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();
    }

    public function getInfo(): string
    {
        return "{$this->name} ({$this->email}) - Member since: {$this->createdAt->format('Y-m-d')}";
    }
}

// Usage
$user = new User('John Doe', 'john@example.com');
echo $user->getInfo();
// Output: John Doe (john@example.com) - Member since: 2024-01-15

PHP 8+ Constructor Property Promotion:

PHP 8 introduced a shorter syntax for declaring and initializing properties directly in the constructor:

<?php

class Product
{
    public function __construct(
        private string $name,
        private float $price,
        private int $stock = 0
    ) {}

    public function getPrice(): float
    {
        return $this->price;
    }
}

$product = new Product('Laptop', 999.99, 10);
echo $product->getPrice(); // Output: 999.99

__destruct() - Cleanup Operations

The destructor is called when an object is destroyed or when the script ends. It's useful for cleanup tasks like closing database connections or file handles.

<?php

class FileHandler
{
    private $file;
    private string $filename;

    public function __construct(string $filename)
    {
        $this->filename = $filename;
        $this->file = fopen($filename, 'a');
        echo "File opened: {$filename}\n";
    }

    public function write(string $content): void
    {
        fwrite($this->file, $content . "\n");
    }

    public function __destruct()
    {
        if ($this->file) {
            fclose($this->file);
            echo "File closed: {$this->filename}\n";
        }
    }
}

// Usage
$handler = new FileHandler('log.txt');
$handler->write('First log entry');
$handler->write('Second log entry');
// When script ends or $handler is unset, __destruct() is called automatically

__get() and __set() - Property Overloading

These methods are triggered when accessing or modifying inaccessible (private/protected) or non-existent properties. They're great for implementing dynamic properties or adding validation.

<?php

class Configuration
{
    private array $settings = [];

    public function __set(string $name, mixed $value): void
    {
        echo "Setting '{$name}' to '{$value}'\n";
        $this->settings[$name] = $value;
    }

    public function __get(string $name): mixed
    {
        if (array_key_exists($name, $this->settings)) {
            return $this->settings[$name];
        }

        throw new Exception("Configuration '{$name}' does not exist.");
    }

    public function __isset(string $name): bool
    {
        return isset($this->settings[$name]);
    }

    public function __unset(string $name): void
    {
        unset($this->settings[$name]);
    }
}

// Usage
$config = new Configuration();
$config->database = 'mysql';      // Triggers __set()
$config->host = 'localhost';      // Triggers __set()

echo $config->database;           // Triggers __get(), Output: mysql
echo isset($config->host);        // Triggers __isset(), Output: 1 (true)

unset($config->host);             // Triggers __unset()

Real-World Example: Fluent Setter with Validation

<?php

class UserProfile
{
    private array $data = [];
    private array $allowedFields = ['name', 'email', 'age', 'bio'];

    public function __set(string $name, mixed $value): void
    {
        if (!in_array($name, $this->allowedFields)) {
            throw new InvalidArgumentException("Field '{$name}' is not allowed.");
        }

        // Add validation based on field
        match($name) {
            'email' => filter_var($value, FILTER_VALIDATE_EMAIL) 
                ? $this->data[$name] = $value 
                : throw new InvalidArgumentException("Invalid email format."),
            'age' => is_numeric($value) && $value > 0 && $value < 150 ? $this->data[$name] = (int) $value
                : throw new InvalidArgumentException("Age must be between 1 and 150."),
            default => $this->data[$name] = $value
        };
    }

    public function __get(string $name): mixed
    {
        return $this->data[$name] ?? null;
    }
}

// Usage
$profile = new UserProfile();
$profile->name = 'Jane Doe';
$profile->email = 'jane@example.com';
$profile->age = 28;

echo $profile->name;  // Output: Jane Doe
// $profile->email = 'invalid-email'; // Throws InvalidArgumentException

__call() and __callStatic() - Method Overloading

__call() is triggered when invoking inaccessible instance methods, while __callStatic() handles static method calls.

<?php

class QueryBuilder
{
    private string $table;
    private array $wheres = [];

    public function __construct(string $table)
    {
        $this->table = $table;
    }

    public function __call(string $method, array $arguments): self
    {
        // Handle whereColumn methods dynamically
        if (str_starts_with($method, 'where')) {
            $column = strtolower(substr($method, 5)); // Remove 'where' prefix
            $this->wheres[$column] = $arguments[0];
            return $this;
        }

        throw new BadMethodCallException("Method {$method} does not exist.");
    }

    public function toSql(): string
    {
        $sql = "SELECT * FROM {$this->table}";
        
        if (!empty($this->wheres)) {
            $conditions = [];
            foreach ($this->wheres as $column => $value) {
                $conditions[] = "{$column} = '{$value}'";
            }
            $sql .= " WHERE " . implode(' AND ', $conditions);
        }

        return $sql;
    }
}

// Usage
$query = new QueryBuilder('users');
$sql = $query
    ->whereName('John')
    ->whereStatus('active')
    ->whereRole('admin')
    ->toSql();

echo $sql;
// Output: SELECT * FROM users WHERE name = 'John' AND status = 'active' AND role = 'admin'

__callStatic() Example:

<?php

class Model
{
    protected static string $table;

    public static function __callStatic(string $method, array $arguments): mixed
    {
        // Handle findByColumn methods
        if (str_starts_with($method, 'findBy')) {
            $column = strtolower(substr($method, 6));
            $value = $arguments[0];
            
            return "SELECT * FROM " . static::$table . " WHERE {$column} = '{$value}'";
        }

        throw new BadMethodCallException("Static method {$method} does not exist.");
    }
}

class User extends Model
{
    protected static string $table = 'users';
}

// Usage
echo User::findByEmail('john@example.com');
// Output: SELECT * FROM users WHERE email = 'john@example.com'

echo User::findById(5);
// Output: SELECT * FROM users WHERE id = '5'

__toString() - String Representation

This method is called when an object is treated as a string, such as in echo or string concatenation.

<?php

class Money
{
    public function __construct(
        private float $amount,
        private string $currency = 'USD'
    ) {}

    public function __toString(): string
    {
        $symbol = match($this->currency) {
            'USD' => '$',
            'EUR' => '€',
            'GBP' => '£',
            default => $this->currency . ' '
        };

        return $symbol . number_format($this->amount, 2);
    }

    public function add(Money $other): Money
    {
        return new Money($this->amount + $other->amount, $this->currency);
    }
}

// Usage
$price = new Money(99.99);
$tax = new Money(8.50);
$total = $price->add($tax);

echo "Price: {$price}\n";     // Output: Price: $99.99
echo "Tax: {$tax}\n";         // Output: Tax: $8.50
echo "Total: {$total}\n";     // Output: Total: $108.49

__invoke() - Callable Objects

This method allows an object to be called as a function. It's useful for creating single-action classes or command objects.

<?php

class Validator
{
    public function __construct(
        private string $pattern,
        private string $errorMessage
    ) {}

    public function __invoke(string $value): bool|string
    {
        if (preg_match($this->pattern, $value)) {
            return true;
        }

        return $this->errorMessage;
    }
}

// Usage
$emailValidator = new Validator(
    '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/',
    'Invalid email format'
);

$phoneValidator = new Validator(
    '/^\+?[0-9]{10,14}$/',
    'Invalid phone number'
);

// Call objects as functions
$result1 = $emailValidator('john@example.com');  // Returns: true
$result2 = $emailValidator('invalid-email');      // Returns: 'Invalid email format'
$result3 = $phoneValidator('+1234567890');        // Returns: true

var_dump($result1); // bool(true)
var_dump($result2); // string(20) "Invalid email format"

Practical Example: Middleware-like Filter

<?php

class PriceFilter
{
    public function __construct(
        private float $minPrice = 0,
        private float $maxPrice = PHP_FLOAT_MAX
    ) {}

    public function __invoke(array $products): array
    {
        return array_filter($products, function($product) {
            return $product['price'] >= $this->minPrice 
                && $product['price'] <= $this->maxPrice;
        });
    }
}

// Usage
$products = [
    ['name' => 'Mouse', 'price' => 25.00],
    ['name' => 'Keyboard', 'price' => 75.00],
    ['name' => 'Monitor', 'price' => 300.00],
    ['name' => 'Laptop', 'price' => 999.00],
];

$budgetFilter = new PriceFilter(0, 100);
$premiumFilter = new PriceFilter(200, 1000);

$budgetProducts = $budgetFilter($products);
$premiumProducts = $premiumFilter($products);

print_r($budgetProducts);
// Output: Mouse, Keyboard

print_r($premiumProducts);
// Output: Monitor, Laptop

__clone() - Object Cloning

This method is called when an object is cloned using the clone keyword. It's useful for handling deep copies of objects with references.

<?php

class Document
{
    public DateTime $createdAt;
    public ?DateTime $modifiedAt = null;

    public function __construct(
        public string $title,
        public string $content
    ) {
        $this->createdAt = new DateTime();
    }

    public function __clone(): void
    {
        // Create a new DateTime for the clone
        $this->createdAt = new DateTime();
        $this->modifiedAt = null;
        $this->title = 'Copy of ' . $this->title;
    }
}

// Usage
$original = new Document('Report', 'This is the report content.');
sleep(1); // Wait a second to see different timestamps
$copy = clone $original;

echo $original->title . " - Created: " . $original->createdAt->format('H:i:s') . "\n";
// Output: Report - Created: 10:30:00

echo $copy->title . " - Created: " . $copy->createdAt->format('H:i:s') . "\n";
// Output: Copy of Report - Created: 10:30:01

__serialize() and __unserialize() - Modern Serialization

These methods (PHP 7.4+) provide more control over serialization than the older __sleep() and __wakeup().

<?php

class Session
{
    private string $sessionId;
    private array $data;
    private DateTime $expiresAt;
    private $connection; // Resource - cannot be serialized

    public function __construct(array $data = [])
    {
        $this->sessionId = bin2hex(random_bytes(16));
        $this->data = $data;
        $this->expiresAt = new DateTime('+1 hour');
        $this->connection = $this->createConnection();
    }

    private function createConnection()
    {
        // Simulate database connection
        return 'active_connection';
    }

    public function __serialize(): array
    {
        return [
            'sessionId' => $this->sessionId,
            'data' => $this->data,
            'expiresAt' => $this->expiresAt->format('Y-m-d H:i:s'),
        ];
        // Note: $connection is not included
    }

    public function __unserialize(array $data): void
    {
        $this->sessionId = $data['sessionId'];
        $this->data = $data['data'];
        $this->expiresAt = new DateTime($data['expiresAt']);
        $this->connection = $this->createConnection(); // Recreate connection
    }

    public function getData(): array
    {
        return $this->data;
    }
}

// Usage
$session = new Session(['user_id' => 123, 'role' => 'admin']);
$serialized = serialize($session);

// Later...
$restored = unserialize($serialized);
print_r($restored->getData());
// Output: Array ( [user_id] => 123 [role] => admin )

Real-World Example: Active Record Pattern

Let's combine multiple magic methods to create a simple Active Record implementation:

<?php

abstract class ActiveRecord
{
    protected array $attributes = [];
    protected array $original = [];
    protected static string $table;
    protected static string $primaryKey = 'id';

    public function __construct(array $attributes = [])
    {
        $this->fill($attributes);
        $this->original = $this->attributes;
    }

    public function __get(string $name): mixed
    {
        return $this->attributes[$name] ?? null;
    }

    public function __set(string $name, mixed $value): void
    {
        $this->attributes[$name] = $value;
    }

    public function __isset(string $name): bool
    {
        return isset($this->attributes[$name]);
    }

    public function __call(string $method, array $arguments): mixed
    {
        // Handle scope methods: scopeActive() can be called as active()
        $scopeMethod = 'scope' . ucfirst($method);
        if (method_exists($this, $scopeMethod)) {
            return $this->$scopeMethod(...$arguments);
        }

        throw new BadMethodCallException("Method {$method} does not exist.");
    }

    public static function __callStatic(string $method, array $arguments): mixed
    {
        // Handle static find methods
        if (str_starts_with($method, 'findBy')) {
            $column = strtolower(substr($method, 6));
            return static::where($column, $arguments[0]);
        }

        // Delegate to instance for scope methods
        return (new static())->$method(...$arguments);
    }

    public function __toString(): string
    {
        return json_encode($this->attributes, JSON_PRETTY_PRINT);
    }

    public function __debugInfo(): array
    {
        return [
            'table' => static::$table,
            'attributes' => $this->attributes,
            'isDirty' => $this->isDirty(),
        ];
    }

    public function fill(array $attributes): self
    {
        foreach ($attributes as $key => $value) {
            $this->attributes[$key] = $value;
        }
        return $this;
    }

    public function isDirty(): bool
    {
        return $this->attributes !== $this->original;
    }

    public static function where(string $column, mixed $value): string
    {
        return "SELECT * FROM " . static::$table . " WHERE {$column} = '{$value}'";
    }

    public function save(): string
    {
        if (isset($this->attributes[static::$primaryKey])) {
            return $this->update();
        }
        return $this->insert();
    }

    protected function insert(): string
    {
        $columns = implode(', ', array_keys($this->attributes));
        $values = "'" . implode("', '", array_values($this->attributes)) . "'";
        return "INSERT INTO " . static::$table . " ({$columns}) VALUES ({$values})";
    }

    protected function update(): string
    {
        $sets = [];
        foreach ($this->attributes as $key => $value) {
            if ($key !== static::$primaryKey) {
                $sets[] = "{$key} = '{$value}'";
            }
        }
        $id = $this->attributes[static::$primaryKey];
        return "UPDATE " . static::$table . " SET " . implode(', ', $sets) . " WHERE " . static::$primaryKey . " = '{$id}'";
    }
}

class Post extends ActiveRecord
{
    protected static string $table = 'posts';

    public function scopePublished(): string
    {
        return "SELECT * FROM " . static::$table . " WHERE published = 1";
    }
}

// Usage
$post = new Post([
    'title' => 'Hello World',
    'content' => 'This is my first post',
    'author' => 'John Doe'
]);

echo $post->title . "\n";           // __get(): Hello World
$post->published = true;             // __set()
echo $post->save() . "\n";           // INSERT INTO posts...

echo Post::findByAuthor('John') . "\n";  // __callStatic(): SELECT * FROM posts WHERE author = 'John'
echo Post::published() . "\n";            // __callStatic() -> scopePublished(): SELECT * FROM posts WHERE published = 1

echo $post . "\n";                   // __toString(): JSON output

var_dump($post);                     // __debugInfo(): Shows table, attributes, isDirty

Magic Methods Summary Table

Method Triggered When Common Use Case
__construct() Object is created Initialize properties, inject dependencies
__destruct() Object is destroyed Cleanup resources, close connections
__get() Reading inaccessible property Dynamic properties, lazy loading
__set() Writing to inaccessible property Validation, computed properties
__isset() isset() or empty() on inaccessible property Property existence checking
__unset() unset() on inaccessible property Property removal handling
__call() Calling inaccessible instance method Method chaining, dynamic methods
__callStatic() Calling inaccessible static method Static factory methods, facades
__toString() Object used as string String representation, debugging
__invoke() Object called as function Single-action classes, callbacks
__clone() Object is cloned Deep copying, resetting state
__serialize() Object is serialized Custom serialization format
__unserialize() Object is unserialized Restore connections, recalculate values
__debugInfo() var_dump() is called Custom debug output

Best Practices and Tips

  • Don't Overuse Magic Methods: They can make code harder to understand and debug. Use them when they provide clear benefits.
  • Document Your Magic: Since IDEs can't always detect magic methods, use PHPDoc annotations like @property and @method.
  • Type Declarations: Always use proper return types and parameter types for better code quality.
  • Performance Consideration: Magic methods are slightly slower than direct property/method access. For performance-critical code, consider alternatives.
  • Throw Meaningful Exceptions: When a magic method can't handle a request, throw descriptive exceptions.

Conclusion

PHP Magic Methods are powerful tools that allow you to create more flexible and expressive classes. From basic initialization with __construct() to advanced patterns using __call() and __get(), these methods open up possibilities for cleaner APIs and more maintainable code. Start with the basics like constructors and __toString(), then gradually explore more advanced methods as your needs grow. Remember to use them judiciously - with great power comes great responsibility! Happy coding!