Магия 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()

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

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

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

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

__get(), __set()

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

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

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

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

Пример:

// Автоматическая загрузка отношения
$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);
}

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

Примеры:

$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);
}

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

Примеры:

$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);
}

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

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

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

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

Примеры:

/* 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, но и учитесь проектировать более качественные абстракции в своих проектах.

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

Источники

  1. PHP: Магические методы
  2. PHP: Перегрузка
  3. Laravel 5.8 Eloquent: Relationships
  4. Laravel 5.8 Eloquent: Mutators
  5. Laravel 5.8 Eloquent: Serialization