[Mashup]jQueryプラグインで無限スクロールページをつくる(NAVERまとめのマッシュアップ)

今回使うjQueryプラグイン

Wookmark

使用方法はこちらです。

このプラグインを利用することでグリッドレイアウトをベースとした、無限スクロールのページが作成できます。

スクリーンショット 2013-05-30 23.57.07

サンプル

サンプルはこちら

NAVERまとめのページから、画像とタイトルとURLをスクレイピングで取得。

スクロールに応じて、非同期に画像ファイル等を取得して、どんどん表示してきます。

ページングがだるい、一つのページでより多くのコンテンツをみたいという方は是非。

実行環境/言語

実装には以下のものを利用しました。

言語:PHP
フレームワーク:Symfony , jQuery, Wookmark, htmlSQL, Snoopy

htmlSQL

htmlSQLはSQLベースでHTMLの要素を取得できるライブラリです。スクレイピングをするさいに利用しました。
慣れ親しんだSQLベースで要素の取得ができる便利なライブラリです。

GitHub htmlSQL

以下のような記法で配列形式のデータが取得可能となります。

SELECT (取得したい属性) FROM (タグ) WHERE $(属性) == (属性の値);

[Symfony]jsonレスポンスを返す設定方法

外部API

最近では、WebサービスとしてAPIが多く提供されています(Google Map API…)。そのコンテンツを充実させるのに注力するために、ここでは外部APIとしてよくある「json」レスポンスを返す方法について紹介します。

例)
Google Map API
ATND API

jsonレスポンスを返すための設定

この記事はPHPとSymfonyをベースとした実装方法を紹介していきます。従ってPHPおよびSymfonyの基礎知識はある前提で紹介していきます。

まず、レスポンスとしてjsonを返すので、アプリケーションのcontent-Typeをapplication/jsonにします。また、HTMLとしてのレスポンスを返す必要がないのでhas_layoutもfalseとします。それらの設定はview.ymlにて設定可能です。

default:
  http_metas:
    content-type: application/json; charset=UTF-8
#   通常の場合
#   content-type: text/html

...

  has_layout: false
# 通常の場合
# has_layout: true
  layout: layout

[PHP]HTTPリクエスト実行時にタイムアウトを指定する

タイムアウトを設定する要件

WEBシステムを作成していて、外部との通信に処理がかかった場合にタイムアウトを検知して、ハンドリングを行いたいという要件はよくある話、PHPで実装した場合にはまったのでメモ。

PHPでHTTPリクエストを実行する方法

大きく以下の三つがあります。
1. file_get_contents
2. fsockopen
3. curl

タイムアウトを検証するために

適当なPHPファイルを用意します。こんな感じで書いておけば、OK.

<?php
// 10秒スリープ
sleep(10);
?>

file_get_contentsで動かなかった実装

/**
  * HTTPリクエストを実行します。
  * @param string $url HTTPリクエストを実行する対象URL
  * @param string $header 送信するHTTPヘッダ
  * @param string $post 送信するPOSTパラメータ
  */
function request($url, $header, $post) {
	$options = array(
		'method' => 'POST',
		'header' => $header,
		'content' => $content,
		'timeout' => 30
	));
	$context = stream_context_create($options);
	$f = file_get_contents($url, false, $context);

	var_dump($f);
}

timeoutで指定すれば問題ないと思ったのですが、そんなことはなかった、、、いろいろ調べた結果「stackoverflow」に無理だよと。

curlでの動く実装

/**
  * HTTPリクエストを実行します。
  * @param string $url HTTPリクエストを実行する対象URL
  * @param string $header 送信するHTTPヘッダ
  * @param string $post 送信するPOSTパラメータ
  */
function request($url, $header, $post) {
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
	curl_setopt($ch, CURLOPT_POST, TRUE);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
	curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
	curl_setopt($ch, CURLOPT_TIMEOUT, 30);
	
	$f = curl_exec($ch);
	curl_close($ch);
	
	var_dump($f);
}

これで、問題なく動きました。

file_get_contentsでどうにかこうにかできないかと相当やりましたが、無理です。curlを使いましょう。

[株]HumbleFinanceを使ってみた

