あーさーの備忘録

ゆっくり自由に生きてます

ファイルアップロードの際に文字化けする問題を解消する

あるサーバー

私の環境ではないが、 CORESERVER あるサーバーで、「ファイルアップロードの際にファイル名からフィールド名まで文字化けしてしまう」という問題が発生したので、解消するためのメモ。

文字コードがおかしい?

最初はmb_internal_encodingが違う値なのかなぁとか適当に考えて文字コードを変換して見たが、上手くいかない。まず、文字化けの仕方がいつも見慣れるようなタイプではないので、何から何にエンコードされているのかが分からない。メジャーな文字コードの組み合わせで試してみても、同じような文字化けを再現できませんでした。

困ったときのphpinfo()

ということで、phpinfo()の結果を、自分の環境と比べてみた。狙いをmbstring系に絞って差分を取ってみると、いかにも怪しそうな名前の設定が見つかった。

mbstring.http_input = auto;

mbstring.encoding_translationとは

mbstring.encoding_translationというものがあるらしく、$_POST$_GET文字コードを、スクリプト上で変換しなくても自動でやってくれるらしい。正直自分でやるから勝手にやらないでほしい。とりあえずinputの場合だけ書き換えてみる。

mbstring.http_input = "pass";

デフォルト値に戻してみた。すると、文字化けも直った。やったぜ。

ちなみにこの設定は古いものらしく、PHP5.6~は非推奨になっている。とりあえず空にしておけばいいらしい。

PHP: 実行時設定 - Manual

FuelPHPで日本語URLを扱う

FuelPHPって日本語URL使えるの?

FuelPHPは自動でURLからリクエストを処理するController、actionを指定します。その関係で、クラス名・メソッド名に日本語や%が使えない以上、日本語URLは扱えない、と思っていたのですが、routes.phpというConfigファイルを思い出しました。

結論を言うと、日本語URLは、できます!!

日本語URLを使う方法

APPPATH/config/routes.php

<?php
return array(
    '_root_'  => 'index',  // The default route
    '_404_'   => '404/index',    // The main 404 route
    'hello(/:name)?' => array('welcome/hello', 'name' => 'hello'),
    'テスト' => 'test/index',  // ←ここ追記
);

こんな感じで日本語と、それに対応するController名/action名を配列に追加すると、アクセスできちゃいます。

元のURLでアクセスしてきた人を弾く

URLの正規化のために、test/indexとアルファベットのURLでアクセスしてきた人を弾く方法は、Uriクラスを使うことで簡単にできます。詳しくはあまり興味が無いので書きません。

自分でさえ興味のない誰得な記事を書いてしまった……。

FuelPHPのEmailパッケージでSMTP-AUTHを使用して送信する

Emailパッケージ

久々にFuelPHPの話です。FuelPHPには便利なEmailパッケージが同梱されていて、メールをphp標準のmail()関数を使わなくてもメールが送れます。使用方法は以下の通り。

<?php
$email = Email::forge();
$email->subject(mb_convert_encoding('テストメール', 'ISO-2022-JP'))
      ->body(mb_convert_encoding(View::forge('mail', [], false), 'ISO-2022-JP'))
      ->to('hoge@hoge.com')
      ->from('huga@huga.com', mb_convert_encoding('ぽよぽよ', 'ISO-2022-JP'));
try
{
    $email->send();
}
catch (\EmailValidationFailedException $e)
{
    // メールアドレスが正しい形式じゃないとき
}
catch (\EmailSendingFailedException $e)
{
    // ドライバがメールの送信に失敗したとき
}

同様の方法でbccやccを設定したり、添付ファイルをつけたりなんてこともできます。View::forge()の第2引数に配列を渡したり、予めViewオブジェクトを生成し$view->hoge = 'hoge';とプロパティを指定してあげることで、変数を使ってメール本文を生成することもできます。詳しくは、FuelPHPのドキュメントを読んでください。

Usage - Email Package - FuelPHP Documentation

Emailパッケージのデフォルト設定の問題

