Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

YAML+Smartyでコードを自動生成する

先のPEAR::Services_Recruit_Abroad作ってみた - Do You PHP はてなですが、アクセサ(getter/setter)があまりに多く、手書きするのはちょっと現実的ではないなぁ、と思ってました。PHP対応のIDEを使っていれば自動生成もできるんでしょうが、コメントを手書きすることなども含めると、どうもなぁ。。。となってしまいます。
ということで、今回はYAMLSmartyでコードを生成するバッチをちょこっと作ってみました。
仕様としては、以下の通りです。

  • 生成されたコードはコピペして使うことを前提
  • YAMLを扱うため、syck拡張モジュールを使う
  • YAMLファイルにメンバー変数を定義する
    • メンバー変数名
    • メンバー変数の型(array/intのみ)
    • 型によってアクセサ(getter/setter)を作り分ける
  • コードの雛形はSmartyのテンプレートで定義する
  • メインスクリプトYAMLファイルを読み込み、Smartyにassign・fetch。fetchした内容をfile_get_contentsで出力

で、コードの方ですが、まずYAMLファイルから。このデータから作成されたコードは、先のPEAR::Services_Recruit_Abroadに含まれるAirline.phpで使っています。

properties:
 airline:
  description: "a airline name"
  type: array
 keyword:
  description: "your search keyword"
  type: array
 start:
  description: "the number of the first row"
  type: int
 count:
  description: "the row number of fetching data"
  type: int

次はテンプレートファイル。サクッと作るために、{litetal}が多くなっちゃってます;-)

<?php
{foreach from=$properties item=property key=key}
    /**
     * {$property.description}
     * @var {$property.type}
     * @access private
     */
    private ${$key};

{/foreach}
    /**
     * constructor
     *
     * @param  string $apikey the apikey string
     * @return void
     * @access public
     */
    public function __construct($apikey) {literal}{{/literal}
        parent::__construct($apikey);
{foreach from=$properties item=property key=key}
{if $property.type === 'array'}
        $this->{$key} = array();
{else}
        $this->{$key} = null;
{/if}
{/foreach}
    }

    /**
     * build the Query-String string
     *
     * @return string   built the Query-String string
     * @access protected
     */
    protected function buildParameters() {literal}{{/literal}
        return '' .
{foreach from=$properties item=property key=key}
               $this->build{$key|ucfirst}Parameters() .
{/foreach}
               '';
    }


{foreach from=$properties item=property key=key}
    /**
     * build the 'count' part of Query-String string
     *
     * @return string    the 'count' part of Query-String string
     * @access protected
     */
    protected function build{$key|ucfirst}Parameters() {literal}{{/literal}
        $params = '';
{if $property.type === 'array'}
        foreach ($this->get{$key|ucfirst}() as $key => $dummy) {literal}{{/literal}
            $params .= '&{$key}=' . $key;
        }
{else}
        if (!is_null($this->get{$key|ucfirst}())) {literal}{{/literal}
            $params .= '&{$key}=' . $this->get{$key|ucfirst}();
        }
{/if}
        return $params;
    }

{/foreach}
{foreach from=$properties item=property key=key}
{if $property.type === 'array'}
    /**
     * replace the large category codes
     *
     * @param  array $code an array of the large category codes
     * @return void
     * @access public
     */
    public function put{$key|ucfirst}(array $code) {literal}{{/literal}
        $this->{$key} = $code;
    }

    /**
     * add the large category code
     *
     * @param  string $code the large category code
     * @return void
     * @access public
     */
    public function add{$key|ucfirst}($code) {literal}{{/literal}
        $this->{$key}[$code] = true;
    }

    /**
     * remove the large category code
     *
     * @param  string $code the large category code
     * @return void
     * @access public
     */
    public function remove{$key|ucfirst}($code) {literal}{{/literal}
        if (isset($this->{$key}[$code])) {literal}{{/literal}
            unset($this->{$key}[$code]);
        }
    }
{else}
    /**
     * set {$property.description}
     *
     * @param  {$property.type} {$key} {$property.description}
     * @return void
     * @access public
     */
    public function set{$key|ucfirst}(${$key}) {literal}{{/literal}
{if $property.type === 'int'}
        ${$key} = $this->validateDigit(${$key});
{/if}
        $this->{$key} = ${$key};
    }
{/if}

    /**
     * return {$property.description}
     *
     * @return type {$property.description}
     * @access public
     */
    public function get{$key|ucfirst}() {literal}{{/literal}
        {literal}return{/literal} $this->{$key};
    }

{/foreach}
    /**
     * validate if the given value is digit, and return the int value
     *
     * @param  string    $param a parameter
     * @return int     a int value of the given parameter
     * @access private
     * @throws Exception throws when the given parameter is not digit
     */
    private function validateDigit($param) {literal}{{/literal}
        if (!preg_match('/^\d+$/', $param)) {literal}{{/literal}
            throw new Exception('Invalid format "' . $param . '"');
        }
        return (int)$param;
    }

最後にメインのPHPスクリプトです。

<?php
require_once 'Smarty/Smarty.class.php';

$YAML_FILENAME = './data/airline_property.yml';

$smarty = new Smarty();
$smarty->template_dir = './';
$smarty->compile_dir = './templates_c';

$contents = file_get_contents($YAML_FILENAME);
$properties = syck_load($contents);

$smarty->assign('properties', $properties['properties']);
$contents = $smarty->fetch('code.tpl');
echo $contents;
file_put_contents(basename($YAML_FILENAME) . '_code.php', $contents);

まあ、コード自体は全然難しくないですね :-)
データの作成がちょっと面倒なので実質手間的に変わらない気もしますが、データから自動生成することで「テンプレートに従った間違いのないコード」を作ることができます。よく言われる「楽をする」というメリットよりは、この「間違いのないコード」が作成できる=手戻りが発生しないというのが一番のメリットではないかと思います。まあ、テンプレートが間違っていれば当然テンプレートを修正する必要はありますが、再作成も当然自動なので大した問題にはならないと考えています。
また、HTML出力ではよくお目にかかるSmartyですが、個人的にはこういった何らかのコード・キュメントの出力やメール本文の作成でよく使います。結構便利ですよ。