作ったもの

Go To Chart Navi

Humble Finance

チャートをHTML5で描画してくれるJavascriptフレームワークです。これを使ってWEBページを作ってみます。

Humble Software Development

他に使ったライブラリとか

CHAPI! – 株価API
株価を検索するためのフリーAPIです。IKACHI Projectなる団体が管理しているらいです。
ここが提供する四本値データと銘柄データを利用してチャート描画に必要なデータを収集しています。

IKACHI Project

Prototype
Humble Financeの動作条件として必須なJavascriptフレームワーク。

Prototype

JQuery
Javascriptフレームワーク。Prototypeと文法上競合するので記述方法に気をつけます。

JQuery

KickStart
cssフレームワーク。

KickStart

さあ、どうぞ

PHP & Symfonyで実装しました。

Go To Chart Navi

[Symfony]SymfonyアプリケーションにKCAPTCHAを取り込む

CAPTCHA

CAPTCHAとはコンピュータが認識できない揺れた英数字を表示し、コンピュータによるスパムを防止するための機能である。

CAPTCHAとは

KCAPTCHAを単体で動作させる

まず、KCAPTCHAを以下のリンクからダウンロードする。
KCAPTCHA

ダウンロードしたパッケージをそのまま公開ディレクトリに配置し、単体で動作することを確認しよう。内包されているindex.phpにアクセスすると以下のように表示される。

SymfonyアプリケーションにKCAPTCHAを導入する

KCAPTCHAはRAW PHPで記述されており、Symfonyアプリケーション配下に取り込むとうまく動作しない。そこで、公開ディレクトリに配置し、Symfonyとは別のアプリケーションとして動作させることを考える。

単体で動作することは確認したので、Symfonyから呼び出して使う。Symfonyのテンプレートからimgタグを利用して以下のように呼び出す。

apps/frontend/module/XXXX/templates/XXXX.php

...
<img src="<?php echo $sf_request->getRelativeUrlRoot() . '/captcha/' ?>index.php ?>">
...

これで、問題なく表示されるはず。次に認証の実装を考える。KCAPTCHAをSymfonyと独立させたことで、KCAPTHCAがセッションに保存した認証文字列はSymfonyから正規ルートでアクセスすることは不可能である。クッキーからセッション情報を取り出して、無理やり認証文字列を取り出す必要がある。以下、action.class.phpの実装となる。(KCAPTCHAのセッション情報はキーを「captcha_keystring」として保存する)

apps/frontend/module/XXXX/actions/action.class.php

$sessionCaptcha = '';
$phpSessionData = file_get_contents(session_save_path() . '/sess_' . $request->getCookie('PHPSESSID'));
$arr = explode(";", $phpSessionData);
foreach ($arr as $value) {
    if (preg_match("/^captcha_keystring/", $value) === 1) {
        $arr1 = explode(":", $value);
        $sessionCaptcha = $arr1[2];
    }
}

COOKIEからPHPのセッションを取り出し、captcha_keystringをキーとし、その値を取得するというロジックである。これと、入力された文字列を比較し、認証を行う。

[PHP]PEAR::PagerをPOST遷移するときに$optionsで指定したonclickを有効にする方法

PEAR::Pagerとは

一覧で表示を行う場合にページを自動的に分割して表示する機能を提供してくれるPEARライブラリです。詳細な実装および設定方法に関しては以下のサイトを参照ください。
ページング(PEAR::Pager)

POSTで遷移する場合にonclickがきかない問題

PEAR::Pagerのオブジェクトを生成する方法はfactoryファンクションを利用するのですが、この引数として配列形式のオプションを渡してあげるとPEAR::Pagerの詳細な振る舞いについて定義することが可能となっています。このオプションでhttpMethodをPOSTにした場合にonclickがきかなくなります。関数定義とオプションの一覧を下記表に示します。

Pager::factory()
object &factory (array $options)
引数:
 $options オプションを格納した配列
戻り値:
 Pagerクラスオブジェクト。失敗した場合はPEAR_Errorオブジェクト

$optionsにて設定可能なオプションは以下の通りです。

