Сообщения высшего порядка - насколько же это удобно!

Сообщения высшего порядка - насколько же это удобно!

В 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);

 

На этом всё, надеюсь, данный материал был полезным. В официальной документации на эту тему сказано немного, поэтому в данном случае (впрочем, как и всегда) лучший способ изучения - эксперименты и применение на практике.

Успехов!