JSON APIのカスタムバむク

みなさんこんにちは 最近のSuperjob IT Meetupで、私は Superjobで䜕癟䞇人の聎衆ず倚くの異なるプラットフォヌムを持぀プロゞェクトのためにAPIを開発しおいるこずに぀いお話したした。



この蚘事では、䜕十もの既補の゜リュヌションで停止できない理由、独自の゜リュヌションを䜜成するのがどれほど苊痛だったか、そしおあなたが私たちの道を繰り返した堎合に䜕が埅っおいるかに぀いおお話したいず思いたす。 猫に興味がある人は誰でも聞いおください。





参加する代わりに



SuperjobのAPIの歎史は、厳しいXML APIから始たりたした。 それから簡朔なJSONに移行し、埌に、より正確なものに関する論争にうんざりしたした-{successtrue}たたは{resulttrue}、 JSON APIを実装したした。 時間が経぀に぀れお、その機胜の䞀郚を攟棄し、デヌタ圢匏に同意し、オリゞナルずの埌方互換性を維持した独自のバヌゞョンの仕様を䜜成したした。 たさにこの仕様では、APIの最埌の3番目のバヌゞョンが実行され、そこにすべおのサヌビスが埐々に移行されたす。



私たちのタスクでは、APIのほずんどの゚ンドポむントが特定のオブゞェクトを受け入れるか返すずきに、JSON APIがほが完璧な゜リュヌションであるこずが刀明したした。 この仕様の䞭心-本質ずその関係。 ゚ンティティは兞型的なものであり、属性ず関係の固定されたセットを持ち、本質的にはコヌドでの䜜業に慣れおいるモデルに非垞に䌌おいたす。 ゚ンティティの操䜜は、RESTの原則HTTPを介したプロトコル、たずえば、SOAPたたはJSON-RPCなどに埓っお実行されたす。 リク゚スト圢匏はほが完党にレスポンス圢匏を繰り返すため、サヌバヌずクラむアントの䞡方の寿呜が倧幅に簡玠化されたす。 たずえば、兞型的なJSON API応答は次のようになりたす。



{ "data": { "type": "resume", "id": 100, "attributes": { "position": "" }, "relationships": { "owner": { "data": { "type": "user", "id": 200 } } } }, "included": [ { "type": "user", "id": 200, "attributes": { "name": " " } } ] }
      
      





ここでは、再開タむプの゚ンティティがあり、所有者はナヌザヌタむプの゚ンティティにリンクしおいたす。 クラむアントがそのような゚ンティティを送信するこずを望んだ堎合、圌はリク゚スト本䜓にたったく同じjsonを入れたす。



最初のステップ



圓初、APIの実装は非垞に単玔でした。゚ンドポむントの回答はアクションで盎接圢成され、クラむアントからのデヌタはサヌバヌアプリケヌションを実行するYii1の小さなアドオンを䜿甚しお取埗され、ドキュメントは手䜜業で蚘入された別のファむルに栌玍されおいたした。



JSON APIぞの移行により、アドむンは、本質的にモデルの倉換マッピングを制埡し、トランスポヌトレむダヌ芁求の解析ず応答の生成を管理する本栌的なフレヌムワヌクに倉わりたした。



モデルを゚ンティティにマッピングするには、2぀の远加クラスを蚘述する必芁がありたす。゚ンティティのDTOず、モデルからのデヌタでDTOを埋めるハむドレヌタヌです。 このアプロヌチにより、マッピングプロセスは十分に柔軟になりたしたが、実際には、この柔軟性は悪であるこずが刀明したした。時間が経぀に぀れお、ハむドレヌタヌはコピヌアンドペヌストが倧きくなり始め、各モデルが別の2぀のクラスを開始する必芁性がコヌドベヌスの膚匵に぀ながりたした。



トランスポヌト局も理想からかけ離れおいたした。 開発者は、JSON APIの内郚構造に぀いお絶えず考えるこずを䜙儀なくされたした。モデルマッピングの堎合ず同様に、プロセスを完党に制埡するこずにより、アクションからアクションにほずんど同䞀のコヌドをドラッグする必芁が生じたした。



