Pull to refresh

Система автоматического документирования REST-API в Laravel проектах

Reading time 8 min
Views 23K

Преамбула


Для того, чтоб описать и задокументировать правила клиент-серверного
взаимодействия используя Rest-api можно выделить три основных метода:


  1. Описывать своим коллегам правила обращения к серверу на пальцах
    Этот метод быстр и не требует долгосрочной поддержки, но высока вероятность, что вас за это будут бить.
  2. Руками составлять Google-docs/Wiki/Readme в проекте
    Удобно тем, что однажды написанная документация не требует повторного объяснения. Её можно показать коллегам и даже иногда заказчику. Минусом данного метода является долгосрочная поддержка такой документации. Когда Api в проекте вырастает до таких размеров, что сама мысль "А когда же я обновлял документацию?" вызывает холодок по спине, тогда вы понимаете, что дальше так продолжаться не может. Формально вы можете обновлять документацию очень часто и маленькими фиксами, но это до первого отпуска.
  3. Использовать систему автодокументирования
    И вот для того, чтобы решить минусы первых двух методов человечество придумало системы автоматического документирования. Основная идея заключается в том, что к проекту пристыковывается некий плагин, который собирает информацию по вашему коду, сам составляет документацию и обёртывает её в удобочитаемый формат. Но большинство решений по этому методу не идеальны. Давайте попробуем сделать инструмент, который поможет получить документацию нашего проекта с минимальным количеством телодвижений


    Проблема


    Для того, чтоб обеспечить самодокументируемость кода в основном используют аннотации, то есть закомментированные участки кода, которые несут определенную информацию. Но у этого подхода тоже можно выделить несколько серьёзных недостатков:


    • Добавление аннотаций в уже написанный проект.
      Добавление аннотаций для всех методов всех контроллеров уже реализованного проекта – задача довольно рутинная. При большом объёме кода легко допустить ошибки даже в такой простой задаче.


    • Поддержка аннотаций.
      В действительности аннотации не решают этой проблемы, они лишь объединяют её с поддержкой кода. Так что вам неизбежно придётся следить за актуальность всех комментариев разбросанных по проекту.


    • Захламление кода
      Для демонстрации обратимся к классическому примеру от системы Swagger и посмотрим как устроен контроллер


      /**
      * @SWG\Get(
      *     path="/pet/{petId}",
      *     summary="Find pet by ID",
      *     description="Returns a single pet",
      *     operationId="getPetById",
      *     tags={"pet"},
      *     consumes={
      *         "application/xml",
      *         "application/json",
      *         "application/x-www-form-urlencoded"
      *     },
      *     produces={"application/xml", "application/json"},
      *     @SWG\Parameter(
      *         description="ID of pet to return",
      *         in="path",
      *         name="petId",
      *         required=true,
      *         type="integer",
      *         format="int64"
      *     ),
      *     @SWG\Response(
      *         response=200,
      *         description="successful operation",
      *         @SWG\Schema(ref="#/definitions/Pet")
      *     ),
      *     @SWG\Response(
      *         response="400",
      *         description="Invalid ID supplied"
      *     ),
      *     @SWG\Response(
      *         response="404",
      *         description="Pet not found"
      *     ),
      *     security={
      *       {"api_key": {}},
      *       {"petstore_auth": {"write:pets", "read:pets"}}
      *     }
      * )
      */
      public function doSomethingSmart()
      {
        return failItAllAndReturn500Response();
      }

      А теперь представьте, что у вас в контроллере 5 методов, каждый из которых по 10 строчек максимум? Соотношение комментариев к коду будет удручающее.


    • Актуальность документации
      Данный пример демонстрирует еще один недостаток — документация не зависит от того как реально работает ваш код. Несмотря на то, что данный метод всегда будет возвращать ответ с кодом 500, в документации будут значиться ответы 200, 400, 404.


Решение


