<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · JUNIOR ЧАСТО ПИТАЮТЬ

Які магічні методи є в PHP і коли ними користуватись, а коли ні?

Магічні методи - це хуки, які движок кличе замість помилки: `__get`/`__set`/`__call` підміняють доступ до неоголошених членів класу, `__invoke` робить обʼєкт callable, `__toString` дає рядкове представлення, `__construct` і `__destruct` тримають життєвий цикл. За `__get`/`__set`/`__call` платять автодоповненням, типами й рефакторингом, тому їх лишають для проксі та ORM, а в звичайному коді оголошують властивості й методи явно.

Назвіть магічні методи, які ви реально писали руками, і навіщо вони там були.
Чому IDE не підказує властивості моделі Eloquent, хоча вони очевидно є?
Чим `__get` гірший за звичайний геттер, якщо результат однаковий?
Чому `$obj->items[] = 'x'` нічого не додає, хоча `__get` повертає масив?
magic-methods __get __call __invoke __toString oop

Магічні методи - це точки, де движок передає керування вашому коду замість того, щоб кинути помилку або застосувати типову поведінку. Їх близько півтора десятка: __construct, __destruct, __get, __set, __isset, __unset, __call, __callStatic, __invoke, __toString, __clone, __serialize/__unserialize (з PHP 7.4 замість __sleep/__wakeup), __debugInfo, __set_state. Ключове слово в описі __get - «недоступна»: він викликається тільки тоді, коли властивості на обʼєкті немає або вона закрита від поточного контексту. Публічну оголошену властивість він не перехопить ніколи, а от після unset() оголошена властивість зникає з обʼєкта, і наступне читання вже піде через магію. З PHP 8.0 сигнатури перевіряються при компіляції: private function __get() або статичний __toString() дають фатальну помилку одразу, а не дивну поведінку в рантаймі.

Проблема __get/__set не в швидкості, хоча зайвий виклик методу там теж є. Вони видаляють інформацію з коду. Повернений тип - mixed, назви властивостей існують тільки у вигляді рядків усередині методу, і жоден інструмент їх не бачить: IDE не підказує, «Find usages» не знаходить, rename не перейменовує, PHPStan не має що перевіряти. Приклад у коді показує наслідок у чистому вигляді: $config->timeuot віддає null, і помилку ви знайдете на проді. Лікують це @property-анотаціями в PHPDoc класу, і саме тому в екосистемі Laravel живе laravel-ide-helper, який генерує сотні @property для моделей Eloquent. Анотації працюють, але це ручна копія схеми, яка застаріває тихо.

Окремий сюрприз чекає на масивах. $config->limits['db'] = 20 вимагає посилання на реальні дані, а __get повертає значення, тому PHP змінює тимчасову копію і повідомляє Indirect modification of overloaded property ... has no effect. Це повідомлення легко не помітити в продакшені з вимкненим виводом помилок. Теоретично рятує public function &__get(), який повертає посилання, але тоді ви віддаєте назовні внутрішній стан і втрачаєте контроль над записом. Простіше дати класу явний метод setLimit(string $key, int $value).

__call і __callStatic живуть за тими самими правилами, але мають чіткішу нішу: проксі, декоратор, фасад, query builder зі згенерованими методами. Умова придатності одна: невідомий виклик має закінчуватися BadMethodCallException, інакше опечатка в назві методу стає тихим null. Пара __invoke і __toString стоїть окремо, бо їхній контракт описаний у самій мові. Обʼєкт з __invoke проходить is_callable() і лягає в array_map() як звичайна функція, а клас з __toString з PHP 8.0 автоматично реалізує інтерфейс Stringable, тож його можна вимагати в типі параметра. Викидати винятки з __toString дозволено з PHP 7.4, до того це давало фатальну помилку. Ці два методи роблять код коротшим, нічого не приховуючи від інструментів.

