
Сообщения высшего порядка - насколько же это удобно!
13.02.2019 22:54 | Laravel
В Laravel коллекции поддерживают так называемые сообщения высшего порядка (higher order messages) - шорткаты для выполнения некоторых общих действий. Рассмотрим несколько примеров, дабы убедиться, насколько элегантный и удобный инструмент есть в нашем распоряжении.
Для экспериментов создадим новое приложение, выполнив в консоли команду:
laravel new larexp
Добавим категории и продукты:
php artisan make:model -m Category
php artisan make:model -m Product
Отредактируем файлы миграций. Категории:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCategoriesTable extends Migration
{
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('categories');
}
}
Продукты:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateProductsTable extends Migration
{
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('category_id');
$table->string('title')->unique();
$table->text('description');
$table->boolean('active');
$table->unsignedInteger('price');
$table->timestamps();
$table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
});
}
public function down()
{
Schema::dropIfExists('products');
}
}
В двух словах: мы "привязали" продукт к категории, добавили название, описание и цену (в центах), а также создали поле для определения активен ли продукт. Отредактируем файлы моделей. Категория:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
protected $fillable = [
'title'
];
public function products()
{
return $this->hasMany(Product::class);
}
}
Продукт:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = [
'title', 'description', 'price', 'active'
];
public function category()
{
return $this->belongsTo(Category::class);
}
}
Пояснения: мы сразу определили отношения (один ко многим - категория может включать много продуктов, продукт принадлежит к какой-либо из категорий). Теперь выполним миграции:
php artisan migrate
И добавим фабрики для наполнения таблиц фейковыми данными:
php artisan make:factory CategoryFactory --model=Category
php artisan make:factory ProductFactory --model=Product
Заменим содержимое файлов-фабрик. CategoryFactory.php
:
<?php
use Faker\Generator as Faker;
$factory->define(App\Category::class, function (Faker $faker) {
return [
'title' => $faker->sentence(rand(1, 3))
];
});
ProductFactory.php
:
<?php
use Faker\Generator as Faker;
$factory->define(App\Product::class, function (Faker $faker) {
return [
'title' => $faker->unique()->sentence(1, 3),
'description' => $faker->paragraph(),
'price' => $faker->numberBetween(500, 5000),
'active' => $faker->boolean()
];
});
В консоли с помощью тинкера сгенерируем данные - пусть это будет 5 категорий с 10-ю продуктами в каждой:
php artisan tinker
>>> factory(App\Category::class, 5)->create()->each(function ($category) {
$category->products()->saveMany(factory(App\Product::class, 10)->make());
});
Выведем количество созданных продуктов, должно быть 50:
>>> App\Product::count()
=> 50
Отлично, можно переходить к главной теме.
filter
Допустим, нужно отфильтровать коллекцию и получить только те товары, которые активны. Обычная запись выглядит так:
$activeProducts = $products->filter(function ($product) {
return $product->active;
});
Воспользуемся сообщением высшего порядка:
$activeProducts = $products->filter->active;
Результат идентичен, но насколько проще запись? При этом код всё также хорошо читабелен. Что если условия фильтрации несколько сложней и опираются не только на значение булева поля? Допустим, в модели Product
мы определили метод isActive
с необходимым условиями. Теперь можем сделать и так:
$activeProducts = $products->filter->isActive();
Данный код "пройдётся" по всем продуктам и "выберет" только те, на которых метод isActive
будет возвращать true
.
map
Из коллекции продуктов хотим получить названия продуктов (на некоторое время забудем о методе pluck
):
$titles = $products->map(function ($product) {
return $product->title;
});
Зная о шорткатах, можно получить названия проще:
$titles = $products->map->title;
Пойдём ещё дальше: имея коллекцию продуктов, нужно получить коллекцию (уникальных) категорий, к которым принадлежат эти продукты:
$categories = $products->map->category->unique();
или так (уникальность будем определять по id
):
$categories = $products->map->category->unique->id;
sum, min, max, avg
Тут всё предельнго просто:
$sum = $products->sum->price;
$min = $products->min->price;
$max = $products->max->price;
$avg = $products->avg->price;
Нужна средняя цена активных продуктов? Просто скомбинируем методы:
$avg = $products->filter->active->avg->price;
keyBy
Хотим получить коллекцию, в которой ключами будут id
элементов? Запросто:
$byKeys = $products->byKey->id;
sortBy, sortByDesc
Сортировка по названию:
$sortedByTitle = $products->sortBy->title;
Сортировка по id
в обратном порядке:
$descSortedById = $products->sortByDesc->id;
each
Добавим в модель Product
метод для активации продукта:
public function activate()
{
$this->update(['active' => true]);
}
Теперь, чтобы активировать все продукты, достаточно одной строки:
$products->each->activate();
Можем немного поизвращаться. В модели категории создадим следующий метод:
public function createFakeProducts($count)
{
$this->products()->saveMany(factory(Product::class, $count)->make());
}
Помните, как мы генерировали продукты? Напомню, это выглядело вот так:
>>> factory(App\Category::class, 5)->create()->each(function ($category) {
$category->products()->saveMany(factory(App\Product::class, 10)->make());
});
Теперь же достаточно такой записи:
>>> factory(App\Category::class, 5)->create()->each->createFakeProducts(10);
На этом всё, надеюсь, данный материал был полезным. В официальной документации на эту тему сказано немного, поэтому в данном случае (впрочем, как и всегда) лучший способ изучения - эксперименты и применение на практике.
Успехов!