Sunday, July 26, 2009

The Definitive Guide to Catalyst

The Definitive Guide to Catalyst: Writing Extensible, Scalable and Maintainable Perl–Based Web Applications

My boss (Jeffrey Ng) just brought this book for me (thanks to my college Calvin getting it for me from USA).

I haven't read it yet, but I will. it's a good book. I'll post a review once I read it through.

:)

Labels: ,

Wednesday, June 17, 2009

QueryLog support for Catalyst::Model::DBIC::Schema

well, I wrote Catalyst::Model::DBIC::Schema::QueryLog before. it's fine. it works.

mst released the new Catalyst::Model::DBIC::Schema today. built on top of Moose, with two traits: Caching and Replicated.
It reminds me to write Catalyst::TraitFor::Model::DBIC::Schema::QueryLog
trait is great. easy to read, configurable and sane.

Enjoy. Thanks

Labels: , ,

Monday, December 15, 2008

Catalyst::Action::Fixup::XHTML

Catalyst::Action::Fixup::XHTML is on CPAN now.

as what I said yesterday, I just made up a Catalyst Action for XHTML header.
the code is really very simple and most are copied from Catalyst::View::TT::XHTML.
and the test code too.

I also wrote tests for my MojoX::Fixup::XHTML. you can find here: http://fayland.googlecode.com/svn/trunk/CPAN/MojoX-Fixup-XHTML/t/live-test.t

Just enjoy!

Thanks.

Labels: ,

Tuesday, October 14, 2008

Catalyst on Moose

http://search.cpan.org/~mramberg/Catalyst-Runtime-5.8000_01/

try it. it's new and on Moose.

$ cpan M/MR/MRAMBERG/Catalyst-Runtime-5.8000_01.tar.gz

ok, it fails, but u can download the source code and view a bit. I'm sure the next developer version will be kicked out ASAP.

Enjoy!

=updated

$ cpan parent
$ cpan M/MR/MRAMBERG/Catalyst-Runtime-5.8000_02.tar.gz

it should be working.

Thanks.

Labels: ,

Thursday, October 02, 2008

Foorum 0.2.7 release

Please download from http://foorum.googlecode.com/files/Foorum-0.2.7.tar.gz

it's a good code source to learn Catalyst+DBIx::Class+Template and others.

try http://www.foorumbbs.com/

Patches or suggestion is really appreciated.

Thanks.

Labels: ,

Monday, September 29, 2008

two new CPAN modules

* Task::BeLike::FAYLAND

OK, it's not so useful for u I think. just ignore it.

* Catalyst::Plugin::CHI

mix the CHI and Catalyst. :)

Labels: ,

Friday, April 11, 2008

Catalyst trap: param and params

well, if we write code as follows:
sub test : Local {
my ($self, $c) = @_;
my $test = {
a => $c->req->param('a'),
b => $c->req->param('b'),
c => $c->req->param('c'),
};
$c->res->body(Dumper(\$test));
}
then try to visit test?b=1, guess what we get? that's not what we expected.
$VAR1 = \{
'1' => 'c',
'a' => 'b'
};
a weird result!

please use follows, that's much better.
sub test : Local {
my ($self, $c) = @_;

my $test = {
a => $c->req->params->{'a'},
b => $c->req->params->{'b'},
c => $c->req->params->{'c'},
};
$c->res->body(Dumper(\$test));
}
and get what we want:
$VAR1 = \{
'a' => undef,
'b' => '1',
'c' => undef
};

so do remember that. and don't write dbix insert/update code like that.

Labels:

Friday, March 14, 2008

Catalyst Tip: bind STDOUT with res->body

sub test_body : Local {
my ($self, $c) = @_;

$c->res->body(\*STDOUT);
print "a\n";
print "b\n";
}

I think that's pretty useful when u are trying to use "Benchmark" module (or others use STDOUT).

Have fun!

Labels:

Sunday, February 24, 2008

Catalyst Book Short Review

