多くのプログラミング言語には文字列を正規表現で置換する機能が備わっています。これを使うことで制御命令を組み込んだテンプレートを簡易に作れます。例えば次です。
<?php
class Replacer {
protected array $params;
/**
* @param array $params テンプレートに当てはめる文字列を決定するためのパラメータ。大体外部から来る。例えば ['name' => 'ホゲ太郎', 'question_1' => '1',]
*/
public function __construct( array $params ) {
$this->params = $params;
}
/**
* 外部から命令されて実行する置き換え処理(テンプレートの穴埋め)
*
* @param string $content 置き換え対象
*
* @return string
*/
public function replace(string $content): string {
$content = $this->replace_yes_or_no($content);
$content = $this->replace_word($content);
return $content;
}
/**
* 単純な置き換えメソッド。構文内を全て食べるので最後に実行する必要あり
*
*
* @param string $content
*
* @return string
*/
protected function replace_word(string $content): string{
$matches = [];
// 開き文字閉じ文字とその内部の文字列をくくる正規表現
// 例えば $this->params['name'] === 'ホゲ太郎' ならば %[name]% を ホゲ太郎 と置き換えます
// +?の指定で最左最短マッチとしないと複数の構文を巻き込んでしまいます
preg_match_all( '/%\[(.+?)\]%/', $content, $matches );
foreach ( $matches[1] as $key ) {
// マッチしたキー値を$this->paramsの文字列と置き換えていきます。
// この例では POST された値を元にすることを想定しているので数値 or 文字列 or 配列のパラメータで制御しています
if ( isset( $this->params[ $key ] ) ) {
$replaceStr = is_array( $this->params[ $key ] ) ? $this->params[ $key ][0] : $this->params[ $key ];
} else {
// パラメータがなければ空文字列に置き換え
$replaceStr = '';
}
$content = str_replace( "%[{$key}]%", $replaceStr, $content );
}
return $content;
}
protected function replace_yes_or_no( string $content ): string {
$matches = [];
// 開き文字閉じ文字とその内部の制御をくくる正規表現
// 例えば $this->params['question_1'] === 1 ならば %[yn question_1 1]% を YES と置き換えます
preg_match_all( '/%\[yn (.+?) (.+?)\]%/', $content, $matches );
foreach ( $matches[1] as $index => $key ) {
$requireStr = $matches[2][ $index ];// この文字列と一致するパラメータがあれば YES
if ( isset( $this->params[ $key ] ) ) {
if ( is_array( $this->params[ $key ] ) ) {
// パラメータが配列ならば含んでいるか否か
$replaceStr = in_array( $requireStr, $this->params[ $key ], true ) ? 'YES' : 'NO';
} else {
// パラメータが文字列的ならば完全一致
$replaceStr = $this->params[ $key ] == $requireStr ? 'YES' : 'NO';
}
} else {
// パラメータがなければ NO
$replaceStr = 'NO';
}
// 扱う変数が増えた時は変に変数を使わずに最初の正規表現のグループを使って置き換え対象文字列を参照すると良いです。
$content = str_replace( $matches[0][$index], $replaceStr, $content );
}
return $content;
}
}
$r = new Replacer(['name' => 'ホゲ太郎', 'question_1' => '1',]);
echo $r->replace("%[name]%:%[yn question_1 1]%");// ホゲ太郎:YES
この様に文字列の置き換えを簡単に制御できます。これならば構文木やトークンへの分解の様な気合の入った構文解析は必要なく、どの関数を実行するかという意味解析のみに集中できます。この作り(というか多分構文解析でも)の問題は開き文字、閉じ文字のトークンがそうでない何かしらと混ざり出すと処理がひどく複雑化することです。これを用いる場面でエスケープ処理の出番が必要ない様なトークンを指定すれば潜在的に異常動作を含んでいても、その異常動作が顕在化する場面はこれを用いられる場面でない、とできるのでトークン選定は重要です。
余談ですがPHP8の新機能の一つであるアトリビュートの決定には色々な人が右往左往しました。Qiitaや#externalsを追うとこの機能の説明における構文が<<>>であったり、@@であったり、#[]であったりと多様になっていて感じ入ります。