Магия PHP на примере Eloquent Model

1. Введение

По мере опыта PHP программист сталкивается с магическими методами, но бывает сложно сразу понять их потенциал.

Например, работая с моделью Laravel, можно обратить внимание на методы и свойства, которых явно нет:

$user = User::ative()->first();
echo $user->name;

Только с опытом, когда погружаешься в исходники фреймворка, начинаешь понимать как используют магию профессионалы.

В данной статье предлагаю рассмотреть Eloquent Model и понять какие магические методы используются в её основе.

Примеры кода на Laravel 5.8, но принципы работы актуальны и для новых версий.

2. Инициализация и представление

2.1. __construct()

Инициализирует модель и обрабатывает начальные данные.

public function __construct(array $attributes = [])
{
    $this->bootIfNotBooted();      // Загрузка boot-методов
    $this->initializeTraits();     // Инициализация трейтов
    $this->syncOriginal();         // Сохранение оригинальных значений
    $this->fill($attributes);      // Заполнение атрибутов
}

Конструктор гарантирует, что модель всегда находится в корректном состоянии, независимо от способа создания.

$user = new User();
// или
$user = new User(['name' => 'John', 'email' => 'john@example.com']);

2.2. __toString()

Преобразовывает модель в строку (JSON-представление).

public function __toString()
{
    return $this->toJson();
}

На практике метод делает работу с моделью более удобной и предсказуемой, особенно при отладке и логировании.

$user = User::find(1);

// Все три варианта идентичны:
echo $user;                    // Магия!
echo $user->toJson();          // Явный вызов
echo json_encode($user);       // Стандартный PHP

// Результат: {"id":1,"name":"John",...}

2.3. __wakeup()

Восстанавливает состояние модели после десериализации.

public function __wakeup()
{
    $this->bootIfNotBooted(); // Перезагрузка boot-методов
}

Когда объект модели сериализуется и потом десериализуется, он теряет:

  • Состояние загрузки (booted state)
  • Связи с другими компонентами
  • Настроенные события и т.д.

Этот метод гарантирует, что после десериализации модель будет правильно инициализирована. Используется везде, где объекты нужно сохранять и восстанавливать (кеш, очереди, сессии).

$serialized = serialize($user);
$unserialized = unserialize($serialized); // Вызывает __wakeup()

3. Перегрузка методов и свойств

Перегрузка в PHP означает возможность динамически создавать свойства и методы. Эти динамические сущности обрабатываются с помощью магических методов, которые можно создать в классе для различных видов действий.[2]

3.1. __get(), __set()

Эти методы перехватывают обращения к свойствам, которых нет в модели, но которые могут быть атрибутами, отношениями или вычисляемыми свойствами.

__get() — автоматический загрузчик свойств.

public function __get($key)
{
    return $this->getAttribute($key);
}

Когда вызывается:

  • Обращение к несуществующему публичному свойству
  • Загрузка отношения, которое не было предварительно загружено
  • Доступ к свойству через аксессор (getter)
// Автоматическая загрузка отношения
$posts = $user->posts; // __get('posts') загрузит отношения "на лету"

// Работа с обычными атрибутами
$name = $user->name;   // __get('name') вернет значение атрибута

// Вычисляемые свойства через аксессоры
$fullName = $user->full_name; // Вызовет getFullNameAttribute()

__set() — автоматический установщик свойств.

public function __set($key, $value)
{
    $this->setAttribute($key, $value);
}

Когда вызывается:

  • Установка значения несуществующему свойству
  • Установка отношения
  • Установка значения с мутатором
$user = new User();

// Установка атрибутов
$user->name = 'John'; // __set('name', 'John')

// Установка отношений
$user->posts = [new Post()]; // __set('posts', [...])

// Автоматическое применение мутаторов
$user->password = 'secret'; // Вызовет setPasswordAttribute()

Более подробно логику обработки свойств модели можно посмотреть в самом трейте Laravel vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php через методы getAttribute и setAttribute.

3.2. __isset(), __unset()

Эти методы обеспечивают согласованное поведение при работе со свойствами Eloquent моделей, но требуют понимания их внутренней логики.

__isset() — проверка существования свойств.

public function __isset($key)
{
    return $this->offsetExists($key);
}

Когда вызывается:

  • При использовании isset() или empty() на свойстве модели
  • Проверка атрибута, отношения или аксессора
  • Важно: isset() на отношении вернет true даже если оно не загружено!
$user = new User();

// Проверка обычного атрибута
isset($user->name);    // true — вызовет __isset('name')
isset($user->age);     // false — атрибут не существует

// Проверка отношения (даже если оно не загружено)
isset($user->posts);   // true — отношение существует в модели
empty($user->posts);   // true — но отношение еще не загружено

// Правильная проверка загруженного отношения:
if ($user->relationLoaded('posts') && !empty($user->posts)) {
    // Отношение загружено и не пустое
}

// Правильная проверка существования связанных записей:
if ($user->posts()->exists()) {
    // В базе есть связанные записи
}

__unset() — удаление свойств.

public function __unset($key)
{
    $this->offsetUnset($key);
}

