月別アーカイブ: 2011年2月

Perl製の小悪魔テンプレートエンジン「Text::PSTemplate」

 

小悪魔はうそです。Perl製のテンプレートエンジンを再発明しました。「Text::PSTemplate」です。

ひたすら変数や関数を書き込むのがメインのテンプレートエンジンです。ore_blog_ore_format_list1(category => 'fuck')みたいなアプリ固有の関数を作って、制御構造はそっちでやるのが手っ取り早いと思ってます。とはいえ、コアプラグインってものがあって、if文、each文、switch文、if_in_array文などの制御構文、ファイルのinclude、Django風テンプレート継承などの機能も使えます。あと、PHPのテンプレートエンジンDwooの機能を少しだけ移植してみたサンプルプラグインもあります。

PurePerlで依存モジュールもたぶんClass::C3くらいです依存モジュール特になし。少なくともPerl5.8.8以降では動きます。5年前から使ってますがアルファバージョンです。APIは変更するかもしれません。POD書きかけ。

以下、テンプレートの書式。

Masonっぽいタグが基本なので、エディタのMasonモードで開くといい感じにハイライトしてくれます。なお、デリミタは変更可能です。

<% ... %>

変数。

<% $var %>

関数。

<% html_escape($var) %>

関数の引数はPerlのまんまです。プラグインの設計次第で配列やファットカンマもいけます。

<% your_func($var, 'something') %>
<% your_func(name1 => $var, name2 => 'something') %>

関数は入れ子にできます。内側には&が必要。

<% your_func(&your_func($var)) %>

if文。関数はブロック内でも使え、外側のスコープの変数は継承され、内側から参照できます。

<% if_equals($var, 1)<<THEN,ELSE %>
    <% $var %> is 1.
<% THEN %>
    <% your_func($var2) %>
<% ELSE %>

構文と言っても、中身は単なる関数です。関数内でタグに後続するブロックを取得するAPIがあるので、それを使用すれば何となく制御構文っぽい感じに見えます。ちなみにブロックの名前は処理内容には無関係で、出現順だけが意味を持ちます。今のところ。

<% your_control($var)<<FOO,BAR %>
    block argument1
<% FOO %>
    block argument2
<% BAR %>

ファイル挿入。includeは入れ子にでき、例によって変数は継承されます。

<% include('path/to/file.txt') %>

テンプレート内に書かれたパス名はデフォルトではそのままPerlのopenに渡されますが、ファイル名の整形のためのコールバックをロジック側で指定することができるので、例えば、常に現在のテンプレートからの相対パスで指定できるようにしたり、基底ディレクトリを指定したり、.htmlは省略可にしたり、予めファイルの有無をチェックしたり、などできます。

テンプレート継承構文。Djangoのドキュメントを真似た例です。

こちらがbase.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <link rel="stylesheet" href="style.css" />
    <title><% placeholder('title')<<DEFAULT %>My amazing site<% DEFAULT %></title>
</head>

<body>
    <div id="sidebar">
        <% placeholder('sidebar')<<DEFAULT %>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/blog/">Blog</a></li>
        </ul>
        <% DEFAULT %>
    </div>

    <div id="content">
        <% placeholder('content')<<DEFAULT %><% DEFAULT %>
    </div>
</body>
</html>

extends構文で継承します。

<% extends('base.html')<<EXTENDS %>
    <% block('title')<<BLOCK %>My amazing blog<% BLOCK %>
    <% block('content')<<BLOCK %>
    <% each($blog_entries, 'entry')<<ENTRIES %>
        <h2><% $entry->{title} %></h2>
        <p><% $entry->{body} %></p>
    <% ENTRIES %>
    <% BLOCK %>
<% EXTENDS %>

下記はif_equals文を含むプラグインの実装例です。引数とブロック指定の2ウェイのインターフェースです。

package SomeModule;
use strict;
use warnings;
use base qw(Text::PSTemplate::PluginBase);
use Text::PSTemplate;

    sub if_equals : TplExport {

        my ($self, $target, $value, $then, $else) = @_;

        my $tpl = Text::PSTemplate->new;

        if ($target eq $value) {
            if ($then) {
                return $then;
            } elsif (my $inline = Text::PSTemplate::inline_data(0)) {
                return $tpl->parse($inline);
            }
        } else {
            if ($else) {
                return $else;
            } elsif (my $inline = Text::PSTemplate::inline_data(1)) {
                return $tpl->parse($inline);
            }
        }
        return;
    }

PluginBaseを継承するとTplExportアトリビュートを指定できるようになります。TplExportなサブルーチンはテンプレート関数と一対一に対応します。if文などの制御構文も実際はサブルーチンなので同じです。また、PluginBaseは同梱のClass::FileCacheable::Liteというクラスを継承していて、関数毎にファイルキャッシュをすることもできます。db_record_listなんて関数を作ったらFileCacheableアトリビュートを付与するとよいです。

このプラグインは下記のようにアクティベートできます。

use Text::PSTemplate::Plugable;
use SomePlugin; # 必要なくなった

my $tpl = Text::PSTemplate::Plugable->new;
$tpl->plug('SomePlugin',''); # 第二引数で名前空間を指定できます
my $parsed = $tpl->parse_file('path/to/file');

基底クラスのText::PSTemplateを継承したPlugableがプラグイン機能を拡張した使いやすいクラスなので、通常これを使います。

Class::FileCacheable::LiteなるPerlモジュールを作った

 

Class::FileCacheableです。PODです。

何にも気にせず作ったOOPスタイルのモジュールに、後から手軽にキャッシュ機構を導入できます。

package RemoteContentGetter;
use strict;
use warnings;
use base 'Class::FileCacheable::Lite';
use LWP::Simple;

    sub new {
        my ($class, $url) = @_;
        return bless {url => $url}, $class;
    }

    sub get_url : FileCacheable {
        my $self = shift;
        return LWP::Simple::get($self->{url});
    }

    sub file_cache_expire {
        my ($self, $timestamp) = @_;
        if (time() - $timestamp > 86400) {
            return 1;
        }
    }

    sub file_cache_options {
        my $self = shift;
        return {
            namespace => 'Test',
            cache_root => 't/cache',
            default_key => $self->{url},
        };
    }

やることは、

  • Cache::FileCacheable::Liteを継承する
  • 必要ならfile_cache_expireメソッドをオーバーライドし、キャッシュの失効条件を指定する。
  • 必要ならfile_cache_optionsメソッドをオーバーライドし、オプションを明示する。
  • 対象のメソッドにFileCacheableアトリビュートを付与する。

以上です。

オプション指定などはCache::FileCacheとほぼ同じです。というか、Cache::FileCacheに依存したClass::FileCacheableってのを作ったんだけど重すぎたので、ファイル操作まで自前でやる本モジュールができたのでした。っていうか、実はこのモジュールは何年も前からひそかに使っていて、すごく気に入っていたので、今回、ちゃんとテスト書いてアップしてみた次第。継承ツリーを汚さない、MixInバージョンも作りたい。

Github上のPerlソースからpod2htmlするブックマークレットだよ

 

GithubにあるPerlモジュールのPodを速攻htmlで見るブックマークレットです。実行したら別タブでプロキシサイトにアクセスするよ。

PODをHTMLで見る

下記のようなページで使えるよ。rawのページでもOKだよ。

https://github.com/jamadam/Class-FileCacheable/blob/master/lib/Class/FileCacheable.pm

SQL::OOPなるPerlモジュールです

SQL::OOPです。Perlです。

ORマッパーとか難しくてよくわかりません。SQL::Abstractの書式が難しすぎて覚えれません。OOPスタイルのSQLジェネレータも色々あるようですが、あまり気に入ったものがなかった。

SQL::OOPはオブジェクト指向なインターフェースでSQL::Abstractライクな結果を得るものです。簡単なことは難しく、難しいことは簡単にできます。ここ1年くらいでいくつかのプロジェクトで使ってるけど、今のところ問題なさげ。

詳しくはpodを参照ください。

例1

my $select = SQL::OOP::Select->new();
$select->set(
    $select->ARG_FIELDS => '*',
    $select->ARG_FROM   => 'table',
    $select->ARG_WHERE  => sub {
        my $where = SQL::OOP::Where->new;
        return $where->and(
            $where->cmp('=', 'a', 1),
            $where->cmp('=', 'b', 1),
        )
    },
);

こうなります。改行とインデントはフェイクです。

SELECT
    *
FROM
    table
WHERE
    "a" = ? AND "b" = ?

例2

こんな使い方しないってもの含めて、結構何でもできます。

my $select = SQL::OOP::Select->new();
$select->set(
    $select->ARG_FIELDS => SQL::OOP->new(q{"ky1", "ky2", *}),
    $select->ARG_FROM   => q("tbl1", "tbl2", "tbl3"),
    $select->ARG_WHERE  => sub {
        my $where = SQL::OOP::Where->new();
        return $where->and(
            $where->cmp('>=', 'hoge1', 'hoge1'),
            $where->cmp('=', 'hoge2', 'hoge2'),
            $where->or(
                $where->cmp('=', 'hoge3', 'hoge3'),
                $where->cmp('=', 'hoge4', 'hoge4'),
                $where->between('price', 10, 20),
                $where->is_null('vprice'),
                SQL::OOP->new('a = b'),
                'a = b',
                SQL::OOP->new('c = ? ?', ['code1', 'code2']),
                $where->between('price', 10, 20),
            ),
            $where->or(
                $where->cmp('=', 'hoge3', undef),
                $where->cmp('=', 'hoge4', undef),
            ),
        )
    },
    $select->ARG_ORDERBY => SQL::OOP::Order->abstract([['hoge1', 1], ['hoge2']]),
    $select->ARG_LIMIT  => 11315,
    $select->ARG_OFFSET => 1,
);

こうなります。

SELECT
    "ky1", "ky2", *
FROM
    "tbl1", "tbl2", "tbl3"
WHERE
    "hoge1" >= ?
    AND
    "hoge2" = ?
    AND
    (
        "hoge3" = ?
        OR
        "hoge4" = ?
        OR
        "price" BETWEEN ? AND ?
        OR
        "vprice" IS NULL
        OR
        a = b
        OR
        a = b
        OR
        c = ? ?
        OR
        "price"
        BETWEEN ? AND ?
    )
ORDER BY
    "hoge1" DESC, "hoge2"
LIMIT
    11315
OFFSET
    1

ところで、setメソッドなどの引数が変態的な書式になってますが、これはタイポをコンパイルエラーにさせるのと、入力補完するためです。