In PHP, when exactly are __get, __set, __isset and __unset called, and what breaks if a class implements only __get?
answer
- inaccessible means undeclared or not visible
- object context only, never static
- isset() and empty() ask __isset
- no __isset: isset() says false
- indirect modification notice on arrays
basics
~20 sThey 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 sPHP 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
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
Recall the four methods and that they only fire for properties that are missing or not visible from the caller.
Explain why __isset is needed for isset() and empty(), how ?? differs, and why array writes through __get need a reference.
Decide when magic properties are worth their cost and make them fail loudly on unknown names instead of returning null.
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