[PHP]DOMDocumentクラスを使ってHTMLを構築してみる

PHPを使ってHTMLを構築する時、一般的にはechoやヒアドキュメント、あるいはphpタグから脱出させて書くことになると思います。今回は、PHPでHTMLを出力する方法はこれだけではない！という記事です。

結論から言ってしまいますが、実用性は欠落しているので、「こんな方法もあるんだ！」みたいな温度感で読んでもらえたら幸いです。

##DOMDocumentクラスって何？
Web制作に携わっている方ならPHPはある程度使い慣れた言語かもしれませんが、`DOMDocument`クラスを使ったことがあるという方は少ないかもしれません。なぜならスクレイピングやDOM解析を行わない限りはお目にかかることのないクラスだからです。

改めて、`DOMDocument`クラスは、公式の説明によると、`HTML ドキュメントあるいは XML ドキュメント全体を表し、 ドキュメントツリーのルートとなります。`ということです。だいたい以下のようなケースで使用することが想定されます。

- スクレイピング
- DOM解析
- fopenと組み合わせて外部HTMLを開いてDOMの微調整をする
- XMLの生成

こんなところだと思います。あとは、XMLサイトマップの生成なんかに使っている人もいました。

##DOMDocumentを使ってHTMLを構築する
さて、本題ですがこの`DOMDocument`メソッドを使ってHTMLを構築することができます。例えば以下のようなコード

```php
$dom = new DOMDocument();
$box = $dom->createElement("div");
$text = $dom->createElement("p", "hogehoge");
$box->appendChild($text);
$dom->appendChild($box);
echo $dom->saveHTML();

//実行結果
//<div><p>hogehoge</p></div>
```

見ていただくとわかる通り、JacaScriptの`createElement`のような構文でDOMを生成していくことができるわけです。

ちなみに`class`をつけたり`href`をつける場合はJavaScriptと同じで以下のようにします。

```php
$box->setAttribute("class", "wrapper"); //クラスを追加
$link->setAttribute("href", "http://example.com"); //hrefを追加
```

##使いにくいのでDOM生成用にクラスを作ってみる
さて、しかし実際のWEB制作現場では`class`をつける必要があったり、するので、今のままだとHTMLを生成するのは結構面倒くさいです。誰が得するのかわかりませんが、使いやすくしてみましょう。

```php
class PHPDOM
{
    public function __construct()
    {
        $this->doc = new DOMDocument('1.0', 'UTF-8');
    }

    public function setNode(string $tagName, ?string $className = null, ?string $text = null): object
    {
        $node = $this->doc->createElement($tagName, $text);
        if (!empty($className)) {
            $node->setAttribute("class", $className);
        }

        return $node;
    }

    public function a(string $href, ?string $className = null, ?string $text = null): object
    {
        $node = $this->doc->createElement("a", $text);
        if (!empty($className)) {
            $node->setAttribute("class", $className);
        }
        $node->setAttribute("href", $href);

        return $node;
    }

    public function img(string $src, ?string $className = null): object
    {
        $node = $this->doc->createElement("img");
        if (!empty($className)) {
            $node->setAttribute("class", $className);
        }
        $node->setAttribute("src", $src);

        return $node;
    }

    public function generator(object $obj):void
    {
        $this->doc->appendChild($obj);
        echo $this->doc->saveHTML();
    }
}
```

こうしておけば、`class`や特殊な属性をつける必要のあるタグを使う時でもある程度スリム化することができます。例えば以下のように使えます。

```php
$array = [
    "hoge" => "https://hoge.com",
    "fuga" => "https://fuga.com",
    "piyo" => "https://piyo.com"
];

$dom = new PHPDOM();
$list = $dom->setNode("ul", "list"); //<ul class="list"></ul>をつくる

foreach ($array as $name => $url) {
    $item = $dom->setNode("li", "list-item"); //<li class="list-item"></li>を作る
    $item->appendChild($dom->a($url, "list-item__link", $name)); //liの中にaタグを入れる
    $list->appendChild($item); //ulの中にliを入れる
}
$dom->generator($list); // $listをHTMLとしてecho


//実行結果
// <ul class="list">
//     <li class="list-item"><a class="list-item__link" href="https://hoge.com">hoge</a></li>
//     <li class="list-item"><a class="list-item__link" href="https://fuga.com">fuga</a></li>
//     <li class="list-item"><a class="list-item__link" href="https://piyo.com">piyo</a></li>
// </ul>
```

