Bladeを利用して、コードを自動生成する(Lumen版)

f:id:sumzap_engineer_blog:20200205103404p:plain

はじめに

はじめまして。サムザップで新規のゲーム開発プロジェクトのサーバーサイドエンジニアをさせていただいてます。大川内です。

今回は、私が所属するプロジェクトの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)のような機能の多いフレームワークを使用していると、知らなかった機能に出会う機会も沢山あり、日々勉強しつつ新しい出会いを楽しんでいます。
ソースコードの自動生成などは、プロジェクトによってケースバイケースなので、お役に立つような内容かはわかりませんが、そんな使い方してるんだ!と、楽しんでいただけたら幸いです。

f:id:sumzap_engineer_blog:20200130111341j:plain
大川内 翔一

新規ゲームのサーバーサイドエンジニアをしています。
ツール作りが趣味。