Магия PHP на примере Eloquent Model
По мере опыта PHP программист сталкивается с магическими методами[1], но бывает сложно сразу охватить их потенциал. Максимум что получается реализовать у новичков в своих проектах это 1-2 метода в классе. И только лишь с хорошим опытом, когда изучаешь код известных проектов, понимаешь как используют магию профессионалы.
В данной статье предлагаю рассмотреть Eloquent Model и понять какие магические методы используются в ее основе.
Примеры кода на Laravel 5.8, но принципы работа актуальны и для новых версий.
Для системного подхода я разделил магические методы Eloquent на две группы по уровню абстракции.
Инициализация и представление
__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']);
Конструктор гарантирует, что модель всегда находится в корректном состоянии, независимо от способа создания.
__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",...}
На практике метод делает работу с моделью более удобной и предсказуемой, особенно при отладке и логировании.
__wakeup()
Восстанавливает состояние модели после десериализации.
public function __wakeup()
{
$this->bootIfNotBooted(); // Перезагрузка boot-методов
}
Примеры:
$serialized = serialize($user); $unserialized = unserialize($serialized); // Вызывает __wakeup()
Когда объект модели сериализуется и потом десериализуется, он теряет:
- Состояние загрузки (booted state)
- Связи с другими компонентами
- Настроенные события и т.д.
Этот метод гарантирует, что после десериализации модель будет правильно инициализирована. Используется везде, где объекты нужно сохранять и восстанавливать (кеш, очереди, сессии).
Перегрузка методов и свойств
Перегрузка в PHP означает возможность динамически создавать свойства и методы. Эти динамические сущности обрабатываются с помощью магических методов, которые можно создать в классе для различных видов действий.[2]
__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.
__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') - удаляет загруженное отношение
__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; // Неправильно - будет ошибка
На этом остановлюсь. Перечень методов и примеров получился большим, но информативным.
Магия — это не самоцель, а инструмент для создания удобного API. Понимая механику этих методов, вы не только глубже понимаете Laravel, но и учитесь проектировать более качественные абстракции в своих проектах.
Также важно помнить о производительности и понимать, когда магия уместна, а когда дешевле обойтись обычными методами и свойствами.