さて、このEmailパッケージを導入する手順で、APPPATH/configにemail.phpというEmailパッケージの設定ファイルを設置します。こちらの設定ファイルは、文字コードISO-2022-JPエンコードを7bitにする以外に弄ることがなかったのですが、ある時急に思い出しました。

あれ、SMTP認証してなくね?

すぐさまconfigファイルを覗くとこんな設定がありました。

APPPATH/config/email.php

<?php
return array(
    //117行目付近
    /**
     * SMTP settings
     */
    'smtp' => array(
        'host'     => '',
        'port'     => 25,
        'username' => '',
        'password' => '',
        'timeout'  => 5,
        'starttls' => false,
    ),
);

今更ですけどPHPってプログラム部分が<?php ?>で囲われてるせいで、ブログでsyntax highlightするのにいちいち<?phpってつけなきゃいけないんですよね。見た目が煩雑になるので嫌ですね。

そんなことはさておき、SMTP settingsという設定を見つけ、その中には25の文字が!あ~、25番ポートで送ってたのか~。じゃあこの設定をちゃんと埋めればいいんですね。

Configを弄る

だいたいの環境ではメールアドレスごとにusernameが異なるはずなのですが、デフォルト設定を書き換えるだけだと1種類のアドレスからしか送れない…。もちろんこちらのConfigファイルに新しいプロファイルを作成(setupのところに配列を追加)すればいいのですが、この設定そんなにたくさん使わない!ということもあるでしょうし、今回はEmail::forge()の第2引数に設定を追記していく形で設定していきます。

<?php
$email = Email::forge('default', [
    'smtp' => [
        'host'     => 'poyopoyo.com',
        'port'     => 587, //サブミッションポート
        'username' => 'poyo@poyopoyo.com',
        'password' => '********',
        'timeout'  => 5,
        'starttls' => false,
    ],
]);

メールソフトを設定するような気持ちで入力すればOKです。これで送ってみたのですが、ポートが変わらない……どころかパスワードが違っても送れてしまう始末。たしかにこのEmailインスタンスには設定情報が入っているのですが……。

driver設定を追加

FuelPHPのEmailパッケージのドキュメントはお世辞にもしっかりしているとは言えない(情報量が少ない)ので、仕方なくコードをひたすら読んでいきました。すると、こんな設定があることがわかりました。

APPPATH/config/email.php

<?php
return array(
    //48行目付近
    /**
     * Mail driver (mail, smtp, sendmail, noop)
     */
    'driver' => 'mail',
);

この値をsmtpにしないとsmtpの設定が反映されないようですね。

先程のconfig配列に追記します。

<?php
$email = Email::forge('default', [
    'smtp' => [
        'host'     => 'poyopoyo.com',
        'port'     => 587,
        'username' => 'poyo@poyopoyo.com',
        'password' => '********',
        'timeout'  => 5,
        'starttls' => false,
    ],
]);

これでメールを送ってみると、見事にSMTP-AUTHを使用して587ポートからの送信に成功しました。

おまけ①SSLを使う

<?php
$email = Email::forge('default', [
    'smtp' => [
        'host'     => 'poyopoyo.com',
        'port'     => 465,
        'username' => 'poyo@poyopoyo.com',
        'password' => '********',
        'timeout'  => 5,
        'starttls' => true,
    ],
]);

これでSTARTTLSで接続できるかなぁと思ったのですが、残念ながら、私の環境では以下のエラーが発生し、認証ができませんでした。

ERROR - ***time*** –> Notice - fputs(): send of 6 bytes failed with errno=32 Broken pipe in ***path*** on line ***line***

私の設定が間違っている可能性のほうが高いので、またいつか試してみます。誰か原因が分かる人がいたら教えてください。

2017/09/26 16:37追記(できました)

hostsの値の前にssl://をつけたらふつうにできました。おかしいな、朝はこれでダメだったのにな。

<?php
$email = Email::forge('default', [
    'smtp' => [
        'host'     => 'ssl://poyopoyo.com',
        'port'     => 465,
        'username' => 'poyo@poyopoyo.com',
        'password' => '********',
        'timeout'  => 5,
        'starttls' => true,
    ],
]);

おまけ②mailドライバーは何だったのか