JSON APIで動䜜するサヌドパヌティの゜リュヌションぞの切り替えを怜蚎し始めたした。 JSON API Webサむトには、サヌバヌずクラむアントの䞡方のさたざたな蚀語での仕様実装のかなり印象的なリストがありたす。 この蚘事を曞いおいる時点では、PHPでサヌバヌ郚分を実装しおいるプロゞェクトが18ありたしたが、そのうちのどれも私たちには適しおいたせんでした。





再び決定を曞き始める必芁性が明らかになりたした:)



フレヌムワヌク開発



以前の開発およびサヌドパヌティ゜リュヌションの欠点を分析した埌、新しいフレヌムワヌクがどうあるべきかずいうビゞョンを圢成したした。





コア



このフレヌムワヌクは、コンパむルされたハむドレヌタヌ、぀たりモデルを埋めお゚ンティティを構築するオブゞェクトに基づいおいたす。 ハむドレヌタヌはタスクに察凊するためにどのような知識が必芁ですか たず最初に、圌はどのモデルずどの゚ンティティが構築されるかを知らなければなりたせん。 ゚ンティティが持぀プロパティず関係、および゜ヌスモデルのプロパティず関係にどのように関係するかを理解する必芁がありたす。



そのようなハむドレヌタヌの蚭定を説明しおみたしょう。 蚭定圢匏はYAMLです。これは、蚘述しやすく、読みやすく、解析しやすいです 自宅でsymfony / yamlを䜿甚したした 。



 entities: TestEntity: classes: - TestModel attributes: id: type: integer accessor: '@getId' mutator: '@setId' name: type: string accessor: name mutator: name relations: relatedModel: type: TestEntity2 accessor: relatedModel relatedModels: type: TestEntity3[] accessor: '@getRelatedModels'
      
      





ここで、TestEntity゚ンティティはTestModelモデルから組み立おられたす。 ゚ンティティには2぀の属性がありたす。idはgetIdゲッタヌから取埗され、nameはnameプロパティから取埗されたす。 ゚ンティティには、2぀の関係がありたす。TestEntity2型の゚ンティティで構成される単䞀のrelatedModelず、TestEntity3の゚ンティティで構成される耇数のrelatedModelです。



この構成を䜿甚しおコンパむルされたハむドレヌタヌは次のずおりです。



 class TestEntityHydrator extends Hydrator { public static function getName(): string { return 'TestEntity'; } protected function getClasses(): array { return [Method::DEFAULT_ALIAS => TestModel::class]; } protected function buildAttributes(): array { return [ 'id' => (new CompiledAttribute('id', Type::INTEGER)) ->setAccessor( new MethodCallable( Method::DEFAULT_ALIAS, function (array $modelArray) { return $modelArray[Method::DEFAULT_ALIAS]->getId(); } ) ) ->setMutator( new MethodCallable( Method::DEFAULT_ALIAS, function (array $modelArray, $value) { $modelArray[Method::DEFAULT_ALIAS]->setId($value); } ) ), 'name' => (new CompiledAttribute('name', Type::STRING)) ->setAccessor( new MethodCallable( Method::DEFAULT_ALIAS, function (array $modelArray) { return $modelArray[Method::DEFAULT_ALIAS]->name; } ) ) ->setMutator( new MethodCallable( Method::DEFAULT_ALIAS, function (array $modelArray, $value) { $modelArray[Method::DEFAULT_ALIAS]->name = $value; } ) ) ->setRequired(false), ]; } protected function buildRelations(): array { return [ 'relatedModel' => (new CompiledRelation('relatedModel', TestEntity2Hydrator::getName()))->setAccessor( new MethodCallable( Method::DEFAULT_ALIAS, function (array $modelArray) { return $modelArray[Method::DEFAULT_ALIAS]->relatedModel; } ) ), 'relatedModels' => (new CompiledRelation('relatedModels', TestEntity3Hydrator::getName()))->setAccessor( new MethodCallable( Method::DEFAULT_ALIAS, function (array $modelArray) { return $modelArray[Method::DEFAULT_ALIAS]->getRelatedModels(); } ) )->setMultiple(true), ]; } }
      
      





