[Utility.php] キャッシュファイルを作成する関数

Utility.phpに記述しておきたいfunctionを掲載していきます。
記事中【 C_ 】 で始まるコードは定数です。server_dependence.phpを参照してください。

このエントリでは、【キャッシュファイルを作成する saveCacheFile()関数】を作成します。

PHPを使用したページでは、get・postなど外部変数やデータベースなどからデータを取得し、状況に応じたHTMLを組立ててから出力することになります。

さて、例えば全く同じパラメータ(getやpost)でページにアクセスされたとき、1回目の出力と2回目の出力で違いはあるでしょうか?おそらく1回目も2回目も出力するHTMLに違いがないことのほうが多いかと思います。

掲示板サイトなんかでも、新しい投稿があるまでは何度アクセスしても画面に変化はありませんよね。

でも、見た目には変化がなくても、PHPプログラムはアクセスがある度に実行されています。
データベース接続を伴うなら、アクセスの度にデータベースへのアクセスが発生します。

簡単な短いプログラムやアクセスの少ないサイトなら気にならないかもしれませんが、PHP内部で複雑な処理をしている場合やアクセスが増大してきたときには、少しでも無駄な処理を減らしたいことと思います。

そこで、1回目と2回目のアクセスで出力内容に違いが生じないことに注目です。
違いがないのであれば、1回目の出力内容を保存しておき、2回目は1回目に保存された内容を読み出すようにすれば、かなり高速化が期待できそうです。
もちろん、2回目が1回目と変化がない、ということが前提なので、変化がなければキャッシュされたファイルを読み出して出力する。という流れになります。

といっても、一気に書くと長くなるので、今回は、キャッシュファイルに保存する関数だけを作成します。

前提条件

  • 保存するのはキャッシュファイル
  • キャッシュファイルは、本来不要なファイルである
  • キャッシュファイルへの書込みが失敗しても問題としない
  • キャッシュファイルへの読込みが失敗しても問題としない
  • 同時書込みが発生した場合は、後に書込みを始めたほうは書込み処理をしない

つまり、本線の処理が正常であれば、キャッシュファイルに多少問題があっても全体としては正常に機能するハズです。

キャッシュの書込みができなかったとしても、キャッシュに保存されるデータは"本線"にて作成された変数であり、本線側の変数をそのまま出力してしまえば問題ありません。同時書込みがあった場合は、同時=タイミング的に同一データである、と考え、後から発生した書込み処理は何もせずに関数を抜けることとします。

キャッシュの読込みができなかったとしても、通常通りに"本線"の処理を進めばいいだけなので、問題ありません。

という考えのもと、以下にコードを書いていきます。
説明の都合上、分けて書きますが、実際には下記のコードをつなげてください。

/*
 * キャッシュファイルを作成する
 *
 * $fileName : キャッシュファイルのフルパス
 * $headers  : 配列 改行文字不可, タブ不可
 * $content  : キャッシュする内容
 */
