Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

LaravelでAPI定義を管理する

1,162 views

Published on

Laravel/Vue.js勉強会#2

Published in: Software
  • Be the first to comment

LaravelでAPI定義を管理する

  1. 1. LaravelでAPI定義を管理する Laravel/Vue勉強会#2@2017/11/24 KenjiroKubota
  2. 2. Pro le Kenjiro Kubota istyle.inc 自称テクノロジー本部銀髪担当 PHP, HHVM/Hack, Javascript
  3. 3. 今日話すこと ADR REST API Level3 Swagger PHP
  4. 4. Vueの話はありません。ごめんなさい。
  5. 5. REST APIの定義管理ってどうしてます Wiki? Markdownファイル? 何かしらのAPIドキュメントツール?
  6. 6. API利用者(フロントエンジニア)にこんなこと言われない? 「実際に返ってくるパラメータと定義書違くない 」 (あ、ドキュメント更新してないっす)
  7. 7. 理想は実装 = 定義書ですよね
  8. 8. 実装から定義書を生成する話
  9. 9. 踏まえて、今日話すこと ADR = API実装の話 REST API Level3 = レスポンス形式の話 Swagger PHP = APIドキュメントの話
  10. 10. ADR
  11. 11. ADR Action Domain Responder の略です。Laralab vol.1という勉強会で紹介させていただきました。 Responsableを使ったADR実装 https://www.slideshare.net/KenjiroKubota/responsableadr MVCから派生したUIアーキテクチャパターン
  12. 12. Model Domain View Responder Controller Action
  13. 13. MVCと違うところ 1つのEndpointに1つのActionClass
  14. 14. 参考実装 今週発売のゲームソフトを返すエンドポイントを作ります。 (返すのはダミーデータです) Github: kubotak-is/laravel-swagger-sample
  15. 15. <?php namespace AppHttpActionApi; final class GetThisWeeksGameSoftwareRelease { private $service; public function __construct(ThisWeeksGameSoftwareRelease $service) { $this->service = $service; } public function __invoke(GetThisWeeksGameSoftwareReleaseRequest $request): Responder { $validated = $request->validated(); $limit = $validated['limit'] ?? 3; $offset = $validated['offset'] ?? 0; $collection = $this->service->getCollection((int) $limit, (int) $offset); return new GetThisWeeksGameSoftwareReleaseResponder($collection); } }
  16. 16. class RouteServiceProvider extends ServiceProvider { public function register() { parent::register(); /** @var Router $router */ $router = $this->app['router']; $router->group(['prefix' => 'api'], function (Router $router) { $router->get( '/game_software/release/week', ['uses' => GetThisWeeksGameSoftwareRelease::class] ); }); } }
  17. 17. RouterでDispatchされたClassは __invoke() が実行される FatContollerにならない
  18. 18. (おまけ)FormRequest
  19. 19. <?php namespace AppHttpRequestApi; use IlluminateFoundationHttpFormRequest; class GetThisWeeksGameSoftwareReleaseRequest extends FormRequest { public function authorize(): bool { return true; } public function rules(): array { return [ 'limit' => 'integer|min:1', 'offset' => 'integer|min:0', ]; } }
  20. 20. ActionClass メソッドインジェクションしたFormRequestは既にバリデーション済 みのパラメータを取得できる。 Laraevl5.5からは ->validated(); でバリデーション定義されてかつ、バ リデーションされた値のみ取得が可能 public function __invoke(GetThisWeeksGameSoftwareReleaseRequest $request): Responder { $validated = $request->validated(); $limit = $validated['limit'] ?? 3; $offset = $validated['offset'] ?? 0; $collection = $this->service->getCollection((int) $limit, (int) $offset); return new Responder($collection); }
  21. 21. Responder
  22. 22. <?php namespace AppHttpResponderApi; use AppHttpResponderHateoasResponder; use IlluminateContractsSupportResponsable; use AppDomainCollectionGameSoftwareCollection; class GetThisWeeksGameSoftwareReleaseResponder implements Responsable { use HateoasResponder; private $resource; public function __construct(GameSoftwareCollection $collection) { $this->resource = $collection; } public function toResponse($request): IlluminateHttpResponse { return $this->hal($this->resource); } }
  23. 23. REST API Level3
  24. 24. RESTの4段階のModel https://martinfowler.com/articles/richardsonMaturityModel.html
  25. 25. REST API Model Level 0 HTTPを用いてXMLレスポンスを返却すること Level 1 URLでリソースを表すこと Level 2 HTTPメソッドを正しく使い分けること Level 3 ハイパーメディアコントロール
  26. 26. ハイパーメディアコントロール Webサイトの様にテキストのリンクを利用して利用者をナビゲートす るように、 APIのレスポンスにも別のエンドポイントへのナビゲートを含めること
  27. 27. HATEOAS Hypermedia As The Engine Of Application State
  28. 28. HATEOASとは RestfullAPIを拡張するアーキテクチャパターン 状態遷移を表現する レスポンス内にリンクを含める そのリンクを辿ることで状態遷移を表現する
  29. 29. willdurand/hateoas https://github.com/willdurand/Hateoas アノテーションを追加することでレスポンスパラメータを拡張するライ ブラリ $ composer require willdurand/hateoas
  30. 30. Laravelでアノテーションを利用できるようにする <?php namespace AppProviders; use DoctrineCommonAnnotationsAnnotationReader; use DoctrineCommonAnnotationsAnnotationRegistry; use IlluminateSupportServiceProvider; class AnnotationRegisterServiceProvider extends ServiceProvider { public function register() { $loader = require base_path().'/vendor/autoload.php'; AnnotationRegistry::registerLoader([$loader, 'loadClass']); } } AppProvidersAnnotationRegisterServiceProvider::class,
  31. 31. Entity <?php namespace AppDomainEntity; use AppHttpResponderHateoasResource; use PHPMentorsDomainKataEntityEntityInterface; class GameSoftware implements EntityInterface, HateoasResource { protected $id; protected $title; protected $description; protected $releaseDate; protected $price; protected $retailPriceDesired; protected $platform; protected $thumbnail; }
  32. 32. Add Annotation /** * Class GameSoftware * @package AppDomainEntity * @HateoasRelation( * "self", * href = "expr('/api/game_software/' ~ object.getId())" * ) * @HateoasRelation( * "page", * href = "expr('/game_software/' ~ object.getId())" * ) */ class GameSoftware implements EntityInterface, HateoasResource {
  33. 33. /** * @var string * @Accessor("getReleaseDate") */ protected $releaseDate; /** * @var int * @Type("int") */ protected $price; public function getId(): int { return $this->id; } public function getReleaseDate(string $format = "Y-m-d H:i:s"): string { return (new DateTime($this->releaseDate))->format($format); }
  34. 34. <?php namespace AppHttpResponder; use HateoasHateoas; use HateoasHateoasBuilder; use IlluminateHttpResponse; use HateoasUrlGeneratorCallableUrlGenerator; trait HateoasResponder { protected function hal( HateoasResource $resource, int $status = 200, array $headers = [] ): Response { return new Response( $this->builder()->serialize($resource, 'json'), $status, array_merge(['Content-Type' => 'application/hal+json'], $headers) ); }
  35. 35. protected function builder(): Hateoas { return HateoasBuilder::create() ->setUrlGenerator( null, new CallableUrlGenerator(function ($route, array $parameters) { return route($route, $parameters); }) ) ->build(); } }
  36. 36. <?php namespace AppHttpResponderApi; use AppHttpResponderHateoasResponder; use IlluminateContractsSupportResponsable; use AppDomainCollectionGameSoftwareCollection; class GetThisWeeksGameSoftwareReleaseResponder implements Responsable { use HateoasResponder; private $resource; public function __construct(GameSoftwareCollection $collection) { $this->resource = $collection; } public function toResponse($request): IlluminateHttpResponse { return $this->hal($this->resource); } }
  37. 37. こんな感じのレスポンスになります。 { "id": 1, "title": "スーパーマリオオデッセイ", "description": "マリオ、世界の旅へ。Nintendo Switch向けソフトに新作3Dマリオが登場します。…", "release_date": "2017-10-27 00:00:00", "price": 5545, "retail_price_desired": 6458, "platform": "Nintendo Switch", "thumbnail": "https://images-na.ssl-images-amazon.com/images/I/….jpg", "_links": { "self": { "href": "/api/game_software/1" }, "page": { "href": "/game_software/1" } } }
  38. 38. (おまけ)今回使ったHateoas以外のライブラリの紹介 zendframework/zend-hydrator https://docs.zendframework.com/zend-hydrator/
  39. 39. public function findReleaseThisWeek(int $limit, int $offset): GameSoftwareCollection { $data = $this->criteria->getReleaseThisWeek($limit, $offset); $collection = new GameSoftwareCollection(); foreach ($data as $item) { $hydrator = new ReflectionHydrator(); $namingStrategy = new CompositeNamingStrategy([ 'release_date' => new MapNamingStrategy([ 'release_date' => 'releaseDate' ]), 'retail_price_desired' => new MapNamingStrategy([ 'retail_price_desired' => 'retailPriceDesired' ]), ]); $hydrator->setNamingStrategy($namingStrategy); $gameSoft = $hydrator->hydrate( $item, new GameSoftware() ); $collection->add($gameSoft); } return $collection; }
  40. 40. protected, privateプロパティに対して constructやsetterなしでオブジェクトマッピングしてくれます
  41. 41. Swagger PHP
  42. 42. Swaggerとは https://swagger.io THE WORLD'S MOST POPULAR API TOOLING RESTful APIのドキュメントや、サーバ、クライアントコード、エディ タ、またそれらを扱うための仕様などを提供するフレームワーク Swagger Speci cationをSwagger UIに読み込ませることで定義書を生 成する
  43. 43. Swagger PHPではアノテーションを利用して Swagger Speci cation(json)を生成
  44. 44. 実装 = 定義書ができる!
  45. 45. DarkaOnLine/L5-Swagger https://github.com/DarkaOnLine/L5-Swagger LaravelでSwaggerPHPを使うライブラリ ArtisanコマンドでSwagger Speci cationを生成してSwaggerUIに適用 したページが表示できる $ composer require "darkaonline/l5-swagger:5.5.*" L5SwaggerL5SwaggerServiceProvider::class,
  46. 46. 開発環境のみ利用したいのでProviderでLocalとDevelop環境の場合の み登録する $env = $this->app->environment(); if ($env === 'develop' || $env === 'local') { $this->app->register(L5SwaggerL5SwaggerServiceProvider::class); } $ php artisan l5-swagger:publish con g/l5-swagger.php public/vendor/l5-swagger resources/views/vendor/l5-swagger 必要なファイルをpublishで移動
  47. 47. HATEOASで利用したアノテーション登録でSwagger PHPのアノテーシ ョン @SWG のClassが見つからないエラーが発生するのでこのアノテーシ ョンはIgnoreさせる class AnnotationRegisterServiceProvider extends ServiceProvider { public function register() { $loader = require base_path().'/vendor/autoload.php'; AnnotationRegistry::registerLoader([$loader, 'loadClass']); AnnotationReader::addGlobalIgnoredNamespace('SWG'); // Add } }
  48. 48. Modelの定義 /** * Class GameSoftware * @package AppDomainEntity * @HateoasRelation( * "self", * href = "expr('/api/game_software/' ~ object.getId())" * ) * @HateoasRelation( * "page", * href = "expr('/game_software/' ~ object.getId())" * ) * @SWGDefinition( * type="object", * @SWGXml(name="GameSoftware") * ) */ class GameSoftware implements EntityInterface, HateoasResource
  49. 49. プロパティのアノテーション /** * @var int * @SWGProperty(format="int64") */ protected $id; /** * @var string * @SWGProperty() */ protected $title;
  50. 50. /** * @SWGProperty( * property="_links", * type="object", * @SWGProperty( * property="self", * type="object", * @SWGProperty( * property="href", * type="string" * ) * ), * @SWGProperty( * property="page", * type="object", * @SWGProperty( * property="href", * type="string" * ) * ) * ) */
  51. 51. Collectionの定義 /** * Class GameSoftwareCollection * @package AppDomainCollection * @SWGDefinition( * type="object", * @SWGXml(name="GameSoftwareCollection") * ) */ class GameSoftwareCollection implements EntityCollectionInterface, HateoasResource { /** * @var GameSoftware[] * @SerializedName("game_software") * @SWGProperty( * property="game_software", * @SWGXml(name="GameSoftware", wrapped=true) * ) */ protected $entities = [];
  52. 52. Responseの定義 (ResponderClass) /** * @param IlluminateHttpRequest $request * @return IlluminateHttpResponse * @SWGResponse( * response="GetThisWeeksGameSoftwareReleaseResponder", * description="今週発売のゲームを返す", * @SWGSchema(ref="#/definitions/GameSoftwareCollection") * ) */ public function toResponse($request): IlluminateHttpResponse { return $this->hal($this->resource); }
  53. 53. RequestParameterの定義 /** * @SWGParameter( * parameter="GetThisWeeksGameSoftwareReleaseRequest_limit", * name="limit", * description="取得件数", * in="query", * required=false, * type="integer", * format="int32" * ) * @SWGParameter( * parameter="GetThisWeeksGameSoftwareReleaseRequest_offset", * name="offset", * description="取得位置", * in="query", * required=false, * type="integer", * format="int32" * ) */ public function rules(): array
  54. 54. Endpointの定義 /** * Class GetThisWeeksGameSoftwareRelease * @package AppHttpActionApi * @SWGGet( * path="/game_software/release/week", * summary="今週発売のゲームソフト", * description="", * consumes={"application/json"}, * produces={"application/hal+json"}, * @SWGParameter(ref="#/parameters/GetThisWeeksGameSoftwareReleaseRequest_limit"), * @SWGParameter(ref="#/parameters/GetThisWeeksGameSoftwareReleaseRequest_offset"), * @SWGResponse( * response="default", * ref="#/responses/GetThisWeeksGameSoftwareReleaseResponder" * ) * ) */ final class GetThisWeeksGameSoftwareRelease
  55. 55. swagger.jsonの生成 $ php artisan l5-swagger:generate 開発環境にデプロイする際に実行 もしくは config/l5-swagger.php の 'generate_always' => true にすることで SwaggerUIにアクセスするたびに生成されます。 /api/documentation にアクセスするとSwaggerUIが展開される。 エンドポイントを変更したい場合は config/l5-swagger.php の 'api' => 'api/documentation', の項目から変更できます。
  56. 56. まとめ ADRで責務を明確に REST API Level3はHAETOASで実現 Swagger-php(L5-Swagger)を利用して実装=定義書 ADRで実装することでアノテーションもModel,Response,Request で分離できる PRレビューを行っている場合はアノテーションと実装が一致して いるかを確認事項にできる フロントエンジニアに最新の状態の定義書を提供できる
  57. 57. thanks :)

×