はじめに
はじめまして。サムザップで新規のゲーム開発プロジェクトのサーバーサイドエンジニアをさせていただいてます。大川内です。
今回は、私が所属するプロジェクトのAPI及びマスタの自動生成機能についてお話をさせていただこうと思います。
背景
自分の担当している新規のゲームでは、当初、別のプロジェクトで製作した開発基盤を使って開発をしていました。
ざっくり言えば、CSVで定義したスキーマを元に、APIインターフェース部分のソースコードやマスタ取得クラスのソースコードを自動生成する、といったものです。
開発が進んでいくに連れて、この自動生成のシステムを見直そう!という話になりました。
その際に、真っ先に出てきたのがSwagger Codegenでした。
しかし、そのときすでに開発はかなり進んでおり、
- 今からAPIの実装を、Swagger Codegenの出力する形式に沿った形に修正するのはコストが高い。
- かといって、自分たちの生成したいソースコードに合わせてmustacheのテンプレートや、スクリプトを作り直すのも同じようにコストがかかる。
という状況でした。
以上の理由から、プロジェクトで使用しているフレームワークであるLumenを活用して、自動生成のシステムを作り直そう!というお話です。
方針
- APIインターフェースとマスタのスキーマについてはYAMLで作成
- 各種ソースコード生成に使うテンプレートエンジンにはBladeを利用
実装
YAMLの中身
基本的には、JSON Schemaの定義方法を採用。
ただし、マスタの記述等にも使うため、独自の拡張を加えています。
例えば、カードマスタがあったとしたらこんな感じです。
title: card description: card master sample type: object properties: id: type: number desctiption: this is card id foreign_key: chara.yml#properties/id name: type: string description: this is card name
APIの定義はこちら。
title: sample_api description: sample api path: api/sample request: type: object properties: id: type: number description: card id response: type: object properties: card_list: type: array items: $ref: ../component/card.yaml#properties status: type: number description: server status code
YAMLの読み込み
PHPでのJSON Schema読み込みで、外部ファイルへの依存が解決できそうなのが見当たらなかったため、独自実装しました。
YAMLの読み込みには、symfony/yaml
を利用し、依存性の解決は、下記のソースコードで行なっています。
<?php /** * @param array $o * @param string $file_dir * @return array */ public static function resolveDependencies(array $o, string $file_dir): array { array_walk_recursive($o, function (&$item, &$key) use ($file_dir) { if ($key === '$ref') { $path_list = preg_split("/#/", $item, 2); $file = $file_dir . '/' . $path_list[0]; if (!file_exists($file)) { throw new \Exception('The referenced file ' . $file . ' was not found'); } $route = preg_split('/\//', $path_list[1]); $ref = Yaml::parseFile($file); $ref = self::getObjectByRoute($ref, $route, $path_list[1] . ' was not defined in ' . $file); $key = $route[count($route) - 1]; $item = self::resolveDependencies($ref, $file_dir); } }); return $o; } /** * * 指定したルートで渡されたオブジェクトを探索し、見つかったオブジェクトを返す * * @param array $object * @param array $route * @param string $err_msg * @return mixed * @throws \Exception */ public static function getObjectByRoute(array $object, array $route, string $err_msg) { if (array_key_exists($route[0], $object)) { $value = $object[$route[0]]; array_shift($route); return empty($route) ? $value : self::getObjectByRoute($value, $route, $err_msg); } throw new \Exception($err_msg); }
上記のソースコードにある通り、 $ref
のvalueに、外部ファイルの該当部分を埋め込んでいます。
しかし、このままだと、 $ref
と言うキーは残ってしまうので、出てきたオブジェクトを整形する必要があります。(が、今回は本旨じゃないのと、プロジェクトでは、テンプレート側で対応しているので割愛します。)
コマンドの作成
コマンド内では、持ってきたオブジェクトをBladeで処理しやすいように変換しつつ、Bladeに渡しています。 やってることは、(オブジェクトの変換等を除けば)これだけです。
<?php $text = view($view_name, [ 'specification' => $define_object, // 依存解決したオブジェクト 'classname' => $this->camelize($define_object['title']) // タイトルをキャメルケースに変換して渡す ])->render();
コードの自動生成用のテンプレートへの反映
こんな感じでテンプレートに反映しています。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class {{$classname}}GenController extends BaseController { public function validation(Request $request) { @if (is_array($specification['request']['properties'])) $this->validate($request, [ @foreach($specification['request']['properties'] as $name => $val) '{{$name}}' => 'required', @endforeach @endif ]); } /** @if (is_array($specification['response']['properties'])) @foreach($specification['response']['properties'] as $name => $val) <?php if ($val['type'] == 'long') $val['type'] = 'int'; echo ' * @param ' . $val['type'] . ' $' . $name; ?> @endforeach @endif <?php echo ' * @return \App\Libraries\MsgpackResponse|\Illuminate\Http\JsonResponse'; ?> */ public function retval( @if (is_array($specification['response']['properties'])) @foreach($specification['response']['properties'] as $name => $val) <?php if ($val['type'] == 'long') $val['type'] = 'int'; ?> @if ($loop->last) {{$val['type']}} ${{$name}} @else {{$val['type']}} ${{$name}}, @endif @endforeach @endif ) { $response = []; @if (is_array($specification['response']['properties'])) @foreach($specification['response']['properties'] as $name => $val) $response['{{$name}}'] = ${{$name}}; @endforeach @endif return parent::return_value($response); } }
最後に
今回はLumenで各種ソースコード生成を自作したお話でしたが、Bladeをソースコードの自動生成に使うという経験は初めてだったので、面白いな、と思い記事にしてみました。
Laravel(Lumen)のような機能の多いフレームワークを使用していると、知らなかった機能に出会う機会も沢山あり、日々勉強しつつ新しい出会いを楽しんでいます。
ソースコードの自動生成などは、プロジェクトによってケースバイケースなので、お役に立つような内容かはわかりませんが、そんな使い方してるんだ!と、楽しんでいただけたら幸いです。