オプション名 内容
itemData array 対象となるアイテムを格納した配列
totalItems integer 対象となるアイテム数(itemDataが未指定の場合に使用)
perPage integer 1ページごとに表示されるアイテム数
delta integer 現在のページの前後に表示するページ番号の数
mode string 動作モードを指定。ジャンプ型(Jumping)またはスライド型(Sliding)
httpMethod string 使用するHTTPメソッド。「GET」または「POST」
formID string httpMethodでPOSTを指定した場合のHTMLフォームを指定
importQuery boolean 変数と値がクエリ文字列からインポートするならTRUE
currentPage integer 初期ページ番号
expanded boolean TRUEの場合、ウィンドウサイズは2×delta+1
linkClass string リンクのスタイルのためのCSSクラス
urlVar string ページ番号を示すためのクエリ変数名(デフォルトは「pageID」)
path string ページへの絶対パス(ページ名は除く)
fileName string ページ名。appendがTRUEなら”%d”
fixFileName boolean FALSEなら、オプションfileNameは上書きされません
append boolean TRUEならpageIDはクエリ文字列としてURLに追加。FALSEならfileNameに従ってURLに埋め込まれる(デフォルトは「TRUE」)
altFirst string 最初のページのリンクに表示されるalt文。デフォルトは「first page」
altPrev string 前のページのリンクに表示されるalt文。デフォルトは「previous page」
altNext string 次のページのリンクに表示されるalt文。デフォルトは「next page」
altLast string 最後のページのリンクに表示されるalt文。デフォルトは「last page」
altPage string ページ番号の前に表示されるalt文。デフォルトは「page」
prevImg string 前のページのリンクである「<<」の代替文字。タグも可能
nextImg string 次のページのリンクである「&lgt;>」の代替文字。タグも可能
separator string ページ番号を分けるための区切り文字。カンマやハイフンの他にタグも可能
spacesBeforeSeparator integer 区切り文字の前のスペース数
spacesAfterSeparator integer 区切り文字の後のスペース数
curPageLinkClassName string 現在のページのリンクのスタイルのためのCSSクラス
curPageSpanPre string 現在のページのリンクの前のテキスト
curPageSpanPost string 現在のページのリンクの後のテキスト
firstPagePre string 最初のページ番号の前の文字。任意の文字列の他にタグも可能
firstPageText string 最初のページ番号の文字
firstPagePost string 最初のページ番号の後の文字。任意の文字列の他にタグも可能
lastPagePre string 最後のページ番号の前の文字。任意の文字列の他にタグも可能
lastPageText string 最後のページ番号の文字
lastPagePost string 最後のページ番号の後の文字。任意の文字列の他にタグも可能
clearIfVoid boolean 1ページしか無い場合にPageリンクを表示しないかどうか
useSessions boolean TRUEの場合、ページごとに表示するアイテム数は$_SESSION[$_sessionVar]変数に保存
closeSession boolean TRUEの場合、セッションはR/Wされた後にクローズする
sessionVar string perPageの値を格納するセッション変数名(1ページに複数のPagerを使用する場合に利用)
showAllText string generated by getPerPageSelectBox()で生成されたセレクトボックスでの’show all’オプションのために使用されるテキスト
pearErrorMode constant raiseError()でPEAR_ERRORモードを使用するかどうか(デフォルトは「PEAR_ERROR_RETURN」)

POSTで遷移する場合にonclickがきかない問題はなぜ発生するのか