Yup, my boss brought this Catalyst book for me. I spent one day to read through this book.

Frankly, it's too short. less than 200 pages. and some are already known stuff.
but it does inspire me at some point. and I'm pretty happy with it (esp. the model stuff)

well, I'm not going to blame the author, but it's not good enough. I thought there should be some knowledge about DBIx::Class and TT2, but it's too less. and it talks too less plugins. I know he can't cover all, but I think he should talk some useful plugins like Cache.

All can be in one sentence. it's too short. He should write 600 pages at least. Catalyst is worth 600 pages at least. :)

Any way, it's still a good book. I'm hoping the next Catalyst book.

Labels:

Friday, February 22, 2008

three books from Amazon

yup, my boss Jeffrey Ng bought those for me.

* Catalyst
* Perl Cookbook
* Beautiful Code

I'm pretty happy.

Labels: ,

Monday, December 24, 2007

Release Often - Foorum 0.1.0

I release Foorum 0.1.0 as a Christmas gift. :)

http://foorum.googlecode.com/files/Foorum-0.1.0.tar.gz

Live demo please see: http://www.foorumbbs.com/forum

Changes:

0.1.0 Fri Dec 24 20:56:00 2007
- move trunk to http://foorum.googlecode.com/svn/trunk/
- use jQuery to deal with browser time zone
- UBB js localize
- use Catalyst::Plugin::Cache instead of Catalyst::Plugin::Cache::Memcached
- use jQuery validate.js for register etc. (add validate/messages_cn.js)
- rewrite Model/Email.pm to send email after compose a message
- add Plain/Textile Formatter to post/reply/edit
- add Text::GooglewikiFormat
- new language: Chinese Traditional
- add =pod formatter supports
- My Shared Items

Labels: ,

Monday, December 17, 2007

Catalyst new feature: Catalyst::Stats

I guess I really need Catalyst::Stats.

I just move "Catalyst-Plugin-tv_interval" to DEPRECATED.

I think every Catalyst developer should use Catalyst::Stats.

:)

Labels:

Friday, December 14, 2007

Dreamhost Catalyst

It's a best Catalyst Calendar ever for Dreamhost:

local::lib and Catalyst

Labels: ,

Saturday, December 08, 2007

jQuery Flot For Daily Chart in Foorum

I know Google just release his Chart days ago. it's powerful, more than jQuery flot.

but I still want to use flot because it's simpler. yet badly it's not supporting day as its X. like 20071131 is next to 20071201, not far away as treated in flot.

Foorum has a feature that it will record the count of some tables every day. we have a table named 'stat', and columns are "stat_id", "stat_key", "stat_value", "date". so somehow we would have something like
20071201 user_counts 510
20071202 user_counts 640
then go on.
we use a cron script to collect those data.

