Could we help you? Please click the banners. We are young and desperately need the money
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.
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().
Magic methods provide several powerful capabilities:
Let's explore each magic method with practical examples.
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
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
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() 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'
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
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
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
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 )
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
| 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 |
@property and @method.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!