zz log

zaininnari Blog

CakePHP + CacheHelper + html-minifier を CacheHelper と併用して使用する

概要

CakePHP + CacheHelper + html-minifier を併用できるようにしました。
エレメントとレイアウトのビューファイルの圧縮済みファイルを相対パスで保存し、かつ、読み込み時には、App::build で View パスの先頭にキャッシュパスを追加しています。

キャッシュファイルからのViewクラスでは、エレメントのビューファイルを App::pasth('View') に従い、検索します。
この時、App::build で View パスの先頭にキャッシュパスを追加しているので、キャッシュファイルがあればキャッシュファイルを、キャッシュファイルがなければ、通常のビューファイルを使ってレンダリングを行います。

環境

  • PHP 5.4.17
  • CakePHP 2.3.10
    • CacheHelper でビューキャッシュを有効
  • html-minifier 0.3.0

機能

  • キャッシュファイルがあればキャッシュファイルを優先して使用をする
    • プラグイン経由でビューもしっかり考慮
    • ビューキャッシュ化を回避するnocacheコメントは残す
  • _getViewFileName で検索されるビューファイルは相対パスでキャッシュをしない
    • 絶対パス等での読み込みが可能なため、View のパスが複数経路がある場合、後にあるファイルを読み込むことができ、意図しないファイルを読み込む可能性がある
    • エレメントやレイアウトのビューの検索は、絶対パス等での読み込みが不可能で、Viewパスの先頭から行われるので、後方にある同名のファイル名を読み込むことはできない
  • Emails/text にあるビューファイルはHTML圧縮しない
    • テキストが想定されているので、改行や空白に意味がある

コード

[composer.json]

{
    "require": {
        "zaininnari/html-minifier": "*"
    }
}
curl -sS https://getcomposer.org/installer | php
php composer.phar install

# HTML圧縮後のビューファイルの置場への書き込み権限を付加する

[app/Config/boostrap.php]

<?php
// パスは適宜変更
require(dirname(ROOT) . DS . 'vendor' . DS . 'autoload.php');

// ビューパスにHTML圧縮済みのキャッシュパスを追加する
// App::PREPEND は既存の先頭への追加
App::build(['View' => [CACHE . 'cached' . DS]], App::PREPEND);
<?php

App::uses('View', 'View');
App::uses('Folder', 'Utility');

class MyView extends View {

    protected function _getElementFileName($name) {
        $path = parent::_getElementFileName($name);
        $cacheRelativePath = $this->_getCacheRelativePath($name, $path);
        if ($cacheRelativePath === false) {
            return $path;
        }
        return $this->minifyHtml($path, $cacheRelativePath);
    }

    protected function _getViewFileName($name = null) {
        $path = parent::_getViewFileName($name);
        // View のテンプレートファイルは、
        // 絶対パスや相対パスの指定で任意のファイルが読み込めるので、
        // 相対パスの生成は行わない
        return $this->minifyHtml($path);
    }

    protected function _getLayoutFileName($name = null) {
        $path = parent::_getLayoutFileName($name);
        $cacheRelativePath = $this->_getCacheRelativePath($name, $path);
        if ($cacheRelativePath === false) {
            return $path;
        }
        return $this->minifyHtml($path, $cacheRelativePath);
    }

    protected function _getCacheRelativePath($name, $path) {
        // HTML圧縮済みのテンプレートパスからの読み込みの場合は、何もしない
        if (strpos($path, CACHE . 'cached' . DS) === 0) {
            return false;
        }

        $relative_path = null;
        list($plugin) = $this->pluginSplit($name);
        // 定義済みのパスから相対パスを生成
        foreach ($this->_paths($plugin) as $_path) {
            if (strpos($path, $_path) === 0) {
                $relative_path = substr($path, strlen($_path));
                break;
            }
        }

        if ($relative_path === null) {
            return false;
        }

        // プラグインの場合は混在しないよう'Plugin'ディレクトリに保存
        if ($plugin !== null) {
            $relative_path = 'Plugin' . DS . $plugin . DS . $relative_path;
        }
        return $relative_path;
    }

    protected function isHTMLTemplate($fileName) {
        $noHtmlTemplatePaths = [
            DS . 'Emails' . DS . 'text' . DS,
        ];

        foreach ($noHtmlTemplatePaths as $noHtmlTemplatePath) {
            if (strpos($fileName, $noHtmlTemplatePath) !== false) {
                return false;
            }
        }
        return true;
    }

    /**
     * テンプレートファイルのHTML部分を minify して、キャッシュしたファイルパスを返す。
     * 失敗した場合、元のファイルパスを返す。
     * @param string $fileName テンプレートファイルのフルパス
     * @param string|null $relative_path 相対パス
     * @return string テンプレートファイルのフルパス
     */
    protected function minifyHtml($fileName, $relative_path = null) {
        if (!$this->isHTMLTemplate($fileName)) {
            return $fileName;
        }

        if ($relative_path) {
            $fileNameCached = CACHE . 'cached' . DS . $relative_path;
            $fileNamePath = dirname($fileNameCached);
            $Folder = new Folder();
            if (!$Folder->create($fileNamePath, 0777)) {
                return $fileName;
            }
        } else {
            $fileNameCached = $fileName;
            if (strpos($fileName, ROOT) === 0) {
                $fileNameCached = substr($fileName, strlen(ROOT));
            }
            $fileNameCached = CACHE . 'cached' . DS . strtolower(str_replace(DS, '_', $fileNameCached));
        }

        if (file_exists($fileNameCached)) {
            return $fileNameCached;
        }
        if (!is_writable(dirname($fileNameCached))) {
            return $fileName;
        }

        $content = file_get_contents($fileName);
        $tokens = token_get_all($content);
        $minify = '';
        $option = [
            'excludeComment' => ['/<!--\/?nocache-->/'],
        ];

        foreach ($tokens as $token) {
            if (is_array($token)) {
                $token_name = $token[0];
                if ($token_name === T_COMMENT || $token_name === T_DOC_COMMENT) {
                    continue;
                }
                $token_data = $token[1];
                if ($token_name === T_CLOSE_TAG) {
                    $minify .= trim($token_data);
                } elseif ($token_name === T_WHITESPACE) {
                    $minify .= ' ';
                } elseif ($token_name === T_INLINE_HTML) {
                    // PHP で属性等を生成する場合、半角空白の区切りを削除してしまうことへの対策
                    if (preg_match('/^\s+$/', $token_data)) {
                        $minify .= ' ';
                    } else {
                        $minify .= zz\Html\HTMLMinify::minify($token_data, $option);
                    }
                } else {
                    $minify .= $token_data;
                }
            } else {
                $minify .= $token;
            }
        }
        //@codingStandardsIgnoreStart
        if (@file_put_contents($fileNameCached, $minify, LOCK_EX) !== false) {
            return $fileNameCached;
        }
        //@codingStandardsIgnoreEnd
        return $fileName;
    }
}