Что нового в Eloquent ORM

Рассмотрим новый функционал, появившийся в Laravel с момента первоначального релиза версии 8. Сегодня поговорим о новых методах в Database и Eloquent. Пройдемся прямо по каждой версии, в которых появлялось что-то новое.

8.5 Метод crossJoinSub конструктора запросов

Используем подзапрос CROSS JOIN

use Illuminate\Support\Facades\DB;

$totalQuery = DB::table('orders')->selectRaw('SUM(price) as total');

DB::table('orders')
    ->select('*')
    ->crossJoinSub($totalQuery, 'overall')
    ->selectRaw('(price / overall.total) * 100 AS percent_of_total')
    ->get();

8.10 Метод is() отношений Один-к-одному для сравнения моделей

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

// ДО: внешний ключ берется из модели Post
$post->author_id === $user->id;

// ДО: выполняется дополнительный запрос для получения модели User из отношений Author
$post->author->is($user);

// ПОСЛЕ
$post->author()->is($user);

8.10 Метод upsert()

Если нужно выполнить несколько upserts в одном запросе, то можно использовать метод upsert вместо вызова нескольких updateOrCreate.

Flight::upsert([
    ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
    ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], ['departure', 'destination'], ['price']);

8.12 Метод explain()

Позволяет получить информацию о запросе из SQL.

User::where('name', 'Illia Sakovich')->explain();

User::where('name', 'Illia Sakovich')->explain()->dd();

8.15 Поддержка ограничений жадной загрузки отношений MorphTo

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

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphTo;

$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
    $morphTo->constrain([
        Post::class => function (Builder $query) {
            $query->whereNull('hidden_at');
        },
        Video::class => function (Builder $query) {
            $query->where('type', 'educational');
        },
    ]);
}])->get();

8.17.2 Метод BelongsToMany::orderByPivot()

Позволяет вам напрямую сортировать результаты запроса отношений BelongsToMany.

class Tag extends Model
{
    public $table = 'tags';
}

class Post extends Model
{
    public $table = 'posts';

    public function tags()
    {
        return $this->belongsToMany(Tag::class, 'posts_tags', 'post_id', 'tag_id')
            ->using(PostTagPivot::class)
            ->withTimestamps()
            ->withPivot('flag');
    }
}

class PostTagPivot extends Pivot
{
    protected $table = 'posts_tags';
}

// Где-то в Контроллере
public function getPostTags($id)
{
    return Post::findOrFail($id)->tags()->orderPivotBy('flag', 'desc')->get();
}

8.23 Метод BuildsQueries::sole()

Метод sole() вернёт только одну запись, соответствующую критериям. Если такая запись не найдена, то будет выброшено исключение NoRecordsFoundException. Если будет найдено несколько записей, то выбросится исключение MultipleRecordsFoundException.

DB::table('products')->where('ref', '#123')->sole()

8.27 Возможность добавлять несколько полей после определенного поля

Метод after() теперь может использоваться для добавление нескольких полей.

Schema::table('users', function (Blueprint $table) {
    $table->after('remember_token', function ($table){
        $table->string('card_brand')->nullable();
        $table->string('card_last_four', 4)->nullable();
    });
});

8.37 Анонимные миграции

Laravel автоматически присваивает имя класса всем миграциям. Теперь вы можете вернуть анонимный класс из файла миграции.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up()
    {
        Schema::table('people', function (Blueprint $table) {
            $table->string('first_name')->nullable();
        });
    }
};

8.27 Метод chunkMap()

Похож на метод each(), но проще в использовании. Автоматически разбивает результат на части (chanks — чанки).

return User::orderBy('name')->chunkMap(fn ($user) => [
    'id' => $user->id,
    'name' => $user->name,
]), 25);

8.28 Кастомные касты ArrayObject + Collection

Приведение json к PHP-экземпляру ArrayObject, который позволяет объекту вести себя как массив.

// Внутри модели...
$casts = ['options' => AsArrayObject::class];

// Изменяем параметры
$user = User::find(1);

$user->options['foo']['bar'] = 'baz';

$user->save();

8.40 Eloquent\Builder::withOnly()

Метод для переопределения значений для конкретного запроса, заданных в параметре $with.

class Product extends Model{
    protected $with = ['prices', 'colours', 'brand'];

    public function colours(){ ... }
    public function prices(){ ... }
    public function brand(){ ... }
}

Product::withOnly(['brand'])->get();

8.41 Cursor-пагинация

