CakePHPでpugる

この記事は、[FORK Advent Calendar 2018](https://qiita.com/advent-calendar/2018/fork) の1日目の記事です。

もうアドベントカレンダーの季節ですか。
時間の流れが早すぎて困りますね。。

私は普段CakePHPにSmartyを組み込んで開発しているのですが、
ビューのテンプレートとして組み込む前のHTMLは、pugで書いたのをコンパイルしたものだったりするので、
pugをそのまま使えちゃった方が良いよね？と思って色々試した記録です。

# ライブラリ探し

ググったら、速攻でCakePHP用のものを見つけました。
https://github.com/elquimista/cakephp-jade
が、GitHubを見ると3年前で止まっている。。

次に見つけたのはコレ。
https://github.com/pug-php/pug
動きも活発そうだし、良さそうですね。
これを利用したLaravelとかSymfonyなどのフレームワーク用のやつもあるし！
あれ、、CakePHPのがない。

ということで、2つ目のライブラリを継承しつつ、
1つ目のコードを~~パクって~~参考にして、ちょっと作ってみました。

# PugView（簡易版）

出来上がったのがこちらです。

```php
<?php
namespace App\View;

use Cake\View\View;
use Pug\Pug;

class PugView extends View
{
    protected $_ext = '.pug';
    protected $pug;

    public function initialize()
    {
        $this->pug = new Pug([
            'pretty' => true,
            'cache' => CACHE . 'views' . DS,
        ]);

        // レイアウトを使わないようにする
        $this->setLayout('');
    }

    protected function _render($viewFile, $data = [])
    {
        if (empty($data)) {
            $data = $this->viewVars;
        }

        $data = array_merge(
            $data,
            [
                'view' => $this,
            ]
        );

        return $this->pug->renderFile($viewFile, $data);
    }
}
```

ほとんど何も書いてないですね！
上記のソースを `src/View/PugView.php` として保存し、使いたいコントローラーで、

```php
$this->viewBuilder()->setClassName('Pug');
```
としてやればOKです！

## 簡易版の仕様

- pugファイルは通常通り `src/Template/` に配置する
- 拡張子は .pug でOK
- .ctp は使わない
- CakePHPのレイアウト、エレメントは使えないので、pug の extends や include を使おう
- テンプレートファイルでは、`$this` の代わりに `$view` を使おう
- ヘルパーは使える

# お試し

簡易版が出来たので、お問い合わせフォームを想定して使ってみましょう。

**テンプレートファイルの構成**

```
src/Template/
    _parts/             -- extends や include で使うやつ
        _header.pug     -- ヘッダーのパーツ
        _footer.pug     -- フッターのパーツ
        _layout.pug     -- ベースとなるレイアウト
    Contact/
        complete.pug   -- 完了画面
        confirm.pug    -- 確認画面
        index.pug      -- 入力画面
```

## _parts/_layout.pug

レイアウトはとりあえずシンプルに。

```
doctype
html(lang="ja")
  head
    meta(charset="utf-8")
    title!=title
    link(rel="stylesheet", href="/common/base.css")

  body
    include _header

    block content

    include _footer
```

## Contact/input.pug

Formヘルパー使わずにベタで書いてみます。

```
extends ../_parts/_layout

block content

  form(method="post" action!=$view->Url->build(['action' => 'confirm']))
    .form_column
      label.form_label(for="company")
        |お名前
        span.form_required ※必須
      .form_control
        input.form_input(type="text" placeholder="山田太郎" name="name" value=$input.name)
        if $errors.name
          p.form_error
           = $errors.name

    .form_column
      label.form_label
        |メールアドレス
        span.form_required ※必須
      .form_control
        input.form_input(type="email" placeholder="user@example.com" name="email" value=$input.email)
        if $errors.email
          p.form_error
            = $errors.email

    .form_column
      label.form_label
        | 問い合わせ内容
        span.form_required ※必須
      .form_control
        textarea.form_input(name="content")
          = $input.content
        if $errors.content
          p.form_error
            = $errors.content

    .formBtnWrap
      button(type="submit") 入力内容のご確認
```

変数は下記をセットしているものとします。
`$input` : POSTデータ
`$errors` : バリデーションエラー（※デフォルトだと2次元配列になって扱いづらいので、下記のようにフォーマットしています）

```php
$errors = [
  'name'    => 'お名前を入力してください',
  'email'   => 'メールアドレスを入力してください',
  'content' => 'お問い合わせ内容を入力してください',
];
```

## 出力結果

何も入力せずにフォームを送信したときのHTMLソースはこんな感じになりました。
うまく行ってますね！

```html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title></title>
    <link rel="stylesheet" href="/common/base.css">
  </head>
  <body>
    <header>
      <div class="header">ヘッダー</div>
    </header>
    <form method="post" action="/contact/confirm">
      <div class="form_column">
        <label class="form_label" for="company">お名前<span class="form_required">※必須</span></label>
        <div class="form_control"><input class="form_input" type="text" placeholder="山田太郎" name="name" value="">          <p class="form_error">お名前を入力してください</p>
</div>
      </div>
      <div class="form_column">
        <label class="form_label">メールアドレス<span class="form_required">※必須</span></label>
        <div class="form_control"><input class="form_input" type="email" placeholder="user@example.com" name="email" value="">          <p class="form_error">メールアドレスを入力してください</p>
</div>
      </div>
      <div class="form_column">
        <label class="form_label">問い合わせ内容<span class="form_required">※必須</span></label>
        <div class="form_control">
          <textarea class="form_input" name="content"></textarea>
          <p class="form_error">お問い合わせ内容を入力してください</p>
        </div>
      </div>
      <div class="formBtnWrap">
        <button type="submit">入力内容のご確認</button>
      </div>
    </form>
    <footer>
      <div class="footer">フッター</div>
    </footer>
  </body>
</html>
```


# その他 Tips など

触ってみて個人的にハマったポイントなどを少し書いておきます。

## 文法

Pug-php は Phug をベースにしていますので、
まず基本的な文法はこちらを読んでおきましょう。
https://www.phug-lang.com/

## レイアウト使えない問題

上述したPugView クラスでは、レイアウトを使わないように設定していますが、
これは、Cakeのレイアウトの機能を使うと doctype が変わってしまったり、意図しないHTMLが出力されてしまったためです。
なので、そこは使わないようにして、 pug の extends を使う方針にしました。

おそらく、レイアウトとビューを2重にコンパイルしようとしているためだと思うのですが、
細かくは追えてません。。

## 同じ名前の変数を使うとぶつかる
当たり前な話ですが、コントローラーからセットした変数と同じ名前の変数がテンプレート内で宣言・代入されていると、そちらが優先されてしまいます。
そのため、元から pug で使われている変数名と、コントローラーからセットする変数名はぶつからないようにしないと行けません。
これはちょっと命名を工夫しないとマズイですね。。

## 変数の出力
Phug では`$`を付けることになっていますが、Pug-phpでは`$`がなくても出力されるようです。

```
// どっちでもOK
= $a
= a
```

※オブジェクトのメソッドを呼ぶ場合は、`$`が必要です。

```
!= $obj->method()
```


## エスケープ処理
変数を出力する際にエスケープするかどうかは、`=` か `!=` のどちらを使うかで切り替えられます。

```
= $a   // エスケープされる
!= $a  // エスケープされない
```

## checked の切り替え
ラジオやチェックボックスの checked を付けるときは、下記のように書くと使えます。
$checked の値は、'checked' ではなく、true/false の bool を入れます。

```
:php
  $checked = (isset($input['checked']) && $input['checked'] == 1);
input(type="checkbox" checked=$checked)
```

## Formヘルパーも使えます
上述したサンプルはベタで書いてましたが、こんな感じでFormヘルパー使って書くことも出来ます。
エスケープされないように `!=` を使います。

```
  != $view->Form->create('contact')
  != $view->Form->control('name')
  != $view->Form->control('email')
  != $view->Form->control('content', ['type' => 'textarea'])
```

# 感想

手探りな状態なので、まだまだ気づいていない罠とかありそうな気もしてますが、
結構行けるんじゃないか？という感触でした。
なので、引き続き探ってみようかと思います。