ViewをレンダリングするPEAR::Pagerライブラリを見ればそれは一発です。Pager/Common.phpの問題のコードは以下となります。

    /**
     * Renders a link using the appropriate method
     *
     * @param string $altText  Alternative text for this link (title property)
     * @param string $linkText Text contained by this link
     *
     * @return string The link in string form
     * @access private
     */
    function _renderLink($altText, $linkText)
    {
        if ($this->_httpMethod == 'GET') {
            if ($this->_append) {
                $href = '?' . $this->_http_build_query_wrapper($this->_linkData);
            } else {
                $href = str_replace('%d', $this->_linkData[$this->_urlVar], $this->_fileName);
            }
            $onclick = '';
            if (array_key_exists($this->_urlVar, $this->_linkData)) {
                $onclick = str_replace('%d', $this->_linkData[$this->_urlVar], $this->_onclick);
            }
            return sprintf('<a href="%s"%s%s%s%s title="%s">%s</a>',
                           htmlentities($this->_url . $href, ENT_COMPAT, 'UTF-8'),
                           empty($this->_classString) ? '' : ' '.$this->_classString,
                           empty($this->_attributes)  ? '' : ' '.$this->_attributes,
                           empty($this->_accesskey)   ? '' : ' accesskey="'.$this->_linkData[$this->_urlVar].'"',
                           empty($onclick)            ? '' : ' onclick="'.$onclick.'"',
                           $altText,
                           $linkText
            );
        } elseif ($this->_httpMethod == 'POST') {
            $href = $this->_url;
            if (!empty($_GET)) {
                $href .= '?' . $this->_http_build_query_wrapper($_GET);
            }
            return sprintf("<a href='javascript:void(0)' onclick='%s'%s%s%s title='%s'>%s</a>",
                           $this->_generateFormOnClick($href, $this->_linkData),
                           empty($this->_classString) ? '' : ' '.$this->_classString,
                           empty($this->_attributes)  ? '' : ' '.$this->_attributes,
                           empty($this->_accesskey)   ? '' : ' accesskey=\''.$this->_linkData[$this->_urlVar].'\'',
                           $altText,
                           $linkText
            );
        }
        return '';
    }

    // }}}
    // {{{ _generateFormOnClick()

    /**
     * Mimics http_build_query() behavior in the way the data
     * in $data will appear when it makes it back to the server.
     *  For example:
     * $arr =  array('array' => array(array('hello', 'world'),
     *                                'things' => array('stuff', 'junk'));
     * http_build_query($arr)
     * and _generateFormOnClick('foo.php', $arr)
     * will yield
     * $_REQUEST['array'][0][0] === 'hello'
     * $_REQUEST['array'][0][1] === 'world'
     * $_REQUEST['array']['things'][0] === 'stuff'
     * $_REQUEST['array']['things'][1] === 'junk'
     *
     * However, instead of  generating a query string, it generates
     * Javascript to create and submit a form.
     *
     * @param string $formAction where the form should be submitted
     * @param array  $data       the associative array of names and values
     *
     * @return string A string of javascript that generates a form and submits it
     * @access private
     */
    function _generateFormOnClick($formAction, $data)
    {
        // Check we have an array to work with
        if (!is_array($data)) {
            trigger_error(
                '_generateForm() Parameter 1 expected to be Array or Object. Incorrect value given.',
                E_USER_WARNING
            );
            return false;
        }

        if (!empty($this->_formID)) {
            $str = 'var form = document.getElementById("'.$this->_formID.'"); var input = ""; ';
        } else {
            $str = 'var form = document.createElement("form"); var input = ""; ';
        }

        // We /shouldn't/ need to escape the URL ...
        $str .= sprintf('form.action = "%s"; ', htmlentities($formAction, ENT_COMPAT, 'UTF-8'));
        $str .= sprintf('form.method = "%s"; ', $this->_httpMethod);
        foreach ($data as $key => $val) {
            $str .= $this->_generateFormOnClickHelper($val, $key);
        }

        if (empty($this->_formID)) {
            $str .= 'document.getElementsByTagName("body")[0].appendChild(form);';
        }

        $str .= 'form.submit(); return false;';
        return $str;
    }

    // }}}
    // {{{ _generateFormOnClickHelper

    /**
     * This is used by _generateFormOnClick().
     * Recursively processes the arrays, objects, and literal values.
     *
     * @param mixed  $data Data that should be rendered
     * @param string $prev The name so far
     *
     * @return string A string of Javascript that creates form inputs
     *                representing the data
     * @access private
     */
    function _generateFormOnClickHelper($data, $prev = '')
    {
        $str = '';
        if (is_array($data) || is_object($data)) {
            // foreach key/visible member
            foreach ((array)$data as $key => $val) {
                // append [$key] to prev
                $tempKey = sprintf('%s[%s]', $prev, $key);
                $str .= $this->_generateFormOnClickHelper($val, $tempKey);
            }
        } else {  // must be a literal value
            // escape newlines and carriage returns
            $search  = array("\n", "\r");
            $replace = array('\n', '\n');
            $escapedData = str_replace($search, $replace, $data);
            // am I forgetting any dangerous whitespace?
            // would a regex be faster?
            // if it's already encoded, don't encode it again
            if (!$this->_isEncoded($escapedData)) {
                $escapedData = urlencode($escapedData);
            }
            $escapedData = htmlentities($escapedData, ENT_QUOTES, 'UTF-8');

            $str .= 'input = document.createElement("input"); ';
            $str .= 'input.type = "hidden"; ';
            $str .= sprintf('input.name = "%s"; ', $prev);
            $str .= sprintf('input.value = "%s"; ', $escapedData);
            $str .= 'form.appendChild(input); ';
        }
        return $str;
    }

