Враження від використання Resources, Observers в Laravel та їх використання для пришвидшення завантаження контенту в SPA
Ділюся досвідом використання Resources, Observers та оптимізацією завантаження контенту
Ресурс або Resource – це спеціалізований клас в Laravel, який використовується переважно в API для формування відповіді цього самого API.
Це, на перший погляд, може здатися зайвою абстракцією, бо чи не простіше повертати звичайний JSON з контроллера. Проте досвід підказує, що ця абстракція дає додаткову гнучкість та уніфікацію. Можно думати про ресурс як про View у контексті API.
Наприклад, є модель Post
у неї є відносина image(): BelongsTo
з моделлю Image
. Сам Image
має атрибут url
. Я хочу отримувати в API пост, проте не хочу показувати атрібути моделі Image
, хочу просто щоб в полі post.image
лежав URL з моделі Image
.
Наш ресурс PostResource
буде виглядати наступним чином:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public static $wrap = null;
public function toArray(Request $request): array
{
return [
"id" => $this->id,
"image" => $this->image?->url,
];
}
}
Таким чином ми не показуємо фронтенду зайвих даних і спрощуємо структуру даних, які потрібні на фронті. Плюс маємо абстракцію де можемо робити прийнятну для фронтенду структуру, не міняючи контролер.
В масштабі це виглядає ще цікавіше. Для прикладу, мені потрібно показати стрічку постів. Маємо сутність Update
, яка має відносину BelongsTo
з Post
. В ресурсі UpdateResource
ми звертаємося до PostResource
щоб відформатувати поле post
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UpdateResource extends JsonResource
{
public static $wrap = null;
public function toArray(Request $request): array
{
return [
"id" => $this->id,
"post" => (new PostResource($this->post))->toArray($request),
];
}
}
Таким чином я отримав подібність до вкладених View для API, що виявилося дуже зручним підходом.
Спостерігач, або Observer – спеціалізований клас в Laravel, в котрому я можу описати так звані хуки, за допомогою яких можу відслідковувати життєвий стан будь якої моделі і робити додаткові дії відповідно до змін які відбуваються з моделлю.
Покажу один конкретний приклад, який робить дамп моделі за допомоги ресурсу в S3/R2, щоб зробити завантаження контенту на клієнті не з сервера, а з CDN, що знімає навантаження на сервер та пришвидшує завантаження, тому що статика.
Ресурс я вже описав раніше, показую Observer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
namespace App\Observers;
use App\Http\Resources\PostResource;
use App\Models\Post;
use App\Utils\ContentPaths;
use App\Utils\ContentStorage;
class PostObserver
{
public function created(Post $post): void
{
$post->refresh();
$path = app(ContentPaths::class)->getPostPath($post->getFilename());
app(ContentStorage::class)->store(
$path,
(new PostResource($post))->toJson()
);
}
public function updated(Post $post): void
{
$post->refresh();
$path = app(ContentPaths::class)->getPostPath($post->getFilename());
app(ContentStorage::class)->store(
$path,
(new PostResource($post))->toJson()
);
}
}
Цей спостерігач зареєстрований в AppServiceProvider.php
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
namespace App\Providers;
use App\Observers\PostObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Post::observe(PostObserver::class);
}
}
В самому спостерігачі я підписуюсь на події created
та updated
і за допомогою класів ContentPaths
та ContentStorage
роблю дамп ресурса PostResource
в CDN.
Цікавий момент, що $post->getFilename()
формує назву файлу по ID моделі sha1('post:' . $this->id) . '.json'
.
На фронтенді я маю ID посту з URL і отримую пост з CDN:
1
const { data } = await cdn.get(await contentPaths.getExternalPath(id));
В данному прикладі cdn
це інстанс Axios зі сконфігурованим URL на R2. Функція getExternalPath
робить ту саму работу, що ми робили раніше на бекенді – формує адресу до файлу по ID поста, використовуя SHA-1.
1
2
3
async getExternalPath(id) {
return `/p/${await digest(`post:${id}`, "SHA-1")}.json`;
}
В результаті за допомогою ресурсу та спостерігача я отримую автоматичний дамп моделі в CDN. Додатково я маю можливість отримати динамічну версію моделі в JSON напряму з API. Будь які зміни в форматі я додаю в ресурс, що впливає одразу на API та на CDN версію моделі.