前にテンプレートエンジンでも型チェックしたいということを書いたが、まだ一点不満があった。
そもそもテンプレートエンジンを使う唯一にして最大のメリットは、htmlspecialchars
を漏れなく実行することにある。
PHP自体がテンプレートエンジンと言われていても、別途テンプレートエンジンのライブラリを使うのは
<?= $message ?>
というような出力を書いてしまってXSSが発生するということを防ぐのが目的である。
他の機能は、例えばBladeなら
@if ($flg)
<span>OK</span>
@endif
@foreach ($data as $key => $row)
<div>{{ $row->id }}: {{ $row->name }}</div>
@endforeach
<?php if ($flg): ?>
<span>OK</span>
<?php endif; ?>
<?php foreach ($data as $key => $row): ?>
<div><?= $row->id ?>: <?= $row->name ?></div>
<?php ehdforeach; ?>
素のPHPを使ってもあまり変わらない。
違いは、<?= $row->name ?>
の箇所が少々危険である、ということぐらいだ。
おそらく<?= htmlspecialchars($row->name) ?>
と書きたかったはずだ。
つまり、素のPHPをテンプレートエンジンとして使ってPHPStanで型チェックをするためには、あと一手htmlspecialchars
をチェックする機能も必要になってくる。
幸いPHPStanは拡張機能というプラグイン的な仕組みがあるので、ちょっとしたエラーチェックのルールを独自に追加できる。
なので作った。
https://github.com/nishimura/phpstan-echo-html-rule
<?php
namespace App;
class ProductDto
{
/** @var int */
public $product_id;
/** @var string */
public $name;
/** @var ?string */
public $description;
}
このようなデータがあるとき、表示用のHTMLを含むPHPテンプレートクラスは
<?php
namespace App;
class ProductHtml {
public function view(ProductDto $product): void {
?>
<div>
<div>
<?= $product->product_id ?>
</div>
<div>
<?= $product->name ?>
</div>
<div>
<?= $product->description ?>
</div>
</div>
<?php
}
}
こんな感じになるだろう。
phpstan-echo-html-rule
を使えば
3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ ----------------------------------------------------
Line ProductHtml.php
------ ----------------------------------------------------
16 Parameter #1 (string) is not safehtml-string.
19 Parameter #1 (string|null) is not safehtml-string.
------ ----------------------------------------------------
[ERROR] Found 2 errors
htmlspecialchars
を使わずに出力している箇所のエラーを報告してくれるようにした。
独自の仮想的な型safehtml-string
を導入したので、htmlspecialchars
はsafehtml-string
を返す必要がある。
<?php
/**
* @param int|string|null $input
* @return safehtml-string
*/
function h($input)
{
return htmlspecialchars((string)$input);
}
/**
* @param int|string|null $input
* @return safehtml-string
*/
function raw($input)
{
return (string)$input;
}
このようなユーティリティ関数を用意する。
そしてエラーを修正する。
<?php
namespace App;
class ProductHtml {
public function view(ProductDto $product): void {
?>
<div>
<div>
<?= $product->product_id ?>
</div>
<div>
<?= h($product->name) ?>
</div>
<div>
<?= h($product->description) ?>
</div>
</div>
<?php
}
}
これでテンプレートエンジンを使わなくてもhtmlspecialchars
なしの出力を網羅的にチェックできるようになった。