Магия 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. Источники и ссылки
- Магические методы – Руководство по PHP
- Перегрузка – Руководство по PHP
- Eloquent: Getting Started – Laravel Docs
- Eloquent: Relationships – Laravel Docs
- Eloquent: Mutators – Laravel Docs
- Eloquent: Serialization – Laravel Docs