View
548
Download
0
Category
Preview:
Citation preview
Perl web frameworksCatalyst & Mojolicious
Curs avançat de Perl 201210/03/2012
Perl web frameworksHola!
Diego Kupermandiegok | @freekey
Web frameworks~
MVC
Router~
Dispatcher
Rutas//prueba-curso/event/23/event/23/where/event/23-una-prueba/picture/3/event/23/picture/3/event/una-prueba/pic/3
Rutas//prueba-curso/event/23/event/23/where/event/23-una-prueba/picture/3/event/23/picture/3/event/una-prueba/pic/3
Controller
Invocado por el dispatcher
Manipulación de capturas del router
Validaciones
Pegamento entre otros componentes:modelos y vistas
Idealmente poco código: thin controller,fat models.
Model~
Storage
ModelHabitualmente base de datos
Lógica de negocio
Uso fuera de la app
Tests independientes de la app
Otros modelos: git, api-rest, ...
View~
Templates / Serializers
ViewNormalmente un motor de templates
MUCHAS opciones en CPAN
Template toolkit en Catalyst
EP en Mojolicious
Serialización: JSON, XML, YAML, ...
Install~
CPAN
Catalyst$ cpanm -n Catalyst::Runtime Catalyst::Devel $ cpanm -n Catalyst::View::TT Catalyst::View::JSON $ cpanm -n Catalyst::Plugin::Unicode::Encoding $ cpanm -n Catalyst::Plugin::Session $ cpanm -n Catalyst::Plugin::Session::Store::File $ cpanm -n Catalyst::Plugin::Session::State::Cookie $ cpanm -n Catalyst::Plugin::Authentication $ cpanm -n Catalyst::Plugin::Authorization::Roles $ cpanm -n Catalyst::Authentication::Store::DBIx::Class$ cpanm -n HTML::FormHandler HTML::FormHandler::Model::DBIC
Mojolicioushttp://mojolicio.usThe web in a box
$ cpanm -n Mojolicious
Catalyst vs Mojolicious
Catalyst
$ git clone git://github.com/diegok/dbic.curs.barcelona.pm.git$ cd dbic.curs.barcelona.pm$ dzil build; cpanm -n *.tar.gz; dzil clean$ git clone git://github.com/diegok/app.curs.barcelona.pm.git$ cd app.curs.barcelona.pm$ cpanm -n --installdeps .
CatalystThe elegant MVC framework
CatalystCrear nueva App
$ catalyst.pl MyCatAppcreated "MyCatApp"created "MyCatApp/script"created "MyCatApp/lib"created "MyCatApp/root"created "MyCatApp/root/static"...created "MyCatApp/script/mycatapp_server.pl"created "MyCatApp/script/mycatapp_test.pl"created "MyCatApp/script/mycatapp_create.pl"
├── Changes├── Makefile.PL├── README├── lib│ └── Curs| ├── App│ │ ├── Controller│ │ │ └── Root.pm│ │ ├── Model│ | └── View│ └── App.pm├── curs_app.conf├── curs_app.psgi
├── root│ ├── favicon.ico│ └── static│ └── images│ ├── ...│ └── catalyst_logo.png├── script│ ├── ...│ ├── curs_app_create.pl│ └── curs_app_server.pl└── t ├── 01app.t ├── 02pod.t └── 03podcoverage.t
package Curs::App;use Moose;use namespace::autoclean;use Catalyst::Runtime 5.80;use Catalyst qw/ -Debug ConfigLoader Static::Simple/;extends 'Catalyst';our $VERSION = '0.01';__PACKAGE__->config( name => 'Curs::App', disable_component_resolution_regex_fallback => 1, enable_catalyst_header => 1, # Send X-Catalyst header);__PACKAGE__->setup();
package Curs::App;use Moose;use namespace::autoclean;use Catalyst::Runtime 5.80;use Catalyst qw/ ConfigLoader Static::Simple/;extends 'Catalyst';our $VERSION = '0.01';__PACKAGE__->config( name => 'Curs::App', disable_component_resolution_regex_fallback => 1, enable_catalyst_header => 1, # Send X-Catalyst header);__PACKAGE__->setup();
$ ./script/curs_app_server.pl -r -d[debug] Debug messages enabled[debug] Statistics enabled[debug] Loaded plugins:.-------------------------------------------------------.| Catalyst::Plugin::ConfigLoader 0.30 |'-------------------------------------------------------'[debug] Loaded dispatcher "Catalyst::Dispatcher"[debug] Loaded engine "Catalyst::Engine"[debug] Found home "/.../Curs-App"[debug] Loaded Config "/.../Curs-App/curs_app.conf"[debug] Loaded components:.--------------------------------------------+----------.| Class | Type |+--------------------------------------------+----------+| Curs::App::Controller::Root | instance |'--------------------------------------------+----------'[debug] Loaded Private actions:.-------------+-----------------------------+------------.| Private | Class | Method |+-------------+-----------------------------+------------+| /default | Curs::App::Controller::Root | default || /end | Curs::App::Controller::Root | end || /index | Curs::App::Controller::Root | index |'-------------+-----------------------------+------------'
$ ./script/curs_app_server.pl -r -d[debug] Loaded Path actions:.--------------------------------+-----------------------.| Path | Private |+--------------------------------+-----------------------+| / | /index || /... | /default |'--------------------------------+-----------------------'[info] Curs::App powered by Catalyst 5.90010HTTP::Server::PSGI: Accepting connections at http://0:3000/
package Curs::App::Controller::Root;use Moose; use namespace::autoclean;BEGIN { extends 'Catalyst::Controller' }__PACKAGE__->config(namespace => '');sub index :Path :Args(0) { my ( $self, $c ) = @_; $c->response->body($c->welcome_message);}sub default :Path { my ( $self, $c ) = @_; $c->response->body('Page not found'); $c->response->status(404);}sub end : ActionClass('RenderView') {}
TieneRouter + Dispatcher
Static::Simple
Controller Root
Acción por defecto
aún no tiene...Vista/s
Modelo/s
+Controllers
Ninguna gracia!
Contexto~$c
Catalyst::Request$c->request$c->req # alias$c->req->params->{foo};$c->req->cookies->{name};$c->req->headers->content_type;$c->req->base;$c->req->uri_with( { page => 3 } );
Catalyst::Response$c->response$c->res # alias$c->res->body('Hello World');$c->res->status(404);$c->res->redirect('http://barcelona.pm');# CGI::Simple::Cookie$c->res->cookies->{foo} = { value => '123' };
Catalyst::Log$c->log$c->log->debug('Something happened');$c->log->info('Something you should know');
Stash$c->stash( key => 'value' );$c->stash( 'key' ); # 'value'$c->stash->{key} = [1..10];$c->stash->{key}; # [1..10]
Dura un request-response completoPaso de datos entre componentes
Routes~
Controller actions
Nuevo Controller$ ./script/curs_app_create.pl controller Example exists ".../Curs-App/script/lib/Curs/App/Controller" exists ".../Curs-App/script/t"created ".../Curs-App/lib/Curs/App/Controller/Example.pm"created ".../Curs-App/t/controller_Example.t"
lib/Curs/App/Controller/Example.pmpackage Curs::App::Controller::Example;use Moose; use namespace::autoclean;BEGIN {extends 'Catalyst::Controller'; }# /examplesub index :Path :Args(0) { my ( $self, $c ) = @_; $c->res->body('Example index match!');}
Controller ActionsLiteral match (:Path)
Root-level (:Global) = Path('/...')
Namespace-prefixed (:Local) = Path('.../')
Restricción de argumentos (:Args)
/example/cero/...sub cero :Local { my ( $self, $c, @args ) = @_; $c->res->body('Args: ' . join ', ', @args);}
/example/unosub uno :Local :Args(0) { my ( $self, $c ) = @_; $c->res->body(':Local :Args(0)');}
/example/dossub dos :Path('dos') :Args(0) { my ( $self, $c ) = @_; $c->res->body(":Path('dos') :Args(0)");}
/example/tressub tres :Path('/example/tres') :Args(0) { my ( $self, $c ) = @_; $c->res->body(":Path('/example/tres') :Args(0)");}
/hola/mundosub cuatro :Path('/hola') :Args(1) { my ( $self, $c, $arg1 ) = @_; $c->res->body("Hola $arg1!");}
Controller ActionsPattern-match
:Regex() & :LocalRegex()
/item23/order32sub cinco :Regex('^item(\d+)/order(\d+)$') { my ( $self, $c ) = @_; my $item = $c->req->captures->[0]; my $order = $c->req->captures->[1]; $c->res->body( "(cinco) Item: $item | Order: $order" );}
/example/item23/order32sub seis :LocalRegex('^item(\d+)/order(\d+)$') { my ( $self, $c ) = @_; my $item = $c->req->captures->[0]; my $order = $c->req->captures->[1]; $c->res->body( "(seis) Item: $item | Order: $order" );}
Controller ActionsPrivadas & control flow
:Privateforward() & detach()
sub now :Local :Args(0) { my ( $self, $c ) = @_; $c->forward('stash_now'); $c->detach('say_now'); $c->log->debug('ouch!');} sub stash_now :Private { my ( $self, $c ) = @_; $c->stash( now => DateTime->now );} sub say_now :Private { my ( $self, $c ) = @_; $c->res->body($c->stash->{now});}
Built-in special actions
Default controller actionsub default : Path {}
Como default, con mas precedenciasub index :Path Args(0) {}
Antes de la acción, solo una vezsub begin :Private {}
Despues de la acción, solo una vezsub end :Private {}
Despues de begin, de menos especifico a masespecifico
sub auto :Private {}
Si retorna false se salta hasta end()
Chained actions:Chained
sub with_now : PathPart('example/now') Chained( '/' ) CaptureArgs( 0 ) { my ( $self, $c ) = @_; $c->forward('stash_now');}sub show_now : PathPart('show') Chained( 'with_now' ) Args( 0 ) { my ( $self, $c ) = @_; $c->detach('say_now');}
Chained es MUY potente,pero antes tenemos que
añadir algunas cosas mas...
VistasTemplate toolkit
+JSON
$ script/curs_app_create.pl view Web TTexists ".../Curs-App/script/../lib/Curs/App/View"exists ".../Curs-App/script/../t"created ".../Curs-App/script/../lib/Curs/App/View/Web.pm"created ".../Curs-App/script/../t/view_Web.t"
lib/Curs/App/View/Web.pmCurs::App::View::Web;use Moose;extends 'Catalyst::View::TT';__PACKAGE__->config( TEMPLATE_EXTENSION => '.tt', CATALYST_VAR => 'c', TIMER => 0, ENCODING => 'utf-8' WRAPPER => 'layout', render_die => 1,);1;
lib/Curs/App.pm__PACKAGE__->config( # ... 'View::Web' => { INCLUDE_PATH => [ __PACKAGE__->path_to('root', 'src'), __PACKAGE__->path_to('root', 'lib'), ], },);
root/lib/layout
<!DOCTYPE HTML><html lang="en-us"> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8"> <title>Curs avançat de Perl 2012</title> <link rel="stylesheet" href="/css/style.css" type="text/css"> </head> <body> [% content %] </body></html>
TT y layout en su sitio,hora de cambiar la home
root/src/index.tt<h1>[% message %]</h1>
lib/Curs/App/Controller/Root.pmsub index :Path :Args(0) { my ( $self, $c ) = @_; $c->stash( message => 'Hola mundo!', template => 'index.tt' );}
$ ./script/curs_app_create.pl view JSON JSON exists "lib/Curs/App/View" exists "t/"created "lib/Curs/App/View/JSON.pm"created "t/view_JSON.t"
lib/Curs/App.pm__PACKAGE__->config({ ... 'View::JSON' => { expose_stash => 'json', # defaults to everything }, default_view => 'Web',});
Uso de View::JSONsub status :Path('/status') :Args(0) { my ( $self, $c ) = @_; $c->stash( json => { value => 'testing' } ); $c->forward('View::JSON');}
ModeloDBIC
Curs::Schema$ script/curs_app_create.pl model DB DBIC::Schema Curs::Schemaexists ".../Curs-App/script/../lib/Curs/App/Model"exists ".../Curs-App/script/../t"created ".../Curs-App/script/../lib/Curs/App/Model/DB.pm"created ".../Curs-App/script/../t/model_DB.t"
Config por defectocurs_app.conf
name Curs::App<Model::DB> connect_info dbi:SQLite:dbname=curs_schema.db connect_info connect_info <connect_info> sqlite_unicode 1 RaiseError 1 </connect_info></Model::DB>
Deploy!$ ./script/schema_deploy.pl Creating sql/Curs-Schema-1-SQLite.sql => done.Making initial deploy (ddbb has no version) => done.
Nuestro schema es uncomponente más ahora!
sub action :Local { my ( $self, $c ) = @_; $c->res->body( $c->model('DB::User')->first->email );}
Authentication&
Authorization
Catalyst::Plugin::Authentication&
Catalyst::Plugin:Authorization::Roles+ Catalyst::Plugin::Session
lib/Curs/App.pmuse Catalyst qw/ ... Session Session::State::Cookie Session::Store::File Authentication Authorization::Roles/;
__PACKAGE__->config( ... 'Plugin::Authentication' => { default_realm => 'users', realms => { users => { credential => { class => 'Password', password_field => 'password', password_type => 'self_check', }, store => { class => 'DBIx::Class', user_model => 'DB::User', role_relation => 'roles', role_field => 'name', id_field => 'email' } } }
} } },);
Nuevos metodos en la app$c->authenticate( email => $email, password => $pwd );$c->user_exists;$c->user;
Todo listoNecesitamos un form para login
:-(
HTML::FormHandleral rescate! :-)
lib/Curs/App/Form/Login.pmpackage Curs::App::Form::Login;use HTML::FormHandler::Moose;extends 'HTML::FormHandler';use Email::Valid;has_field 'email' => ( type => 'Text', required => 1, apply => [{ check => sub { Email::Valid->address( $_[0] ) }, message => 'Must be a valid email address' }]);
lib/Curs/App/Form/Login.pmhas_field 'password' => ( type => 'Password', required => 1 );has_field 'submit' => ( type => 'Submit', value => 'Login' );
Ahora sí!
Un controller nuevo paraauth
$ ./script/curs_app_create.pl controller Auth...
lib/Curs/App/Controller/Auth.pmpackage Curs::App::Controller::Auth;use Moose; use namespace::autoclean;BEGIN {extends 'Catalyst::Controller'; }use Curs::App::Form::Login;
sub login :Path(/login) Args(0) { my ( $self, $c ) = @_; my $form = Curs::App::Form::Login->new(); my $creds = { email => $form->value->{email}, password => $form->value->{password} }; if ( $form->process( params => $c->req->params ) ) { if ( $c->authenticate( $creds ) ) { $c->detach('after_login_redirect'); } else { $form->field('password')->add_error( 'Invalid password' ); } } $c->stash( template => 'auth/login.tt', form => $form );}
root/src/auth/login.tt<div id="login"> [% form.render %]</div>
=head2 need_login Ensure user exists on the chain.=cutsub need_login :PathPart( '' ) Chained( '/' ) CaptureArgs( 0 ) { my ( $self, $c ) = @_; unless ( $c->user_exists ) { $c->session->{after_login_path} = '/' . $c->req->path; $c->res->redirect( $c->uri_for_action( $c->controller('Auth') ->action_for('login') ) ); $c->detach; }}
=head2 need_role_admin Ensure user with the admin role.=cutsub need_role_admin :PathPart('admin') Chained('need_login') CaptureArgs(0) { my ( $self, $c ) = @_; unless ( $c->check_user_roles( 'admin' ) ) { $c->res->body('You need admin role for this action!'); $c->detach(); }}
En otro controller... perdido en otra galaxia ...
=head2 element_chainBase chain for actions related to one user=cutsub element_chain :PathPart('user') Chained('/auth/need_login') CaptureArgs(1) { my ( $self, $c, $user_id ) = @_; $c->stash( user => $c->model('DB::User') ->find( $user_id ) ); unless ( $c->stash->{user} ) { $c->detach( '/error/element_not_found', [ 'user' ] ); }}
sub view :PathPart() Chained('element_chain') Args(0) { my ( $self, $c ) = @_; $c->stash( template => 'user/view.tt' );}sub delete :PathPart() Chained('element_chain') Args(0) { my ( $self, $c ) = @_; $c->stash->{user}->delete; # ...}
Pluginvs
TraitFor
Plugin globalvs
Plugin for component
TraitFor ControllerRole para el controller
package Catalyst::TraitFor::Controller::WithDateTime;use MooseX::MethodAttributes::Role;use namespace::autoclean;use DateTime;has 'stash_key' => ( is => 'ro', default => 'datetime' );after 'auto' => sub { my ( $self, $c ) = @_; $c->stash( $self->stash_key => DateTime->now );};sub auto : Private { 1 }
Trait's locales
package Curs::App::TraitFor::Controller::WithDBIC;use MooseX::MethodAttributes::Role;use namespace::autoclean;require 'model_name';require 'base_chain';has stash_key => ( is => 'ro', default => sub { lc @{[split /::/, shift->model_name ]}[-1] } );
...sub item :PathPart('') Chained('base_chain') CaptureArgs(1) { my ( $self, $c, $id ) = @_; $c->stash->{ $self->stash_key } = $c->model( $self->model_name )->find($id) || $c->detach('missing');}sub missing { my ( $self, $c ) = @_; $c->res->code(404); $c->res->body('Not found!');}1;
A consumir!
package Curs::App::Controller::Event;use Moose; use namespace::autoclean;BEGIN {extends 'Catalyst::Controller'}has model_name => ( is => 'ro', default => 'DB::Event' );with 'Curs::App::TraitFor::Controller::WithDBIC';sub base_chain :PathPart('event') Chained('/') CaptureArgs(1) {}
sub delete :PathPart('delete') Chained('item') Args(0) { my ( $self, $c ) = @_; $c->stash->{event}->delete; }
https://metacpan.org/search?q=catalyst
896 results
Plack(ya lo estamos usando)
$ cpanm -n Starman...$ starman curs_app.psgi2012/03/10-11:25:36 Starman::Server (type Net::Server::PreFork) starting! pid(73661)Binding to TCP port 5000 on host *Setting gid to "20 20 20 204 100 98 81 80 79 61 12 403 402 401"
Más CatalystIRC
#catalyst en irc.perl.org.
#catalyst-dev en irc.perl.org (desarrollo).
Mailing lists
http://lists.scsys.co.uk/mailman/listinfo/catalyst
http://lists.scsys.co.uk/mailman/listinfo/catalyst-dev
Manual
https://metacpan.org/module/Catalyst::ManualEjercicios
Añadir un metodo (API) que deje verdatos de UN usuario en JSON:/user/1/json
Extra: vista json que devuelva array deusuarios (sin repetir codigo)
Añadir una vista que liste los eventos(Creados en la práctica anterior)
Crear una acción (solo para admins), unformulario y su plantilla para crear unevento y otra para editarlo.
Recommended