skip to content

Magic Methods

Magic methods such as __get, __call and __toString let the engine route missing members, string casts and calls into your code. Interviewers ask what each one intercepts and what it hides from tools.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In PHP, when exactly are __get, __set, __isset and __unset called, and what breaks if a class implements only __get?

level: middleimportance: must knowfreq 66%

answer

  1. inaccessible means undeclared or not visible
  2. object context only, never static
  3. isset() and empty() ask __isset
  4. no __isset: isset() says false
  5. indirect modification notice on arrays

basics

~20 s

They run only for inaccessible properties: undeclared ones, or ones not visible from the caller's scope. __get reads, __set writes, __isset answers isset() and empty(), __unset handles unset(). With only __get, isset() on a magic property returns false.

solid answer

~40 s

PHP calls the property-overloading methods only when code touches an **inaccessible** property: one that is not declared, or is private/protected and read from outside. A declared public property never reaches them. `__get($name)` handles reads, `__set($name, $value)` writes (its return value is ignored), `__isset($name)` answers `isset()` and `empty()`, and `__unset($name)` handles `unset()`. They work in object context only. Implementing only `__get` is the classic bug: `isset($settings->timeout)` returns `false` because there is no `__isset` to ask, so templates and guard clauses think the key is missing. Another trap: `$settings->db['host'] = 'x'` modifies a copy returned by `__get` and PHP emits `Indirect modification of overloaded property ... has no effect` unless `__get` returns by reference. Inside `__get`, reading `$this->sameName` does not recurse; it accesses the real property.

code

php · 25 lines
php
<?php
declare(strict_types=1);

final class Settings
{
    public function __construct(private array $values) {}

    public function __get(string $name): mixed
    {
        if (!array_key_exists($name, $this->values)) {
            throw new OutOfBoundsException("Unknown setting: $name");
        }
        return $this->values[$name];
    }

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

    public function __set(string $name, mixed $value): void { throw new LogicException('read-only'); }
}

$s = new Settings(['timeout' => 30]);
var_dump($s->timeout, isset($s->timeout), isset($s->retries)); // int(30) bool(true) bool(false)

go deeper

for a junior

Recall the four methods and that they only fire for properties that are missing or not visible from the caller.

for a middle

Explain why __isset is needed for isset() and empty(), how ?? differs, and why array writes through __get need a reference.

for a senior

Decide when magic properties are worth their cost and make them fail loudly on unknown names instead of returning null.

for a principal

Set a rule for where magic properties are allowed in a codebase, weighing flexibility against tooling, refactoring and review cost.