Когда вызывается:

  • При использовании unset() на свойстве модели
  • При удалении атрибута из модели
  • При удалении загруженного отношения из памяти
$user = User::find(1);

// Удаление атрибута
unset($user->name);    // Вызовет __unset('name')
echo $user->name;      // null — атрибут удален

// Удаление отношения
$user->load('posts');  // Предварительно загружаем отношение
unset($user->posts);   // Вызовет __unset('posts') — удаляет загруженное отношение

3.3. __call(), __callStatic()

Эти методы обеспечивают гибкий API для работы с Eloquent, позволяя использовать удобный синтаксис для запросов, отношений и scopes.

__call() — обработка динамических методов.

public function __call($method, $parameters)
{
    if (in_array($method, ['increment', 'decrement'])) {
        return $this->$method(...$parameters);
    }

    return $this->forwardCallTo($this->newQuery(), $method, $parameters);
}

Когда вызывается:

  • При вызове несуществующего нестатического метода модели
  • Создается экземпляр отношения ($user->posts())
  • Применяется scope ($user->active())
  • Делегируется вызов в Query Builder ($user->where())

__callStatic() — обработка статических методов.

public static function __callStatic($method, $parameters)
{
    return (new static)->$method(...$parameters);
}

Когда вызывается:

  • При вызове несуществующего статического метода модели
  • Применяется scope статически (User::active())
  • Нужно начать Query Builder статически (User::where())
/* Scope через __call() */

// __callStatic() создает экземпляр и вызывает __call()
$activeUsers = User::active()->get();


/* Отношения через __call() */

$user = User::find(1);

// __call() создает экземпляр отношения
$profile = $user->profile();           // Возвращает Relation объект
$orders = $user->orders();             // Возвращает Relation объект

// Дальнейшие вызовы идут уже на Relation объект
$recentOrders = $user->orders()->latest()->limit(5)->get();


/* Query Builder делегирование */

// Все через __callStatic() и __call()
$articles = Article::where('published', true)
                  ->where(function($query) {
                      $query->where('featured', true)
                            ->orWhere('views', '>', 1000);
                  })
                  ->orderBy('created_at', 'desc')
                  ->with('author')
                  ->get();


/* Обработка ошибок */

// Это вызовет MethodNotFoundException
User::nonExistentMethod();

// Это вызовет BadMethodCallException
$user = new User();
$user->nonExistentMethod();


/* Важные особенности */

// Разница между отношением как свойство и как метод:
$user = User::find(1);

// Как свойство (через __get()) — возвращает коллекцию
$posts = $user->posts;  // Коллекция постов

// Как метод (через __call()) — возвращает Relation объект
$postsQuery = $user->posts(); // Builder отношений

// Scope всегда вызывается как метод
$activeUsers = User::active()->get();  // Правильно
// $activeUsers = User::active;        // Неправильно — будет ошибка

4. Заключение

Перечень методов и примеров получился большим, но информативным. Также важно помнить о производительности и понимать, когда магия уместна.

1. Проблема N+1 запросов из-за __get()

// Плохо: каждый вызов $post->author в цикле -> 101 запрос
$posts = Post::all();  // 1 запрос
foreach ($posts as $post) {
    echo $post->author->name; // __get() подгружает автора отдельным запросом
}

// Хорошо: жадная загрузка (eager loading) – всего 2 запроса
$posts = Post::with('author')->get();
foreach ($posts as $post) {
    echo $post->author->name; // автор уже загружен, __get() просто возвращает данные
}

2. Множественные вызовы __get() при доступе к одному атрибуту

// В шаблоне Blade три раза вызывается $user->name
{{ $user->name }}
Welcome, {{ $user->name }}
Last seen: {{ $user->name }}

// Каждый вызов -> __get('name') -> getAttribute('name')
// Решение: сохранить значение в переменную
@php($name = $user->name)
{{ $name }}
...

3. Накладные расходы на __call() и динамическую диспетчеризацию

// Каждый вызов where() проходит через __call() -> forwardCallTo() -> магию
$users = User::where('active', 1)->where('age', '>', 18)->get();

// Для высоконагруженных участков можно использовать прямой Query Builder
$query = DB::table('users'); // обычный объект без магии
$users = $query->where('active', 1)->where('age', '>', 18)->get();
// Разница в микросекундах, но при тысячах вызовов на один запрос она становится ощутимой.

Используйте магию для создания удобного API библиотек, фреймворков, высокоуровневых абстракций.

Избегайте в критических по производительности циклах, в deep-вложенных вызовах, которые выполняются тысячи раз за запрос.

Магия — это не самоцель, а инструмент для создания удобного API. Понимая механику этих методов, вы не только глубже понимаете Laravel, но и учитесь проектировать более качественные абстракции в своих проектах.

5. Источники и ссылки

  1. Магические методы — Руководство по PHP
  2. Перегрузка — Руководство по PHP
  3. Eloquent: Getting Started — Laravel Docs
  4. Eloquent: Relationships — Laravel Docs
  5. Eloquent: Mutators — Laravel Docs
  6. Eloquent: Serialization — Laravel Docs