← Заметки и статьи о Laravel PHP

13 сентября 2015

Магия Laravel: динамические where

Laravel предоставляет нам возможность получать данные из базы вызывая методы класса IlluminateDatabaseQueryBuilder на наших моделях. Сегодня речь пойдёт об одном из таких методов, а именно о динамическом where. Благодаря нему мы можем выбирать данные, фильтруя их по различным атрибутам нашей модели. Например:

$activeUsers = User::whereActive(true)->get();
$publishedPosts = Post::wherePublished(true)->get();
$activeMenuProducts = Product::whereActiveAndShowInMenu(true, true)->get();

Разберёмся, что за магия здесь происходит и откуда берутся эти методы. Для начала посмотрим в наш класс IlluminateDatabaseEloquentModel, так как его наследуют все наши модели. Ничего похожего на методы выше в нём нет, но есть магические методы __call и __callStatic:

/**
 * Handle dynamic method calls into the model.
 *
 * @param  string  $method
 * @param  array   $parameters
 * @return mixed
 */
public function __call($method, $parameters)
{
    if (in_array($method, ['increment', 'decrement'])) {
        return call_user_func_array([$this, $method], $parameters);
    }

    $query = $this->newQuery();

    return call_user_func_array([$query, $method], $parameters);
}

/**
 * Handle dynamic static method calls into the method.
 *
 * @param  string  $method
 * @param  array   $parameters
 * @return mixed
 */
public static function __callStatic($method, $parameters)
{
    $instance = new static;

    return call_user_func_array([$instance, $method], $parameters);
}

При обращении к несуществующим статическим методам (а в примерах выше именно они и есть) нашей модели, вызывается метод __callStatic. Он создаёт экземпляр класса в котором мы вызвали метод и пытается вызвать его в нём. Тут в игру вступает метод __call, потому как и в экземпляре нашего класса тоже нет динамических методов where. Он создаёт экземпляр класса IlluminateDatabaseEloquentBuilder в котором также нет нашего динамического where. Но, зато в нём есть ещё один магический метод __call, который и приводит нас к конечному классу, который нас интересует, а именно IlluminateDatabaseQueryBuilder. В нём, как вы уже наверное догадались, нас ждёт ещё один метод __call, который и помогает нам использовать динамические методы начинающиеся с where:

/**
 * Handle dynamic method calls into the method.
 *
 * @param  string  $method
 * @param  array   $parameters
 * @return mixed
 *
 * @throws BadMethodCallException
 */
public function __call($method, $parameters)
{
    if (Str::startsWith($method, 'where')) {
        return $this->dynamicWhere($method, $parameters);
    }

    $className = get_class($this);

    throw new BadMethodCallException("Call to undefined method {$className}::{$method}()");
}

Если наш метод начинается с where, то вызывается метод dynamicWhere. В нём заботливо написаны комментарии на английском, которые я ниже распишу на русском:

/**
 * Handles dynamic "where" clauses to the query.
 *
 * @param  string  $method
 * @param  string  $parameters
 * @return $this
 */
public function dynamicWhere($method, $parameters)
{
    // Убираем слово `where` из названия нашего метода
    $finder = substr($method, 5);

    // Разбиваем название метода на сегменты по заданному регулярному выражению, и возвращаем строку сегментов метода.
    // Например `nameAndId` преобразуется в массив ['Name', 'And', 'Id']
    $segments = preg_split('/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE);

    // Оператор по умолчанию у нас `and`. То есть, например, если у нас цепочка методов `whereName()->whereId()`, то между ними по умолчанию стоит `and`.
    // Этот оператор служит для связи с предыдущей частью запроса
    $connector = 'and';

    $index = 0;

    foreach ($segments as $segment) {
        // Проходя по всем нашим сегментам мы проверяем, если сегмент не `And` и не `Or`, то добавляем наш динамический where.
        // В методе `addDynamic` можно увидеть по сути одну строку(помимо преобразования оператора в lowercase): `$this->where(Str::snake($segment), '=', $parameters[$index], $bool);`.
        // Именно в нём наш динамический метод превращается в обычный `where` с параметрами
        if ($segment != 'And' && $segment != 'Or') {
            $this->addDynamic($segment, $connector, $parameters, $index);

            $index++;
        }

        // Иначе мы меняем наш оператор связи с предыдущей частью запроса, в случае если у нас более сложное название метода, например `whereNameAndId` или `whereNameAndIdOrColor`
        else {
            $connector = $segment;
        }
    }

    return $this;
}

Дальше происходит различная магия Eloquent для создания и подготовки запроса и тд, но где преобразуются наши динамические where в обычные, мы нашли.

Плюсануть
Поделиться
Отправить