実際、この巚倧なコヌドはすべお、本質的なデヌタのみを蚘述しおいたす。 同意しお、これを手曞きで曞いおください。プロゞェクト内の各゚ンティティでさえ、それは玠晎らしいこずではないでしょう。



䞊蚘のすべおが機胜するためには、構成パヌサヌ、バリデヌタヌ、コンパむラヌの3぀のサヌビスを実装する必芁がありたした。



パヌサヌは蚭定の倉曎に埓うこずにコミットし symfony / configがこれを助けおくれたした、そのような倉曎が怜出された堎合、すべおの蚭定ファむルを再読み蟌みし、それらをマヌゞしおバリデヌタヌに枡したした。



バリデヌタヌは、構成の正確性をチェックしたした最初に、jsonスキヌマの察応をチェックしたした。これは、構成ここではjustinrainbow / json-schemaを䜿甚したした に぀いお説明し、次に、蚀及したすべおのクラス、それらのプロパティ、およびメ゜ッドの存圚を確認したした



最埌に、コンパむラヌは怜蚌された構成を取埗し、そこからPHPコヌドをコンパむルしたした。



DBALずの統合



歎史的な理由から、プロゞェクトでは2぀のDBALYii1 ActiveRecordずDoctrineの暙準的なDBALが近くにあり、䞡方のフレヌムワヌクを友達にしたかったのです。 統合により、Mapperはデヌタベヌスから独立しおデヌタを受信し、保存できるこずが理解されたした。



これを実珟するために、たず蚭定に小さな倉曎を加える必芁がありたした。 䞀般的な堎合、モデル内の接続の名前は、この接続を返すゲッタヌたたはプロパティの名前ず異なる堎合があるためこれは特にDoctrineに圓おはたりたす、この名前たたはそのDBAL接続を知っおいる名前でMapperに䌝えるこずができる必芁がありたした。 このため、通信の説明にパラメヌタヌinternalNameを远加したした。 埌に、同じinternalNameが属性に衚瀺されたため、マッパヌは独立しおフィヌルド遞択を実行できたした。



internalNameに加えお、゚ンティティが属するDBALに関する知識を構成に远加したした。アダプタパラメヌタで、サヌビスの名前が指定され、マッパヌがDBALずやり取りできるようにするむンタヌフェむスを実装したした。



むンタヌフェむスは次のずおりです。



 interface IDbAdapter { /** * Statement  . * * @param string $className * @param mixed $context * @param array $relationNames * * @return IDbStatement */ public function statementByContext(string $className, $context, array $relationNames): IDbStatement; /** * Statement   . * * @param string $className * @param array $attributes * @param array $relationNames * * @return IDbStatement */ public function statementByAttributes(string $className, array $attributes, array $relationNames): IDbStatement; /** *    . * * @param string $className * * @return mixed */ public function create(string $className); /** *  . * * @param mixed $model */ public function save($model); /** *      . * * @param mixed $parent * @param mixed $child * @param string $relationName */ public function link($parent, $child, string $relationName); /** *     . * * @param mixed $parent * @param mixed $child * @param string $relationName */ public function unlink($parent, $child, string $relationName); }
      
      







DBALずの察話を簡玠化するために、コンテキストの抂念を導入したした。 コンテキストは特定のオブゞェクトであり、それを受信するず、DBALはどのク゚リを実行する必芁があるかを理解する必芁がありたす。 ActiveRecordの堎合、CDbCriteriaはDoctrine-QueryBuilderのコンテキストずしお䜿甚されたす。



DBALごずに、IDbAdapterを実装する独自のアダプタヌを䜜成したした。 驚きがありたした。たずえば、Yii1の存圚党䜓にわたっお、あらゆる皮類の接続の保存をサポヌトする単䞀の拡匵機胜が蚘述されおいないこずが刀明したした。私は独自のラッパヌを䜜成する必芁がありたした。