ここで問題となるのはリンクをレンダリングする関数の「function _renderLink($altText, $linkText)」です。POSTとGETで実装が分岐しています。POST箇所のonclickのレンダリングを見てみると、L.37「$this->_generateFormOnClick($href, $this->_linkData)」でやっているようです。しかし、onclickをレンダリングする実装に$optionsで指定した「$this->_onclick」が呼ばれている実装はありません。これが原因。GETメソッドの場合はL.20で呼ばれているのに。

解決方法

POSTのonclickレンダリングで、$this->_onclickを設定してあげればよいのです。以下、実装です。

    // }}}
    // {{{ _generateFormOnClick()

    /**
     * Mimics http_build_query() behavior in the way the data
     * in $data will appear when it makes it back to the server.
     *  For example:
     * $arr =  array('array' => array(array('hello', 'world'),
     *                                'things' => array('stuff', 'junk'));
     * http_build_query($arr)
     * and _generateFormOnClick('foo.php', $arr)
     * will yield
     * $_REQUEST['array'][0][0] === 'hello'
     * $_REQUEST['array'][0][1] === 'world'
     * $_REQUEST['array']['things'][0] === 'stuff'
     * $_REQUEST['array']['things'][1] === 'junk'
     *
     * However, instead of  generating a query string, it generates
     * Javascript to create and submit a form.
     *
     * @param string $formAction where the form should be submitted
     * @param array  $data       the associative array of names and values
     *
     * @return string A string of javascript that generates a form and submits it
     * @access private
     */
    function _generateFormOnClick($formAction, $data)
    {
        // Check we have an array to work with
        if (!is_array($data)) {
            trigger_error(
                '_generateForm() Parameter 1 expected to be Array or Object. Incorrect value given.',
                E_USER_WARNING
            );
            return false;
        }

        $onclick = str_replace('%d', $this->linkData[$this->_urlVar], $this->onclick);
        $str = $onclick;

        if (!empty($this->_formID)) {
            $str = 'var form = document.getElementById("'.$this->_formID.'"); var input = ""; ';
        } else {
            $str = 'var form = document.createElement("form"); var input = ""; ';
        }

        // We /shouldn't/ need to escape the URL ...
        $str .= sprintf('form.action = "%s"; ', htmlentities($formAction, ENT_COMPAT, 'UTF-8'));
        $str .= sprintf('form.method = "%s"; ', $this->_httpMethod);
        foreach ($data as $key => $val) {
            $str .= $this->_generateFormOnClickHelper($val, $key);
        }

        if (empty($this->_formID)) {
            $str .= 'document.getElementsByTagName("body")[0].appendChild(form);';
        }

        $str .= 'form.submit(); return false;';
        return $str;
    }

追加したのはL.38, L.39です。これで、$optionsで設定したonclickがきくはず。

[PHP]Windows(XAMPP環境)でmemcachedを使う

memcachedとは

YouTube, Wikipedia, SouceForage, Facebookといった大規模有名サイトで利用されている分散型メモリキャッシュシステムである。利用方法としては、データベースの検索結果をmemcached内に保持し、データベースのアクセスの前にmemcached内のメモリを読み出して、なるべく低速な記憶媒体へのアクセスを減らすことに利用される。memcache内のメモリ領域を超えた場合は、以降の新規データ挿入は一番最後に利用されたキャッシュの順に削除される。
これは、Javaのサーブレットアプリケーションのようにサーバ内にプロセスが常駐してメモリを共有する機構のない言語、即ちPHPなどのスクリプト系の言語で利用される。

