• Час читання ~12 хв
  • 20.02.2023

Створення API в Ларавелі - це вид мистецтва. Ви повинні думати, крім доступу до даних і загортаючи свої красномовні моделі в кінцеві точки API.

Перше, що вам потрібно зробити, це розробити свій API; найкращий спосіб зробити це - подумати про мету вашого API. Навіщо ви створюєте цей API і який цільовий варіант використання? Після того, як ви зрозумієте це, ви можете ефективно розробити свій API на основі того, як він повинен бути інтегрований.

Зосередивши свою точку зору на тому, як ваш API повинен бути інтегрований, ви можете усунути будь-які потенційні больові точки у вашому API ще до того, як він буде випущений. Ось чому я завжди тестую інтеграцію будь-яких API, які я створюю, щоб забезпечити плавну інтеграцію, яка охоплює всі випадки використання, які я маю намір мати.

Поговоримо на прикладі, щоб намалювати картину. Я будую новий банк, Laracoin. Мені потрібно, щоб мої користувачі могли створювати облікові записи та створювати транзакції для цих облікових записів. У мене є модель рахунку, модель транзакції та модель постачальника, до якої належатиме кожна транзакція. Прикладом цього є:

Account -> Has Many -> Transaction -> Belongs To -> Vendor
Spending Account -> Lunch 11.50 -> Some Restaurant

Отже, у нас є три основні моделі, на яких нам потрібно зосередитися для нашого API. Якби ми підійшли до цього без будь-якого дизайнерського мислення, то створили б такі маршрути:

GET /accounts
POST /accounts
GET /accounts/{account}
PUT|PATCH /accounts/{account}
DELETE /accounts/{account}
GET /transactions
POST /transactions
GET /transactions/{transaction}
PUT|PATCH /transactions/{transaction}
DELETE /transactions/{transaction}
GET /vendors
POST /vendors
GET /vendors/{vendor}
PUT|PATCH /vendors/{vendor}
DELETE /vendors/{vendor}

Однак, які переваги цих маршрутів? Ми просто створюємо доступ JSON для наших красномовних моделей, що працює - але додає нульової цінності, і з точки зору інтеграції це змушує речі відчувати себе дуже роботизованими.

Замість цього давайте подумаємо про дизайн і призначення нашого API. Наш API, ймовірно, буде доступний переважно внутрішнім мобільним та веб-додаткам. Для початку ми зосередимося на цих випадках використання. Знаючи це, ми можемо точно налаштувати наш API відповідно до подорожей користувачів у наших додатках. Тому, як правило, в цих додатках ми побачимо список облікових записів, оскільки ми можемо керувати нашими обліковими записами. Нам також доведеться перейти до облікового запису, щоб побачити список транзакцій. Потім нам доведеться натиснути на транзакцію, щоб побачити більше деталей. Нам ніколи не потрібно було б бачити постачальників безпосередньо, оскільки вони там більше для категоризації, ніж будь-що інше. Маючи це на увазі, ми можемо розробити наш API на основі цих випадків використання та принципів:

GET /accounts
POST /accounts
GET /accounts/{account}
PUT|PATCH /accounts/{account}
DELETE /accounts/{account}
GET /accounts/{account}/transactions
GET /accounts/{account}/transactions/{transaction}
POST /transactions

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

Тепер, коли ми знаємо, як має бути розроблений наш API, ми можемо зосередитися на тому, як створити цей API, щоб гарантувати, що він швидко реагує і може масштабуватися з точки зору його складності.

По-перше, ми зробимо припущення, що ми будуємо додаток Laravel тільки для API - тому нам не знадобиться жодна приставка api. Давайте подумаємо, як ми можемо зареєструвати ці маршрути, оскільки це часто перша частина вашої заявки, яка бачить проблеми. Завантажений файл маршрутів важко розібрати подумки, а когнітивне навантаження - це перша битва в будь-якому додатку.

Якби цей API був публічним, я б розглянув підтримку версії API, і в цьому випадку я створив би каталог версій і зберіг би кожну основну групу у спеціальному файлі. Однак у цьому випадку ми не використовуємо керування версіями, тому організуємо їх по-іншому.

Перший файл маршрутів, який ми хочемо створити, - це маршрути / api / облікові записи.php, які ми можемо додати до наших маршрутів / api.php.