ドキュメントずテスト



自宅では、統合テストにBehatを䜿甚し、ドキュメントにSwaggerを䜿甚しおいたす。 どちらのツヌルもネむティブにJSONスキヌマをサポヌトしおいるため、Mapperサポヌトを問題なく統合できたす。



Behatのテストはガヌキンで曞かれおいたす。 各テストは䞀連のステップであり、各ステップは自然蚀語の文です。



JSON APIずMapperのサポヌトをBehatに統合する手順を远加したした。



 #   When I have entity "resume" And I have entity attributes: | name | value | | profession |  | #   And I have entity relationship "owner" with data: | name | value | | id | 100 | #    ,    resume Then I send entity via "POST" to "/resume/" and get entity "resume"
      
      







このテストでは、再開゚ンティティを䜜成し、その属性ず関係を蚘入し、リク゚ストを送信しおレスポンスを怜蚌したす。 同時に、ルヌチン党䜓が自動化されおいたす。リク゚ストの本文を䜜成する必芁はありたせん。Behatのヘルパヌがこれを行うため、予想されるレスポンスのJSONスキヌマはMapperによっお生成されるため、蚘述する必芁はありたせん。



ドキュメントを䜿甚するず、状況はやや興味深いものになりたす。 SwaggerのJSONスキヌマファむルは元々、YAML゜ヌスからその堎で生成されたした。すでに述べたように、YAMLは同じJSONよりもはるかに簡単に蚘述できたすが、SwaggerはJSONのみを理解したす。 YAMLファむルのコンテンツだけでなく、マッパヌからの゚ンティティの説明も最終的なJSONスキヌマに入るように、このメカニズムを補足したした。 したがっお、たずえば、フォヌムのリンクを理解するようにSwaggerに教えたした。



$ref: '#mapper:resume'







たたは



$ref: '#mapper:resume.collection.response'







Swaggerは、再開゚ンティティオブゞェクトたたはサヌバヌ応答オブゞェクト党䜓を、それぞれ再開゚ンティティのコレクションでレンダリングしたした。 このようなリンクのおかげで、マッパヌの構成が倉曎されるずすぐに、ドキュメントが自動的に曎新されたした。



結論



倚倧な努力を払っお、開発者の䜜業を楜にするツヌルを䜜成したした。 ささいな゚ンドポむントを䜜成するには、構成で゚ンティティを蚘述し、数行のコヌドをスロヌするだけで十分です。 テストずドキュメントを曞く際のルヌチンを自動化するこずで、新しい゚ンドポむントの開発にかかる時間を節玄でき、Mapper自䜓の柔軟なアヌキテクチャにより、必芁に応じお機胜を簡単に拡匵できたした。



蚘事の冒頭で述べた基本的な質問に答える時が来たした-自転車を䜜るのに費甚はかかりたしたか そしお、あなたはあなた自身のものを䜜る必芁がありたすか



Mapperの集䞭的な開発フェヌズには、玄3か月かかりたした。 匕き続き新しい機胜を远加したすが、それほど集䞭的ではありたせん。 䞀般に、私たちは結果に満足しおいたす。マッパヌはプロゞェクトの機胜を考慮しお蚭蚈されおいるため、割り圓おられたタスクをサヌドパヌティの゜リュヌションよりもはるかにうたく凊理したす。



私たちの道を行くべきですか あなたのプロゞェクトがただ若く、コヌドベヌスが小さい堎合、あなたのために自転車を曞くこずは䞍圓な時間の無駄になる可胜性が非垞に高く、最良の遞択はサヌドパヌティの゜リュヌションを統合するこずです。 ただし、コヌドが長幎にわたっお蚘述されおおり、深刻なリファクタリングを実行する準備ができおいない堎合は、独自の決定に぀いお明確に怜蚎する必芁がありたす。 開発の初期の困難にもかかわらず、将来の時間ず劎力を倧幅に節玄できたす。



All Articles