Так как мы имеем довольно хорошее покрытие тестами, то решение напрашивалось само собой — тесты всё равно прогоняют все необходимые сценарии. В процессе прохождения тестов можно собирать коды и примеры ответов, список роутов, входные параметры и правила валидации к ним. Иными словами, большую часть документации. Оставшаяся часть — лишь ремарки. В действительности этот инструмент мы написали почти два года назад, только всё руки не доходили написать статью про него.


Реализация


Весь принцип действия основан на паттерне Middleware, то есть посредник. Для каждого из роутов вы можете конфигурировать свой список посредников. Каждый из запросов прежде чем попасть в контроллер пройдёт через цепочку посредников, каждый из которых может сделать нечто умное(или не очень).


    public function handle($request, Closure $next)
    {
        $response = $next($request);

        if ((config('app.env') == 'testing')) {
            $this->service->addData($request, $response);
        }

        return $response;
    }

По этому коду видно, что плагин начинает сбор информации во время тестирования. В сервисе происходит сбор необходимой информации из запроса и ответа. Request нам возвращает URI и хедеры, а так же роут, по которому производится текущее действие. Единственная сложность состоит в получении правил валидации. В middleware приходит экземпляр класса Illuminate\Http\Request, из которого получить данные о валидации невозможно. Поэтому рекомендуется для валидации "инжектить" к методам контроллера классы реквестов.
Например, вот так


    public function someAction(MyAwersomeRequest $request)
    {
        .....
    }

Зная какой метод какого контроллера должен вызываться, мы сможем получить доступ к тому какой реквест в него инжектится. Зная конкретный класс реквеста можно получить правила валидации. Мне показалось уместным в аннотации к классу реквеста поместить полное и краткое описания, описания кодов ответов и описания параметров.


Весь смысл задумки сводится к тому, чтоб аннотации были скорей уточнением, а не обязательным элементом.


Применение


Для демонстрации возможностей этого плагина создадим тестовый проект, ссылка на который находится внизу статьи. В рамках этой статьи рассмотрим пошаговое создание документации.
Давайте создадим тестовый контроллер.


/app/Http/Controllers/TestController.php


class TestController extends Controller
{
    public function lists()
    {
        $data = [
            'some' => 'complex',
            'structure' => [
                'with' => 'multiple',
                'nesting' => []
            ]
        ];

        return response()->json($data);
    }
    ...
}

Как вы можете заметить этот метод просто возвращает json объект. Далее требуется прописать роут, по которому будет вызываться метод lists нашего контроллера.
/routes/api.php


...
Route::get('/test', ['uses' => TestController::class . '@lists']);

И соответственно применить middleware AutoDoc-плагина
/app/Http/Kernel.php


    protected $middlewareGroups = [
        'api' => [
            ...
            AutoDocMiddleware::class
        ],
    ];

Чтоб его протестировать давайте создадим следующий тест


class ExampleTest extends TestCase
{
    public function testGetList() {
        $this->json('get', '/api/test');

        $this->assertResponseOk();
    }
}

Для того, чтоб отслеживать последний тест и успешность прохождения тестов создан специальный AutoDocTestCase. Для корректной работы плагина требуется сделать свой TestCase наследником от него или добавить следующий код в метод tearDown вашего родительского TestCase:


    public function tearDown()
    {
        $currentTestCount = $this->getTestResultObject()->count();
        $allTestCount = $this->getTestResultObject()->topTestSuite()->count();

        if (($currentTestCount == $allTestCount) && (!$this->hasFailed())) {
            $autoDocService = app(SwaggerService::class);

            $autoDocService->saveProductionData();
        }

        parent::tearDown();
    }

После запуска тестов мы можем увидеть собранную документацию по тому роуту, который указали для документации в конфиге config/auto-doc.php


Выглядеть она будет примерно вот так:



Как мы видим нет ни полного, ни краткого описания, всё сухо — вот запрос, вот метод, вот ответ. Ничего большего из данного кода получить невозможно(отчасти потому что больше там ничего и нет). Теперь давайте создадим реквест через


