Laravel предоставляет нам возможность получать данные из базы вызывая методы классаIlluminate\Database\Query\Builder
на наших моделях. Сегодня речь пойдёт об одном из таких методов, а именно о динамическом where
. Благодаря нему мы можем выбирать данные, фильтруя их по различным атрибутам нашей модели. Например:
$activeUsers = User::whereActive(true)->get();
$publishedPosts = Post::wherePublished(true)->get();
$activeMenuProducts = Product::whereActiveAndShowInMenu(true, true)->get();
Разберёмся, что за магия здесь происходит и откуда берутся эти методы. Для начала посмотрим в наш класс Illuminate\Database\Eloquent\Model
, так как его наследуют все наши модели. Ничего похожего на методы выше в нём нет, но есть магические методы__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
. Он создаёт экземпляр класса Illuminate\Database\Eloquent\Builder
в котором также нет нашего динамического where
. Но, зато в нём есть ещё один магический метод __call
, который и приводит нас к конечному классу, который нас интересует, а именноIlluminate\Database\Query\Builder
. В нём, как вы уже наверное догадались, нас ждёт ещё один метод __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 в обычные, мы нашли.