Eden CardimPublicado em 05/01/2007
r10 - 05 Jan 2007 - EdenCardim
Como criar, rapidamente, uma aplicação de WebLog usando Catalyst
Para instalar o catalyst você deve ter privilégios de super-usuário.
eden@anubis:/opt/eden/workspace$ su -
password:
anubis:~# cpan
cpan[1]> install Catalyst
cpan[2]> install Catalyst::Devel
cpan[3]> exit
anubis:~# exit
eden@anubis:/opt/eden/workspace$ catalyst.pl WebLog
created "WebLog"
created "WebLog/script"
created "WebLog/lib"
created "WebLog/root"
created "WebLog/root/static"
created "WebLog/root/static/images"
created "WebLog/t"
created "WebLog/lib/WebLog"
created "WebLog/lib/WebLog/Model"
created "WebLog/lib/WebLog/View"
created "WebLog/lib/WebLog/Controller"
created "WebLog/weblog.yml"
created "WebLog/lib/WebLog.pm"
created "WebLog/lib/WebLog/Controller/Root.pm"
created "WebLog/README"
created "WebLog/Changes"
created "WebLog/t/01app.t"
created "WebLog/t/02pod.t"
created "WebLog/t/03podcoverage.t"
created "WebLog/root/static/images/catalyst_logo.png"
created "WebLog/root/static/images/btn_120x50_built.png"
created "WebLog/root/static/images/btn_120x50_built_shadow.png"
created "WebLog/root/static/images/btn_120x50_powered.png"
created "WebLog/root/static/images/btn_120x50_powered_shadow.png"
created "WebLog/root/static/images/btn_88x31_built.png"
created "WebLog/root/static/images/btn_88x31_built_shadow.png"
created "WebLog/root/static/images/btn_88x31_powered.png"
created "WebLog/root/static/images/btn_88x31_powered_shadow.png"
created "WebLog/root/favicon.ico"
created "WebLog/Makefile.PL"
created "WebLog/script/weblog_cgi.pl"
created "WebLog/script/weblog_fastcgi.pl"
created "WebLog/script/weblog_server.pl"
created "WebLog/script/weblog_test.pl"
created "WebLog/script/weblog_create.pl"
O Catalyst vem com um servidor HTTP lightweight para testes
eden@anubis:/opt/eden/workspace$ cd WebLog
eden@anubis:/opt/eden/workspace$ script/weblog_server.pl
[debug] Debug messages enabled
[debug] Loaded plugins:
.----------------------------------------------------------------------------.
| Catalyst::Plugin::ConfigLoader 0.13 |
| Catalyst::Plugin::Static::Simple 0.14 |
'----------------------------------------------------------------------------'
[debug] Loaded dispatcher "Catalyst::Dispatcher"
[debug] Loaded engine "Catalyst::Engine::HTTP"
[debug] Found index.t "/opt/eden/workspace/WebLog"
[debug] Loaded Config "/opt/eden/workspace/WebLog/weblog.yml"
[debug] Loaded components:
.-----------------------------------------------------------------+----------.
| Class | Type |
+-----------------------------------------------------------------+----------+
| WebLog::Controller::Root | instance |
'-----------------------------------------------------------------+----------'
[debug] Loaded Private actions:
.----------------------+--------------------------------------+--------------.
| Private | Class | Method |
+----------------------+--------------------------------------+--------------+
| /default | WebLog::Controller::Root | default |
| /end | WebLog::Controller::Root | end |
'----------------------+--------------------------------------+--------------'
[info] WebLog powered by Catalyst 5.7006
You can connect to your server at http://anubis:3000
Agora, abra seu browser predileto no endereço localhost:3000
MVC (Model/View/Controller) é um conceito de interface com o usuário que divide a aplicação em 3 camadas:
Nosso Model será baseado em DBIx::Class, um mapeador objeto relacional. Por motivos de praticidade, iremos utilizar o banco de dados SQLite.
Agora é uma boa hora para instalar o DBIx::Class e o DBD::SQLite:
eden@anubis:/opt/eden/workspace/WebLog$ su -
password:
anubis:~# cpan
cpan[1]> install Catalyst::Helper::Model::DBIC
cpan[2]> install DBD::SQLite
cpan[3]> exit
anubis:~# exit
Se o servidor de testes ainda estiver rodando, encerre-o teclando Ctrl-C.
eden@anubis:/opt/eden/workspace/WebLog$ script/weblog_create.pl model DB DBIC::Schema WebLog::Schema create=static 'dbi:SQLite:dbname=weblog.db' '' ''
exists "/opt/eden/workspace/WebLog/script/../lib/WebLog/Model"
exists "/opt/eden/workspace/WebLog/script/../t"
No tables found in database, nothing to load at /usr/share/perl5/DBIx/Class/Schema/Loader/Base.pm line 443.
Dumping manual schema for WebLog::Schema to directory /opt/eden/workspace/WebLog/script/../lib ...
Schema dump completed.
created "/opt/eden/workspace/WebLog/script/../lib/WebLog/Model/DB.pm"
created "/opt/eden/workspace/WebLog/script/../t/model_DB.t"
Observe que o assitente reclamou por não encontrar tabelas no banco de dados, isso é porque não criamos o esquema do banco ainda, é o que iremos fazer agora.
eden@anubis:/opt/eden/workspace/WebLog$ mkdir lib/WebLog/Schema
Dentro da pasta recém-criada, crie um arquivo chamado Post.pm com seu editor de código predileto:
package WebLog::Schema::Post;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('post');
__PACKAGE__->add_columns(
id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0
},
title => {
data_type => 'text',
size => 64
},
body => {
data_type => 'text',
size => 1024
},
date => { data_type => 'datetime', default_value => 'CURRENT_TIMESTAMP' },
parent_id => {
data_type => 'integer',
is_nullable => 1
}
);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many( 'replies', 'WebLog::Schema::Post', 'parent_id' );
__PACKAGE__->might_have('parent', 'WebLog::Schema::Post', 'parent_id');
1;
Obs.: Deve haver um esquema melhor de banco de dados para este tipo de aplicação, consulte o DBA mais próximo... wink
Para inicializar o arquivo que o DBD::SQLite irá usar para armazenar seus dados:
eden@anubis:/opt/eden/workspace/WebLog$ perl -Ilib -MWebLog::Schema -e "WebLog::Schema->connect('dbi:SQLite:dbname=weblog.db', '', '')->deploy"
Observe que foi criado um arquivo chamado weblog.db na pasta raíz do aplicativo.
Nossa View será baseada no módulo Template, um front-end Perl para o Template Toolkit.
eden@anubis:/opt/eden/workspace/WebLog$ script/weblog_create.pl view TT TT
exists "/opt/eden/workspace/WebLog/script/../lib/WebLog/View"
exists "/opt/eden/workspace/WebLog/script/../t"
created "/opt/eden/workspace/WebLog/script/../lib/WebLog/View/TT.pm"
created "/opt/eden/workspace/WebLog/script/../t/view_TT.t"
Vamos criar um controller para gerenciar os posts do WebLog.
eden@anubis:/opt/eden/workspace/WebLog$ script/weblog_create.pl controller Post
exists "/opt/eden/workspace/WebLog/script/../lib/WebLog/Controller"
exists "/opt/eden/workspace/WebLog/script/../t"
created "/opt/eden/workspace/WebLog/script/../lib/WebLog/Controller/Post.pm"
created "/opt/eden/workspace/WebLog/script/../t/controller_Post.t"
Quando é feita uma requisição HTTP ao sistema, o Catalyst automaticamente mapeia o endereço requisitado a uma chamada de subrotina dentro de algum Controller. Para evitar que usuários executem subrotinas arbitrariamente, precisamos especificar quais subrotinas podem ser executadas e a qual URL elas estão associadas. Uma Action é uma subrotina associada a um endereço no domínio da aplicação.
As funcionalidades básicas do Catalyst podem ser extendidas através de plugins, neste exemplo em particular, iremos utilizar um plugin para construção e validação prática de formulários:
eden@anubis:/opt/eden/workspace/WebLog$ su -
password:
anubis:~# cpan
cpan[1]> install Catalyst::Plugin::FormBuilder
cpan[2]> exit
anubis:~# exit
No arquivo lib/WebLog.pm substitua a linha
use Catalyst qw/-Debug ConfigLoader Static::Simple/;
por
use Catalyst qw/-Debug ConfigLoader Static::Simple FormBuilder/;
Esta é a lista de plugins que o Catalyst irá carregar junto com sua aplicação. Observe que -Debug não é um plugin e sim uma opção que ativa a exibição das mensagens de depuração que são mostradas na STDERR enquanto sua aplicação estiver rodando.
Abra o arquivo lib/WebLog/Controller/Post.pm no seu editor predileto e inclua a seguinte Action:
sub add : Local Form {
my ( $self, $c, $parent_id ) = @_;
if ( $c->form->submitted && $c->form->validate ) {
my %args = (
title => $c->req->param('title'),
body => $c->req->param('body')
);
if ($parent_id) {
$args{parent_id} = $parent_id;
}
$c->model('DB::Post')->create( \%args );
}
}
O atributo Local está caracterizando a subrotina add como uma Action que responde dentro do namespace do pacote atual. Resumindo, o Catalyst irá mapear a URL
host-da-aplicacao/post/add
para
WebLog::Controller::Post->add
Observe que foi usado o nome do controller, seguido do nome da subrotina para criar o endereço que será utilizado na web.
O Atributo Form indica ao FormBuilder que esta action tem um formulário associado a ela. Para especificar como será o formulário:
eden@anubis:/opt/eden/workspace/WebLog$ mkdir root/forms
eden@anubis:/opt/eden/workspace/WebLog$ mkdir root/forms/post
Dentro da pasta root/forms/post crie um arquivo chamado add.fb com o seguinte conteúdo:
name: add_post
method: post
fields:
title:
label: Post Title
type: text
size: 40
required: 1
body:
type: textarea
cols: 40
required: 1
rows: 5
submit: Post
Agora iremos criar o template da página onde será exibido o form:
eden@anubis:/opt/eden/workspace/WebLog$ mkdir root/post
Na pasta recém-criada, crie um arquivo chamado add.tt contendo:
[% form.render %]
Reinicie o servidor de testes novamente, para atualizar o código.
eden@anubis:/opt/eden/workspace/WebLog$ script/weblog_server.pl
[debug] Debug messages enabled
[debug] Loaded plugins:
.----------------------------------------------------------------------------.
| Catalyst::Plugin::ConfigLoader 0.13 |
| Catalyst::Plugin::Static::Simple 0.14 |
'----------------------------------------------------------------------------'
[debug] Loaded dispatcher "Catalyst::Dispatcher"
[debug] Loaded engine "Catalyst::Engine::HTTP"
[debug] Found index.t "/opt/eden/workspace/WebLog"
[debug] Loaded Config "/opt/eden/workspace/WebLog/weblog.yml"
[debug] Loaded components:
.-----------------------------------------------------------------+----------.
| Class | Type |
+-----------------------------------------------------------------+----------+
| WebLog::Controller::Post | instance |
| WebLog::Controller::Root | instance |
| WebLog::Model::DB | instance |
| WebLog::Model::DB::Post | class |
| WebLog::View::TT | instance |
'-----------------------------------------------------------------+----------'
[debug] Loaded Private actions:
.----------------------+--------------------------------------+--------------.
| Private | Class | Method |
+----------------------+--------------------------------------+--------------+
| /default | WebLog::Controller::Root | default |
| /end | WebLog::Controller::Root | end |
| /post/index | WebLog::Controller::Post | index |
| /post/add | WebLog::Controller::Post | add |
'----------------------+--------------------------------------+--------------'
[debug] Loaded Path actions:
.-------------------------------------+--------------------------------------.
| Path | Private |
+-------------------------------------+--------------------------------------+
| /post/add | /post/add |
'-------------------------------------+--------------------------------------'
[info] WebLog powered by Catalyst 5.7006
You can connect to your server at http://anubis:3000
Agora acesse o endereço localhost:3000/post/add no seu browser predileto, preencha e submeta o formulário
Observe a saída na STDERR
[snip...]
[debug] Body Parameters are:
.-------------------------------------+--------------------------------------.
| Parameter | Value |
+-------------------------------------+--------------------------------------+
| _submit | Post |
| _submitted_add_post | 1 |
| body | test |
| title | test |
'-------------------------------------+--------------------------------------'
[debug] "POST" request for "post/add" from "127.0.0.1"
[debug] Path is "post/add"
[debug] Form (post/add): Set action to /post/add
[debug] Form (post/add): Looking for config file post/add.fb
[debug] Form (post/add): Found form config /opt/eden/workspace/WebLog/root/forms/post/add.fb
[debug] Form (post/add): Calling FormBuilder->new to create form
[debug] Rendering template "post/add.tt"
[info] Request took 0.120552s (8.295/s)
.----------------------------------------------------------------+-----------.
| Action | Time |
+----------------------------------------------------------------+-----------+
| /post/add | 0.088585s |
| /end | 0.013087s |
| -> WebLog::View::TT->process | 0.011510s |
'----------------------------------------------------------------+-----------'
Já deu pra perceber que vai ser chato ficar reiniciando manualmente o servidor de testes sempre que modificarmos o código. Por isso o servidor de testes tem uma funcionalidade interessante que faz com que ele se reinicie sempre que houver uma alteração no código. Basta ativar o switch -r:
eden@anubis:/opt/eden/workspace/WebLog$ script/weblog_server.pl -r
Apesar disso ser bom para acelerar o início de um projeto, a medida que a aplicação for crescendo, o processo de reinicio vai ficar cada vez mais lento (principalmente se seu Schema for carregado dinamicamente).
A aplicação já está criando os posts no banco de dados, porém ainda não há como visualiza-los. Abra o arquivo lib/WebLog/Controller/Post.pm no seu editor predileto e inclua a seguinte Action:
sub list : Local {
my ( $self, $c ) = @_;
my $posts = $c->model('DB::Post');
$c->stash->{posts} =
$posts->search( undef, { where => { parent_id => undef } } );
}
A função $c->stash retorna uma referência para um hash para armazenamento global durante uma requisição. Isso significa que todos os componentes da sua aplicação terão acesso às informações armazenadas no stash. No nosso caso, ao final de cada requisição, o stash é passado para o template que iremos usar para gerar o HTML que será exibido no browser.
Agora, altere a Action add para:
sub add : Local Form {
my ( $self, $c, $parent_id ) = @_;
if ( $c->form->submitted && $c->form->validate ) {
my %args = (
title => $c->req->param('title'),
body => $c->req->param('body')
);
if ($parent_id) {
$args{parent_id} = $parent_id;
}
$c->model('DB::Post')->create( \%args );
$c->res->redirect('/post/list');
}
}
Observe que apenas a última linha dentro do if mudou, redirecionando para a lista de posts assim que for adicionado um post.
Quando criamos nosso Controller, o Catalyst automaticamente incluiu a Action index, ela é chamada caso seja requisitado o endereço que equivale a um nome de controller sem ser seguido de um nome de subrotina. Substitua a Action index que o Catalyst gerou por esta:
sub index : Private {
my ( $self, $c ) = @_;
$c->res->redirect('/post/list');
}
E no arquivo lib/WebLog/Controller/Root.pm, substitua a action default por esta:
sub default : Private {
my ( $self, $c ) = @_;
$c->res->redirect('/post/list');
}
Isto faz com que estas Actions redirecionem o processamento para mostrar a lista de posts.
Agora vamos criar o template para exibir a lista de posts. Crie o arquivo root/post/list.tt com o seguinte conteúdo:
[% FOREACH post IN posts.all %]
[% post.title %]
[% post.date %]
[% post.body %]
[% END %]
Post
Reinicie o servidor e abra o endereço localhost:3000 no browser. Clique em 'Post' para acrescentar mais posts.
Crie as seguintes Actions no arquivo lib/WebLog/Controller/Post.pm:
sub get : PathPart('post') Chained CaptureArgs(1) {
my ( $self, $c, $post_id ) = @_;
$c->stash->{post} = $c->model('DB::Post')->find($post_id);
}
sub delete : Chained('get') {
my($self, $c) = @_;
$c->stash->{post}->delete;
$c->res->redirect('/post/list');
}
Aqui, estamos criando Actions encadeadas. O atributo Chained sem argumentos indica o início da cadeia, o argumento PathPart('post'), indica que esta action será associada a uma URL começando com 'post' e o atributo CaptureArgs(1) indica que um pedaço da URL será passada como argumento para a action. O atributo Chained('get'), significa que a Action será invocada depois da Action get.
Resumindo, quando for solicitada a URL /post/3/delete, serão invocadas as seguintes Actions, nesta ordem:
Reinicie o servidor. Observe que a lista de Actions encadeadas é exibida separadamente:
[debug] Loaded Chained actions:
.-------------------------------------+--------------------------------------.
| Path Spec | Private |
+-------------------------------------+--------------------------------------+
| /post/*/delete/... | /post/get (1) |
| | => /post/delete |
'-------------------------------------+--------------------------------------'
Agora, faça a seguinte alteração no arquivo root/post/list.tt:
[% FOREACH post IN posts.all %]
[% post.title %]
[% post.date %]
(delete)
[% post.body %]
[% END %]
Post
Abra o browser no endereço localhost:3000 e experimente criar e apagar posts.
Crie essa Action no arquivo lib/WebLog/Controller/Post.pm:
sub edit : Chained('get') Args(0) Form {
my ( $self, $c ) = @_;
if ( $c->form->submitted && $c->form->validate ) {
$c->stash->{post}->title( $c->req->param('title') );
$c->stash->{post}->body( $c->req->param('body') );
$c->stash->{post}->update;
$c->res->redirect('/post/list');
}
$c->form->values(
title => $c->stash->{post}->title,
body => $c->stash->{post}->body
);
}
Observe que o formulário de edição é o mesmo do formulário de adição, porém vem com valores já preenchidos por uma consulta ao banco. Iremos reaproveitar o formulário utilizado na action add para manter a consistência da aplicação:
eden@anubis:/opt/eden/workspace/WebLog$ mv root/forms/post/add.fb root/forms/post/edit.fb
Agora, altere a declaração da Action add para:
sub add : Local Form('/post/edit')
No template root/post/list.tt:
[% FOREACH post IN posts.all %]
[% post.title %]
[% post.date %]
(edit)
(delete)
[% post.body %]
[% END %]
Post
Crie um template em root/post/edit.tt:
[% form.render %]
Enfim, a funcionalidade mais interessante da aplicação, a criação de posts hierárquicos. Cada post deve ser capaz de ser associado com uma lista de respostas (que também são posts) e estas respostas, por sua vez, são "respondíveis" e assim por diante, recursivamente.
Crie a seguinte Action no controller lib/WebLog/Controller/Post.pm:
sub reply : Chained('get') Form('/post/edit') {
my ( $self, $c ) = @_;
$c->res->redirect( '/post/add/' . $c->stash->{post}->id );
}
Até agora, o atributo parent_id estava sem uso, agora ele é utilizado para indicar a qual post a resposta pertence.
Agora, precisamos 'ensinar' o template root/post/list.tt a permitir a criação de respostas e exibir a hierarquia. Para isso, usamos uma macro recursiva:
[% MACRO show(posts) BLOCK %]
[% FOREACH post IN posts %]
[% post.title %]
[% post.date %]
(edit)
(reply)
(delete)
[% post.body %]
[% IF (replies = post.replies) %]
[% count = 0 %]
[% FOREACH reply IN replies %][% count = count + 1 %][% END %]
[% count %]
Repl[% IF count > 1 %]ies[% ELSE %]y[% END %]:
[% show(replies) %]
[% END %]
[% END %]
[% END %]
[% show(posts.all) %]
Post
Voilá! Temos um Web Log simples, rápido e funcional com apenas 195 linhas de código (sem contar os templates HTML). Vale ressaltar que boa parte das linhas de código existentes foram geradas pelos bootstrappers do Catalyst. O uso de um esquema de banco de dados pré-definido reduziria 28 linhas. O uso de alguma IDE fornecendo templates de código poderiam reduzir mais ainda a quantidade de trabalho.
Obviamente, há várias melhorias a serem feitas, mas estas serão deixadas como exercício ao leitor. Eis algumas sugestões para acréscimo de funcionalidades:
A respeito de quaisquer erros encontrados neste artigo, dúvidas e considerações adicionais, não hesite em contatar o autor.
Eden Cardim