function saveCacheFile($fileName, $headers, $content)
{
  $tempFileName = $fileName . ".tmp"; // 一時ファイル
  $lockDir = $fileName . ".d"; // ロックディレクトリ
  $lockExpire = 60 * 1; // ロックディレクトリの有効期限(秒)

【引数について】

  • $fileName は、キャッシュを保存するファイルのパスです。URL単位でファイルの名前を付けることになります。
  • $headers は、1回目と2回目のアクセスで、出力内容に変化が発生するか識別するための情報や、2回目以降に引き継ぎたい個別の変数があるときに使用します。
    配列で指定します。HTML、css、javascriptでは、改行文字(n)が必須な場面はありませんので、改行文字の入ったデータは、改行文字を外してからセットしてください。
  • $content は、キャッシュに保存するデータです。この変数の内容を保存するための saveCacheFile()です。

$tempFileName と $lockDir は、特に変更する必要はありません。
$lockExpire は、ロックディレクトリの有効期限です。秒単位で指定します。

//	ロックディレクトリの有無
  if (file_exists($lockDir)){
    writeLog(C_LOG_DEBUG, __FILE__, __LINE__
      , "ロックディレクトリが既に存在するt" . $lockDir);

    // ロックディレクトリが作成されて期限が経過しているか?
    if (filemtime($lockDir) > time() - $lockExpire){
      return; // 経過していなければ何もせずに戻す

    } else {
      // 経過しているからロックディレクトリを削除する
      if ( ! rmdir($lockDir)){
        writeLog(C_LOG_DEBUG, __FILE__, __LINE__
          , "ロックディレクトリの削除に失敗t" . $lockDir);
        return; // 何もせずに戻す
      }
      // ロックディレクトリ削除に成功した場合はキャッシュ作成処理へ
    }
  }

webの場合、AさんとBさんが偶然、同じタイミングでページを表示することがあります。
ということは、キャッシュファイルへの書込みも同じタイミングで発生する可能性があります。1つのファイルへの書込みが同じタイミングで発生すると、意図しない内容のファイルができてしまいます。

ってことで、『今書き込み中ですよ~』ってのを伝えるために、ロックディレクトリというものを作成します。ロックディレクトリが存在するときは、書き込み中ということになります。

ロックディレクトリが存在すれば、いつ作成されたのか確認します。エラーか何かで、ロックディレクトリが正常に削除されずに永遠に残ってしまうことがあります。なので、$lockExpire で指定した時間が経過した場合は、強制的にロックディレクトリを削除します。

ロックディレクトリが有効期限内か、期限が切れていて削除に失敗した場合は、何もせずにreturnします。
期限が切れていて削除に成功した場合は、次の処理に移ります。
ロックディレクトリが存在しなかった場合も、if文の中を通らずに次の処理に移ります。

// ロックディレクトリを作成
  if ( ! mkdir($lockDir)){
    writeLog(C_LOG_DEBUG, __FILE__, __LINE__
      , "ロックディレクトリの作成に失敗t" . $lockDir);
    return;
  }

「今から自分が書込みを行いますよ~」ってことで、ロックディレクトリを作成します。
ディレクトリの作成に失敗した場合は、何もせずにreturnします。

// 一時ファイルに書き込み
  if ($fw = fopen($tempFileName, 'w')){
    // ヘッダを書込む
    fwrite($fw, "---------- header ----------n");
    $arr = array_keys($headers);
    foreach ($arr as &$value){
      $buf = (isset($headers[$value]) ? $headers[$value] : "");
      $buf = str_replace("t", " ", $buf);
      $buf = str_replace("n", "", $buf);
      fwrite($fw, $value . "t" . $buf . "n");
    }
    // 本体を書込む
    fwrite($fw, "---------- content ----------n");
    fwrite($fw, $content);
    // ファイルを閉じる
    fclose($fw);
    // 一時ファイルを正規のファイルにリネーム
    if ( ! rename($tempFileName, $fileName)){
      writeLog(C_LOG_DEBUG, __FILE__, __LINE__
        , "リネームに失敗t" . $tempFileName);
    }

  } else {
    writeLog(C_LOG_ERROR, __FILE__, __LINE__
      , "ファイルオープンエラーt" . $tempFileName);
  }

ここから実際に書込みを開始します。
ファイルを書込みモードでオープンして、$headerを書込み、続けて$contentを書き込みます。
書込みを行ったファイルというのは、実際のファイルではなく一時的なファイルです。
そこで、正規のファイルにリネームします。

書込み中に、別のアクセスがありキャッシュファイルを読み込もうとしたときは、正規のファイルが読み込まれることになります。新しいキャッシュファイルはまだ『書き込み中』ですので、途中までしか書込めてないファイルを読み込まれても困りますよね。なので、一度異なる一時ファイルに全て書込んだあと、ファイル名を正規に変える。という手順にしています。

ファイルのオープンに失敗した場合も、正常に書込めた場合も、次の処理へ移ります。

// ロックディレクトリを削除
  if ( ! rmdir($lockDir)){
    writeLog(C_LOG_DEBUG, __FILE__, __LINE__
      , "ロックディレクトリの削除に失敗t" . $tempFileName);
  }
}

やることが済んだので、ロックディレクトリを削除します。
ディレクトリの削除に失敗しても気にしません。有効期限が切れるとどうせ削除されますので。
一応、ログには出力しているので、ログファイルで確認できます。

69行目の } は、function を閉じる括弧です。

さて、今回はここまでです。
次回は、キャッシュファイルを読込む関数を作成します。
読み書きができたところで、サンプルコードを書こうと思います。
あと、増大するキャッシュファイルの削除と、キャッシュファイルの保存先なども後の記事に書こうと思います。

【追記 2008/08/30】
$headersについて、最初は連想配列不可の配列としていましたが、連想配列可に書き換えました。これに合わせて、$headersに格納する値で タブ文字(t) が含まれる場合は、半角スペースに置き換える処理を加えています。改行文字(n)は、削除するようにしています。