Renato CRONPublicado em 01/03/2013
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).
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.
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 eles 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.
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.
No path action, é exibido o Path e o private pth 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 pagina, arquivo ou diretorio, impressora), enquanto os endpoints são textos que determinam qual serviço deve ser acesso.
No catalyst, é utlizado 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 quem utiliza ele.
Se você acessar http://0:3000/ você irá ver a tela inicial do catalyst, e no log, ira 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 os actions foram executados.
o método /end
mais proximo 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 método não recebe nenhum argumento. O :Path
significa esse método deve representar uma action, cujo endpoint será '/' (pois não enviar nada para o Path significa 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 /
, porem, 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 satisfazer 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 influenciam no comportamento deles. Portanto se você alterar de
sub index
, parasub index.t_page :Path('/') :Args(0) {
esub not_found_page :Path {
o código vai continuar funcionando perfeitamente.
Você pode estar se perguntando, "se funciona definindo os endpoints usando path, porque 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//
/post/new
/post//edit
/post
Portanto, para acessar o post, você faria um GET em /post/
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.
Calma, antes de aprender o chained, você precisava saber o que é path (ou endpoint) e o que são actions!
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->{métodos}}, ':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
tem 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 o 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->{métodos}}, ':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->{métodos}}, ':lista de posts:';
push @{$c->stash->{métodos}},
"\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, 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->{métodos}}
em forma de texto.
Novamente no Root.pm, altere sub end : ActionClass('RenderView') {}
por:
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->{métodos}} );
}
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, isso boa parte das vees não é realmente o que você quer, mas de qualquer maneira, se você realmente quer implementar umend
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->{métodos}}, ':carregar post:';
if ($id =~ /^[0-9]$/ && exists $c->stash->{posts}[$id]){
push @{$c->stash->{métodos}}, 'Carregou post ' . $c->stash->{posts}[$id];
}else{
push @{$c->stash->{métodos}}, '!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->{métodos}}, '^^^^^^^^^^ é 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 foi definido 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->{métodos}}, '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->{métodos}}, 'Criando novo post:';
}
Então depois de ver todos estes exemplos, fica muito mais simples entender como funciona os chained actions do catalyst.
CaptureArgs(XX)
e deixar os endpoints sempre com Args(0)
ou Args
, isso vai poupar algumas dores de cabeças quando você ter muitos actions chained espalhados.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 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 usuário atual para acessar o conteúdo
# 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_usuário_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 já foi carregado e já 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->{conteúdo_editado}){
$c->stash->{collection}->update( { } );
}
}
sub new_post: Chained('base') PathPart('new') Args(0) {
my ( $self, $c, $id ) = @_;
if ($c->req->params->{conteúdo_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} já 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_usuário_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 ) = @_;
# já 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->{conteúdo_editado}){
$c->stash->{collection}->update( { } );
}
}
sub new_comment: Chained('base') PathPart('new') Args(0) {
my ( $self, $c, $id ) = @_;
if ($c->req->params->{conteúdo_comment}){
# insere na tabela de posts já associado ao
# post, gracas ao DBIC
$c->stash->{collection}->create( { } );
}
}
__PACKAGE__->meta->make_immutable;
1;
Isso 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 próprio objecto, assim como sua collection inteira, facilita, pois a action que fazer chained não precisa saber exatamente qual o nome foi utilizado na chain anterior. E criar uma copia do objecto atual ajuda a você não perder nenhum objecto já carregado (por exemplo, quando carregar os comentarios, não perder o post que já foi feito query para consultar ele)
Gostou? Tem alguma sugestão ou dúvida? Deixe nos comentários abaixo ou no twitter. 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 entende-las bem para não se confundir!
Renato CRON
github.com/renatoaware / github.com/renatocron / CPAN:RentoCRON / @renato_cron
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/