## What "overloading" means in PHP In most languages *overloading* means several methods with one name and different parameters. PHP uses the word differently: **property overloading** lets a class intercept access to properties that do not exist or cannot be seen, and handle them in code. Four magic methods do it: | Method | Triggered by | Signature when typed | |---|---|---| | `__get` | reading `$obj->name` | `__get(string $name): mixed` | | `__set` | writing `$obj->name = $v` | `__set(string $name, mixed $value): void` | | `__isset` | `isset($obj->name)`, `empty($obj->name)` | `__isset(string $name): bool` | | `__unset` | `unset($obj->name)` | `__unset(string $name): void` | All four should be `public` (anything else draws a warning), must not be `static`, and cannot take parameters by reference. ## When the engine calls them The manual's term is **inaccessible property**: a property that is either **not declared** or **not visible** from the calling scope. Consequences: - A declared `public` property is read and written directly; the magic methods never see it. - A `private` property is accessed directly from inside the class, but from outside the same access goes to `__get`/`__set`. That is how a class can keep a private array and expose its entries as properties. - Property overloading works only in **object context**. `Settings::$timeout` on an undeclared static property throws; no magic method is consulted. - Inside `__get('timeout')`, reading `$this->timeout` does **not** call `__get` again. PHP guards against recursion per property name and performs a direct access instead, which gives a warning and `null` if the property is undeclared. - In a chained assignment `$a = $obj->b = 8;`, `__set` runs but `__get` does not; `$a` receives 8 directly. ## The settings object, done right A settings class that exposes config keys as properties usually stores them in a private array: 1. `__get` returns the value or throws for an unknown key, so a typo fails loudly instead of returning `null`. 2. `__isset` reports whether the key exists **and** is not `null`, matching what `isset()` means for real properties. 3. `__set` and `__unset` either write to the array or throw, if the settings are meant to be read-only. ## Compound writes and unset properties Overloading methods can trigger one another. For an inaccessible property, `$obj->hits++` or `$obj->log .= 'x'` is a **read followed by a write**: PHP calls `__get('hits')`, computes the new value, then calls `__set('hits', $new)`. A class that implements `__set` but not `__get` therefore gets an undefined-property warning on every increment and starts counting from `null`, even though plain assignment works. A declared property can also become magic. After `unset($this->cache)` on a declared property, later reads of `$obj->cache` from any scope find no value and go to `__get`. Older lazy-loading code used this trick to compute a property on first access; PHP 8.4 lazy objects and property hooks now cover that need without the surprise. ## What breaks with only `__get` The engine does not infer `isset()` from `__get`. Without `__isset`: - `isset($settings->timeout)` is `false` for every magic key, even when `__get` would return a value. - `empty($settings->timeout)` is `true`, for the same reason. - The null coalescing operator is different: `$settings->timeout ?? 30` asks `__isset` first when it exists, and falls through to `__get` directly when it does not. So `??` can "work" while `isset()` in the same codebase says the key is missing, which is a confusing inconsistency to debug. ## Indirect modification `__get` returns a **value**. For an array held behind it, `$settings->db['host'] = 'replica';` asks `__get('db')` for the array, then tries to write into that temporary copy. PHP emits `Notice: Indirect modification of overloaded property Settings::$db has no effect` and nothing changes. Two ways out: - declare `public function &__get(string $name): mixed` so it returns a **reference** into the stored array; - or stop exposing nested arrays and offer a method such as `withDatabaseHost()`. Objects returned by `__get` do not have this problem, because an object value is a handle to the same object. ## Trade-offs Magic properties are flexible but invisible: IDEs and static analysers cannot see the property names or types without extra docblock tags, and every access is a method call. For a fixed set of keys, declared (often `readonly`) properties or PHP 8.4 property hooks are usually the better tool; keep `__get` for genuinely dynamic keys.

  • In PHP, is __get called when code inside the class reads one of its own private properties?
    No. From inside the class a private property is accessible, so the read is direct. `__get` runs only when the property is undeclared or not visible from the calling scope, which is why the same `$obj->secret` can be direct inside the class and magic outside it.
  • In PHP, how does $obj->missing ?? 'default' behave when the class has __get but no __isset?
    The `??` operator asks `__isset` first when the class defines it. Without `__isset`, PHP calls `__get` directly and uses the default only if `__get` returns `null`. `isset($obj->missing)`, by contrast, returns `false` without calling `__get`, so the two checks disagree.
  • In PHP, how do you let $settings->db['host'] = 'x' modify an array stored behind __get?
    Declare `public function &__get(string $name): mixed` and return a reference to the stored element. Without the `&`, `__get` returns a copy, the write lands in a temporary, and PHP emits the `Indirect modification of overloaded property` notice.

saying these in an interview costs you the question

  • Says __get runs for every property read, including declared public ones
  • Expects isset() to call __get when __isset is missing
  • Declares __get static to intercept static property access
  • Thinks $obj->arr['k'] = 1 through __get updates the stored array
  • Believes reading $this->name inside __get recurses into __get forever
open as a page

In PHP, what do the __toString and __invoke magic methods let an object do, and when does PHP call each?

level: juniorimportance: should knowfreq 50%

basics

~20 s

__toString lets an object act as a string: PHP calls it for echo, concatenation, interpolation, (string) casts and coercive string parameters. __invoke lets an object be called like a function, $obj($x), so it passes callable checks.

open as a page

In PHP, when do __call and __callStatic run, and which one handles self::missing() inside an instance method?

level: middleimportance: should knowfreq 52%

basics

~20 s

__call runs when an inaccessible method is called on an object; __callStatic when one is called statically, like Settings::missing(). A static-style call such as self::missing() from inside an instance method goes to __call, because an object is available.

open as a page

A PHP settings class exposes config keys as properties through __get; what does that hide from IDEs and static analysers, and how would you redesign it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Magic __get hides every property name and type from tools, so typos, renames and wrong types surface only at runtime. For known keys use typed readonly properties or 8.4 property hooks; if magic stays, document it with @property-read tags.

open as a page

In PHP, what do the __debugInfo and __set_state magic methods customise, and which built-in functions call them?

level: middleimportance: nice to knowfreq 14%

basics

~20 s

__debugInfo returns the array var_dump and print_r show for an object, for example to hide a secret. __set_state is a static factory that var_export's output calls when that code is evaluated, rebuilding the object from an array of properties.

open as a page

In PHP 8, which rules does the engine enforce when a class declares magic methods such as __get, __call or __toString?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

Magic methods have fixed parameter counts, no by-reference parameters, and only __callStatic and __set_state may be static. Since PHP 8.0, declared types must also match the documented signatures. A non-public magic method just triggers a warning.

open as a page