デフォルトで指定されていたdriver => 'mail'はどんな処理をしているのかな、と思ってソースを覗いてみました。そこには@mailの文字が…!!。結局mail()使ってるんか~い。まぁ確かに認証情報ないなら使わないと送れないんですけどね。初めてお目にかかった関数前の@ですが、エラーを出力しないようにするエラー制御演算子というものらしい。なるほど。あまり使用は推奨されていないようだけど。

Youtubeのデザイン変更とCustom Elements

Youtubeのデザインが変わった

静岡からこんばんは。あーさーです。ところで、誰もが知っている動画サイトYoutube(https://www.youtube.com/?gl=JP&hl=ja)のデザインが変わったのをご存知でしょうか。マテリアルデザインを取り入れ、スッキリしたレイアウトになりました。また、グレーテーマに変更できるようになりました。ドロワーメニューを開いたときに、スクロールバーの有無でレイアウトが若干ズレるのが気になりますが、とてもスタイリッシュなデザインだなぁと思います。

せっかくなのでコードの中身を見てみたのですが、<app-drawer><yt-icon>などという見慣れない要素を見かけました。要素名に-を使っていることからCustom Elementsで間違いなさそうですね。

Custom Elementsとは?

独自の要素を定義して、よりセマンティックなマークアップができるようにする、Web Componentsの仕様の1つです。Web Componentsには他にもTemplatesやShadow DOMなど、あらゆる部品をコンポーネント化して再利用しやすく、かつ外部を汚染しないようにする仕様が含まれています。詳しくは、このあたりの記事が分かりやすいので参考にしてください。

qiita.com

liginc.co.jp

Custom Elementsを使うと、今まで

<div class="error">
    <h3 class="error_headline">エラー</h3>
    <ul class="error_list">
        <li>「名前」は必須事項です。</li>
        <li>「年齢」は半角数字で入力してください。</li>
    </ul>
</div>

と、既存のタグにBEMなどの命名規則を利用してエセコンポーネント化していたものが、

<error-box>
    <error-headline>エラー</error-headline>
    <error-list>
        <error-element>「名前」は必須事項です。</error-element>
        <error-element>「年齢」は半角数字で入力してください。</error-element>
    </error-list>
</error-box>

のようにマークアップできるようになります。syntax highlightが上手く対応してくれていませんが……。見ただけで何の部品化分かりやすいですし、いちいちクラスをつけなくても良いし、マークアップしてる感がすごいです。

Custom Elementsの使い方

勝手に独自要素を作るだけではレイアウトするのにブラウザが困ってしまうので、Javascriptでちゃんと定義してあげる必要があります。

document.regisiterElement()というメソッドを使用します。Custom Elementsで作る要素の名前には必ず-を含む必要があります。そうしないと既存のタグとの住み分けができませんからね。

var errorBox = document.registerElement('error-box');

他にも既存のタグを継承して……など細かいところがいろいろあるのですが、自分で説明するよりほかのサイトを見たほうが分かりやすいので、前述のリンクをご参照ください。

Custom Elementsサポート状況

いつもお世話になっているcaniuse(http://caniuse.com)で確認したところ、ChromeOperaでは対応しているようなのですが、Firefoxではin-development、EdgeではUnder Considerationということで、まだまだ本格的に利用できるわけではなさそうです。あれ、じゃあYoutubeはどうやって他ブラウザ対応しているんだろう。IEの開発者ツールで見たら普通にdivとかbuttonとかのタグに置き換わっていました。

どのように要素を作るのか、ShadowDOMの使い方、非対応ブラウザの対応方法、いろいろ調べてみたかったのですが眠いので今日はここまでにしようと思います。

maildropのmailfilterで受信メールをプログラムの標準入力に渡す

こんにちは、最近水曜日のカンパネラにハマっているあーさーです。

maildropとは

今日は、maildropの.mailfilterという振り分け機能のようなものを使用して、受信したメールをプログラムにの標準入力に渡す方法をご紹介します。maildropっていうのはiCloudの話ではなく、MDA(Mail Delivery Agent)といって、届いたメールをメールボックスまで配達するプログラムの一種です。有名なレンタルサーバーだと、さくらのレンタルサーバー(http://www.sakura.ne.jp/)やエックスサーバー(https://www.xserver.ne.jp/)などがmaildropを使用しています。今回はこの2社のレンタルサーバーでの設定方法を載せておきます。

mailfilterの場所

mailfilterとは、文字通り届いたメールが通る振り分けプログラムのようなものになります。このファイルの場所はレンタルサーバーによって違うのですが、さくらとエックスサーバーの場合を載せておくので参考にしてください。

さくらのレンタルサーバーの場合

/home/[サーバーID]/MailBox/[アドレス名]/.mailfilter

よく見ると.procmailrcや.forwardというファイルもあるので、maildropではなくてprocmailを使っているのかもしれませんね。実際Webサイトを見るとprocmailのパスが掲載されていますし。でも.mailfilterの書き方がprocmailじゃないんだよなぁ……

エックスサーバーの場合

/home/[サーバーID]/[ドメイン名]/mail/[サブドメイン]/[アドレス名]/.alias

エックスサーバーの方はちょっとディレクトリが複雑ですね。エックスサーバーにも.mailfilterというファイルが有るのですが、このファイルで他のいろいろなスクリプトを読み込んでいる感じなので、そこで読み込まれている.aliasファイルに記述するのが無難だと思います。

mailfilterの書き方

例えば、spamspam.spamから来たメールをgomibakoディレクトリに配達する場合は、下のように書きます。

if (/^From: spam@spam.spam/:h)
{
    to "/gomibako/"
}

/~/の部分はおなじみの正規表現です。:hと書くことでヘッダのみにマッチさせます。to "[ディレクトリ]"で指定したディレクトリにメールを配達し、配達を終了します。toではなくccを使うことで、配達を終了させずに処理を終了させることができます。また、exitと書くことでも配達処理を終了できます。ディレクトリを入れる部分に"! hoge@hoge.com"と!とメールアドレスを入れることで、指定したメールアドレスにメールを送ることができます。たとえば、

cc "! poyo@poyo.com"
cc "! fuga@fuga.com"

と書くことで、届いたメールを2メールアドレスに転送させることができます。これ使えばメーリングリストとか簡単に作れる気がしてきます。

届いたメールをプログラムに渡す

これを応用することで、届いたメールをプログラムの標準入力に渡すことが出来ます。例えば、ログファイルを生成したり、メールが届いたという通知をLINEやDiscordに送ったりできます。可能性は無限大です。今回はPHPを使ってみます(というより私がPHP以外まともに書けない)。

cc "| /usr/bin/php7.0 /home/hoge/hoge.com/script/notification.php"
exit

シェルコマンドで御用達のパイプが登場します。cc "| [PHPのパス] [スクリプトのパス]"と記述すると届いたメールがスクリプトの標準入力に渡されプログラムが実行されます。最後のexitは書かなくてもいいですが、書いておくとメールボックスにメールが保存されなくなります。

スクリプトを書く

次に、渡されるスクリプト側を書きます。メールのデータを解析するのに、今回はPEARのMail::mimeDecodeを使用します。導入方法は他のサイトでも探してください。惰性でPEAR使ったのですが、もしかしたらphp-mime-mail-parserというライブラリ使ったほうが記述が楽かもしれませんね。気力があったらこちらを使った方法も後日書きたいと思います。

#!/usr/bin/php7.0
<?php
//PEARのMail_mimeDecodeを読み込む
require_once('/home/hoge/hoge.jp/pear/PEAR/Mail/mimeDecode.php');
//文字コードはUTF-8で日本語だよ
mb_internal_encoding('UTF-8');
mb_language('japanese');
//メールのデータが標準入力に渡されるのでそれを$sourceに格納
$source = file_get_contents("php://stdin");
if (!$source) {
    die('Error');
}
//Mail_mimeDecodeの設定
$decode_opt = array(
    'include_bodies' => true,
    'decode_bodies'  => true,
    'decode_headers' => true,
);
$decoder = new Mail_mimeDecode($source);
$mail_data = $decoder->decode($decode_opt);
//メールの内容をUTF-8にエンコードしてそれぞれ変数に格納
$from = mb_convert_encoding($mail_data->headers['from'], 'UTF-8', 'ISO-2022-JP,UTF-8,SJIS');
$subject = mb_convert_encoding($mail_data->headers['subject'], 'UTF-8', 'ISO-2022-JP,UTF-8,SJIS');
$ctype = strtolower($mail_data->ctype_primary);
if ($ctype === 'multipart') {
    foreach ($mail_data->parts as $part)
    {
        if (strtolower($part->ctype_primary) === 'text' and strtolower($part->ctype_secondary) === 'plain') {
            $body = "\n" . mb_convert_encoding($part->body, 'UTF-8', 'ISO-2022-JP,UTF-8,SJIS');
        }
    }
}
elseif ($ctype === 'text') {
    $body = "\n" . mb_convert_encoding($mail_data->body, 'UTF-8', 'ISO-2022-JP,UTF-8,SJIS');
}
else {
    die('Error');
}

HTMLメールとかいう害悪のせいで若干複雑になっていますね。これで、$body・$from・$subjectにそれぞれ目的のデータが格納されます。文字コードEUCじゃないと動かないと書いてあるサイトもありましたがとりあえずはUTF-8で大丈夫そうです。気をつけてほしいことが2点あります。

1点目は、プログラムの最初に、#!/usr/bin/php7.0PHPのパスを書くことです。これはPHP以外の言語を使用する場合も同じなので気をつけてください。

2点目は、プログラムが異常終了すると、メール送信者にエラーメールが届くことです。このプログラムだと、die()が実行されてしまう場合はエラーメールが届いてしまいます。他にも、些細なsyntax errorなどでいちいちエラーメールを生成するので、作成時はメールを送ってチェックするのではなく、メールのデータを直接渡してコマンドラインで実行させるなどすると良いと思います。

ここまで書けたら、あとはstream_context_create()してWebhookにPOST送ってあげるとメール通知プログラムの完成です。PHPfile_get_contents()が優秀すぎてつらいです。

CSSだけで実装したタブは本当に"軽い"のか?

はじめに

どうもご無沙汰しております。あーさーです。ここのところ体調が優れず実家に帰って引きこもり中です。夏休み始まってすぐの検診もあまり数値が良くなかったので、例年にも増しておとなしく夏休みを過ごしています。いや、こんな退屈な夏休みなら早く終わってほしいですね。3Qが待ち遠しいです。

さて今回は、今までFuelPHPの話ばかりしてきたので、話題を少し変えCSSの話をしようと思います。と、その前にPRを。

【PR】受験生応援サイトを公開

goukaku.jizi.jp

私が所属する某実行委員会の某局で受験生応援サイトを作りました。私はデザインとフロントコーディングを主に担当しました。今までサーバーサイドでゴニョゴニョやっていたことが多かったので色々新鮮な感じで製作させていただきました。

実はこのサイト、一見普通のサイトに見えますが、1つ特徴的なことがあります。それは「Javasciptを使っていない」ということです。いや、正確に言うとGoogle Analyticsのコードがあるのですが、それ以外のページのコンテンツにはJavascriptを使っていません。メニューの動きやタブの切り替え、マテリアルデザインのfabチックなボタン、すべてがCSSだけで実装されています。

いきなりPRを挟んで申し訳ありません。ただ、何の気なしにPRを突っ込んだわけではありません。今回のテーマは、「CSSだけで実装したタブは本当に"軽い"のか?」です。このサイトで行われている、「Javascriptを使わない」取り組みが果たして正しいのかどうかを検証していきます。

jQueryでタブを実装する方法

と、その前に、まずはjQueryを使ったオーソドックスな方法でタブの切り替えを実装する方法をご紹介します。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>test1</title>
<style>
.is-hidden {
    display: none;
}
</style>
</head>
<body>
<ul>
    <li id="tab1">TAB 1</li>
    <li id="tab2">TAB 2</li>
    <li id="tab3">TAB 3</li>
</ul>
<div>
    <div id="content1">CONTENT 1</div>
    <div id="content2" class="is-hidden">CONTENT 2</div>
    <div id="content3" class="is-hidden">CONTENT 3</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$(function(){
    $('#tab1').on('click', function(){
        $('#content1').removeClass('is-hidden');
        $('#content2').addClass('is-hidden');
        $('#content3').addClass('is-hidden');
    });
    $('#tab2').on('click', function(){
        $('#content1').addClass('is-hidden');
        $('#content2').removeClass('is-hidden');
        $('#content3').addClass('is-hidden');
    });
    $('#tab3').on('click', function(){
        $('#content1').addClass('is-hidden');
        $('#content2').addClass('is-hidden');
        $('#content3').removeClass('is-hidden');
    });
});
</script>
</body>
</html>

他にもいろいろなやり方があると思いますが、詳しくはGoogle先生に聞いてください。show()hide()よりもCSSのクラスの付け外しのほうが若干軽いとか。li#tab[n]を押すと、対応するdiv#content[n]が表示されます。[n]には1~3の数字が入ります(属性セレクタと混同するような表記をしてしまい申し訳ありません)。

CSSだけでタブを実装する方法

次に、CSSだけでこれを行う方法です。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>test1</title>
<style>
.tab-input {
    display: none;
}
#content1, #content2, #content3 {
    display: none;
}
#radio1:checked + #content1 {
    display: block;
}
#radio2:checked + #content2 {
    display: block;
}
#radio3:checked + #content3 {
    display: block;
}
</style>
</head>
<body>
<ul>
    <li id="tab1"><label for="radio1">TAB 1</label></li>
    <li id="tab2"><label for="radio2">TAB 2</label></li>
    <li id="tab3"><label for="radio3">TAB 3</label></li>
