まいける's Tech Blog

LAMP関係のメモなどを不定期に掲載します

Smarty3 x Bootstrap3 用のページャープラグインを作ってみた

 都度都度ページャー作るのも面倒なので、作ってみました。

 まずは、プラグイン本体(function.paging.php)。Smartyプラグインディレクトリに保存してください。

<?php
function smarty_function_paging($params, $smarty)
{
    $total_page = ceil($params['paging_data']['total_row_count'] / $params['paging_data']['page_row_count']);
    $total_page_str = $total_page;
    if (empty($params['paging_data']['current_page'])) {
        $params['paging_data']['current_page'] = 1;
    }

    $result = null;

    // ページが2ページ以上ある場合にページャーを表示
    if ($total_page > 1) {
        $prev = $params['paging_data']['current_page'] - 1;
        $prev_str = $prev;
        $next = $params['paging_data']['current_page'] + 1;
        $next_str = $next;

        $result = '<nav aria-label="Page navigation">' . "\n";
        $result .= '<ul class="pagination">' . "\n";

        if ($prev < 1) {
            // 最初のページ
            $result .= '<li class="disabled"><span><span aria-hidden="true">&laquo;</span></span></li>' . "\n";
            // 前のページ
            $result .= '<li class="disabled"><span><span aria-hidden="true">&lt;</span></span></li>' . "\n";
        } else {
            // 最初のページ
            $result .= '<li><a href="' . str_replace('%p', '1',
                    $params['paging_data']['url']) . '" aria-label="First"><span aria-hidden="true">&laquo;</span></a></li>' . "\n";
            // 前のページ
            $result .= '<li><a href="' . str_replace('%p', $prev_str,
                    $params['paging_data']['url']) . '" aria-label="Previous"><span aria-hidden="true">&lt;</span></a></li>' . "\n";
        }

        $start_page = max(1, $params['paging_data']['current_page'] - 3);
        $end_page = min($total_page, $params['paging_data']['current_page'] + 3);

        for ($page = $start_page; $page <= $end_page; $page++) {
            $page_str = $page;

            if ($page == $params['paging_data']['current_page']) {
                // 現在のページ
                $result .= '<li class="active"><span>' . $page_str . '<span class="sr-only">(current)</span></span></li>
' . "\n";
            } else {
                $result .= '<li><a href="' . str_replace('%p', $page_str,
                        $params['paging_data']['url']) . '">' . $page_str . '</a></li>' . "\n";
            }
        }

        if ($next > $total_page) {
            // 次のページ
            $result .= '<li class="disabled"><span><span aria-hidden="true">&gt;</span></span></li>' . "\n";
            // 最後のページ
            $result .= '<li class="disabled"><span><span aria-hidden="true">&raquo;</span></span></li>' . "\n";
        } else {
            // 次のページ
            $result .= '<li><a href="' . str_replace('%p', $next_str,
                    $params['paging_data']['url']) . '" aria-label="Next"><span aria-hidden="true">&gt;</span></a></li>' . "\n";
            // 最後のページ
            $result .= '<li><a href="' . str_replace('%p', $total_page_str,
                    $params['paging_data']['url']) . '" aria-label="Last"><span aria-hidden="true">&raquo;</span></a></li>' . "\n";
        }
        $result .= '</ul>' . "\n";
        $result .= '</nav>' . "\n";
    }

    if ($params['debug']) {
        $result .= '<div class="bs-callout bs-callout-info"><h4>ページングデバッグ</h4>' . "\n";
        $result .= '<ul class="list-group">' . "\n";
        $result .= '<li class="list-group-item">全ページ数:' . $total_page . '</li>';
        $result .= '<li class="list-group-item">URL:' . $params['paging_data']['url'] . '</li>';
        $result .= '<li class="list-group-item">ページ内表示件数:' . $params['paging_data']['total_row_count'] . '</li>';
        $result .= '<li class="list-group-item">件数:' . $params['paging_data']['page_row_count'] . '</li></ul></div>';
    }

    $smarty->assign('pager', $result);
}

 次に呼び出し側のPHPですが、こちらは

<?php
$smarty = new Smarty();

$page = (int)filter_input(INPUT_GET, 'page');
if ($page == 0) {
    $page = 1;
}

// データベースから取得したデータが $data として

$paging_data = array(
    'page_row_count' => 30,
    'current_page' => $page,
    'url' => $_SERVER['SCRIPT_NAME'] . '?page=%p',
    'total_row_count' => $data->count();
);

$smarty->assign('paging_data', $paging_data);
$smarty->display('page.tpl');

みたいな感じで。%p の部分が実際にはページ番号に置き換えられます

最後にテンプレートファイルですが

{paging paging_data=$paging_data}

プラグインを呼び出して、ページャーを入れたいところに

{$pager}

を入れる感じです

ちなみに

{paging paging_data=$paging_data debug=1}

とすると、プラグインに渡した値が確認できます。
基本的に Bootstrap3 のみで表示できるようになっていますが、デバッグモードの部分だけ

.bs-callout {
  padding: 20px;
  margin: 20px 0;
  border: 1px solid #eee;
  border-left-width: 5px;
  border-radius: 3px;
}
.bs-callout h4 {
  margin-top: 0;
  margin-bottom: 5px;
}
.bs-callout p:last-child {
  margin-bottom: 0;
}
.bs-callout code {
  border-radius: 3px;
}
.bs-callout+.bs-callout {
  margin-top: -5px;
}
.bs-callout-info {
  border-left-color: #5bc0de;
}
.bs-callout-info h4 {
  color: #5bc0de;
}

css に追加してください。Bootstrapの公式サイトでも使われているデザインなのですが、BootstrapのCSSには定義されていないので。

Bootstrap 4 版を作ってみました。
gn-office.pro

APC導入後、Unable to allocate memory for pool エラーが出る場合の対処法

 Webエンジニアが知っておきたいインフラの基本 ~インフラの設計から構成、監視、チューニングまで~を読んで、遅まきながら管理しているサーバに APC を導入してみました。体感的にも処理が早くなっていい感じだったのですが、しばらくしてログに

[xx-Jun-2015 xx:xx:xx UTC] PHP Warning:  Unknown: Unable to allocate memory for pool. in Unknown on line 0

のエラーが続々と記録されるようになりました。

 どうやら原因は、APC の設定で追加した

apc.ttl=3600

の部分だったようで、上記の設定を追加すると、設定した時間(3600秒)が経過するまでキャッシュされているエントリが滞留してしまい、apc.shm_size で設定している容量を超えてしまうと、それ以上のメモリを確保できなくなり、上記のエラーが発生するようです。

 したがって、対処法としては、apc.shm_size を十分に大きくするか、apc.ttl をメモリがいっぱいにならない程度に小さくするかのいずれかとなります。apc.ttl を小さくしすぎてしまうと、キャッシュしている意味がなくなるため、調整が難しそうですが

apc.ttl=0

とすることで、メモリ上限に達した時点でキャッシュの破棄がなされる(逆に上限に達しない限りずっと残り続ける)ので、とりあえずはこの設定で様子を見ればよいと思います(ちなみに、apc.ttl=0 がデフォルトの設定で、あえていじらなければ問題は起きなかった、というのは内緒)。

Gmail から特定の条件を満たすメールを抽出して RSS 化する

 ちょっとした必要に迫られて作ってみました。Gmail にはもともと、特定のラベルが付いたメールを ATOM で出力する機能が付いているのですが(このあたりの記事を参照)、HTTP 認証を使っているので、Feedly では利用することができません。ということで、Gmail API を利用して条件を満たすメールを抽出したうえで、RSS 化してみました。

 用意するものは

の2つ。Composer などを利用してインストールしておいてください。

 また、Gmail API を利用するための準備については、Google Developers のこちらの記事をご覧ください。RSS 化するスクリプトは、cron で定期的に実行することを念頭に置いているので、アプリケーションの種類は、“インストールされているアプリケーション”を選びます。

 以下、コードです。

<?php
require 'vendor/autoload.php';  // composer の autoload.phpの場所を指定

define('APPLICATION_NAME', 'Gmail To RSS');
define('CREDENTIALS_PATH', '~/.credentials/gmail-to-rss.json');
define('CLIENT_SECRET_PATH', 'client_secret.json'); // 準備で作成したクライアントIDに対応したJSONファイルの場所を指定
define('SCOPES', implode(' ', array(
  Google_Service_Gmail::GMAIL_READONLY) // 今回のスクリプトではメールに編集を加える必要がないので、与える権限は読み込みのみに
));

/**
 * Returns an authorized API client.
 * @return Google_Client the authorized client object
 */
function getClient() {
  $client = new Google_Client();
  $client->setApplicationName(APPLICATION_NAME);
  $client->setScopes(SCOPES);
  $client->setAuthConfigFile(CLIENT_SECRET_PATH);
  $client->setAccessType('offline');

  // Load previously authorized credentials from a file.
  $credentialsPath = expandHomeDirectory(CREDENTIALS_PATH);
  if (file_exists($credentialsPath)) {
    $accessToken = file_get_contents($credentialsPath);
  } else {
    // Request authorization from the user.
    $authUrl = $client->createAuthUrl();
    printf("Open the following link in your browser:\n%s\n", $authUrl);
    print 'Enter verification code: ';
    $authCode = trim(fgets(STDIN));

    // Exchange authorization code for an access token.
    $accessToken = $client->authenticate($authCode);

    // Store the credentials to disk.
    if(!file_exists(dirname($credentialsPath))) {
      mkdir(dirname($credentialsPath), 0700, true);
    }
    file_put_contents($credentialsPath, $accessToken);
    printf("Credentials saved to %s\n", $credentialsPath);
  }
  $client->setAccessToken($accessToken);

  // Refresh the token if it's expired.
  if ($client->isAccessTokenExpired()) {
    $client->refreshToken($client->getRefreshToken());
    file_put_contents($credentialsPath, $client->getAccessToken());
  }
  return $client;
}

