Magic Methods
What Are Magic Methods?
In simple terms: Magic methods are special methods that PHP calls automatically in certain situations — when an object is created, converted to a string, called like a function, cloned, or when code accesses a property or method that does not exist. By defining them, you control what happens in those situations.
Technically: Magic methods are methods whose names begin with two underscores (__). PHP reserves all such names. You never call them directly in normal code; the engine invokes them in response to specific operations. All magic methods except __construct(), __destruct(), and __clone() must be public, and their signatures are checked by PHP.
Overview
| Method | Called When |
|---|---|
__construct() / __destruct() | An object is created / destroyed (see PHP Constructor, PHP Destructor) |
__toString() | The object is used as a string |
__get($name) / __set($name, $value) | Reading / writing an inaccessible or undeclared property |
__isset($name) / __unset($name) | isset() / unset() on such a property |
__call($name, $args) | Calling an inaccessible or undefined instance method |
__callStatic($name, $args) | Calling an inaccessible or undefined static method |
__invoke(...) | The object is called like a function |
__clone() | The object is cloned (see Object Cloning) |
__debugInfo() | var_dump() or print_r() inspects the object |
__serialize() / __unserialize() | The object is serialized / unserialized (see Object Serialization) |
__set_state() | var_export() output is evaluated |
__toString(): Objects as Strings
<?php
final class Money
{
public function __construct(private int $paise) {}
public function __toString(): string
{
return '₹' . number_format($this->paise / 100, 2);
}
}
$total = new Money(129900);
echo "Total: $total\n";
echo 'Length: ' . strlen((string) $total) . "\n";
var_dump($total instanceof Stringable);
Output
Total: ₹1,299.00
Length: 11
bool(true)
__toString()runs whenever the object is used in a string context:echo, interpolation, concatenation, or a(string)cast.- Since PHP 8.0, classes with
__toString()automatically implement theStringableinterface, so they can be passed to parameters typedstring|Stringable. - The length is 11 bytes because
₹takes 3 bytes in UTF-8.
Use __toString() for a natural textual representation (money, email addresses, identifiers), not for complex formatting that needs options.
Property Overloading: __get, __set, __isset, __unset
These methods are called when code accesses a property that is not declared or not accessible from the calling scope. They allow objects to expose data stored in other ways — for example, in an array:
<?php
final class Settings
{
private array $values = ['theme' => 'light', 'language' => 'en'];
public function __get(string $name): mixed
{
$this->assertKnown($name);
return $this->values[$name];
}
public function __set(string $name, mixed $value): void
{
$this->assertKnown($name);
$this->values[$name] = $value;
}
public function __isset(string $name): bool
{
return isset($this->values[$name]);
}
public function __unset(string $name): void
{
throw new LogicException('Settings cannot be removed.');
}
private function assertKnown(string $name): void
{
if (!array_key_exists($name, $this->values)) {
throw new OutOfBoundsException("Unknown setting: $name");
}
}
}
$settings = new Settings();
$settings->theme = 'dark';
echo $settings->theme . "\n";
var_dump(isset($settings->language), isset($settings->fontSize));
try {
$settings->fontSize = 14;
} catch (OutOfBoundsException $e) {
echo $e->getMessage() . "\n";
}
Output
dark
bool(true)
bool(false)
Unknown setting: fontSize
How It Works
$settings->theme = 'dark'— there is no declaredthemeproperty, so PHP calls__set('theme', 'dark').$settings->themecalls__get('theme').isset($settings->language)calls__isset(). Without__isset(),isset()would always returnfalsefor such virtual properties — a common bug.- Unknown names are rejected, preventing silent typos.
Note: In PHP 8.4, property hooks (see Property Hooks (PHP 8.4)) cover many cases that previously required
__get()/__set()— validation or computed values for declared properties — with better performance and IDE support.
Method Overloading: __call and __callStatic
__call() handles calls to methods that do not exist (or are inaccessible). It receives the method name and the arguments as an array:
<?php
final class UserRepository
{
private array $users = [
['name' => 'Asha', 'city' => 'Chennai'],
['name' => 'Bala', 'city' => 'Madurai'],
['name' => 'Charu', 'city' => 'Chennai'],
];
public function __call(string $method, array $arguments): array
{
if (str_starts_with($method, 'findBy') && count($arguments) === 1) {
$field = lcfirst(substr($method, 6));
return array_values(array_filter(
$this->users,
fn(array $user): bool => ($user[$field] ?? null) === $arguments[0]
));
}
throw new BadMethodCallException("Method $method() does not exist.");
}
}
$repository = new UserRepository();
echo implode(', ', array_column($repository->findByCity('Chennai'), 'name')) . "\n";
try {
$repository->deleteEverything();
} catch (BadMethodCallException $e) {
echo $e->getMessage() . "\n";
}
Output
Asha, Charu
Method deleteEverything() does not exist.
findByCity('Chennai') is not declared; __call() interprets the name, extracts the field (city), and filters the users. Unknown methods must throw BadMethodCallException — otherwise typos would silently return nothing.
__callStatic() does the same for static calls:
<?php
final class Log
{
public static function __callStatic(string $level, array $arguments): void
{
echo strtoupper($level) . ': ' . $arguments[0] . "\n";
}
}
Log::warning('Disk usage at 90%');
Log::info('Backup completed');
Output
WARNING: Disk usage at 90%
INFO: Backup completed
Laravel's "facades" and Eloquent's dynamic query methods are built on __callStatic() and __call().
__invoke(): Callable Objects
<?php
final class GstCalculator
{
public function __construct(private float $ratePercent) {}
public function __invoke(float $amount): float
{
return round($amount * $this->ratePercent / 100, 2);
}
}
$gst18 = new GstCalculator(18);
echo $gst18(1299) . "\n";
print_r(array_map($gst18, [100, 250]));
Output
233.82
Array
(
[0] => 18
[1] => 45
)
An object with __invoke() can be called like a function and passed wherever a callable is expected (see PHP Callback Functions).
__debugInfo(): Controlling Debug Output
<?php
final class User
{
public function __construct(public string $name, private string $passwordHash) {}
public function __debugInfo(): array
{
return ['name' => $this->name, 'passwordHash' => '[hidden]'];
}
}
var_dump(new User('Asha', '$2y$10$abcdefghijklmnopqrstuv'));
Output
object(User)#1 (2) {
["name"]=>
string(4) "Asha"
["passwordHash"]=>
string(8) "[hidden]"
}
__debugInfo() decides what var_dump() and print_r() show, which helps keep secrets out of debug output. It does not affect var_export(), json_encode(), or serialization, so sensitive data must still be handled carefully there.
Costs of Magic
| Drawback | Explanation |
|---|---|
| Hidden API | IDEs and static analysis cannot see virtual properties and methods (use @property and @method docblocks to describe them) |
| Typos become runtime behavior | Without validation, $obj->nmae silently returns null |
| Performance | Magic calls are slower than direct property access or method calls |
| Debugging | Behavior is harder to trace |
Use magic methods where they provide real value — string conversion, callable objects, safe debug output, or carefully validated proxies — and prefer explicit properties and methods elsewhere.
Common Mistakes
__get() Without __isset()
Effect: isset($obj->virtualProperty) and empty() return wrong results.
Fix: Implement __isset() alongside __get().
__call() That Ignores Unknown Methods
Effect: Typos in method names fail silently.
Fix: Throw BadMethodCallException for names you do not handle.
Calling Magic Methods Directly
Effect: Code like $obj->__toString() bypasses the intended usage.
Fix: Use the operation that triggers them ((string) $obj).
Using Magic to Avoid Declaring Properties
Effect: Classes without a visible structure.
Fix: Declare properties; use hooks (PHP 8.4) for validation.
Best Practices
- Implement
__toString()for value objects with a natural string form. - Use
__invoke()for single-purpose callable objects. - Use
__debugInfo()to hide sensitive data from debug output. - Reserve
__get/__set/__callfor proxies and dynamic APIs, always with validation and documentation. - Prefer explicit code and PHP 8.4 property hooks over property overloading.
Official reference: https://www.php.net/manual/en/language.oop5.magic.php