Знакомство с Laravel Sushi — драйвером массива для Eloquent

Источник: «Learn Laravel Sushi - The array driver for Eloquent»
Sushi — это недостающий драйвер массивов Eloquent, поскольку иногда хочется использовать Eloquent без работы с базой данных.

Недавно я опубликовал статью о создании примера приложения с использованием Volt и Folio. В этой статье я использовал пакет Sushi Caleb Porzio для создания примера данных. Мне стало интересно, для чего ещё люди используют Sushi. И я написал в Твиттере, спросив, для чего они его используют. В этой статье мы рассмотрим основные концепции Sushi и несколько примеров его использования.

Что такое Laravel Sushi и как он работает

Согласно README пакета, Sushi — это недостающий драйвер "массива" Eloquent. Другими словами, он позволяет создавать модели Eloquent из источников данных, отличных от базы данных. Проще всего использовать его, предоставляя данные в виде жёстко закодированного массива прямо в файле Model, задавая свойство $rows. В других случаях можно использовать getRows для предоставления динамических данных — из API-источника, CSV-файла или из любого другого источника по вашему выбору.

Как же это работает на самом деле? Sushi берет данные, которые вы ему предоставляете, создаёт модели Eloquent, а затем кэширует их в базе данных sqlite. После этого к данным можно обращаться как к любой стандартной модели Eloquent.

Приведём очень простой пример модели Sushi:

<?php

namespace App\Models;

use Sushi\Sushi;

class Role extends Model
{
// Добавляем трейт
use Sushi;

// Предоставляем данные в виде жёстко закодированного массива
protected $rows = [
['id' => 1, 'label' => 'admin'],
['id' => 2, 'label' => 'manager'],
['id' => 3, 'label' => 'user'],
];
}

Сообщество Laravel Sushi

Давайте рассмотрим несколько реальных примеров, которые использовал я и другие специалисты. Самый простой из них — создание списка или таблицы состояний. Ken и Facundo уже упоминали об этом примере, но я лично также использовал его.

<?php

namespace App\Models;

use Sushi\Sushi;

class Role extends Model
{
use Sushi;

protected $rows = [
[
'id' => 1,
'name' => 'Alabama',
'abbreviation' => 'AL',
],
[
'id' => 2,
'name' => 'Alaska',
'abbreviation' => 'AK',
],
[
'id' => 3,
'name' => 'Arizona',
'abbreviation' => 'AZ',
],
[
'id' => 4,
'name' => 'Arkansas',
'abbreviation' => 'AR',
],
[
'id' => 5,
'name' => 'California',
'abbreviation' => 'CA',
],
// ...
];
}

Примечание: столбец 'id' является опциональным. Sushi может создавать автоинкрементные id для каждого элемента, но если элементы изменятся (и кэш будет разрушен), то нет гарантии, что элементы получат те же id, что и раньше. Если вы собираетесь связывать с моделями Sushi другие данные, то лучше предоставить статический id столбец для каждого элемента.

Laravel Sushi для блогов, курсов и инфо-продуктов

Ещё один удобный вариант использования — простые блоги и курсы. Иногда мне, как разработчику, нужно сохранить несколько страниц для блога или курса, но мне не нужен вес полноценной CMS. Я бы предпочёл, чтобы она была лёгкой, но в то же время все содержимое хранилось непосредственно в коде, и его можно было синхронизировать через Git.

Aaron упомянул, что использует подобную настройку для блога на aaronfrancis.com. Caleb упомянул, что платформа для скринкастов Livewire v2 использует нечто подобное:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Sushi\Sushi;

