Criando um WebLog com Catalyst

Eden Cardim
Publicado em 05/01/2007

r10 - 05 Jan 2007 - EdenCardim

Criando um WebLog com Catalyst

Como criar, rapidamente, uma aplicação de WebLog usando Catalyst

Sumário

* Instalando o Catalyst
* Criando o Esqueleto da Aplicação
* Executando o Servidor de Testes
* Uma Breve Introdução a MVC
* Criando um Model
* Criando uma View
* Criando um Controller
* Catalyst Controller Actions
* Plugins
* Criando posts
* Listando os Posts
* Deletando posts
* Editando posts
* Posts hierárquicos
* Considerações Finais

Instalando o 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

Criando o Esqueleto da Aplicação

 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"

Executando o Servidor de Testes

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

Uma Breve Introdução a MVC

MVC (Model/View/Controller) é um conceito de interface com o usuário que divide a aplicação em 3 camadas:

* Modelos - Camada contendo os dados e lógica de negócio da aplicação.
* Visões - Camada de que exibe os Modelos ao usuário
* Controller - Camada que controla o intercâmbio de dados entre os Modelos e as Visões

Criando um Model

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.

Criando uma View

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"

Criando um Controller

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"

Catalyst Controller Actions

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.

Plugins

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.

Criando posts

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).

Listando os Posts

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.

Deletando 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:

* WebLog::Controller::Post->get(3) - O argumento '3' foi capturado da URL por causa do atributo CaptureArgs
* WebLog::Controller::Post->delete

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.

Editando 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 %]

Posts hierárquicos

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

Considerações Finais

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:

* Taint-checks e verificação de sanidade de argumentos.
* Permitir ordenação dos posts por data ou nome.
* Paginação da exibição dos posts.
* Busca.

A respeito de quaisquer erros encontrados neste artigo, dúvidas e considerações adicionais, não hesite em contatar o autor.

AUTHOR

Eden Cardim

blog comments powered by Disqus