</ul>
<div>
    <input type="radio" name="tab-input" class="tab-input" id="radio1" checked>
    <div id="content1">CONTENT 1</div>
    <input type="radio" name="tab-input" class="tab-input" id="radio2">
    <div id="content2" class="is-hidden">CONTENT 2</div>
    <input type="radio" name="tab-input" class="tab-input" id="radio3">
    <div id="content3" class="is-hidden">CONTENT 3</div>
</div>
</body>
</html>

どのタブが選択されているかを、非表示にしたラジオボタンの値で保存することで実装しています。クラスを使えばCSS部分がもっと短く書けるはずなのですが、jQuery版との対応を取ってidセレクタを使用しています。idセレクタは詳細度が大きいのでできれば使いたくないんですがね。ついつい#hoge[id="hoge"]と書き直したくなってしまいます(idセレクタを属性セレクタに置き換えることで詳細度がクラスレベルまで下がります)。

ページの読み込み速度

jQueryで実装したものとCSSだけで実装したものと、どちらが"軽い"か。まずは、ページの読み込み速度を測って検証します。検証には、私のPCのGoogle Chrome(60.0.3112.90)の開発者ツールを利用し、ローカルに保存したhtmlファイルをスーパーリロードで10回読み込み、平均値を取ります。

試行回数 jQuery CSS
1回目 80 16
2回目 83 16
3回目 89 16
4回目 84 17
5回目 74 17
6回目 83 16
7回目 81 17
8回目 77 17
9回目 89 16
10回目 81 16
平均 82.1 16.4

