Какие плюсы и минусы у магических методов __get() и __set() в PHP?

«Какие плюсы и минусы у магических методов __get() и __set() в PHP?» — вопрос из категории PHP Core, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Плюсы:

  • Динамическое управление свойствами: Позволяют реализовать «виртуальные» свойства, значения которых вычисляются на лету или хранятся в нестандартном месте (например, в массиве $data).
  • Инкапсуляция и валидация: Можно добавить логику при чтении или записи свойства — проверку типов, санитизацию, логгирование, вычисление производных значений.
  • Обратная совместимость: Можно добавить геттер/сеттер для приватного поля, не меняя публичный интерфейс класса, если код уже использовал прямое обращение к свойству.

Минусы:

  • Производительность: Вызов магических методов значительно медленнее прямого доступа к свойству или вызова обычного метода.
  • Отсутствие статического анализа: IDE и статические анализаторы кода (например, Psalm, PHPStan) не могут отследить типы и существование таких свойств, что ухудшает автодополнение и выявление ошибок.
  • Скрытие ошибок: Опечатка в имени свойства не вызовет ошибку, а тихо вызовет __get(), что может привести к трудноуловимым багам.
  • Нарушение принципа наименьшего удивления: Поведение класса становится менее предсказуемым для других разработчиков.

Практический пример с валидацией:

class User {
    private array $attributes = [];
    private array $allowedAttributes = ['email', 'age'];

    public function __get(string $name): mixed {
        if (!in_array($name, $this->allowedAttributes)) {
            trigger_error("Undefined property: $name", E_USER_NOTICE);
            return null;
        }
        return $this->attributes[$name] ?? null;
    }

    public function __set(string $name, mixed $value): void {
        if ($name === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email format");
        }
        if ($name === 'age' && (!is_int($value) || $value < 0)) {
            throw new InvalidArgumentException("Age must be a positive integer");
        }
        $this->attributes[$name] = $value;
    }

    // Явные геттеры/сеттеры для статического анализа (опционально, но рекомендуется)
    public function getEmail(): ?string { return $this->__get('email'); }
    public function setEmail(string $email): void { $this->__set('email', $email); }
}

$user = new User();
$user->email = 'valid@example.com'; // Вызовет __set() с валидацией
// $user->email = 'invalid-email'; // Выбросит InvalidArgumentException
// $user->undefinedProp = 'test'; // Вызовет notice в __set