wikipediaより

インストール手順

大きく以下の流れで設定をしていきます。前提としてXAMPPがインストールされている必要があります。

1. memcachedをインストールする
2. PHP extensionとしてのmemcachedをインストールする

1. memcachedをインストールする

インターネットからダウンロードしてきて適切なディレクトリに配置します。実態はexeファイルひとつのみです。

以下にダウンロードリンクをはりつけておきます。
http://code.jellycan.com/files/memcached-1.2.5-win32-bin.zip

memcachedをC直下に配置した場合のサービスの登録方法(C:¥memcached¥memcached.exe)
ファイル名を指定して実行から以下のコマンドを発行します。

C:¥memcached¥memcached.exe -d install

memcachedの起動方法
ファイル名を指定して実行から以下のコマンドを発行します。

C:¥memcached¥memcached.exe -d start

PHP extensionとしてのmemcachedをインストールする

インターネットからダウンロードして適切なディレクトリに配置します。例えば、XAMPPのホームが「C:¥xampp」の場合はダウンロードしたファイル(php_memcache.dll)をC:¥xampp¥php¥extに配置してください。

次に、PHPの設定ファイル(php.ini)を開き有効化を行います。
php.iniを開き、extensionを追加します。またはコメントアウトされている場合、そのコメントアウトをはずします。

extension=php_memcache.dll

memcacheの設定を追加します。

[Memcache]
memcache.allow_failover = 1
memcache.max_failover_attempts = 20
memcache.chunk_size = 8192
memcache.default_port = 11211

以下にダウンロードリンクをはりつけておきます。
http://downloads.php.net/pierre/php_memcache-2.2.6-5.3-nts-vc9-x86.zip
http://downloads.php.net/pierre/php_memcache-2.2.6-5.3-vc9-x86.zip

確認

まず、phpinfo()にmemcacheが有効化されていることを確認ください。その後、サンプルコードを作ってみて。

[Symfony]テンプレートにアクションで定義した変数を埋め込む方法

テンプレートにアクションで定義した変数を渡すには

テンプレート(モジュールではなくアプリケーション)でアクションで定義した変数を呼び出したい場面が多々あるが、それにはslotを使う。

方法

大まか以下の流れで実現可能。
①アクションに変数を定義
②モジュールのテンプレートでslotを定義
③アプリケーションのテンプレートでslot呼び出し

順々に、
①アクションクラスにhogeという変数名のパラメータを定義します。

class XXXXaction extends sfActions {
	public function executeIndex() {
		$this->hoge = 'technology.';
	}
}

②モジュールのテンプレートでは(XXXSuccess.php等)では普通に呼べるのでここにslotをまず埋める。

<?php slot('sample', $hoge);

③アプリケーションのテンプレートでslotを呼び出します。

<?php if (has_slot('sample')): ?>
<?php include_slot('sample') ?>  // <- technology.が表示されます。
<?php else: ?>
スロットが定義されていません。
<?php endif; ?>

これで、actionで定義した変数をlayout.php等で利用可能です。もちろん、layout.phpが読み込んでるパーシャル等でも。

[Symfony]対顧機能と社内機能のアクセス制限機能について

概要

フロントエンド向けのアプリケーションとバックエンド向けのアプリケーションを作成した場合、フロントエンドはWANからのLB経由でのアクセスのみ、バックエンドのアプリケーションは社内からのイントラネットでのアクセスのみと制限したいケースがある。その方法について簡単に。

解決方法

結論から言うと、Symfonyのディレクトリ構成を変えてしまえばよい。Symfonyの通常のアプリケーションはすべて、web下にある{アプリケーション名}.phpでのアクセスとなってしまう。

例えば、バックエンド(backend.php)に対してアクセス制限したい場合は、webと同列のbackendディレクトリを作成して、その下にbackend.phpを移動させてしまおう。そうすることで、LBはweb下にあるindex.phpへのアクセスのみを許可し、backend/backend.phpにはアクセスできない。逆に社内イントラネットからはbackend/backend.phpのアクセスだけを許可してやればよい。

