[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がきくはず。