• Время чтения ~12 мин
  • 20.02.2023

Создание API в Laravel — это вид искусства. Необходимо не ограничивать доступ к данным и упаковывать красноречивые модели в конечные точки 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, и в этом случае я бы создал каталог версий и сохранил бы каждую основную группу в выделенном файле. Однако в этом случае мы не используем управление версиями, поэтому мы организуем их по-другому.

Первый файл маршрутов, который мы хотим создать, это routes/api/accounts.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, помимо маршрутов аутентификации, поэтому именно на этом я обычно сосредотачиваюсь в первую очередь. Важно сначала посмотреть на наиболее важные маршруты, чтобы разблокировать другие команды, но также это позволяет конкретизировать стандарты, которым вы хотите следовать в своем приложении.

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

Недавно я написал учебник о том, как использовать Eloquent Эффективно, который погружается в классы запросов. Это мой предпочтительный подход, так как он гарантирует, что у нас будет минимальное количество дублирования кода. Я не буду вдаваться в подробности относительно того, почему я буду использовать этот подход, как я подробно говорил в предыдущем уроке. Тем не менее, я расскажу, как использовать его в вашем приложении. Вы можете следовать этому подходу, если он соответствует вашим потребностям.

Важно помнить, что лучший способ получить максимальную отдачу от вашего 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.
    }
}

На данный момент наш контроллер имеет красноречивый конструктор, который будет передаваться в ответ, поэтому при передаче данных убедитесь, что вы либо вызываете get, либо разбиваете страницы, чтобы правильно передать данные. Это подводит нас к следующему пункту в моем самоуверенном путешествии.

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

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

Я использую стандарты JSON:API в своих API Laravel, так как это отличный стандарт, который хорошо документирован и используется в сообществе API. К счастью, Tim MacDonald создал фантастический пакет для создания ресурсов 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 Facade. Тем не менее, мне нравится вводить контракт 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, как если бы это был фасад, так как это одно и то же. Я использую разрешения здесь, но вы можете использовать can или другие методы. Вы должны сосредоточиться на авторизации, а не на том, как она реализована, так как это незначительная деталь для вашего приложения в конце дня.

Таким образом, мы знаем, как мы хотим, чтобы данные были представлены в 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, а затем подтвердили, что отправлено правильное фоновое задание.

После этого мы можем протестировать задание изолированно, потому что его не нужно включать в наш тест конечной точки. Теперь, как это будет записано в базу данных? Мы используем класс Command для записи наших данных. Я использую этот подход, потому что использование только классов Action беспорядочно. В итоге мы получаем 100 классов действий, которые трудно разобрать при поиске конкретного в нашем каталоге.

Как всегда, поскольку я люблю использовать Dependency Injection, нам нужно создать интерфейс, который мы будем использовать для решения нашей реализации.

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. Мне, однако, нравится создавать специальные классы Response. Они аналогичны классам Query и Command, которые направлены на уменьшение дублирования кода, но также являются наиболее предсказуемым способом возврата ответа.

Первый ответ, который я создаю, — это ответ коллекции, который я буду использовать при возврате списка учетных записей, принадлежащих аутентифицированному пользователю. Я бы также сделал коллекцию других ответов, от ответов одной модели до пустых ответов и ответов об ошибках.

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