class Series extends Model
{
use Sushi;

public function screencasts()
{
return $this->hasMany(Screencast::class);
}

public function getRows()
{
return [
['id' => 1, 'order' => 1, 'title' => 'Getting Started'],
['id' => 2, 'order' => 2, 'title' => 'A Basic Form With Validation'],
//...
];
}
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Sushi\Sushi;

class Screencast extends Model
{
use Sushi;

public function series()
{
return $this->belongsTo(Series::class);
}

public function getNextAttribute()
{
return static::find($this->id + 1);
}

public function getPrevAttribute()
{
return static::find($this->id - 1);
}

public function getDurationInSecondsAttribute()
{
// ...
}

protected $rows = [
[
'title' => 'Installation',
'slug' => 'installation',
'description' => "Installing Livewire is so simple, this 2.5 minute video feels like overkill. Composer require, and two little lines added to your layout file, and you are fully set up and ready to rumble!",
'video_url' => 'https://vimeo.com/...',
'code_url' => 'https://github.com/...',
'duration_in_minutes' => '2:32',
'series_id' => 1,
],
[
'title' => 'Data Binding',
'slug' => 'data-binding',
'description' => "The first and most important concept to understand when using Livewire is "data binding". It's the backbone of page reactivity in Livewire, and it'll be your first introduction into how Livewire works under the hood. Mandatory viewing.",
'video_url' => 'https://vimeo.com/...',,
'code_url' => 'https://github.com/...',
'duration_in_minutes' => '9:11',
'series_id' => 1,
],
// ...
];
}

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

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

$series = Series::with(['screencasts'])->orderBy('order')->get();

Затем можно перебрать их в цикле, в Blade:

<div>
@foreach($series as $s)
<div>
<h2>{{ $series->title }}</h2>
<div>
@foreach($series->screencasts as $screencast)
<div>
<h3>{{ $screencast->title }}</h3>
<p>{{ $screencast->description }}</p>
</div>
@endforeach
</div>
</div>
@endforeach
</div>

Вы даже можете использовать привязку модели к маршруту в Laravel для автоматического запроса моделей Sushi:

Route::get('/screencasts/{screencast:slug}');

Мы с Caleb используем очень похожий подход к хранению компонентов для Alpine Component. Мы используем привязку модели к маршрутам, а затем представления Blade для отображения деталей каждого компонента.

Внутри представлений Blade мы перебираем варианты компонента и с помощью @include($variant->view) включаем отдельные жёстко закодированные представления Blade, содержащие собственно код компонента.

<?php

namespace App\Models;

use App\Enums\ComponentType;
use Illuminate\Database\Eloquent\Model;
use Sushi\Sushi;

class Component extends Model
{
use Sushi;

protected $casts = [
'variants' => 'collection',
'requirements' => 'collection',
'type' => ComponentType::class,
];

public function getRows()
{
return [
[
'title' => 'Dropdown',
'slug' => 'dropdown',
'description' => 'How to build a dropdown component using Alpine.js.',
'screencast_id' => 111,
'variants' => json_encode([
['view' => 'patterns.dropdown'],
]),
'type' => ComponentType::COMPONENT->value,
'is_public' => true,
'is_free' => true,
'requirements' => json_encode([
[
'name' => 'alpinejs',
'version' => 'v3.x',
'url' => 'https://alpinejs.dev/installation',
],

]),
],
[
'title' => 'Modal',
'slug' => 'modal',
'description' => 'How to build a modal component using Alpine.js.',
'screencast_id' => 222,
'variants' => json_encode([
['view' => 'patterns.modal'],
]),
'type' => ComponentType::COMPONENT->value,
'is_public' => true,
'is_free' => false,
'requirements' => json_encode([
[
'name' => 'alpinejs',
'version' => 'v3.x',
'url' => 'https://alpinejs.dev/installation',
],
[
'name' => '@alpinejs/focus',
'version' => 'v3.x',
'url' => 'https://alpinejs.dev/plugins/focus',
],
]),
],
// ...
];
}
}

Как видно из примера, вместо установки свойства $rows мы использовали метод getRows. Это позволило нам использовать функцию json_encode() и использовать JSON-колонки для колонок variants и requirements в каждой модели. Также видно, что Sushi поддерживает приведение атрибутов к различным типам, как и Laravel.

API-источники данных

Ещё один интересный вариант использования — получение данных из источников API. Raúl, Marcel, Adam и Caleb упомянули различные источники API, которые они использовали.

Caleb посылает запросы к GitHub Sponsors API, чтобы определить, кто может получить доступ к скринкастам Livewire v2, затем обрабатывает полученные результаты, получая необходимые атрибуты, и оформляет их в красивую схему для модели. Это упрощённая версия модели Sponsor из кодовой базы Livewire v2:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Sushi\Sushi;

class Sponsor extends Model
{
use Sushi;

protected $keyType = 'string';

public function user()
{
return $this->hasOne(User::class, 'github_username', 'username');
}

public function getsScreencasts()
{
// При спонсорской поддержке на сумму более $8 они получают доступ к скринкастам.
return $this->tier_price_in_cents > 8 * 100;
}

public function getRows()
{
return Cache::remember('sponsors', now()->addHour(), function () {
return collect($this->fetchSponsors())
->map(function ($sponsor) {
return [
'id' => $sponsor['sponsorEntity']['id'],
'username' => $sponsor['sponsorEntity']['login'],
'name' => $sponsor['sponsorEntity']['name'],
'email' => $sponsor['sponsorEntity']['email'],
// ...
];
});
});
}

public function fetchSponsors()
{
return Http::retry(3, 100)
->withToken(
config('services.github.token')
)->post('https://api.github.com/graphql', [
'query' => 'A big ugly GraphQL query'
]);
}
}

Заключение

Sushi - это очень интересный пакет с несколькими замечательными вариантами использования. Уверен, что в этой статье я едва коснулся поверхности. Если вы уже использовали этот пакет, сообщите мне об этом в Twitter!

Дополнительные материалы

Предыдущая Статья

Руководство по четырём новым методам Array.prototype в JavaScript

Следующая Статья

Рефакторинг CSS: Оптимизация размера и производительности (часть 3)