Route::prefix('accounts')->as('accounts:')->middleware(['auth:sanctum', 'verified'])->group(
    base_path('routes/api/accounts.php),
);

Кожна група буде завантажуватися на своїх маршрутах, встановлюючи префікс проміжного програмного забезпечення за замовчуванням і шаблон іменування маршрутів. Наш файл маршруту для облікових записів буде плоским з мінімальним групуванням, відмінним від того, коли ми хочемо подивитися на підресурси. Це дозволяє нам мати лише одну область, на яку можна дивитися, намагаючись зрозуміти самі маршрути, але це означає, що все, що стосується облікових записів, буде належати в цьому файлі.

Route::get(
    '/',
    App\Http\Controllers\Accounts\IndexController::class,
)->name('index');

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

Тепер, коли ми розуміємо, як ми маршрутизуємо наші запити, ми можемо подумати про те, як ми хочемо обробляти ці запити. Де живе логіка і як ми можемо гарантувати, що ми зберігаємо дублювання коду до мінімальної кількості?

Нещодавно я написав підручник про те, як використовувати Красномовно ефективно, який занурюється в класи запитів. Це мій бажаний підхід, оскільки він гарантує, що ми маємо мінімальну кількість дублювання коду. Я не буду вдаватися в конкретику щодо того, чому я буду використовувати цей підхід, як я детально вдавався в попередньому підручнику. Однак я розповім, як використовувати його у вашому додатку. Ви можете дотримуватися такого підходу, якщо він відповідає вашим потребам.

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

При створенні класу запитів потрібно зробити відповідний інтерфейс прив'язкою до контролера. Це не обов'язковий крок. Однак це я пишу підручник - так чого ж ви очікували, насправді?

interface FilterForUserContract
{
    public function handle(Builder $query, string $user): Builder;
}

Потім реалізацію, яку ми хочемо використовувати:

final class FilterAccountsForUser implements FilterForUserContract
{
    public function handle(Builder $query, string $user): Builder
    {
        return QueryBuilder::for(
            subject: $query,
        )->allowedIncludes(
            include: ['transactions'],
        )->where('user_id', $user)->getEloquentBuilder();
    }
}

Цей клас запитів отримає всі облікові записи для переданого користувача, що дозволить вам включити транзакції для кожного облікового запису за бажанням - а потім передати красномовний конструктор, щоб додати додаткові області там, де це необхідно.

Потім ми можемо використовувати це в нашому контролері, щоб запитати облікові записи автентифікованого користувача, а потім повернути їх у нашій відповіді. Давайте розглянемо, як ми можемо використовувати цей запит, щоб зрозуміти доступні варіанти.

final class IndexController
{
    public function __construct(
        private readonly Authenticatable $user,
        private readonly FilterForUserContract $query,
    ) {}
    public function __invoke(Request $request): Responsable
    {
        $accounts = $this->query->handle(
            query: Account::query()->latest(),
            user: $this->user->getAuthIdentifier(),
        );
        // return response here.
    }
}

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

Реагування є основною відповідальністю нашого API. Ми повинні швидко та ефективно реагувати, щоб мати швидкий та чуйний API для наших користувачів. Те, як ми реагуємо як API, можна розділити на дві області: клас відповіді та те, як дані перетворюються для відповіді.

Ці дві області - це відповіді та ресурси API. Я почну з ресурсів API, оскільки я дуже піклуюся про них. Ресурси API використовуються для приховування від структури бази даних і способу перетворення інформації, що зберігається у вашому API, таким чином, щоб найкраще споживатися на стороні клієнта.

Я використовую стандарти JSON:API у своїх API Laravel, оскільки це чудовий стандарт, який добре задокументований і використовується в спільноті API. На щастя, Тім Макдональд створив фантастичний пакет для створення ресурсів JSON:API в Laravel, яким я клянусь у всіх моїх додатках Laravel. Нещодавно я написав підручник про те, як користуватися цим пакетом, тому тут я лише розберуся в деяких деталях.

Почнемо з ресурсу облікового запису, який буде налаштований на наявність відповідних зв'язків і атрибутів. З моменту мого останнього підручника пакет був оновлений нещодавно, що полегшило налаштування відносин.

final class AccountResource extends JsonApiResource
{
    public $relationships = [
        'transactions' => TransactionResource::class,
    ];
    public function toAttributes(Request $request): array
    {
        return [
            'name' => $this->name,
            'balance' => $this->balance->getAmount(),
        ];
    }
}

Поки що ми тримаємо це надзвичайно просто. Ми хочемо повернути ім'я рахунку та баланс, з можливістю завантаження у відносинах транзакцій.

Використання цих ресурсів означає, що для доступу до імені, і нам доведеться використовувати: data.attributes.name, до якого може знадобитися деякий час, щоб звикнути до ваших веб- або мобільних додатків, але ви зрозумієте це досить скоро. Мені подобається такий підхід, оскільки ми можемо розділити відносини та атрибути та розширити їх там, де це необхідно.

Після того, як наші ресурси будуть заповнені, ми зможемо зосередитися на інших областях, таких як авторизація. Це життєво важлива частина нашого API, і його не слід випускати з уваги. Більшість з нас раніше використовували Laravels Gate, використовуючи фасад воріт. Однак мені подобається вводити контракт Gate з самого фреймворку. Це головним чином тому, що я віддаю перевагу ін'єкції залежності перед фасадами, коли отримую шанс. Давайте розглянемо, як це може виглядати в StoreController для облікових записів.

final class StoreController
{
    public function __construct(
        private readonly Gate $access,
    ) {}
    public function __invoke(StoreRequest $request): Responsable
    {
        if (! $this->access->allows('store')) {
            // respond with an error.
        }
        // the rest of the controller goes here.
    }
}

Тут ми просто використовуємо функціонал Gate так, ніби це фасад, так як вони одне і те ж. Я використовую тут дозволяє, але ви можете використовувати банки або інші методи. Ви повинні зосередитися на авторизації над тим, як вона реалізується, оскільки це незначна деталь для вашої заявки в кінці дня. Отже, ми знаємо, як ми хочемо

, щоб дані були представлені в API і як ми хочемо авторизувати користувачів у програмі. Далі ми можемо розглянути, як ми можемо впоратися з операціями запису.

Коли справа доходить до нашого API, операції запису є життєво важливими. Нам потрібно переконатися, що вони швидкі, наскільки це можливо, щоб наш API відчував себе швидким.

Ви можете записувати дані у свій API різними способами, але мій найкращий підхід полягає у використанні фонових завдань і швидкому поверненні. Це означає, що ви можете турбуватися про логіку навколо того, як все створюється у ваш власний час, а не ваших клієнтів. Перевага полягає в тому, що ваші фонові завдання все ще можуть публікувати оновлення через веб-сокети для відчуття в режимі реального часу.

Давайте подивимося на оновлений StoreController для облікових записів, коли ми використовуємо такий підхід:

final class StoreController
{
    public function __construct(
        private readonly Gate $access,
        private readonly Authenticatable $user,
    ) {}
    public function __invoke(StoreRequest $request): Responsable
    {
        if (! $this->access->allows('store')) {
            // respond with an error.
        }
        dispatch(new CreateAccount(
            payload: NewAccount::from($request->validated()),
            user: $this->user->getAuthIdentifier(),
        ));
        // the rest of the controller goes here.
    }
}

Ми надсилаємо нашій фоновій роботі корисне навантаження об'єкта передачі даних, який буде серіалізований у черзі. Ми створили цей DTO, використовуючи перевірені дані, і хочемо надіслати їх через ідентифікатор користувача, оскільки нам потрібно знати, для кого це зробити.

Дотримуючись цього підходу, ми маємо дійсні дані та дані, безпечні для типу, які передаються для створення моделі. У наших тестах все, що нам потрібно зробити тут, це переконатися, що робота відправлена.

it('dispatches a background job for creation', function (string $string): void {
    Bus::fake();

    actingAs(User::factory()->create())->postJson(
        uri: action(StoreController::class),
        data: [
            'name' => $string,
        ],
    )->assertStatus(
        status: Http::ACCEPTED->value,
    );
    Bus::assertDispatched(CreateAccount::class);
})->with('strings');

Ми тестуємо тут, щоб переконатися, що ми проходимо перевірку, отримуємо правильний код статусу назад з нашого API, а потім підтверджуємо, що відповідна фонова робота відправлена.

Після цього ми можемо протестувати роботу ізольовано, оскільки її не потрібно включати в наш тест на кінцеву точку. Тепер, як це буде записано в базу даних? Ми використовуємо командний клас для запису наших даних. Я використовую цей підхід, тому що використання лише класів дій є безладним. В кінцевому підсумку ми отримуємо 100 класів дій, які важко розібрати, шукаючи конкретний у нашому каталозі.

Як завжди, оскільки я люблю використовувати ін'єкцію залежності, нам потрібно створити інтерфейс, який ми будемо використовувати для вирішення нашої реалізації.

interface CreateNewAccountContract
{
    public function handle(NewAccount $payload, string $user): Model;
}

Ми використовуємо DTO нового облікового запису як корисне навантаження і проходимо через ідентифікатор користувача у вигляді рядка. Як правило, я даю це як рядок; Я б використовував UUID або ULID для поля ID у своїх програмах.

final class CreateNewAccount implements CreateNewAccountContract
{
    public function handle(NewAccount $payload, string $user): Model
    {
        return DB::transaction(
            callback: fn (): Model => Account::query()->create(
                    attributes: [
                        ...$payload->toArray(),
                        'user_id' => $user,
                ],
            ),
        );
    }
}

Ми загортаємо нашу дію запису в транзакцію бази даних, щоб ми брали участь у базі даних лише в тому випадку, якщо запис буде успішним. Це дозволяє нам відкотитися назад і кинути виняток, якщо написання буде невдалим.

Ми розглянули, як трансформувати дані моделі для нашої відповіді, як запитувати та записувати дані, а також як ми хочемо авторизувати користувачів у додатку. Завершальним етапом створення надійного API в Laravel є розгляд того, як ми реагуємо як API.

Більшість API смокчуть, коли справа доходить до відповіді. Це іронічно, оскільки це, мабуть, найважливіша частина API. У Laravel є кілька способів, за допомогою яких ви можете відповісти, від використання допоміжних функцій до повернення нових екземплярів JsonResponse. Однак мені подобається створювати спеціальні класи відповідей. Вони схожі на класи запитів і команд, які мають на меті зменшити дублювання коду, але також є найбільш передбачуваним способом повернення відповіді. Перша відповідь, яку я

створюю, - це відповідь колекції, яку я б використав при поверненні списку облікових записів, що належать автентифікованому користувачеві. Я б також зробив колекцію інших відповідей, від одномодельних відповідей до порожніх відповідей та відповідей на помилки.

class Response implements Responsable
{
    public function toResponse(): JsonResponse
    {
        return new JsonResponse(
            data: $this->data,
            status: $this->status->value,
        );
    }
}

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

final class CollectionResponse extends Response
{
    public function __construct(
        private readonly JsonApiResourceCollection $data,
        private readonly Http $status = Http::OK,
    ) {}
}

Це надзвичайно чисто і легко реалізувати, і ви можете перетворити властивість даних на тип об'єднання, щоб бути більш гнучким.

final class CollectionResponse extends Response
{
    public function __construct(
        private readonly Collection|JsonResource|JsonApiResourceCollection $data,
        private readonly Http $status = Http::OK,
    ) {}
}

Вони чисті та зрозумілі, тому давайте розглянемо остаточну реалізацію для IndexController для облікових записів.

final class IndexController
{
    public function __construct(
        private readonly Authenticatable $user,
        private readonly FilterForUserContract $query,
    ) {}
    public function __invoke(Request $request): Responsable
    {
        $accounts = $this->query->handle(
            query: Account::query()->latest(),
            user: $this->user->getAuthIdentifier(),
        );
        return new CollectionResponse(
            data: $accounts->paginate(),
        );
    }
}

Зосередження уваги на цих критичних областях дозволяє масштабувати API за складністю, не турбуючись про дублювання коду. Це ключові області, на яких я завжди буду зосереджуватися, намагаючись з'ясувати, що викликає повільний API Laravel.

Це аж ніяк не вичерпний підручник або список того, на чому вам потрібно зосередитися, але дотримуючись цього дещо короткого посібника, ви можете налаштувати себе на успіх, рухаючись вперед.

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297