data is not so straight for human being. we need CHART.
so I just make vars from stat table like:
stats => {
user_counts => {
20071201 => 510,
20071202 => 640,
20071203 => ...

then we use a TT file to create a HTML file.
$(function () {

[% i = 0 %]
[% FOREACH ctype IN stats.keys %]

$('body').append("<h2>[% ctype %]</h2><div class='placeholder' id='placeholder[% i %]' style='height:300px;'></div>");

var d[% i %] = [];

[% FOREACH key IN stats.${ctype}.keys.sort %]

d[% i %].push([[% key %], [% stats.${ctype}.$key %]]);

[% END %]

$.plot($("#placeholder[% i %]"), [
{
data: d[% i %],
lines: { show: true },
points: { show: true }
}
]);
[% i = i + 1 %]
[% END %]
});


more details @
http://fayland.googlecode.com/svn/trunk/Foorum/lib/Foorum/TheSchwartz/Worker/DailyChart.pm
http://fayland.googlecode.com/svn/trunk/Foorum/templates/stats/chart.html

@Enjoy;

Labels: ,

Friday, December 07, 2007

C::A::S::FromSub::Hash

I just release a new CPAN module for Catalyst - Catalyst::Authentication::Store::FromSub::Hash.

Generally I always hate one situation that http://search.cpan.org/perldoc?Catalyst::Authentication::Store::DBIx::Class hits database every request. to ease database, I create one cache layout between Authentication and database.

In my Foorum,
Foorum.pm
use Catalyst qw/Authentication ../;

foorum.yml
authentication:
default_realm: 'members'
realms:
members:
credential:
class: 'Password'
password_field: 'password'
password_type: "hashed"
password_hash_type: "SHA-1"
store:
class: 'FromSub::Hash'
model_class: "UserAuth"

In this config, we use store "FromSub::Hash" and model_class "UserAuth". so we need create a Foorum::Model::UserAuth:
package Foorum::Model::UserAuth;
use base 'Catalyst::Model';

sub auth {
my ($self, $c, $userinfo) = @_;

my $where;
if (exists $userinfo->{user_id}) {
$where = { user_id => $userinfo->{user_id} };
} elsif (exists $userinfo->{username}) {
$where = { username => $userinfo->{username} };
} else { return; }

my $user = $c->model('User')->get( $c, $where );
return $user;
}

$c->model('User')->get($c, $where); has built-in cache layout. and it return hash from/to cache.

I guess I would create another module - Catalyst::Authentication::Store::FromSub::Object later.
but since I don't use it, I'm not sure when will it kick out.

@Enjoy!

Labels:

Monday, December 03, 2007

Catalyst Advent Calendar 2007

Thursday, October 11, 2007

Catalyst::Plugin::PickComponents

I have written another Catalyst module to pick up desired components for Catalyst App.

Catalyst::Plugin::PickComponents

When I develop Foorum, I find the Controller/Model pm are expanding quickly. and it's not good for mod_perl since it will load all modules into the process. it costs memories.

that's why I wrote this. to reduce memory usage of mod_perl.

Thanks.

Labels:

Friday, October 05, 2007

Foorum v0.07

Foorum, a forum system based on Catalyst + DBIx::Class + Template.

I just spent days of my golden week holiday to write codes for Foorum.

new features:
* job queue: TheSchwartz. to resize profile photo and send scheduled email. it would ease mod_perl to load the whole Foorum.
* use jquery.ui Tabs to beauty most of my pages.
* remove RSS.pm and use template to create RSS feed.

u can download the source code from http://fayland.googlecode.com/files/Foorum.v007.tar.gz

If u want to join me to write this forum system, plz send me an email. Thanks.

Labels: ,

Thursday, October 04, 2007

use TheSchwartz job queue to handle Image-Resize For Catalyst App.

TheSchwartz - reliable job queue. original developped by Brad and used in LiveJournal.

My main aim is to remove 'Image::Magick' out of my Catalyst App since Image::Magick takes a lot of memories for every process under mod_perl. so we need a non-stop cron script to monitor and resize photos. and I picked TheSchwartz up for my Foorum Catalyst App.

Here comes the instructions:

1, create db 'theschwartz' by using schema: http://search.cpan.org/src/BRADFITZ/TheSchwartz-1.04/doc/schema.sql

2, write the main module Foorum::TheSchwartz::Worker::ResizeProfilePhoto use base TheSchwartz::Worker.
package Foorum::TheSchwartz::Worker::ResizeProfilePhoto;

use TheSchwartz::Job;
use base qw( TheSchwartz::Worker );
use Foorum::ExternalUtils qw/schema/;
use File::Spec;
use Image::Magick;
use Cwd qw/abs_path/;
use File::Copy;

my (undef, $path) = File::Spec->splitpath(__FILE__);

sub work {
my $class = shift;
my TheSchwartz::Job $job = shift;

my @args = $job->arg;

my $schema = schema();

# get upload from db
my $upload_id = shift @args;
if ($upload_id !~ /^\d+$/) {
return $job->failed("Wrong upload_id: $upload_id");
}
my $upload = $schema->resultset('Upload')->find( { upload_id => $upload_id } );
unless ($upload) {
return $job->failed("No upload for $upload_id");
}

# get file dir
my $directory_1 = int( $upload_id / 3200 / 3200 );
my $directory_2 = int( $upload_id / 3200 );
my $file = abs_path("$path/../../../../root/upload/$directory_1/$directory_2/" . $upload->filename);

# resize photo
my $p = new Image::Magick;
$p->Read($file);
$p->Scale(geometry=>'120x120');
$p->Sharpen(geometry=>'0.0x1.0');
$p->Set(quality=>'75');

my ($width, $height, $size) = $p->Get('width', 'height', 'filesize');

my $tmp_file = $file . '.tmp';
$p->Write($tmp_file);

move($tmp_file, $file);

# update db
$schema->resultset('UserProfilePhoto')->search( {
type => 'upload',
value => $upload_id,
} )->update( {
width => $width,
height => $height,
} );
($size) = ( $size =~ /^(\d+\.?\d{0,1})/ ); # float(6,1)
$upload->update( { filesize => $size } );

$job->completed();
}

sub max_retries { 3 };

1;


3, write a TheSchwartz_worker.pl and run it. it will monitor the "job" table in "theschwartz" database non-stop.
package Foorum::ExternalUtils;
# ... etc.
use TheSchwartz;
sub theschwartz {

$config = config() unless ($config);

my $theschwartz = TheSchwartz->new(
databases => [ {
dsn => $config->{theschwartz_dsn}, # dbi:mysql:theschwartz
user => $config->{dsn_user},
pass => $config->{dsn_pwd},
} ],
verbose => 1,
);
return $theschwartz;
}
use Foorum::ExternalUtils qw/theschwartz/;
use Foorum::TheSchwartz::Worker::ResizeProfilePhoto;

my $client = theschwartz();
$client->can_do('Foorum::TheSchwartz::Worker::ResizeProfilePhoto');
$client->work();


4, at last, we need insert data into 'job' table then let TheSchwartz_worker.pl to handle it.
package Foorum::Controller::Profile;
use Foorum::ExternalUtils qw/theschwartz/;
sub xxx : Local {
# ... etc.
my $client = theschwartz();
$client->insert('Foorum::TheSchwartz::Worker::ResizeProfilePhoto', $new_upload_id);
}


That's ALL.

The idea behind TheSchwartz is not so complicate.

  • the "theschwartz" database contains 5 tables:

    • 'error' for recording error for every failed job. like in Foorum::TheSchwartz::Worker::ResizeProfilePhoto, $job->failed("No upload for $upload_id");

    • 'funcmap' has two columns: on is funcid and the other is funcname. It's pretty simple. when we call "$client->can_do('Foorum::TheSchwartz::Worker::ResizeProfilePhoto');" in TheSchwartz_worker.pl, it will create a new row ($auto_increasing_func_id, 'Foorum::TheSchwartz::Worker::ResizeProfilePhoto') if there hasn't

    • 'job' is the most important table here. TheSchwartz_worker.pl monitor this table every 5 secs. and if $client->insert one job, it will work_once() immediately.

    • other two tables: exitstatus and notes


  • TheSchwartz_worker.pl can tell what jobs it can handle by using "$client->can_do('Foorum::TheSchwartz::Worker::ResizeProfilePhoto');". add this line means that TheSchwartz_worker.pl can do the jobs which the "funcid" in "job" table is mapping with 'Foorum::TheSchwartz::Worker::ResizeProfilePhoto'.

  • TheSchwartz_worker.pl is a non-stop script to monitor the "job" table every 5 secs. if there are jobs, it will work_once() one by one.

  • $client->insert('Foorum::TheSchwartz::Worker::ResizeProfilePhoto', $new_upload_id); it inserts one row in 'job' table then let worker.pl to handle it. 'Foorum::TheSchwartz::Worker::ResizeProfilePhoto' is the funcname, and $new_upload_id is the args.
    in package Foorum::TheSchwartz::Worker::ResizeProfilePhoto; we need use base TheSchwartz::Worker and write a sub work.
    sub work {
    my $class = shift;
    my TheSchwartz::Job $job = shift;

    my @args = $job->arg;

    # do something

    $job->completed();
    }

  • If u want another worker:
    just write package Foorum::TheSchwartz::Worker::Another
    then in TheSchwartz_worker.pl use it and add $client->can_do('Foorum::TheSchwartz::Worker::Another');
    then in pl or pm. $client->insert('Foorum::TheSchwartz::Worker::Another', @args);



That's ALL. @Enjoy;

Labels: ,

Tuesday, September 25, 2007

two ways to write test cases for Catalyst App

one is using Test::WWW::Mechanize::Catalyst. as recommended in Catalyst::Manual::Tutorial::Testing.

the other way is using MyApp to get $c. like here:
http://lists.scsys.co.uk/pipermail/catalyst/2007-April/012926.html

my $controller = MyApp->controller('MyController');
my $c = MyApp->prepare();

# Monkey with $c to set up a fake context (set req->uri, or params)

my $result = $controller->method_to_test($c, @args);


Enjoy!

Labels:

Friday, July 20, 2007

Catalyst debug trick

well, we always meet this when we develop a Catalyst application:
* we want -Debug and StackTrace or DBIC::Schema::Profiler when we develop the Catalyst App.
* we don't want -Debug like in the production server because it costs much.

Sometimes u may want a config name like debug_mode to control whether we want or not.
Here comes my solution:
__PACKAGE__->setup();

__PACKAGE__->log->levels('error', 'fatal'); # for real server
if( __PACKAGE__->config->{debug_mode} ) {

__PACKAGE__->log->enable('debug', 'info', 'warn'); # for developer server
{
# these code are copied from Catalyst.pm setup_log
no strict 'refs';
my $class = __PACKAGE__;
*{"$class\::debug"} = sub { 1 };
}

my @extra_plugins = qw/ StackTrace DBIC::Schema::Profiler /;
__PACKAGE__->setup_plugins( [ @extra_plugins ] );
}
Full code please check Foorum.pm

now u can set debug_mode: 1 in _local.yml when u develop and forget it in production server.

Explanation:
* why we write code after __PACKAGE__->setup(); is because we want __PACKAGE__->config->{debug_mode}. config is setup in ->setup().
* *{"$class\::debug"} = sub { 1 }; means $c->debug is on. code from Catalyst.pm
* __PACKAGE__->setup_plugins( [ @extra_plugins ] ); will setup extra_plugins whenever u want.

@Enjoy;

Labels: ,

Sunday, June 17, 2007

difference between $c->user and $c->user->obj

when we use Catalyst::Plugin::Authentication::Store::DBIC::User, we will have $c->user and $c->user->obj. but most of people are confusing with them I guess.

differences:
* $c->user is ISA Catalyst::Plugin::Authentication::Store::DBIC::User, while $c->user->obj is just something like sub obj in Catalyst::Plugin::Authentication::Store::DBIC::User. see:
__PACKAGE__->mk_accessors(qw/id config obj store/);
# .skip
bless {
id => $id,
config => $config,
obj => $user_obj,
}, $class;

* why $c->user->username is the same as $c->user->obj->username is because of
sub AUTOLOAD {
my $self = shift;
(my $method) = (our $AUTOLOAD =~ /([^:]+)$/);
return if $method eq "DESTROY";

$self->obj->$method(@_);
}
. when we call ->username of $c->user. it will call sub username in Catalyst::Plugin::Authentication::Store::DBIC::User. then sub AUTOLOAD works here, it call $self->obj->$method(@_) indeed. ($self is $c->user).

so there is something interesting..
* because we have sub for_session in Catalyst::Plugin::Authentication::Store::DBIC::User, we will have $c->user->for_session ( but not $c->user->obj->for_session if user table don't have "for_session" column ).
* when I see something "*user = \&obj;" in the code. I know I can use $c->user->user->username. it's the same as $c->user->obj->username. (JOKE!)

for most situation. I suggest use $c->user->obj->column_name because that's more direct (no AUTOLOAD involved).

Enjoy!.

Labels: ,

Catalyst and DBIx-Class tip

* Catalyst Tip: use set_authenticated instead of login when password is SHA1 hashed.

well, Foorum use SHA1 to encrypt the password. so no one knows the original password including me (db do not store original password).
so if u are trying $c->login($username, $password_sha1_hashed);, it will fail.
if u are checking the module of Catalyst::Plugin::Authentication::Credential::Password which sub login is there, we would know the solution:
$c->set_authenticated($user);

that does the same as login but no password check.

* DBIx::Class Tip: attribute offset.

sometimes when we run a SQL, that's not something like LIMIT 0,6 LIMIT 6,6
it might be something like LIMIT 5,6
well, the attributes of "rows" and "page" does something like LIMIT 6,6. but no luck with 5,6.
so if u want run a SQL with LIMIT 5,6, u might can try
search( {}, {
rows => 6,
offset => 5
} );
that would be what u want.

be careful, offset do no create a Data::Page object for you. I mean ->pager. u need create that yourself.

Enjoy!

Labels: ,

Monday, May 07, 2007

Catalyst PageCache with I18N

well, I have something like as follows in the Foorum Root.pm sub auto:
 # internationalization
$c->stash->{lang} = $c->req->cookie('pref_lang')->value if ($c->req->cookie('pref_lang'));
$c->stash->{lang} ||= $c->user->lang if ($c->user_exists);
$c->stash->{lang} ||= $c->config->{default_pref_lang};
if (my $lang = $c->req->param('set_lang')) {
$lang =~ s/\W+//isg;
if (length($lang) == 2) {
$c->res->cookies->{pref_lang} = { value => $lang };
$c->stash->{lang} = $lang;
}
}
$c->languages( [ $c->stash->{lang} ] );

but the Catalyst::Plugin::PageCache does NOT get along well with the I18N's languages.

so that I patch this module for a bit. to create a new plugin based on that:
package Catalyst::Plugin::PageCacheWithI18N;

use strict;
use warnings;
use Class::C3;
use vars qw/$VERSION/;
$VERSION = '0.01';
use base qw/Catalyst::Plugin::PageCache/;

sub _get_page_cache_key {
my ($c) = @_;

my $key = $c->next::method(@_);
my $lang = $c->req->cookie('pref_lang')->value if ($c->req->cookie('pref_lang'));
$lang ||= $c->user->lang if ($c->user_exists);
$lang ||= $c->config->{default_pref_lang};
if (my $set_lang = $c->req->param('set_lang')) {
$set_lang =~ s/\W+//isg;
if (length($set_lang) == 2) {
$lang = $set_lang;
}
}
$key .= '#' . $lang if ($lang);
return $key;
}

1;


Links: http://fayland.googlecode.com/svn/trunk/Foorum/lib/Catalyst/Plugin/PageCacheWithI18N.pm

Labels: , ,

Tuesday, February 27, 2007

Catalyst::Plugin::Session::Store::Memcached hack

Catalyst::Plugin::Session::Store::Memcached is much faster than DBIC. but that's not so fit our demand.
we have one function called "Ban a user", we remove the session of that user when he is banned. but Memcached Store don't have user_id field as key.

Cache::Memcached::Managed has one function named "group". so I hack that pm to support delete_session_data_by_user_id.

first when new Cache::Memcached::Managed we add:
        my $storage = $cfg->{memcached_obj} || Cache::Memcached::Managed->new(
data => "localhost:11211",
namespace => "catalyst_session",
group_names => [qw(user_id)],
%{ $cfg->{memcached_new_args} || {} },
),
so that we can set user_id when set a memcache key.
    $c->_session_memcached_storage->set(
@{ $c->_session_memcached_arg_fudge },
(
$key =~ /^(?:expires|session|flash)/
? ( expiration => $c->session_expires )
: ()
),
id => $key,
value => $data,
user_id => $user_id,
)
at end, delete_session_data_by_user_id is pretty simple.
sub delete_session_data_by_user_id {
my ( $c, $user_id ) = @_;

return unless ($user_id > 0);
$c->_session_memcached_storage->delete_group( user_id => $user_id );
}

have fun!

Labels: ,

Sunday, December 17, 2006

my first Catalyst Action

Catalyst::Action 是 Catalyst 里的另一种代码复用技术。

我写的一个功能是记录一些 path 信息:比如 get, post, 还有页面载入的时长。
这个页面载入的时长是在 begin 时用 Time::HiRes 记录一个 gettimeofday, 然
后在 end 结束后再用 tv_interval 来获取时长。
“记录 path 信息”要写的地方在 return 之前。比如有 $c->res->body 或
$c->res->location 的时候直接返回了,否则在 $c->view 后返回。这是常见的一
个 sub end 的写法。
如果不用 Catalyst::Action 的话,可以有一些弊端如,sub end 里要写两次(两
个 return 之前),还有如果有很多个 end 代码就可能会写到很多地方。
第二个好处是 Catalyst::Action 所谓的真正的复用。只要是 sub end 在后面加
上 :ActionClass 就会复用之个 Action 的代码。
第一个好处只是 NEXT 所带来的。

可能的代码:
Before:
# Root.pm
sub begin : Private { my ($self, $c) = @_; $c->stash->{start_t0} =
[gettimeofday]; }
sub end : Private { my ($self, $c) = @_;
if ($c->res->body || $c->res->location) {
$c->model('Log')->log_path($c, tv_interval(
$c->stash->{start_t0}, [gettimeofday] ) ); # log path
return;
}
# code here
$c->forward($c->view('TT'));
$c->model('Log')->log_path($c, tv_interval( $c->stash->{start_t0},
[gettimeofday] ) ); # log path
}
如果有其他 pm 的 sub end 覆盖了 Root.pm 的 end 的话,那还要在那个
sub end 里加上 log_path.

而 Catalyst::Action 的复用会很方便:
After:
package Catalyst::Action::PathLogger;

use strict;use warnings;
use base 'Catalyst::Action';
use Time::HiRes qw( gettimeofday tv_interval );

sub execute {
my $self = shift;
my ( $controller, $c ) = @_;

$self->NEXT::execute( @_ );

$c->model('Log')->log_path($c, tv_interval( $c->stash->{start_t0},
[gettimeofday] ) );
}
而 Root.pm 的 sub end 将不在用 :Private 而是 sub end :
ActionClass('PathLogger') 其他 pm 如果有 sub end 也可以这么写。

quite easy and reusable. have fun! :)

Labels:

Saturday, December 16, 2006

easy example for Catalyst::Plugin::Captcha

Catalyst::Plugin::Captcha 在 CPAN 的文档不是很明了。我在 Foorum 中用了一下。把经过简单的写一下。

功能要求是“密码错误”的登录次数超过三次以后就显示出验证码。这样可以防止 script 暴力破解 user 密码。把登录错误次数可以放到 session 里,但是 script 可以通过删除 cookie 来伪造 sessionid, 所以我就把登录错误次数放到了 memcached 里。

首先当然是 cpan 安装模块,然后在 yml 中配置:
captcha:
session_name: captcha_string
new:
width: 80
height: 30
lines: 1
gd_font: giant
create:
- normal
- rect
particle:
- 100
out:
force: jpeg
然后弄一个 captcha 的 Global 函数。
sub captcha : Global {
my ($self, $c) = @_;
$c->create_captcha();
}
create_captcha 返回的是一个 img 的 source, 也就是验证码那个图片的内容。
所以如果你要显示出验证码来的话,代码大致上是这样的:
<input type='text' name='captcha' size='12' /><img src='/captcha' />
img 的地址就是刚才的 Global 所弄的函数。插件将这个图片实际上的字符串内容放到了当前的 session 里。
$c->session->{ $c->config->{captcha}->{session_name} } = $random_string;
然后它提供的 validate_captcha method 就是比较 user 输入的东西跟这个 session 的内容。看看源代码就觉得非常清晰。
我最后的 Logon.pm
还是自己点过去看看吧。:)

Labels:

Friday, November 03, 2006

Catalyst response->redirect and login trick

say, u have a "return $c->res->redirect('/login') unless ($c->user_exists);" in one sub(means one url like "/forum/1/topic/new"). and u tried to get "/forum/1/topic/new" as $c->req->referer; but Catalyst is somehow different, u don't get "/forum/1/topic/new" as your HTTP_REFERER. Catalyst treats $c->res->redirect inside.

mm, maybe you will get this problem when you code in Catalyst.
Here comes my solution:
Root.pm
sub end : Private {
my ( $self, $c ) = @_;

# for login using!
if ($c->res->location and $c->res->location eq '/login') {
$c->res->location('/login?referer=/' . $c->req->path);
}
return if ($c->res->body || $c->res->redirect);
Logon.pm after $c->login OK, use
# redirect
my $referer = $c->req->param('referer');
if ($referer) {
$c->res->redirect($referer);
} else {
$c->res->redirect('/');
}
and in Template: try to put "<input type='hidden' name='referer' value='[% c.req.param('referer') %]' />" in your action='/login' form.

OK, maybe I forget the '$c->req->uri->query' or '$c->req->body_parameters', but you can improve it, right?

Have fun!

Labels:

Wednesday, November 01, 2006

Catalyst TT WRAPPER trick

Catalyst::View::TT has two different ways:
one is set a $c->stash->{template} then $c->forward($c->view)
the other is using $c->view('TT')->render
so if u want to have a hack on View::TT. better hack on 'sub render' since 'sub process' is calling 'sub render' indeed.

assuimg that we have a very common situation. we have a wrapper.html for whole site. but we don't want to apply the wrapper.html in email body templates and /admin pages.
according to this, we might have two solution.
one is to create two view modulues like TT.pm and NoWrapperTT.pm. config the WRAPPER.
the other is to do some trick on 'sub render' as what I did today.
sub render {
my $self = shift;
my ( $c, $template, $args ) = @_;

# view Catalyst::View::TT for more details
my $vars = {
(ref $args eq 'HASH' ? %$args : %{ $c->stash() }),
};

if ($vars->{no_wrapper}) {
$self->template->service->{WRAPPER} = [];
} else {
$self->template->service->{WRAPPER} = ['wrapper.html'];
}

$self->NEXT::render(@_);
}
so that u can use something like:
my $email_body= $c->view('TT')->render($c, 'email/example.html', {
no_wrapper => 1,
another_var => $another_var,
} );
or set $c->stash->{no_wrapper} = 1 in Admin.pm

have fun.
Refer: Code in GoogleCode

Labels: ,

Saturday, October 14, 2006

Session::State::URI redefine

当我们在 Catalyst 里同时用 Session::State::URI 和 Session::State::Cookie 来保持最大的 session 使用可能性的时候,有件事情可能并不是太完美。因为 Session::State::URI 会重新改写输出的结果,把 html 里的 link, a, img, script 等里的链接后面加上一个 sessionid. 但是对于 img, css 和 script 来说,一般情况下都是没必要的(浏览器会缓存 css/js/img, 但是如果后面加了 sid, 它会重新获取一次,增加了服务器的负担)。所以这时候我们可以 redefine Session::State::URI 里的一个 sub 来去掉 img/css/script 的 url 链接改写。
package Catalyst::Plugin::Session::State::URI;

no warnings 'redefine';

sub _session_rewriting_html_tag_map {
return {
a => "href",
form => "action",
# link => "href",
# img => "src",
# script => "src",
};
}
将这段代码随便放到某个 Controller/Model/Plugin 的 pm 里都可以。因为在 Catalyst 里所有的模块都是一次性载入的。

have fun.

Labels: