TÌM HIỂU VỀ LARAVEL API DOCUMENTATION VÀ OPENAPI SPECIFICATION 3.0/SWAGGER UI
24/05/2022 523

Kỳ này, hãy cùng chuyên gia của CO-WELL Asia tìm hiểu về Laravel API Documentation với OpenAPI Specification 3.0/Swagger UI. Mời bạn đọc theo dõi!
1. Tổng quan về OpenAPI Specification/Swagger UI
1.1. OpenAPI Specification là gì?
OpenAPI Specification là một format – chuẩn chung để chúng ta có thể viết tài liệu cho API. Khi phát triển các Server API hay khi phải làm dự án nào liên quan tới API, chúng ta đều mong muốn có một tài liệu chuẩn chỉ, được cập nhật thường xuyên. Tránh trường hợp bên Front-End và Back-End bàn tới bàn lui, hỏi đi hỏi lại; hoặc khi bàn giao cho khách hàng, chúng ta cũng cần những tài liệu mô tả một cách chuẩn chỉ. Không chỉ là những templates sử dụng theo format của một công ty nào đó. Đây có thể là một trong những chuẩn mô tả API chúng ta có thể sử dụng.
1.2. Swagger UI là gì?
Ở trên Swagger UI này chúng ta có thể xem các API: Method là gì? Cách gọi thế nào? Request header/body cụ thể ra sao? Response header/body thế nào? Thậm chí chúng ta có thể tương tác luôn với Server API từ UI này luôn, xịn hơn nữa là nó có thể xác thực đăng nhập để tạo token nếu chúng ta sử dụng Oauth2/Passport hay apiKey; hay xác thực Basic Auth để tăng cường bảo mật.
2. Triển khai trên Server API sử dụng Laravel 8.x và Swagger package
Trong ví dụ này chúng ta sử dụng Laravel 8.x và Swagger package là L5-Swagger
Server API ở đây chúng ta sử dụng một sample đơn giản lấy từ ZeroBug 8. Hoặc chúng ta có thể build nhanh một Server API sử dụng Laravel 8.x khác.
Cài đặt Swagger UI có những bước như sau:
Cài đặt package:
composer require darkaonline/l5-swagger
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"
Tiếp theo chúng ta cần chỉnh sửa một chút trong config/l5-swagger.php
Có một vài chỗ bạn cần để ý như sau. Phần cấu hình route mà chúng ta sẽ truy cập vào:
'routes' => [
/*
* Route for accessing api documentation interface
*/
'api' => 'api/documentation',
]
Dưới đây là chỗ chúng ta cần cấu hình để có thể tương tác với Server API ngay trên Swagger UI. Vì Server API này sử dụng bearer token của Laravel Passport, nên để có thể gọi được API chúng ta phải có thêm cấu hình để Swagger có thể lấy được token cho các request.
'passport' => [ // Unique name of security
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'Laravel passport oauth2 security.',
'in' => 'header',
//'scheme' => 'https',
'scheme' => 'http',
'flows' => [
"password" => [
"authorizationUrl" => config('app.url') . '/oauth/authorize',
"tokenUrl" => config('app.url') . '/oauth/token',
"refreshUrl" => config('app.url') . '/token/refresh',
"scopes" => []
],
],
],
Tiếp theo là phần viết các comments để Swagger Package generate ra cho chúng ta OpenAPI Specification, có cái OAS (OpenAPI Sepcification) thì Swagger UI mới hoạt động được. Cũng nói thêm OAS này cũng có thể import thẳng vào PostMan để chúng ta tương tác một cách dễ dàng hơn, viết các Unit Test … Swagger UI chỉ là một trong những thứ sử dụng lại cái OAS này thôi.
Thêm comment vào app/Http/Controllers/Api/Controller.php – đây là cái Controller base mà chúng ta sử dụng cho các API controllers.
/** * @OA\Info( * version="0.8.1", * title="Zerobug OpenApi Demo Documentation", * description="Swagger OpenApi description", * @OA\Contact( * email="admin@zeroblog.net" * ), * @OA\License( * name="ZeroBlog", * url="https://www.zeroblog.net" * ) * ) * * @OA\Server( * url=L5_SWAGGER_CONST_HOST, * description="Zerobug API Server" * ) * * @OA\Tag( * name="Zerobug", * description="API Endpoints of Projects" * ) * */ class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; }
Tiếp theo là các API controllers.
class NewsApiController extends Controller { use MediaUploadingTrait; /** * @OA\Get( * path="/api/v1/news", * operationId="getNewsList", * tags={"News"}, * summary="Get list of news", * description="Returns list of news", * @OA\Response( * response=200, * description="Successful operation", * @OA\JsonContent(ref="#/components/schemas/NewsResource") * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ), * security={ * {"passport": {}}, * }, * ) */ public function index() { abort_if(Gate::denies('news_access'), Response::HTTP_FORBIDDEN, '403 Forbidden'); return new NewsResource(News::all()); } /** * @OA\Post( * path="/api/v1/news", * operationId="storeNews", * tags={"News"}, * summary="Store news", * description="Returns news data", * @OA\RequestBody( * required=true, * @OA\JsonContent(ref="#/components/schemas/StoreNewsRequest") * ), * @OA\Response( * response=201, * description="Successful operation", * @OA\JsonContent(ref="#/components/schemas/News") * ), * @OA\Response( * response=400, * description="Bad Request" * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ), * security={ * {"passport": {}}, * }, * ) */ public function store(StoreNewsRequest $request) { $news = News::create($request->all()); return (new NewsResource($news)) ->response() ->setStatusCode(Response::HTTP_CREATED); } /** * @OA\Get( * path="/api/v1/news/{id}", * operationId="getNewsById", * tags={"News"}, * summary="Get news information", * description="Returns news data", * @OA\Parameter( * name="id", * description="news id", * required=true, * in="path", * @OA\Schema( * type="integer" * ) * ), * @OA\Response( * response=200, * description="Successful operation", * @OA\JsonContent(ref="#/components/schemas/News") * ), * @OA\Response( * response=400, * description="Bad Request" * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ), * security={ * {"passport": {}}, * }, * ) */ public function show(News $news) { abort_if(Gate::denies('news_show'), Response::HTTP_FORBIDDEN, '403 Forbidden'); return new NewsResource($news); } /** * @OA\Put( * path="/api/v1/news/{id}", * operationId="updateNews", * tags={"News"}, * summary="Update existing news", * description="Returns updated news data", * @OA\Parameter( * name="id", * description="News id", * required=true, * in="path", * @OA\Schema( * type="integer" * ) * ), * @OA\RequestBody( * required=true, * @OA\JsonContent(ref="#/components/schemas/UpdateNewsRequest") * ), * @OA\Response( * response=202, * description="Successful operation", * @OA\JsonContent(ref="#/components/schemas/News") * ), * @OA\Response( * response=400, * description="Bad Request", * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ), * @OA\Response( * response=404, * description="Resource Not Found" * ), * security={ * {"passport": {}}, * }, * ) */ public function update(UpdateNewsRequest $request, News $news) { $news->update($request->all()); return (new NewsResource($news)) ->response() ->setStatusCode(Response::HTTP_ACCEPTED); } /** * @OA\Delete( * path="/api/v1/news/{id}", * operationId="deleteNews", * tags={"News"}, * summary="Delete existing news", * description="Deletes a record and returns no content", * @OA\Parameter( * name="id", * description="News id", * required=true, * in="path", * @OA\Schema( * type="integer" * ) * ), * @OA\Response( * response=204, * description="Successful operation", * @OA\JsonContent() * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ), * @OA\Response( * response=404, * description="Resource Not Found" * ), * security={ * {"passport": {}}, * }, * ) */ public function destroy(News $news) { abort_if(Gate::denies('news_delete'), Response::HTTP_FORBIDDEN, '403 Forbidden'); $news->delete(); return response(null, Response::HTTP_NO_CONTENT); } }
Do chúng ta sử dụng Resource, nên cần viết comments trong các Resources và Models nữa.
Resource:
/** * @OA\Schema( * title="NewsResource", * description="News resource", * @OA\Xml( * name="NewsResource" * ) * ) */ class NewsResource extends JsonResource { /** * @OA\Property( * title="Data", * description="Data wrapper" * ) * * @var \App\Models\News[] */ private $data; public function toArray($request) { return parent::toArray($request); } }
Và Model:
/** * @OA\Schema( * title="News", * description="News model", * @OA\Xml( * name="News" * ) * ) */ class News extends Model implements HasMedia { use SoftDeletes, InteractsWithMedia, HasFactory; public $table = 'news'; protected $dates = [ 'created_at', 'updated_at', 'deleted_at', ]; protected $fillable = [ 'title', 'desc', 'meta', 'content', 'created_at', 'updated_at', 'deleted_at', ]; /** * @OA\Property( * title="ID", * description="ID", * format="int64", * example=1 * ) * * @var integer */ private $id; /** * @OA\Property( * title="Title", * description="Title of the news", * example="A nice article" * ) * * @var string */ public $title; /** * @OA\Property( * title="Description", * description="Description of the news", * example="This is news' description" * ) * * @var string */ public $desc; /** * @OA\Property( * title="Meta", * description="Meta of the news", * example="This is news' meta" * ) * * @var string */ public $meta; /** * @OA\Property( * title="Content", * description="Content of the news", * example="This is news' content" * ) * * @var string */ public $content; /** * @OA\Property( * title="Created at", * description="Created at", * example="2021-07-14 17:50:45", * format="datetime", * type="string" * ) * * @var \DateTime */ private $created_at; /** * @OA\Property( * title="Updated at", * description="Updated at", * example="2021-07-14 17:50:45", * format="datetime", * type="string" * ) * * @var \DateTime */ private $updated_at; /** * @OA\Property( * title="Deleted at", * description="Deleted at", * example="2021-07-14 17:50:45", * format="datetime", * type="string" * ) * * @var \DateTime */ private $deleted_at; protected function serializeDate(DateTimeInterface $date) { return $date->format('Y-m-d H:i:s'); } public function registerMediaConversions(Media $media = null): void { $this->addMediaConversion('thumb')->fit('crop', 50, 50); $this->addMediaConversion('preview')->fit('crop', 120, 120); } }
Generate OAS:
php artisan l5-swagger:generate
Để lần sau chúng ta không phải chạy lại lệnh này mỗi lần thay đổi comments nữa, thì chúng ta quay lại chỉnh sửa một chút trong config/l5-swagger.php file:
/* * Set this to `true` in development mode so that docs would be regenerated on each request * Set this to `false` to disable swagger generation on production */ 'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', false),
Giờ chúng ta truy cập vào uri: api/documentation Việc thao tác thì khá là đơn giản. Đầu tiên thì xác thực để lấy Bearer Token:
Giờ có thể gọi luôn một API trên Swagger UI này. Chọn vào “Try it out”:
Data của Request Body chúng ta có thể sửa đổi được.
Chúng ta sẽ có kết quả như sau, tất cả các APIs khác chúng ta đều có thể xem tham số đầu vào, format output, call luôn tại Swagger UI này.
rên đây là những hướng dẫn từ cơ bản tới chi tiết của chuyên gia từ CO-WELL Asia về Laravel API Documentation với OpenAPI Specification 3.0/Swagger UI. Đừng quên theo dõi chuyên mục CODEWELL trên website CO-WELL Asia để đón đọc những bài viết công nghệ bổ ích nhé!