(単位:ms)

CSSのほうがjQueryを読み込まない分速いですね。通常jQueryはキャッシュされるので、それを考えるとCSSだけで実装した場合との違いはほとんどないようです。

再描画までの時間

次に、クリックしてからレイアウトを再計算して描画するまでの時間を計測します。先ほどと同じ条件で、Google Chromeの開発者ツールの「Performance」を利用しました。

結果だけ言いますとどちらも0.00msでした。目立った違いもないようです。アニメーションとかつけたら変わるのでしょうか。

そもそもこの比較は実験として成立しているか

こんな小学生の夏休みの自由研究のような記事を書いてしまい申し訳ございませんでした。実験の基本は対照実験。比べたいもの以外の条件の同じにしないと比較しようがないのですが、果たして本当に対照実験になっていたんでしょうか。計測の方法は正しかったのでしょうか。Javascript同士のベンチマークはいろいろ記事が上がっているので分かりますがCSSと比べるっていうのは私にはよく分かりませんでした。まぁ、ただ得られた結果としては、特にパフォーマンスの違いがない以上、jQueryを読み込まなくて良いCSSのみの実装の方が良い、ということで。次回は.mailfilterを使って、メールをトリガーとして動くプログラムの解説をします。それでは。

FuelPHPでAssetファイルを圧縮・統合する