##DOMDocumentを使うメリット
初めて見た人からすれば、「なんじゃこりゃ！？」となるはずなので、大人数で開発をする場合などには向いていませんが、それでもメリットがないわけではありません！…多分

###phpタグの外に出さなくてよくなる
例えばWordPressとかだと以下のようなコードは結構見慣れた構文かと思います。

```php
<?php if ($query->have_posts()): ?>
<section class="wrapper">
    <?php
    while ($query->have_posts()):
        $query->the_post();
    ?>
    <div class="item">
        <p class="title"><?=get_the_title()?></p>
    </div>
    <?php endwhile; ?>
</section>
<?php endif; ?>
```
phpタグとhtmlタグがごちゃごちゃしてて見にくい！`DOMDocument`を使えば以下のようにスッキリ（?）します。

```php
<?php
if ($query->have_posts()){
    $dom = new PHPDOM();
    $sec = $dom->setNode('section', "wrapper");
    while($query->have_posts()){
        $item = $dom->setNode("div", "item");
        $item->appendChild($dom->setNode("p", "title", get_the_title()));
        $sec->appendChild($item);
    }
    $dom->generator($sec);
}
?>
```

サムネイルの有無やカスタムフィールドの有無を確かめてechoしていくような、もっとごちゃごちゃしたコードだと割とスッキリしたような錯覚に陥ることもできそうですね！
「ヒアドキュメントを駆使すればわざわざ外に出さずともキレイに書くことができる」という言葉は私の辞書にはない！（錯乱）

###タイプミスが少なくなる
BEMを使ってコーディングをしていくような場合に、class名のタイプミスによってハマった人も少なくはないはずです。

BEMとは、`<div class="block__element--modifire">`のようにclassの命名規則を固定することで、特にSCSSでのコーディングをやりやすくする記法ですが、htmlでタイプミスをすると特定までに時間がかかったりすることがあります。

そんなタイプミスを防ぐためにはこんな感じにすれば良いかもしれません。

```php
if ($query->have_posts()){
    $class = "list";
    $dom = new PHPDOM();
    $sec = $dom->setNode('section', $class);
    while($query->have_posts()){
        $item = $dom->setNode("div", "{$class}__item");
        $item->appendChild($dom->setNode("p", "{$class}__item-title", get_the_title()));
        $sec->appendChild($item);
    }
    $dom->generator($sec);
}
```

もっと簡単な書き方もあるんですけどね…。

##パフォーマンスについて
パフォーマンスについては、言うまでもないかもしれませんが、悪いです。普通にechoしたほうが圧倒的に速いです。div一個つくるのにどんだけ関数コールしてるんだって話です。（泣）
DOMがでかくなればなるほど変数が増えていきますし、そもそもDOM解析に関してもDOMDocumentより正規表現のほうが速いなんて記事も見かけたことがあります。（※要検証）

##結論：使う必要はないけど方法の一つとしてはあり（？）
…とまぁ色々と書いてきましたが、多分普通にヒアドキュメントや三項演算子があればphpタグの外に出す必要も無ければclass名のタイプミスが起こることも少ないと思うので、わざわざ使うメリットはないかもしれません。

そもそもJS界隈でもcreateElementメソッドが使いにくいからReactとかVueみたいなフレームワークが流行ってるわけですし、PHPにおいても分かりにくい構文を使うよりも広く普及している構文を使うほうが良いと思います。

ただ、個人のサイトをつくるような場合には、ひょっとすると使える場面が来るかもしれないので、DOMDocument君のことも忘れないであげてください。