php artisan make:request TestGetRequest

class TestGetRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'not-found' => 'boolean',
            'need-authorization' => 'boolean',
            'test-parameter' => 'integer'
        ];
    }
}

Тут специально присутствуют параметры not-found, need-authorization, test-parameter, чтобы промоделировать разные ответы.
Для того, чтобы это работало как ожидается давайте добавим в метод нашего контроллера пару проверок


    public function lists(TestGetRequest $request)
    {
        if ($request->input('not-found')) {
            return response()->json([
                'error' => 'entity not found'
            ], Response::HTTP_NOT_FOUND);
        }  

        if ($request->input('need-authorization')) {
            return response()->json([
                'error' => 'authorization failed'
            ], Response::HTTP_UNAUTHORIZED);
        }  

        return response()->json([
            'some' => 'complex',
            'structure' => [
                'with' => 'multiple',
                'nesting' => []
            ]
        ]);
    }

Осталось дело за малым — давайте добавим еще три теста!


    public function testGetListNotFound()
    {
        $response = $this->json('get', '/api/test', [
            'not-found' => true
        ]);

        $response->assertStatus(Response::HTTP_NOT_FOUND);
    }

    public function testGetListNoAuth()
    {
        $response = $this->json('get', '/api/test', [
            'need-authorization' => true
        ]);

        $response->assertStatus(Response::HTTP_UNAUTHORIZED);
    }

    public function testGetListWrongParameters()
    {
        $response = $this->json('get', '/api/test', [
            'test-parameter' => 'test'
        ]);

        $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
    }

После запуска тестов мы можем увидеть более полную документацию.




Но чего тут не хватает? Например, отсутствие детального описания нашего запроса. Краткое описание(test get request) — это переформатированное имя класса TestGetRequest. Так же тут используются стандартные описания ответов, а хотелось бы иногда конкретизировать что именно подразумевается под каким-либо кодом ответа и входным параметром. Короче хочется
играться.
Давайте добавим аннотации к классу testGetRequest


/**
 * @description
 *  This request designed to demonstrate request with full annotation and response witch contain some data.
 * It has multi-line description witch will be displayed in Annotation Notes block of documentation.
 * It has custom summary, response code descriptions and parameters descriptions.
 *
 * @summary Test Get Request with full annotations
 *
 * @_204 This request has successfully done
 * @_401 You must remove need-authorization flag from input parameters for pass authorization.
 * @_404 We so sorry but you entity not exists.
 * @_422 Wrong input parameter. It must be integer
 *
 * @need-authorization If this parameter is true then you will get 401 response
 * @not-found If this parameter is true then you will get 404 response
 * @test-parameter This parameter designed for demonstrate unprocesable entity response
 */
 class TestGetRequest extends Request {...}

Ни один из параметров в аннотации не является обязательным.





Так же есть возможность устанавливать стандартное описание кода ответа на уровне приложения. Делается это в файле config/auto-doc.php. Приоритет описаний следующий:


  • описание кода ответа в аннотации
  • значение из конфига
  • стандартное описание кода ответа

Так же в этом конфиге лежит всё необходимое, чтоб сконфигурировать описание проекта в документации.
Когда вы выполняете команду


php artisan vendor:publish

В папку resources/views помещается файл swagger-description.blade.php. Например, если добавить туда следующий код


This project designed to demonstrate working of <b>ronasit/laravel-swagger</b> plugin.

Here is project description from <b>swagger-description.blade.php</b>
<div class="swagger-ui">
    <div class="opblock-body">
        <pre>
            You can add some code here
        </pre>
    </div>
</div>
Or some image
<div style="display: flex; justify-content: center; width: 100%">
    <img src="/img/hqdefault.jpg"/>
</div>

То в конечном итоге описание проекта в документации будет выглядеть следующим образом:



Итог


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


Ссылки


Репозиторий этого плагина находится вот тут


Демонстрационный проект тут

Tags:
Hubs:
+14
Comments 6
Comments Comments 6

Articles