Подробнее об этом виде новой пагинации — «Пагинация: Offset против Cursor». Хорошо подходит для больших наборов данных и интерфейсов с «бесконечной» прокруткой.

use App\Models\User;
use Illuminate\Support\Facades\DB;

$users = User::orderBy('id')->cursorPaginate(10);
$users = DB::table('users')->orderBy('id')->cursorPaginate(10);

8.12 Методы withMax, withMin, withSum и withAvg для QueriesRelationships

В дополнении к методу withCount, Eloquent теперь поддерживает методы withMin, withMax, withAvg и withSum.
Они добавляют атрибут {relation}_{function}_{column} в результат запроса.

Post::withCount('comments');

Post::withMin('comments', 'created_at');
Post::withMax('comments', 'created_at');
Post::withSum('comments', 'foo');
Post::withAvg('comments', 'foo');

Под капотом используется метод withAggregate.

Post::withAggregate('comments', 'created_at', 'distinct');
Post::withAggregate('comments', 'content', 'length');
Post::withAggregate('comments', 'created_at', 'custom_function');

Comment::withAggregate('post', 'title');
Post::withAggregate('comments', 'content');

8.13 Добавлены методы loadMax, loadMin, loadSum и loadAvg в Eloquent\Collection и методы loadMax, loadMin, loadSum, loadAvg, loadMorphMax, loadMorphMin, loadMorphSum и loadMorphAvg в Eloquent\Model

// Eloquent/Collection
public function loadAggregate($relations, $column, $function = null) {...}
public function loadCount($relations) {...}
public function loadMax($relations, $column) {...}
public function loadMin($relations, $column) {...}
public function loadSum($relations, $column) {...}
public function loadAvg($relations, $column) {...}

// Eloquent/Model
public function loadAggregate($relations, $column, $function = null) {...}
public function loadCount($relations) {...}
public function loadMax($relations, $column) {...}
public function loadMin($relations, $column) {...}
public function loadSum($relations, $column) {...}
public function loadAvg($relations, $column) {...}

public function loadMorphAggregate($relation, $relations, $column, $function = null) {...}
public function loadMorphCount($relation, $relations) {...}
public function loadMorphMax($relation, $relations, $column) {...}
public function loadMorphMin($relation, $relations, $column) {...}
public function loadMorphSum($relation, $relations, $column) {...}
public function loadMorphAvg($relation, $relations, $column) {...}

8.13 Модифицирован метод QueriesRelationships::has() для поддержки отношений MorphTo

Добавлено условие count/exists в запрос полиморфических отношений

public function hasMorph($relation, ...)

public function orHasMorph($relation,...)
public function doesntHaveMorph($relation, ...)
public function whereHasMorph($relation, ...)
public function orWhereHasMorph($relation, ...)
public function orHasMorph($relation, ...)
public function doesntHaveMorph($relation, ...)
public function orDoesntHaveMorph($relation,...)

Пример с замыканием для кастомизации запроса отношений:

// Получить комментарии, связанные с сообщениями или видео, при условии, что заголовок содержит «code%»
$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