はじめに

FuelPHPを使ったサイトでPagespeed Insightsで100点を取るシリーズ第1弾として、前回は「FuelPHPでHTMLソースを圧縮する」という記事を書きました。今回第2弾は「FuelPHPでAssetファイルを圧縮・統合する」です。シリーズものにしたはいいものの今回で最後になります。ごめんなさい。

前回と今回の記事を読んだ上で、あとはFuelPHP関係なく下記のことを実行すればPagespeed Insightsで100点出るはずです

  • 画像を圧縮・最適化する
  • ,htaccessにgzip圧縮の記述をする
  • キャッシュの有効期限を10日程度以上に伸ばす

ただし、Google Analyticsを使用していると、どうしても100点がとれず、99点になってしまいます。僕が作っているサイトは基本的にanalyticsを入れているので、残念ながら100点は取れません。Javascriptファイルのキャッシュの有効期限が短いと言われますが、外部サーバーのファイルなのでどうしようもありません。同じGoogleのサービスなのに……。無念。

Cassetの導入

今回は、canton7さんのfuelphp-cassetというパッケージを使って、CSSやJavasciptファイルを圧縮・統合します。Assetクラスに置き換わるもの、という立ち位置ですが、これを使うと各種ファイルを圧縮できるだけでなく、複数のファイルを統合してくれるので、リクエスト数を減らすことができます。前回同様、普通はoilを使うのですが、手動インストールの方法を載せておきます。

  1. GitHub - canton7/fuelphp-cassetにアクセス
  2. Clone or download → Download ZIPでダウンロード&解凍 1, フォルダ名fuelphp-casset-masterをcassetにリネーム
  3. フォルダごとFuelPHPのPKGPATHに入れる
  4. DOCROOT/assetsの中にcacheというフォルダを作る
  5. APPPATH/config/config.phpの262行目以降を以下のように編集する

