Повідомлення вищого порядку - наскільки ж це зручно!

Повідомлення вищого порядку - наскільки ж це зручно!

У 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;

 

Результат ідентичний, але наскільки простіше запис? При цьому код все такий же читабельний. Але якщо умови фільтрації складніші і спираються не тільки на значення boolean поля? Припустимо, в моделі 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);

 

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

Успіхів!