// Получить комментарии, связанные с сообщениями, при условии, что заголовок НЕ содержит «code%»
$comments = Comment::whereDoesntHaveMorph(
    'commentable',
    Post::class,
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

8.41 Метод Model::updateQuietly()

Иногда нужно обновить модель без отправки каких-либо событий. Теперь можно сделать это с помощью метода updateQuietly(), который под капотом использует метод saveQuietly().

$flight->updateQuietly(['departed' => false]);

С версии 8.59, вы также можете использовать методы createOneQuietly, createManyQuietly и createQuietly при использовании Фабрики Моделей.

Post::factory()->createOneQuietly();

Post::factory()->count(3)->createQuietly();

Post::factory()->createManyQuietly([
    ['message' => 'A new comment'],
    ['message' => 'Another new comment'],
]);

8.41 Извлечение ключа модели в id для whereKey() and whereKeyNot()

Можно использовать экземпляр модели с методами whereKey() и whereKeyNot().

$passenger->tickets()
    ->whereHas('airline', fn (Builder $query) => $query->whereKey($airline))
    ->get();

8.42 Метод withExists в QueriesRelationships

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

// ДО:
$users = User::withCount('posts')->get();

$isAuthor = $user->posts_count > 0;

// ПОСЛЕ:
$users = User::withExists('posts')->get();

$isAuthor = $user->posts_exists;

// поле name может быть также псевдонимом
the column name can also be aliased:
$users = User::withExists([
    'posts as is_author',
    'posts as is_tech_author' => function ($query) {
        return $query->where('category', 'tech');
    },
    'comments',
])->get();

8.42 Метод loadExists

Продолжая работу с вышеупомянутым методом withExists — теперь вы можете использовать loadExists в Моделях и Коллекциях.

$books = Book::all();

$books->loadExists(['author', 'publisher']);

8.42 Добавлено отношение «Один из многих»

Подробнее об этом отношении вы можете прочитать в статье Отношения «One of Many».

Это отношение создает связь Один-к-одному из отношений Один-ко-многим. Например, «последний вход в систему», «первый вход», цена на продукт (то есть получить актуальную цену товара).

/**
 * Получить самый последний заказ пользователя
 */
public function latestOrder()
{
    return $this->hasOne(Order::class)->latestOfMany();
}

/**
 * Получить самый старый заказ пользователя
 */
public function oldestOrder()
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

/**
 * Получить самый дорогой заказ пользователя
 */
public function largestOrder()
{
    return $this->hasOne(Order::class)->ofMany('price', 'max');
}

8.43 Строгий режим загрузки

Добавляет возможность включить строгий режим для предотвращения ленивой загрузки отношений. Для этого нужно вызвать метод Model::preventLazyLoading() в методе boot() вашего приложения.

Model::preventLazyLoading(! app()->isProduction());

8.43 Метод beforeQuery

Позволяет изменить конструктор «subselect», при этом изменения, также будут применяться к родительскому конструктору запросов. Сохраненные замыкания будут вызываться перед обработкой запроса. Таким образом, вы можете сохранить конструтор подзапросов и изменять подзапросы после применения к родительскому конструктору:

// 1. Добавляем подзапрос
$builder->beforeQuery(function ($query) use ($subQuery) {
    $query->joinSub($subQuery, ...);
});

// 2. Добавляем условия подзапроса
$subQuery->where('foo', 'bar');

// 3. Выполняем подзапрос с применением условий из пункта 2
$builder->get();

8.50 Трейт Prunable для моделей

Для переодической очистки моделей от устаревших записей. С помощью этого трейта Laravel будет делать это автоматически, только нужно будет настроить частоту выполнение команды model:prune в классе Kernel. Подробнее в статье «Очистка моделей».

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;

class Flight extends Model
{
    use Prunable;

    /**
     * Get the prunable model query.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function prunable()
    {
        return static::where('created_at', '<=', now()->subMonth());
    }
}

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

protected function pruning()
{
    // Удаление дополнительных ресурсов,
    // связанных с моделью. Например, файлы.

    Storage::disk('s3')->delete($this->filename);
}

8.53 Иммутабельные даты и приведение к ним

Более подробно о нововведении — «Иммутабельные даты в Laravel».

Приведение к CarbonImmutable вместо обычного экземпляра Carbon.

class User extends Model
{
    public $casts = [
        'date_field'     => 'immutable_date',
        'datetime_field' => 'immutable_datetime',
    ];
}

8.57 Хелпер where для отношений

Синтаксический сахар whereRelation и whereMorphRelation для запроса отношений через whereHas с простым условием where.

// ДО
User::whereHas('posts', function ($query) {
    $query->where('published_at', '>', now());
})->get();

// ПОСЛЕ
User::whereRelation('posts', 'published_at', '>', now())->get();

// Полиморфные отношения
Comment::whereMorphRelation('commentable', '*', 'public', true);

8.59 Метод whereMorphedTo

Новые методы whereMorphedTo и orWhereMorphedTo для упрощения поиска морфированных моделей без затрат на подзапрос whereHas.

Feedback::whereMorphedTo('subject', $user)->get();

Feedback::whereMorphedTo('subject', User::class)->get();

8.59 Возможность запретить морфирование

Вы можете вызвать метод enforceMorphMap в методе boot класса AppServiceProvider для запрета морфирования без карты.

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
    'post'  => Post::class,
    'video' => Video::class,
]);

8.60 Метод valueOfFail()

В дополнении к методу value() появился новый метод valueOrFail. Он выбросит исключение ModelNotFoundException, если модель не будет найдена.

// ДО:
$votes = User::where('name', 'John')->firstOrFail('votes')->votes;

// ПОСЛЕ:
$votes = User::where('name', 'John')->valueOrFail('votes');

8.63 Метод whereBelongsTo()

Новый метод автоматически определяет правильные отношения и внешний ключ для указанной модели.

// ДО:
$posts = Post::where('user_id', $user->id)->get();

// ПОСЛЕ:
$posts = Post::whereBelongsTo($user)->get();

 

Автор: Pascal Baljet
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.