/**
 * Expands the home directory alias '~' to the full path.
 * @param string $path the path to expand.
 * @return string the expanded path.
 */
function expandHomeDirectory($path) {
  $homeDirectory = getenv('HOME');
  if (empty($homeDirectory)) {
    $homeDirectory = getenv("HOMEDRIVE") . getenv("HOMEPATH");
  }
  return str_replace('~', realpath($homeDirectory), $path);
}

// Get the API client and construct the service object.
$client = getClient();

$service = new Google_Service_Gmail($client);

$user = 'me';

$optParams = array();
$optParams['maxResults'] = 50;
$optParams['q'] = 'label:hogehoge is:unread'; // hogehogeというラベルが付いた未読のメールを検索

try {
    $messages = $service->users_messages->listUsersMessages($user, $optParams); // 条件に合致するメールの一覧を取得

    $list = $messages->getMessages();

    $i = 0;

    foreach($list as $line) {
        $messageId = $line->getId();

        $optParamsGet = array();
        $optParamsGet['format'] = 'full'; // Display message in payload
        try {
            $message = $service->users_messages->get($user, $messageId, $optParamsGet); // 取得したメールIDから個々のメールデータを取得
            
            $messagePayload = $message->getPayload();

            $headers = $message->getPayload()->getHeaders();

            foreach($headers as $header) {
                $header_ary[$header->getName()] = $header->getValue(); // ヘッダ情報を後で使いやすいように配列に代入
            }

            $items[$i]['title'] = $header_ary['Subject'];
            $items[$i]['date'] = strtotime($header_ary['Date']);
            $items[$i]['message_id'] = $messageId;

            $body = $message->getPayload()->getBody(); // メール本文を取得

            $rawData = $body->data;
            $sanitizedData = strtr($rawData, '-_', '+/');
            $decodedMessage = base64_decode($sanitizedData); // Base64でエンコードされているのでデコード

            $items[$i]['description'] = $decodedMessage;

            $i ++;

        } catch (apiServiceException $e) {
           // Error from the API.
            print 'There was an API error : ' . $e->getCode() . ' : ' . $e->getMessage();
        } catch (Exception $e) {
            print 'There was a general error : ' . $e->getMessage();
        }
    }


} catch (apiServiceException $e) {
    // Error from the API.
    print 'There was an API error : ' . $e->getCode() . ' : ' . $e->getMessage();
} catch (Exception $e) {
    print 'There was a general error : ' . $e->getMessage();
}

// ここから RSS 化

date_default_timezone_set("Asia/Tokyo");

use \FeedWriter\ATOM;
$feed = new ATOM;

//チャンネル情報の登録
$feed->setTitle("hogehoge");          //チャンネル名
$feed->setLink("http://www.example.com/");     //URLアドレス
$feed->setDate(new DateTime());         //日付 (変更不要)

foreach($items as $tmp_item) {
    // メール本文中のURLを自動リンク化
    $description = mbereg_replace("(https?|ftp)(://[[:alnum:]\+\$\;\?\.%,!#~*/:@&=_-]+)", "<a href=\"\\1\\2\" target=\"_blank\">\\1\\2</a>" , $tmp_item['description']);
    // 改行を<br />に変換
    $description = nl2br($description);
        
    $item = $feed->createNewItem();
    $item->setTitle($tmp_item['title']);
    $item->setLink('http://www.example.com/archive.php?message_id=' . $tmp_item['message_id']); // この部分については次の記事で
    $item->setDate($tmp_item['date']);
    $item->setDescription($description);
    $feed->addItem($item);
}


file_put_contents('/path/to/hogehoge.rdf', $feed->generateFeed()); // ファイルに書き出す
?>

 このファイルに gmail2rss.php のような適当な名前を付けてサーバにアップロードします。CREDENTIALS_PATH や CLIENT_SECRET_PATH で指定している場所は、外部から見えない場所に設定してください。
 そのうえで、ターミナルから

php gmail2rss.php

を実行すると、初回は

Open the following link in your browser: https://〜

のようなメッセージが表示されますので、指定された URL にブラウザからアクセスしてください。このスクリプトに権限を与えてよいかのダイアログが表示されるので、許可すると、コードが表示されます。それをコピーしてターミナルに戻り

Enter verification code: 

に続けてペーストすると、スクリプトが実行され、指定された場所に RSS ファイルが生成されます。

 あとはこれを RSS リーダーで読めばよいのですが、このままだと、RSS リーダーで既読にしても、Gmail 側は未読のままです。そこで、記事の URL として指定された URL にアクセスすると Gmail 側も既読にしてアーカイブするようにしてみたいと思います。