[Symfony]ログ出力をカテゴリ別に分ける

概要

機能毎にログ出力を別ファイルに行うこと(※)を実現したい場合に、Symfony標準機能ではアプリケーションレベルで機能を分ける必要があります。
同一アプリケーション内でログ出力を分けたい場合には、独自の実装が必要となります。その方法について解決策を紹介します。

※一例として、同一アプリケーションでHTTPリクエストのログと外接システムとのログを分けたい場合

実現方法

同一アプリケーションで複数ログファイルを作成したい場合はsfAggregateLoggerを利用しますが、このクラスでは複数ログは出力できるものの、出力内容はほとんど同じになってしまい、(ログレベルの違いしか変更できない)少しカスタマイズが必要となります。

SymfonyでのログクラスはすべてsfLoggerという抽象クラスを継承しており、ログ出力に関してはdoLog関数に集約されています。
下記は、sfAggregateLoggerのdoLog関数です。

  /**
   * Logs a message.
   *
   * @param string $message   Message
   * @param string $priority  Message priority
   */
  protected function doLog($message, $priority)
  {
    foreach ($this->loggers as $logger)
    {
      $logger->log($message, $priority);
    }
  }

ここで登録されているログクラスについてすべて出力してしまっているので、ここに条件を入れれば問題として解決しそうだということが分かります。
したがって、方針としてはsfAggregateLoggerを拡張したmyAggregateLoggerを作成し、またmyAggregateLoggerに登録するログクラスをsfFileLoggerを拡張したmyFileLoggerを登録することとします。そしてmyFileLoggerに同一インターフェースをもたせるためにmyInterfaceLoggerも作成します。

myInterfaceLoggerを作成する

<?php
/**
 * 本アプリケーションで利用するログが継承すべきインターフェース。
 *
 * @author Yusuke Iwasaki
 */
interface myInterfaceLogger {
    
    /**
     * 実装クラスのログのタイプを返す。
     * このログタイプにより、出力ログファイルを振り分けます。
     */
    public function getType();
}

myFileLoggerを作成する

<?php
/**
 * カスタムファイルロガー。
 *
 * @author Yusuke Iwasaki
 */
class myFileLogger extends sfFileLogger implements myInterfaceLogger {

    protected
        $log_type;

    public function initialize(sfEventDispatcher $dispatcher, $options = array()) {
        if (isset($options['log_type'])) {
            $this->log_type = $options['log_type'];
        }
        return parent::initialize($dispatcher, $options);
    }

    public function getType() {
        return $this->log_type;
    }
}

myAggregateLoggerを作成する

<?php
/**
 * カスタムアグレゲートロガー。
 *
 * @author Yusuke Iwasaki
 */
class myAggregateLogger extends sfAggregateLogger {
    /**
     * ログファイルの分散出力を行います。
     * @param type $message ロギング文言
     * @param type $type ログファイルタイプ(request, circumscription)
     * @param type $priority ログレベル
     */
     public function logging($message, $type, $priority = self::INFO) {
        foreach ($this->loggers as $logger) {
            if (method_exists($logger, 'getType')) {
                if ($logger->getType() === $type) {
                    $logger->log($message, $priority);
                }
            }
        }
    }
}

ここで注意すべきはinterfaceで定義したgetType関数を持つか持たないかを判別する条件式を入れることです。開発環境では、デバックをONにしている場合にsfWebDebugLoggerが入ってくるためです。

factories.ymlに作成したログを登録する

all:
  logger:
    class:   myAggregateLogger
    param:
      level:   info
      loggers: 
        api_logger:
          class: myFileLogger
          param:
            level: info
            file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%_request.log
            log_type: request
        request_logger:
          class: myFileLogger
          param:
            level: info
            file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%_circumscription.log
            log_type: circumscription

利用方法

通常のログクラスと同様に熱かってください。sfContextクラスからログ関数を取得して、通常ならば「info, …, log」関数を呼び出しましたが、独自で作成した「logging」を呼ぶように注意します。