Явний метод виграє щоразу, коли ви знаєте набір полів на етапі написання коду. Оголошена типізована властивість дає перевірку типу, автодоповнення й помилку компіляції при друкарці; геттер дає місце для документації й контрактів. З PHP 8.4 зявилися property hooks і lazy objects, які покривають більшість старих причин тягнутися до __get: обчислювана властивість, валідація при записі, ліниве завантаження. Магії лишається територія, де набір членів справді динамічний: рядок з бази, конфіг з довільного YAML, обгортка над чужим API. __constructі__destructу цьому списку стоять осібно, бо їх пишуть усі, проте деструктор варто сприймати як прибиральника після аварії: момент і порядок його виклику не гарантовані, тому все, що має статися в конкретній точці програми, пишеться явно черезfinally`.

declare(strict_types=1);

final class Config
{
    /** @var array<string, mixed> */
    private array $values = [];

    public function __get(string $name): mixed
    {
        return $this->values[$name] ?? null;   // друкарка в назві поверне null
    }

    public function __set(string $name, mixed $value): void
    {
        $this->values[$name] = $value;         // ні типу, ні валідації
    }

    public function __isset(string $name): bool
    {
        return isset($this->values[$name]);    // без цього isset() завжди false
    }
}

$config = new Config();
$config->timeout = 30;

echo $config->timeuot;                 // null: ні винятку, ні підказки від IDE
var_dump(isset($config->timeout));     // true, і лише завдяки __isset

$config->limits = ['db' => 10];
$config->limits['db'] = 20;            // Indirect modification of overloaded
                                       // property: запис просто зникає

// __invoke: контракт, який розуміє вся мова
final class PriceFormatter
{
    public function __construct(private string $currency = 'грн') {}

    public function __invoke(int $cents): string
    {
        return number_format($cents / 100, 2, ',', ' ') . " {$this->currency}";
    }
}

$format = new PriceFormatter();
echo $format(129_900);                       // 1 299,00 грн
print_r(array_map($format, [100, 250]));     // працює як звичайний callable
Що `__get`/`__set` спрацьовують лише для недоступних або неоголошених властивостей, а не для кожного звернення: публічна оголошена властивість їх не тригерить ніколи.
Що для аналізатора результат `__get` має тип `mixed`, тому автодоповнення, rename і перевірки PHPStan живуть тільки на `@property`-анотаціях, які треба дописувати руками.
Що `__isset` обовʼязковий у парі з `__get`, інакше `isset()`, `empty()` і `??` на магічних властивостях брешуть.
Що `__invoke` і `__toString` мають контракт у самій мові (перевірка `is_callable()`, інтерфейс `Stringable`, який PHP 8.0 додає автоматично), тому виглядають доречно там, де `__get` виглядає підозріло.
Що `__destruct` не дає гарантій щодо моменту й порядку виклику, тому комміт транзакції чи запис у файл туди не кладуть.
Оголосити `__get`/`__set` над приватним масивом «щоб було зручно» і отримати клас, у якому `$config->timeuot` тихо повертає `null`.
Думати, що `__get` перехоплює доступ до вже оголошеної публічної властивості: він викликається лише коли властивості немає або вона недоступна ззовні.
Написати `__get` без `__isset` і потім не розуміти, чому `isset($row->name)` дає `false`, а `$row->name ?? 'default'` завжди підставляє default.
Робити `$obj->list[] = 1` над магічною властивістю: PHP повідомить `Indirect modification of overloaded property ... has no effect`, і запис просто зникне, бо `__get` повертає значення, а не посилання.
Комітити транзакцію або скидати буфер у `__destruct`: порядок руйнування обʼєктів на завершенні скрипта не визначений, а при фатальній помилці деструктор може не дійти до кінця.
Ловити в `__call` будь-яку назву і повертати `null` замість `BadMethodCallException`: помилка в назві методу перетворюється на мовчазний баг.
ПОРАДА

Розділіть магію на дві купки вголос: `__invoke`, `__toString`, `__clone`, `__serialize` реалізують контракт, який движок уже описав, і код з ними лишається читабельним; `__get`, `__set`, `__call` вигадують API, якого в класі немає, тому їх виправдовує тільки роль проксі, декоратора чи ORM. Додайте фразу «магічну властивість не перейменує жоден рефакторинг» - це показує, що ви думали про підтримку, а не тільки про синтаксис.

оновлено 8 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Запис у елемент магічної властивості вимагає посилання на реальні дані. `__get` віддає копію значення, PHP модифікує тимчасове значення і викидає його, повідомляючи `Indirect modification of overloaded property ... has no effect`. Обійти можна оголосивши `public function &__get(string $name): mixed`, але це рідко варте втрачених гарантій.