Как в Laravel получить записи связей отфильтрованные по критерию

Как в Laravel получить записи связей отфильтрованные по критерию

Laravel - это мощный PHP-фреймворк, который для работы с базой данных использует Eloquent, мощную и удивительную ORM, которая позволяет выполнять сложные запросы SQL очень простым и интуитивно понятным способом. Настолько простая и понятная обёртка над SQL-запросами полностью избавляет нас от работы с самим SQL, и построение запросов к базе данных происходит на уровне PHP. Но иногда нам нужно больше, чем простые выборки по ID, в этой статье я покажу примеры того, как можно гибко работать со связями в Laravel, и производить фильтрацию связей по определённым условиям.

При работе с примитивными запросами к базе данных в Laravel, возможно, у вас никогда не возникало никаких проблем с построением запросов через билдер (если все ваши задачи сводятся к использованию методов where, whereIn и т.д.). Но когда ваше приложение начинает расти, или появляется задача, выходящая за рамки примитивных SQL-запросов, вы начинаете думать: "А как я могу сделать это в Laravel"?

В некоторых случаях вы знаете, как это сделать, используя чистый SQL, но, когда вы используете фреймворк, пишите красивый код, используя билдеры, негоже использовать чистый SQL в середине всего. Потому, моя сегодняшняя статья и призвана к тому, чтобы показать, как можно выполнить операции в Eloquent, выходящие за рамки тривиальных.

Иногда, когда вы делаете запрос к базе данных, вы ищите данные, которые имеют соответствующме значения в их связях. То есть, грубо говоря, вы ищите те сущности, у которых существуют связи и определённым значением. И в этой статье я покажу, как выполнять такой поиск, и как получить только те значения связей, параметры которых удовлетворяют фильтр.

Что будем делать

Для демонстрации логики построения кода я описал несколько сущностей: User, который может иметь несколько привязанных кошельков в кабинете, ``

class User extends Model
{
    public function wallets()
    {
        return $this->hasMany(Wallet::class);
    }
}

class Wallet extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

И так, чтобы получить список кошельков пользователя, достаточно выполнить:

$user = User::find(1);
$wallets = $user->wallets;

Здесь ничего нового.

Построение сложных запросов

Иногда бывает необходимость получить те записи из базы данных, у которых связи имеют определённое значение. К примеру, нужно получить список тех пользователей, у которых есть биткоин-кошелёк. Для этого существует 2 метода: whereHas и orWhereHas, которые осуществляют выборку по условию в связи.

$users = User::whereHas('wallets', function($query) {
    $query->where('address', '=', 'BTC');
})->get();

Метод whereHas первым аргументом принимает название связи, в которой будет производиться поиск, а вторым параметром анонимную функцию, в котором мы указываем конкретные критерии этого поиска. Внутри анонимной функции вы можете использовать любые методы, существующие а Eloquent.

Связь

Проблема в том, что после выполнения запроса на получение отфильтрованных данных по связи, при вызове оригинальной связи, данные останутся неизменными (будут получены все записи):

$user = User::whereHas(...)->first();
$user->wallets; // все доступные кошельки

Но иногда просто хочется использовать использовать $user->wallets получив только те данные, которые подпадают под описанный фильтр. В данном случае, BTC-кошельки, а не любой кошелёк пользователя. Другими словами, вы хотели бы использовать ту же логику, которую использовали для фильтрации сообщений в отношении кошельков.

Решение

В Laravel очень просто работать со связями, даже когда они загружаются "жадно". Для жадной загрузки связей используется метод with, с которым все Laravel разработчики уже давно знакомы. Снова ничего нового.

Мой совет здесь заключается в том, чтобы повторно использовать ту же анонимную функцию, описывающую фильтрацию, которую передавали методу whereHas с методом with. Итак, давайте сохраним эту анонимную функцию в локальную переменной, которая называется $filter, и перепишем код со старого формата:

$users = User::whereHas('wallets', $filter = function($query) {
    $query->where('address', '=', 'BTC');
})->get();

На новый, с использованием with:

$users = User::whereHas('wallets', $filter = function($query) {
    $query->where('address', '=', 'BTC');
})
->with(['wallets' => $filter ])
->get();

В этом коде мы сначала описали анонимную функцию для запроса, который возвращает только те записи пользователей, которые имеют запись с соответствующим описанным условием filter. И эту же анонимную функцию мы применили и для фильтрации записей связи wallets.

Ведь, как оказалось, метод with может принимать не только массив ключей с именами связей, а так же, у нас есть возможность передать ассоциативный массив. Где ключом будет имя связи (а текущем случае wallet), и анонимная функция, по которой связи будут фильтроваться.

И теперь, если вы будете вызывать $user->wallets, вы получите только те записи, которые соответствуют фильтру (в текущем случае, будут доступны только кошельки BTC.

К примеру, если вы будете тестировать этот код, создадим несколько записей для тестов.

$user = User::find(1);

$user->wallets()->saveMany([
    Wallet::make(['address' => 'BTC']),
    Wallet::make(['address' => 'WM']),
    Wallet::make(['address' => 'Visa']),
]);

И можем протестировать:

$user = User::whereHas(...)->with(...)->get()->first();
$this->assertCount(1, $user->wallets);
$this->assertEquals('BTV', $user->wallets->first()->address);

Или, если распечатать содержимое, то получим одну запись, описанную в фильтре, чем все существующие: Screenshot_1

Резюме

Работать со сложными запросами в Laravel не так сложно, как кажется. Но программирование сложных запросов - это именно программирование на языке программирование, а не написание чистого SQL. Нетривильные связи могут внести некоторую сложность в вашу логику, потому что вы думаете на уровне объектов и отношений, что, на мой взгляд, хорошо, потому что в этом случае вы полноценно занимаетесь программированием.

Решение, описанное в этой статье очень помогает при создании отчётов, потому что, обычно, вам приходится фильтровать модели и работать с их отношениями, выполняя лишние операции по работе с данными, дополнительно их обрабатывая.

Laravel предоставляет удобный механизм, который может принести вам большую гибкость, потому что вы можете инкапсулировать индивидуальную логику внутри "одного и того же запроса", повторно используя анонимные функции.

А для того, чтобы стать более продвинутым в программировании с Eloquent, советую изучить основные методы при работе с коллекциями Laravel.