Help us understand the problem. What is going on with this article?

PHPStanで素のPHPをテンプレートとして使うとき、htmlspecialcharsをチェックする

前にテンプレートエンジンでも型チェックしたいということを書いたが、まだ一点不満があった。

そもそもテンプレートエンジンを使う唯一にして最大のメリットは、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

src/ProductDto.php
<?php

namespace App;

class ProductDto
{
    /** @var int */
    public $product_id;
    /** @var string */
    public $name;
    /** @var ?string */
    public $description;
}

このようなデータがあるとき、表示用のHTMLを含むPHPテンプレートクラスは

src/ProductHtml.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を導入したので、htmlspecialcharssafehtml-stringを返す必要がある。

functions.php
<?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;
}

このようなユーティリティ関数を用意する。

そしてエラーを修正する。

src/ProductHtml.php
<?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なしの出力を網羅的にチェックできるようになった。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away