APPPATH/config/config.php

<?php
return array(
    /* 262行目まで省略 */
    'always_load' => array(
        'package' => array(
            'casset',
        ),
        /* 省略 */
    ),
);

上記のようになるようにコメントアウトを外して編集します。FuelPHP1.8だと262行目だったのですが、他のバージョンだったりもとから弄ってあったりすると分かりません。テキストエディタalways_loadで検索をかけると良いと思います。

これで、Cassetクラスが使えるようになります。

Cassetクラスを使ってみる

<?php
//DOCROOT/assets/css/test1.cssを読み込む
Casset::css('test1.css');
//DOCROOT/assets/css/test2.cssを読み込む
Casset::css('test2.css');
//2つのファイルを圧縮・統合しlinkタグを出力する
echo Casset::render_css();

このように使います。Javascriptの場合はcssの部分をjsにすれば同じように使用できます。Controller側でCasset::css()しておき、Viewのhead内でCasset::render_css()を実行しControllerで指定したファイルを読み込む、という使い方になります。

templateを使うときに困る!

同様に、templateを使う場合は、以下の通りになります。

APPPATH/classes/controller/test.php

<?php
class Controller_Test extends Controller_Template
{
    public function action_index()
    {
        //このページだけで読み込みたいファイル
        Casset::css('test.css');
        $this->template->contents = View::forge('test');
    }
}

APPPATH/views/template.php

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>template</title>
    <!-- 常に読み込みたいファイル -->
    <?php Casset::css('normalize.css'); ?>
    <?php Casset::css('template.css'); ?>
    <!-- linkタグを出力する場所 -->
    <?= Casset::render_css(); ?>
</head>
<body>
<?= $contents; ?>
</body>
</html>

APPPATH/views/test.php

