PHPMagic Methods (__get, __set, __call...)

Magic Methods (__get, __set, __call...)

PHP reserves a small set of method names, all starting with a double underscore, that the engine calls automatically at specific moments — when you read an undefined property, call an undefined method, treat an object like a string, or invoke it like a function. These "magic methods" let a class intercept operations that would otherwise fail or behave in a fixed way, and hand you a chance to define custom behavior instead. They are the mechanism behind a lot of ORM and framework "magic," but they are also easy to overuse in ways that make code harder to trace. This page walks through the most common ones with small, self-contained examples.

__get and __set: intercepting property access

__get($name) fires when code reads a property that is either undefined or inaccessible from the calling scope. __set($name, $value) fires under the same conditions but for writes. Together they let a class present a public-looking property interface while actually routing reads and writes through your own logic — useful for lazy-loading data, validating input, or storing values in an internal array instead of as real properties.

A class that stores everything in an internal array

PHP
<?php
class Config
{
    private array $data = [];

    public function __get($name)
    {
        echo "Reading '{$name}'\n";
        return $this->data[$name] ?? null;
    }

    public function __set($name, $value)
    {
        echo "Writing '{$name}' = {$value}\n";
        $this->data[$name] = $value;
    }
}

$config = new Config();
$config->timeout = 30;   // __set fires: 'timeout' is not a real property
echo $config->timeout, "\n"; // __get fires
Writing 'timeout' = 30
Reading 'timeout'
30

Notice that __get and __set only run when the property is genuinely missing or not visible from outside the class. If Config declared a real public $timeout property, PHP would read and write it directly and these magic methods would never be called at all — they are a fallback, not an interceptor for every access.

__call and __callStatic: intercepting method calls

__call($name, $arguments) fires when code calls an instance method that doesn't exist (or isn't visible). __callStatic($name, $arguments) is the equivalent for static calls. Both receive the method name as a string and the arguments as an ordered array, which makes them well suited for building small fluent APIs or forwarding calls to another object without writing one wrapper method per delegate method.

Forwarding unknown method calls to a logger

PHP
<?php
class EventDispatcher
{
    public function __call($name, $arguments)
    {
        $joined = implode(', ', $arguments);
        return "Called '{$name}' with arguments: [{$joined}]";
    }

    public static function __callStatic($name, $arguments)
    {
        return "Statically called '{$name}'";
    }
}

$dispatcher = new EventDispatcher();
echo $dispatcher->onUserRegistered('alice', 42), "\n";
echo EventDispatcher::boot(), "\n";
Called 'onUserRegistered' with arguments: [alice, 42]
Statically called 'boot'

EventDispatcher never declares an onUserRegistered or boot method, yet both calls succeed because __call and __callStatic catch anything that doesn't otherwise resolve. This is exactly the technique many query builders use to turn calls like where('name')->orderBy('id') into real behavior without predefining every possible method name.

__toString: controlling string conversion

__toString() fires whenever an object is used somewhere a string is expected — inside echo, string concatenation with ., or interpolation inside double-quoted strings. It must return a string, and it takes no arguments. Without it, converting an object to a string throws a recoverable error.

A Money value object that prints itself sensibly

PHP
<?php
class Money
{
    public function __construct(
        private int $cents,
        private string $currency,
    ) {
    }

    public function __toString(): string
    {
        return sprintf('%.2f %s', $this->cents / 100, $this->currency);
    }
}

$price = new Money(1999, 'USD');
echo "Price: {$price}\n";
echo 'Concatenated: ' . $price . "\n";
Price: 19.99 USD
Concatenated: 19.99 USD
__invoke: making an object callable

__invoke() fires when an object is used as if it were a function — written as $object(...). This turns the object into what's often called an "invokable" and lets it be passed anywhere a callable is expected, such as array_map, usort, or a middleware pipeline, while still carrying internal state or configuration like a normal object.

An invokable object used as a callback

PHP
<?php
class Doubler
{
    public function __invoke($value)
    {
        return $value * 2;
    }
}

$double = new Doubler();
echo $double(21), "\n"; // called directly

$numbers = [1, 2, 3];
print_r(array_map($double, $numbers)); // passed as a callable
42
Array
(
    [0] => 2
    [1] => 4
    [2] => 6
)
__isset and __unset: intercepting isset() and unset()

__isset($name) fires when isset() or empty() is used on an undefined or inaccessible property, and must return a bool. __unset($name) fires when unset() targets such a property. These exist mainly to keep isset()/empty()/unset() behaving consistently with a class that already uses __get/__set — without them, isset($config->timeout) would not correctly reflect what __get would actually return.

Keeping isset()/unset() consistent with __get/__set

PHP
<?php
class Bag
{
    private array $data = ['color' => 'red'];

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

    public function __set($name, $value)
    {
        $this->data[$name] = $value;
    }

    public function __isset($name)
    {
        return isset($this->data[$name]);
    }

    public function __unset($name)
    {
        unset($this->data[$name]);
    }
}

$bag = new Bag();
var_dump(isset($bag->color));
unset($bag->color);
var_dump(isset($bag->color));
bool(true)
bool(false)

Without a __isset implementation, isset($bag->color) would fall back to PHP's default behavior for a missing declared property and report false even while color clearly exists inside $data — a subtle inconsistency that's easy to miss until something built on top of Bag starts behaving strangely.

Magic methods trade discoverability for flexibility
Every magic method you add is a code path that static analyzers, IDE autocompletion, and "find usages" tooling generally cannot see through — `$config->timeout` looks like a normal property access, but the IDE has no way to know it actually flows through `__get` and into a private array. Overusing `__get`/`__set`/`__call` across a large codebase makes it genuinely harder for both tools and teammates to answer "where does this value actually come from?" Reach for magic methods when they solve a real problem — a value object, a small adapter, a well-scoped builder — not as a default way of writing classes.
  • __get/__set only fire for undefined or inaccessible properties, not for real declared public ones.

  • __call/__callStatic receive the method name as a string and the arguments as an array, which is what makes generic forwarding possible.

  • __toString must return a string and has no arguments; it fires on any implicit string conversion.

  • __invoke lets an object be used anywhere a callable is expected, such as array_map or usort.

  • __isset/__unset keep isset()/empty()/unset() consistent with a class that already customizes __get/__set.

Tip
If you find yourself defining `__get` and `__set` mainly to avoid declaring properties up front, consider whether a plain typed property list, or a small explicit accessor method per field, would serve just as well. Explicit methods are slightly more code to write, but every one of them shows up in autocompletion and "find usages," which pays for itself the first time someone else has to debug your class at 2am.