O Catalyst vem com alguns dispatchers instalados por padrão. Se você não sabe o conceito de dispacher, recomendo ler antes o artigo sobre dispacher com Web::Simple.
Neste artigo, vou considerar que você já sabe instalar módulos do cpan e que saiba utilizar o terminal para iniciar programas e editar arquivos (usando seu editor preferido).
Iniciando uma app
Antes de mais nada, vamos criar uma app catalyst. Você precisa do pacote Catalyst::Devel para poder continuar.
$ cd /tmp/; catalyst.pl MyApp
Isso vai criar uma app catalyst com nome MyApp. Os arquivos que vamos modificar são os controllers.
created "MyApp/lib/MyApp/Controller/Root.pm"
Para subir para testes, digite
$ cd MyApp; $ perl script/myapp_server.pl -dr
Nota:-d mostra o debug, e o -r manda reiniciar o catalyst a cada alteração nos arquivos.
Analisando a saída
Com o debug ativado, o catalyst mostra quais são os actions que foram carregados, e quais são as classes e métodos que foram declarados.
[debug] Loaded Private actions: .----------------------+--------------------------------------+--------------. | Private | Class | Method | +----------------------+--------------------------------------+--------------+ | /default | MyApp::Controller::Root | default | | /end | MyApp::Controller::Root | end | | /index | MyApp::Controller::Root | index | '----------------------+--------------------------------------+--------------' [debug] Loaded Path actions: .-------------------------------------+--------------------------------------. | Path | Private | +-------------------------------------+--------------------------------------+ | / | /index | | /... | /default | '-------------------------------------+--------------------------------------'
Veja que, existem duas partes separadas: uma com os Private actions, e outras com os Path actions.
Private actions
Cada Private action é apresentado por 3 colunas, Private, Class e Method. São, respectivamente, o caminho em formato texto para acessar a action, a classe em que ela foi definida, e em qual sub ela foi definida.
Path actions
No path action, é exibido o Path e o private path dele. Path é o caminho do endpoint que dispara os private actions. No exemplo acima, existem apenas 2 endpoints, porém não são 2 URLs.
Uma URL precisa determinar um objeto (seja página, arquivo ou diretório, impressora), enquanto os endpoints são textos que determinam qual serviço deve ser acessado.
No catalyst, são utlizados o … e * como marcadores. Explicarei cada um deles mais para frente.
Utilizando os dispachers padrões, – e desconsiderando que existem os de Regexp – o catalyst trata as URLs recebidas separando-as por / e tratando cada um dos pedaços para tentar encontrar o endpoint.
É importante saber que apenas a última barra é ignorada. Isso é uma “ajuda” do catalyst, mas é para facilitar para quem o utiliza.
Testando-os
Se você acessar http://0:3000/ você irá ver a tela inicial do catalyst e, no log, irá aparecer algo parecido com:
[info] *** Request 1 (0.001/s) [18387] [Sat Mar 2 15:02:25 2013] *** [debug] Path is "/" [debug] "GET" request for "/" from "127.0.0.1" [debug] Response Code: 200; Content-Type: text/html; charset=utf-8; Content-Length: 5472 [info] Request took 0.002763s (361.925/s) .------------------------------------------------------------+-----------. | Action | Time | +------------------------------------------------------------+-----------+ | /index | 0.000193s | | /end | 0.000159s | '------------------------------------------------------------+-----------'
Perceba o debug [debug] Path is “/” diz qual foi o path capturado e logo em seguida quais actions foram executados.
O método /end mais próximo do action sempre é executado, caso exista. Vamos falar sobre isso depois.
Se olharmos o Root.pm, iremos ver sub index :Path :Args(0) {.
Args(0) significa que esse metodo não recebe nenhum argumento. O :Path significa esse metodo deve representar uma action, cujo endpoint será ‘/’ (pois não enviar nada para o Path signfica o mesmo que :Path(‘/’).
Agora se você acessar, por exemplo, http://0:3000/caminho/que-nao-existe ? Nesse caso, o path `/…’ entra em ação.
[info] *** Request 3 (0.001/s) [19108] [Sat Mar 2 15:10:41 2013] *** [debug] Path is "/" [debug] Arguments are "caminho/que-nao-existe" [debug] "GET" request for "caminho/que-nao-existe" from "127.0.0.1" [debug] Response Code: 404; Content-Type: text/html; charset=utf-8; Content-Length: 14 [info] Request took 0.002096s (477.099/s) .------------------------------------------------------------+-----------. | Action | Time | +------------------------------------------------------------+-----------+ | /default | 0.000080s | | /end | 0.000108s | '------------------------------------------------------------+-----------'
Perceba que, o path continua sendo o /, porém, caminho/que-nao-existe virou argumento para o método. Isso porque, na definição do default, não foi dito quantos argumentos ele recebia sub default :Path {. Veja, não existe Args, portanto, tudo que não satisfizer nenhuma action, vai acabar virando argumento este action. Ou seja, é um bom jeito de fazer 404.
Observação: o nome dos métodos não influencia no comportamento deles. Portanto, se você alterar de sub index para sub home_page :Path(‘/’) :Args(0) { e sub not_found_page :Path {, o código vai continuar funcionando perfeitamente.
Por que usar Chained
Você pode estar se perguntando: “se funciona definindo os endpoints usando path, por que preciso usar chained?”.
Quando você começa mais páginas, você precisa tentar diminuir a quantidade de regras de negócio que você escreve mais de uma vez. É por isso que usamos models, para poder reaproveitar as regras em diferentes situações. Isso tem que ocorrer com as regras de dispacher também.
Vamos considerar o exemplo mais usado, que você tem um blog, e que suas urls são:
/post/<id>/<titulo> /post/new /post/<id>/edit /post
Portanto, para acessar o post, você faria um GET em /post//, para ver o template da página, GET /post/new e POST /post/new para salvar, GET /post//edit para ver o form para editar, e POST /post//edit. GET /post/ seria a lista com todos os posts.
O código para carregar o conteúdo do post, seja do banco ou de qualquer outro lugar, só precisa ser escrito uma vez, tanto para /post/<id>/<titulo> como para /post/<id>/edit e veremos isso mais pra frente.
Ok, mas cadê o Chained?
Calma, antes de aprender o chained, você precisava saber o que é path (ou endpoint) e o que são actions!
Modificando o controller
No Root.pm, é de senso comum criar um action que vai ser executado em todos os requests que você construir usando chained.
sub root: Chained('/') PathPart('') CaptureArgs(0) { my ( $self, $c ) = @_; push @{$c->stash->{metodos}}, ':root:'; }
Analisando agora esse código:
- Chained(‘/’) diz que esta sub esta ligada no /, ou seja, é a raiz do site.
- PathPart(”) diz que nada será adicionado no endpoint, então essa sub não muda o caminho urls.
- CaptureArgs(0) diz que nenhum parâmetro será capturado para este action. Nesse caso, CaptureArgs(0) e CaptureArgs têm o mesmo significado, mas com o número aparecendo fica mais claro.
O código adiciona na stash do request uma mensagem para que seja exibida no final. Como a ideia aqui é apenas mostrar o chained, não vou focar em Template nem banco de dados e/ou session.
Se você já salvou o arquivo, a saída agora vai ter uma seção com os Chained Actions, porém vazia.
[debug] Loaded Chained actions: .-------------------------------------+--------------------------------------. | Path Spec | Private | +-------------------------------------+--------------------------------------+ '-------------------------------------+--------------------------------------'
Isso é porque nenhuma sub que utiliza CaptureArgs representa um endpoint sozinha.
Para ter um endpoint, agora, vamos criar um novo controller, chamado Post.pm, assim:
package MyApp::Controller::Post; use Moose; use namespace::autoclean; use utf8; BEGIN { extends 'Catalyst::Controller' } sub base: Chained('/root') PathPart('post') CaptureArgs(0) { my ( $self, $c ) = @_; push @{$c->stash->{metodos}}, ':base do Post.pm:'; $c->stash->{posts} = [ 'Post 1', 'Post 2', 'Post 3' ]; } __PACKAGE__->meta->make_immutable; 1;
Veja que agora foi definido um Chained(‘/root’), e que /root é o caminho para o Private action do método que o chained seja feito.
CaptureArgs novamente vazio, pois não queremos nenhum parâmetro por enquanto. PathPart(‘post’) faz com que o endpoint agora tenha post como parte dele.
Neste momento, o debug continua vazio. Vamos adicionar o método onde ficaria a listagem dos posts.
sub list: Chained('base') PathPart('') Args(0) { my ( $self, $c ) = @_; push @{$c->stash->{metodos}}, ':lista de posts:'; push @{$c->stash->{metodos}}, "\t$_\n" for @{$c->stash->{posts}}; }
Args(0) é o que diz que esse action deve se tornar um endpoint.
.-------------------------------------+--------------------------------------. | Path Spec | Private | +-------------------------------------+--------------------------------------+ | /post | /root (0) | | | -> /post/base (0) | | | => /post/list | '-------------------------------------+--------------------------------------'
Olhando no debug,ele mostra que o endpoint /post irá executar, na ordem, as rotinas /root, depois /post/base, depois termina em /post/list.
Se você abrir essa pagina, vamos encontrar um erro.
[debug] Path is "/post/list" [debug] "GET" request for "post/" from "127.0.0.1" [error] Caught exception in MyApp::Controller::Root->end "Catalyst::Action::RenderView could not find a view to forward to." [debug] Response Code: 500; Content-Type: text/html; charset=utf-8; Content-Length: 13934 [info] Request took 0.008614s (116.090/s) .------------------------------------------------------------+-----------. | Action | Time | +------------------------------------------------------------+-----------+ | /root | 0.000102s | | /post/base | 0.000071s | | /post/list | 0.000065s | | /end | 0.000253s | '------------------------------------------------------------+-----------'
O catalyst executou partindo do /root até chegar em /end, e quando chegou no /end não encontrou como o conteúdo devia ser desenhado. Como aqui é apenas um exemplo, vamos alterar o código do /end para imprimir o conteúdo do @{$c->stash->{metodos}} em forma de texto.
Novamente no Root.pm, altere sub end : ActionClass(‘RenderView’) {} para:
sub end : ActionClass('RenderView') { my ( $self, $c ) = @_; return if $c->res->body; $c->res->content_type('text/plain'); $c->res->body( join "\n", @{$c->stash->{metodos}} ); }
Agora, quando você acessar http://0.0.0.0:3000/post ou http://0.0.0.0:3000/post/ vai retornar:
:root: :base do Post.pm: :lista de posts: Post 1 Post 2 Post 3
Info: Se você adicionar um sub end : ActionClass(‘RenderView’) dentro do Post.pm, o /end do Root.pm não vai ser executado; em boa parte das vezes não é realmente o que você quer, mas, de qualquer maneira, se você realmente quer implementar um end no seu próprio controller, você pode fazer um $c->forward(‘/end’) forçando o método do Root.pm ser executado.
Agora que já temos um método para listar, vamos criar o endpoint que carrega o post acessado na stash.
sub object: Chained('base') PathPart('') CaptureArgs(1) { my ( $self, $c, $id ) = @_; push @{$c->stash->{metodos}}, ':carregar post:'; if ($id =~ /^[0-9]$/ && exists $c->stash->{posts}[$id]){ push @{$c->stash->{metodos}}, 'Carregou post ' . $c->stash->{posts}[$id]; }else{ push @{$c->stash->{metodos}}, '!post não encontrado!'; $c->detach; } }
Lembre-se que esse action não cria nenhum endpoint, portanto, é preciso adicionar um método para exibir.
sub show_post: Chained('object') PathPart('') Args { my ( $self, $c, $id ) = @_; push @{$c->stash->{metodos}}, '^^^^^^^^^^ é o post!'; }
Agora, no debug, foi adicionado:
| /post/*/... | /root (0) | | | -> /post/base (0) | | | -> /post/object (1) | | | => /post/show_post |
Isso faz com que possamos acessar http://0.0.0.0:3000/post/1/oque-for/que-tiver-aqui,que vai aparecer:
:root: :base do Post.pm: :carregar post: Carregou post Post 2 ^^^^^^^^^^ é o post!
E na saída do debug:
[debug] Path is "/post/show_post" [debug] Arguments are "oque-for/que-tiver-aqui" [debug] "GET" request for "post/1/oque-for/que-tiver-aqui" from "127.0.0.1" [debug] Response Code: 200; Content-Type: text/plain; Content-Length: 82 [info] Request took 0.004938s (202.511/s) .------------------------------------------------------------+-----------. | Action | Time | +------------------------------------------------------------+-----------+ | /root | 0.000090s | | /post/base | 0.000058s | | /post/object | 0.000095s | | /post/show_post | 0.000078s | | /end | 0.000298s | '------------------------------------------------------------+-----------'
Veja que, novamente, o “oque-for/que-tiver-aqui” virou argumento para o action show_post, pois não foram definidos quantos argumentos ele receberia, e apenas que ele pode receber. Isso foi dito pelo Args.
Se você alterar para sub show_post: Chained(‘object’) PathPart(”) Args(2) {, o método só será executado em http://0.0.0.0:3000/post/1/um/dois mas http://0.0.0.0:3000/post/1/um/dois/tres vai executar o /… que é o Not Found.
Veja que se você acessar o post 9, http://0.0.0.0:3000/post/9, que não existe, o $c->detach; cuida de desviar o fluxo para o end mais próximo, e não executa os actions seguintes (que seria o show_post, neste caso).
.------------------------------------------------------------+-----------. | Action | Time | +------------------------------------------------------------+-----------+ | /root | 0.000086s | | /post/base | 0.000059s | | /post/object | 0.000123s | | /end | 0.000125s | '------------------------------------------------------------+-----------'
Vamos agora criar o susposto edit. O procedimento é bem semelhante ao do show_post:
sub edit_post: Chained('object') PathPart('edit') Args(0) { my ( $self, $c, $id ) = @_; push @{$c->stash->{metodos}}, 'Editando o post acima!'; }
Agora você pode acessar http://0.0.0.0:3000/post/1/edit e vai aparecer:
:root: :base do Post.pm: :carregar post: Carregou post Post 2 Editando o post acima!
Perceba que, da mesma forma que no show, se você carregar um post inexistente, o código de edit não irá ser executado. Isso significa que você só precisou fazer a verificação que o post existe uma vez, e que toda vez que o código de edit for executado, o post já existe.
Para criar o endpoint /post/new, você faria:
sub new_post: Chained('base') PathPart('new') Args(0) { my ( $self, $c, $id ) = @_; push @{$c->stash->{metodos}}, 'Criando novo post:'; }
Então depois de ver todos esses exemplos, fica muito mais simples entender como funcionam os chained actions do catalyst.
Algumas dicas:
- Tente criar um controller para cada coisa no seu site. Quanto mais separado, mais simples fica de manter e reutilizar o código.
- O nome do controller e dos métodos não interferem nos endpoints. Porém, os private paths são criados com base neles.
- Tente utilizar ao máximo o carregamento de objetos em actions com CaptureArgs(XX) e deixar os endpoints sempre com Args(0) ou Args, isso vai poupar algumas dores de cabeças quando você tiver muitos actions chained espalhados.
Chained + DBIx::Class
Depois de um tempo utilizando Chained para criar endpoints REST, você percebe algumas coisas que facilitam o desenvolvimento. O exemplo abaixo não foi testado, mas deve servir para você com pequenos ajustes.
Post.pm:
package MyApp::Controller::Post; use Moose; use namespace::autoclean; use utf8; BEGIN { extends 'Catalyst::Controller' } sub base: Chained('/root') PathPart('post') CaptureArgs(0) { my ( $self, $c ) = @_; # verificaria as permissoes do usuario atual para acessar o conteudo # carrega o model em stash->{collection} $c->stash->{collection} = $c->model('DB::Post'); } sub list: Chained('base') PathPart('') Args(0) { my ( $self, $c ) = @_; # percorre a lista no collection e adiciona em algum lugar as # linhas para poder renderizar while (my $row = $c->stash->{collection}->next){ push @{$c->stash->{algum_lugar}}, $row; } } sub object: Chained('base') PathPart('') CaptureArgs(1) { my ( $self, $c, $id ) = @_; $c->detach('/erro_usuario_maldito') unless $id =~ /^[0-9]$/; $c->stash->{collection} = $c->stash->{collection}->find({$id}); # especificando collection e separando o object pois o object pode ser apenas um hash # e nao mais um ResultSet com where. $c->stash->{object} = $c->stash->{post} = $c->stash->{collection}->next; if (!$c->stash->{object}){ # coloca na stash alguma coisa pra dizer que foi 404 $c->detach; } } sub show_post: Chained('object') PathPart('') Args { my ( $self, $c, $id ) = @_; # aqui seria apenas o template já utilizar o stash.object e stash.post # pois ja foi carregado e ja existe } # edit e a "mesma" coisa para delete sub edit_post: Chained('object') PathPart('edit') Args(0) { my ( $self, $c, $id ) = @_; if ($c->req->params->{conteudo_editado}){ $c->stash->{collection}->update( { } ); } } sub new_post: Chained('base') PathPart('new') Args(0) { my ( $self, $c, $id ) = @_; if ($c->req->params->{conteudo_post}){ # insere $c->stash->{collection}->create( { } ) # faz redirect para a pagina de lista (?) # isso depende de cada sistema! } } __PACKAGE__->meta->make_immutable; 1;
e, junto com isso, você pode criar o controller Post/Comment.pm assim:
package MyApp::Controller::Post::Comment; use Moose; use namespace::autoclean; use utf8; BEGIN { extends 'Catalyst::Controller' } sub base: Chained('/post/object') PathPart('comment') CaptureArgs(0) { my ( $self, $c ) = @_; # aqui que fica legal # $c->stash->{collection} ja existe, e um resultset com um where de comment_id lá dentro $c->stash->{collection} = $c->stash->{collection}->comments; # a partir de agora, supondo que existe o relacionamento # entre comments e comentarios cujo nome é comments, # stash->{collection} contém todos os comentarios. # lembre-se que nao foi executada query aqui. } sub list: Chained('base') PathPart('') Args(0) { my ( $self, $c ) = @_; # aqui seria o list dos comentarios... while (my $row = $c->stash->{collection}->next){ push @{$c->stash->{lugar_dos_comentarios}}, $row; } } # carregando um comentario apenas sub object: Chained('base') PathPart('') CaptureArgs(1) { my ( $self, $c, $id ) = @_; $c->detach('/erro_usuario_maldito') unless $id =~ /^[0-9]$/; $c->stash->{collection} = $c->stash->{collection}->find({$id}); $c->stash->{object} = $c->stash->{comment} = $c->stash->{collection}->next; if (!$c->stash->{object}){ # coloca na stash alguma coisa pra dizer que foi 404 $c->detach; } } sub show_comment: Chained('object') PathPart('') Args { my ( $self, $c, $id ) = @_; # ja tem na stash tanto object que é o comentario, # como post, que é o post. } sub edit_comment: Chained('object') PathPart('edit') Args(0) { my ( $self, $c, $id ) = @_; if ($c->req->params->{conteudo_editado}){ $c->stash->{collection}->update( { } ); } } sub new_comment: Chained('base') PathPart('new') Args(0) { my ( $self, $c, $id ) = @_; if ($c->req->params->{conteudo_comment}){ # insere na tabela de posts ja associado ao # post, gracas ao DBIC $c->stash->{collection}->create( { } ); } } __PACKAGE__->meta->make_immutable; 1;
Isso faz o seguinte debug:
.-------------------------------------+--------------------------------------. | Path Spec | Private | +-------------------------------------+--------------------------------------+ | /post/*/comment/*/edit | /root (0) | | | -> /post/base (0) | | | -> /post/object (1) | | | -> /post/comment/base (0) | | | -> /post/comment/object (1) | | | => /post/comment/edit_comment | | /post/*/comment | /root (0) | | | -> /post/base (0) | | | -> /post/object (1) | | | -> /post/comment/base (0) | | | => /post/comment/list | | /post/*/comment/new | /root (0) | | | -> /post/base (0) | | | -> /post/object (1) | | | -> /post/comment/base (0) | | | => /post/comment/new_comment | | /post/*/comment/*/... | /root (0) | | | -> /post/base (0) | | | -> /post/object (1) | | | -> /post/comment/base (0) | | | -> /post/comment/object (1) | | | => /post/comment/show_comment | | /post/*/edit | /root (0) | | | -> /post/base (0) | | | -> /post/object (1) | | | => /post/edit_post | | /post | /root (0) | | | -> /post/base (0) | | | => /post/list | | /post/new | /root (0) | | | -> /post/base (0) | | | => /post/new_post | | /post/*/... | /root (0) | | | -> /post/base (0) | | | -> /post/object (1) | | | => /post/show_post | '-------------------------------------+--------------------------------------'
Fazendo com que o object de cada controller carregue na stash o proprio objeto, assim como sua collection inteira, facilita, pois a action que fizer chained não precisa saber exatamente qual o nome foi utilizado na chain anterior. E criar uma cópia do objeto atual ajuda a você não perder nenhum objeto já carregado (por exemplo, quando carregar os comentários, não perder o post em que já foi feito query para consultar).
Fim!
Gostou? Tem alguma sugestão ou dúvida? Deixe nos comentários abaixos. Catalyst não é nenhum bicho de 7 cabeças, basta aprender cada pedaço por vez. Chained actions são utilizadas de monte, e é necessário entendê-las bem para não se confundir!
***
Artigo de Renato CRON, publicado no Equinócio 2013 do grupo São Paulo Perl Mongers – http://sao-paulo.pm.org/equinocio/2013/mar/05-catalyst-aprendendo-dispatchtype-chained
Texto sob Creative Commons – Atribuição – Partilha nos Mesmos Termos 3.0 Não Adaptada, mais informações em http://creativecommons.org/licenses/by-sa/3.0/