<p>てすと</p>

ところが、こうして統合されたCSSファイルの中を見ると、test.cssの方が先に読み込まれてしまいます。CSS設計にもよりますが、後に書いた記述のほうが優先されるため、基本的には各ページで独自に読み込むCSSを後に読み込みたいはずです。

groupをつくる

FuelPHPのAssetクラスもそうですが、各種Assetファイルを読み込むときにgroupを分けることができます。groupを設定することで、テンプレート関係のファイルを先に読み込むことができるようになります。ちなみに、group未指定(今までの記述)の場合はglobalというgroupになります。group名はCasset::css()Casset::js()の第3引数に指定します。Casset::render_css()Casset::render_js()の第1引数にgroupを指定すると、指定したgroupのファイルのコードを出力します。

APPPATH/views/template.php

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>template</title>
    <!-- 常に読み込みたいファイル -->
    <?php Casset::css('normalize.css', false, 'template'); ?>
    <?php Casset::css('template.css', false, 'template'); ?>
    <!-- linkタグを出力する場所(template) -->
    <?= Casset::render_css('template'); ?>
    <!-- (各ページで個別に読み込むファイル) -->
    <?= Casset::render_css('global'); ?>
</head>
<body>
<?= $contents; ?>
</body>
</html>

templateファイルを上記のように変更すると、templateに付随するファイルを先に読み込むことができます。

CSSのインライン化

ここまでやればOK……と思いきや。Google Pagespeed Insightsはこんなことを言ってきました。

このページには、ブロッキング CSS リソースが n あります。これが原因で、ページのレンダリングに遅延が発生しています。 以下のリソースの読み込みが終わるまで、このページでスクロールせずに見えるコンテンツを何もレンダリングできませんでした。 レンダリングをブロックするリソースの読み込みを遅延させるか、非同期に読み込むか、これらのリソースの重要部分を HTML 内に直接インライン化してください。

要約すると、ファイルを読み込んでる間レンダリングできないからインラインにするか非同期に読み込んでくれ、とのこと。よほどの量でなければインライン化したほうがよさそうです。

Casset::render_css()の第2引数にtrueを設定すると、統合したCSSファイルをインライン化することができます。jsの場合も同様です。

APPPATH/views/template.php

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>template</title>
    <!-- 常に読み込みたいファイル -->
    <?php Casset::css('normalize.css', false, 'template'); ?>
    <?php Casset::css('template.css', false, 'template'); ?>
    <!-- linkタグを出力する場所(template) -->
    <?= Casset::render_css('template', true); ?>
    <!-- (各ページで個別に読み込むファイル) -->
    <?= Casset::render_css('global', true); ?>
</head>
<body>
<?= $contents; ?>
</body>
</html>

ここまですれば問題ないでしょう。Javascriptはインラインにしなくても</body>の直前で出力すれば問題ないかもしれません。

うまくいかない場合

記事が間違っていた故の不具合なら申し訳ないのですが、このパッケージはデフォルト設定のままだと、ファイルパスの書き換えミスが起こる可能性があります。特に、シンボリックリンクを使っている場合は上手くいかないようです。その場合は以下の2つのことを試してください。

url()の中のパスを/から始まる相対パスにする

CSSファイル内で画像などを読み込むときに使うurl()ですが、CSSを統合しインライン化するため、CSSファイルの場所が変わってしまいます。基本はCassetパッケージが自動で書き換えてくれるのですが、上手く行かないこともあります。CSSファイルの場所に依存しないよう、URLは/から始まる相対パスでルートから指定してください。

configを書き換える

URLを書き換える際に。Stephen Clayさんという人が作ったアルゴリズムを使っているそうなのですが、これは、シンボリックリンクを使用している場合は正しく動作しません。そこで、現在テスト中ではあるものの、別のアルゴリズムに変えます。

PKGPATH/casset/config/config.php

<?php
return array(
    /* 186行目まで省略 */
    'css_uri_rewriter' => 'relative',
);

もともとはabosoluteとなっているところをrelativeに変更してください。こうすればおそらくシンボリックリンクを使用していても書き換えによる不具合が起こりません。