SlideShare a Scribd company logo
Perl web frameworks
   Catalyst & Mojolicious
     Curs avançat de Perl 2012
            10/03/2012
Perl web frameworks
          Hola!
      Diego Kuperman
      diegok | @freekey
Web frameworks
       ~
     MVC
Perl web frameworks
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
Model
Habitualmente 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
View
Normalmente 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
Mojolicious
                   http://mojolicio.us
                   The 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 .
Catalyst
The elegant MVC framework
Catalyst
                   Crear nueva App
$ catalyst.pl MyCatApp
created "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
    enable_catalyst_header => 1, # Send X-Cataly
);
__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
    enable_catalyst_header => 1, # Send X-Cataly
);
__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.90010
HTTP::Server::PSGI: Accepting connections at http://0:3000/
Perl web frameworks
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') {}
Tiene
Router + 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 completo
      Paso 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.pm
package Curs::App::Controller::Example;
use Moose; use namespace::autoclean;
BEGIN {extends 'Catalyst::Controller'; }
# /example
sub index :Path :Args(0) {
    my ( $self, $c ) = @_;
    $c->res->body('Example index match!');
}
Controller Actions
Literal 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/uno
sub uno :Local :Args(0) {
    my ( $self, $c ) = @_;
    $c->res->body(':Local :Args(0)');
}
/example/dos
sub dos :Path('dos') :Args(0) {
    my ( $self, $c ) = @_;
    $c->res->body(":Path('dos') :Args(0)");
}


           /example/tres
sub tres :Path('/example/tres') :Args(0) {
    my ( $self, $c ) = @_;
    $c->res->body(":Path('/example/tres') :Args(
}
/hola/mundo
sub cuatro :Path('/hola') :Args(1) {
    my ( $self, $c, $arg1 ) = @_;
    $c->res->body("Hola $arg1!");
}
Controller Actions
     Pattern-match
    :Regex() & :LocalRegex()
/item23/order32
sub 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/order32
sub 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 Actions
  Privadas & control flow
            :Private
      forward() & 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 action
sub default : Path {}

     Como default, con mas precedencia
sub index :Path Args(0) {}
Antes de la acción, solo una vez
sub begin :Private {}

     Despues de la acción, solo una vez
sub end :Private {}

Despues de begin, de menos especifico a mas
                especifico
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...
Vistas
Template toolkit
      +
   JSON
$ script/curs_app_create.pl view Web TT
exists ".../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.pm
Curs::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/
  <title>Curs avançat de Perl 2012</title>
  <link rel="stylesheet" href="/css/style.css" t
  </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.pm
sub index :Path :Args(0) {
    my ( $self, $c ) = @_;
    $c->stash(
        message => 'Hola mundo!',
        template => 'index.tt'
    );
}
Perl web frameworks
$ ./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 ev
    },
    default_view => 'Web',
});
Uso de View::JSON
sub status :Path('/status') :Args(0) {
    my ( $self, $c ) = @_;
    $c->stash(
        json => { value => 'testing' }
    );
    $c->forward('View::JSON');
}
Modelo
 DBIC
Curs::Schema
$ script/curs_app_create.pl model DB DBIC::Schema Curs::Schema
exists ".../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 defecto
                 curs_app.conf
name Curs::App
<Model::DB>
    connect_info   dbi:SQLite:dbname=curs_schema
    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 un
    componente 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.pm
use 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 listo
Necesitamos un form para login
             :-(
HTML::FormHandler
  al rescate! :-)
lib/Curs/App/Form/Login.pm
package 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 addres
    }]
);
lib/Curs/App/Form/Login.pm
has_field 'password' => (
    type => 'Password',
    required => 1
);

has_field 'submit'   => (
    type => 'Submit',
    value => 'Login'
);
Ahora sí!
Un controller nuevo para
             auth
$ ./script/curs_app_create.pl controller Auth
...
lib/Curs/App/Controller/Auth.pm
package 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( 'Inva
    }
  }
  $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.
=cut
sub need_login :PathPart( '' )
    Chained( '/' ) CaptureArgs( 0 ) {
  my ( $self, $c ) = @_;

    unless ( $c->user_exists ) {
      $c->session->{after_login_path} = '/' . $c->
      $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.
=cut
sub 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
    $c->detach();
  }
}
En otro controller
... perdido en otra galaxia ...
=head2 element_chain
Base chain for actions related to one user
=cut
sub 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', [ 'u
    }
}
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;
  # ...
}
Plugin
   vs
TraitFor
Plugin global
        vs
Plugin for component
TraitFor Controller
  Role para el controller
package Catalyst::TraitFor::Controller::WithDate
use MooseX::MethodAttributes::Role;
use namespace::autoclean;
use DateTime;

has 'stash_key' => ( is => 'ro', default => 'dat
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::WithDBI
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 ]}[-
    }
);
...
sub item :PathPart('') Chained('base_chain') Cap
    my ( $self, $c, $id ) = @_;
    $c->stash->{ $self->stash_key }
        = $c->model( $self->model_name )->find($
        || $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::
with 'Curs::App::TraitFor::Controller::WithDBIC'

sub base_chain :PathPart('event')
                Chained('/') CaptureArgs(1) {}

sub delete :PathPart('delete') Chained('item') A
    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.psgi
2012/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
Más Catalyst
                  IRC
#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
           Ejercicios
https://metacpan.org/module/Catalyst::Manual
Añadir un metodo (API) que deje ver
datos de UN usuario en JSON:
/user/1/json
   Extra: vista json que devuelva array de
   usuarios (sin repetir codigo)

Añadir una vista que liste los eventos
(Creados en la práctica anterior)
Crear una acción (solo para admins), un
formulario y su plantilla para crear un
evento y otra para editarlo.

More Related Content

Perl web frameworks

  • 1. Perl web frameworks Catalyst & Mojolicious Curs avançat de Perl 2012 10/03/2012
  • 2. Perl web frameworks Hola! Diego Kuperman diegok | @freekey
  • 5. Router ~ Dispatcher
  • 9. 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.
  • 10. Model ~ Storage
  • 11. Model Habitualmente base de datos Lógica de negocio Uso fuera de la app Tests independientes de la app Otros modelos: git, api-rest, ...
  • 12. View ~ Templates / Serializers
  • 13. View Normalmente un motor de templates MUCHAS opciones en CPAN Template toolkit en Catalyst EP en Mojolicious Serialización: JSON, XML, YAML, ...
  • 15. 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
  • 16. Mojolicious http://mojolicio.us The web in a box $ cpanm -n Mojolicious
  • 19. $ 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 .
  • 21. Catalyst Crear nueva App $ catalyst.pl MyCatApp created "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"
  • 22. ├── Changes ├── Makefile.PL ├── README ├── lib │ └── Curs | ├── App │ │ ├── Controller │ │ │ └── Root.pm │ │ ├── Model │ | └── View │ └── App.pm ├── curs_app.conf ├── curs_app.psgi
  • 23. ├── root │ ├── favicon.ico │ └── static │ └── images │ ├── ... │ └── catalyst_logo.png ├── script │ ├── ... │ ├── curs_app_create.pl │ └── curs_app_server.pl └── t ├── 01app.t ├── 02pod.t └── 03podcoverage.t
  • 24. 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 enable_catalyst_header => 1, # Send X-Cataly ); __PACKAGE__->setup();
  • 25. 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 enable_catalyst_header => 1, # Send X-Cataly ); __PACKAGE__->setup();
  • 26. $ ./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 | '-------------+-----------------------------+------------'
  • 27. $ ./script/curs_app_server.pl -r -d [debug] Loaded Path actions: .--------------------------------+-----------------------. | Path | Private | +--------------------------------+-----------------------+ | / | /index | | /... | /default | '--------------------------------+-----------------------' [info] Curs::App powered by Catalyst 5.90010 HTTP::Server::PSGI: Accepting connections at http://0:3000/
  • 29. 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') {}
  • 34. 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' };
  • 36. Stash $c->stash( key => 'value' ); $c->stash( 'key' ); # 'value' $c->stash->{key} = [1..10]; $c->stash->{key}; # [1..10] Dura un request-response completo Paso de datos entre componentes
  • 37. Routes ~ Controller actions
  • 38. 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"
  • 39. lib/Curs/App/Controller/Example.pm package Curs::App::Controller::Example; use Moose; use namespace::autoclean; BEGIN {extends 'Catalyst::Controller'; } # /example sub index :Path :Args(0) { my ( $self, $c ) = @_; $c->res->body('Example index match!'); }
  • 40. Controller Actions Literal match (:Path) Root-level (:Global) = Path('/...') Namespace-prefixed (:Local) = Path('.../') Restricción de argumentos (:Args)
  • 41. /example/cero/... sub cero :Local { my ( $self, $c, @args ) = @_; $c->res->body('Args: ' . join ', ', @args); } /example/uno sub uno :Local :Args(0) { my ( $self, $c ) = @_; $c->res->body(':Local :Args(0)'); }
  • 42. /example/dos sub dos :Path('dos') :Args(0) { my ( $self, $c ) = @_; $c->res->body(":Path('dos') :Args(0)"); } /example/tres sub tres :Path('/example/tres') :Args(0) { my ( $self, $c ) = @_; $c->res->body(":Path('/example/tres') :Args( }
  • 43. /hola/mundo sub cuatro :Path('/hola') :Args(1) { my ( $self, $c, $arg1 ) = @_; $c->res->body("Hola $arg1!"); }
  • 44. Controller Actions Pattern-match :Regex() & :LocalRegex()
  • 45. /item23/order32 sub 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" ); }
  • 46. /example/item23/order32 sub 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" ); }
  • 47. Controller Actions Privadas & control flow :Private forward() & detach()
  • 48. 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}); }
  • 50. Default controller action sub default : Path {} Como default, con mas precedencia sub index :Path Args(0) {}
  • 51. Antes de la acción, solo una vez sub begin :Private {} Despues de la acción, solo una vez sub end :Private {} Despues de begin, de menos especifico a mas especifico sub auto :Private {} Si retorna false se salta hasta end()
  • 52. Chained actions :Chained
  • 53. 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'); }
  • 54. Chained es MUY potente, pero antes tenemos que añadir algunas cosas mas...
  • 56. $ script/curs_app_create.pl view Web TT exists ".../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"
  • 57. lib/Curs/App/View/Web.pm Curs::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;
  • 58. lib/Curs/App.pm __PACKAGE__->config( # ... 'View::Web' => { INCLUDE_PATH => [ __PACKAGE__->path_to('root', 'src'), __PACKAGE__->path_to('root', 'lib'), ], }, );
  • 59. root/lib/layout <!DOCTYPE HTML> <html lang="en-us"> <head> <meta http-equiv="Content-type" content="text/ <title>Curs avançat de Perl 2012</title> <link rel="stylesheet" href="/css/style.css" t </head> <body> [% content %] </body> </html>
  • 60. TT y layout en su sitio, hora de cambiar la home
  • 61. root/src/index.tt <h1>[% message %]</h1> lib/Curs/App/Controller/Root.pm sub index :Path :Args(0) { my ( $self, $c ) = @_; $c->stash( message => 'Hola mundo!', template => 'index.tt' ); }
  • 63. $ ./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"
  • 64. lib/Curs/App.pm __PACKAGE__->config({ ... 'View::JSON' => { expose_stash => 'json', # defaults to ev }, default_view => 'Web', });
  • 65. Uso de View::JSON sub status :Path('/status') :Args(0) { my ( $self, $c ) = @_; $c->stash( json => { value => 'testing' } ); $c->forward('View::JSON'); }
  • 67. Curs::Schema $ script/curs_app_create.pl model DB DBIC::Schema Curs::Schema exists ".../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"
  • 68. Config por defecto curs_app.conf name Curs::App <Model::DB> connect_info dbi:SQLite:dbname=curs_schema connect_info connect_info <connect_info> sqlite_unicode 1 RaiseError 1 </connect_info> </Model::DB>
  • 69. Deploy! $ ./script/schema_deploy.pl Creating sql/Curs-Schema-1-SQLite.sql => done. Making initial deploy (ddbb has no version) => done.
  • 70. Nuestro schema es un componente más ahora! sub action :Local { my ( $self, $c ) = @_; $c->res->body( $c->model('DB::User')->first->email ); }
  • 71. Authentication & Authorization
  • 72. Catalyst::Plugin::Authentication & Catalyst::Plugin:Authorization::Roles + Catalyst::Plugin::Session
  • 73. lib/Curs/App.pm use Catalyst qw/ ... Session Session::State::Cookie Session::Store::File Authentication Authorization::Roles /;
  • 74. __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' } } }
  • 75. } } }, ); Nuevos metodos en la app $c->authenticate( email => $email, password => $pwd ); $c->user_exists; $c->user;
  • 76. Todo listo Necesitamos un form para login :-(
  • 77. HTML::FormHandler al rescate! :-)
  • 78. lib/Curs/App/Form/Login.pm package 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 addres }] );
  • 79. lib/Curs/App/Form/Login.pm has_field 'password' => ( type => 'Password', required => 1 ); has_field 'submit' => ( type => 'Submit', value => 'Login' );
  • 81. Un controller nuevo para auth $ ./script/curs_app_create.pl controller Auth ...
  • 82. lib/Curs/App/Controller/Auth.pm package Curs::App::Controller::Auth; use Moose; use namespace::autoclean; BEGIN {extends 'Catalyst::Controller'; } use Curs::App::Form::Login;
  • 83. 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( 'Inva } } $c->stash( template => 'auth/login.tt', form => $form ); }
  • 84. root/src/auth/login.tt <div id="login"> [% form.render %] </div>
  • 85. =head2 need_login Ensure user exists on the chain. =cut sub need_login :PathPart( '' ) Chained( '/' ) CaptureArgs( 0 ) { my ( $self, $c ) = @_; unless ( $c->user_exists ) { $c->session->{after_login_path} = '/' . $c-> $c->res->redirect( $c->uri_for_action( $c->controller('Auth') ->action_for('login') ) ); $c->detach; } }
  • 86. =head2 need_role_admin Ensure user with the admin role. =cut sub 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 $c->detach(); } }
  • 87. En otro controller ... perdido en otra galaxia ...
  • 88. =head2 element_chain Base chain for actions related to one user =cut sub 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', [ 'u } }
  • 89. 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; # ... }
  • 90. Plugin vs TraitFor
  • 91. Plugin global vs Plugin for component
  • 92. TraitFor Controller Role para el controller
  • 93. package Catalyst::TraitFor::Controller::WithDate use MooseX::MethodAttributes::Role; use namespace::autoclean; use DateTime; has 'stash_key' => ( is => 'ro', default => 'dat after 'auto' => sub { my ( $self, $c ) = @_; $c->stash( $self->stash_key => DateTime->now }; sub auto : Private { 1 }
  • 95. package Curs::App::TraitFor::Controller::WithDBI 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 ]}[- } );
  • 96. ... sub item :PathPart('') Chained('base_chain') Cap my ( $self, $c, $id ) = @_; $c->stash->{ $self->stash_key } = $c->model( $self->model_name )->find($ || $c->detach('missing'); } sub missing { my ( $self, $c ) = @_; $c->res->code(404); $c->res->body('Not found!'); } 1;
  • 98. package Curs::App::Controller::Event; use Moose; use namespace::autoclean; BEGIN {extends 'Catalyst::Controller'} has model_name => ( is => 'ro', default => 'DB:: with 'Curs::App::TraitFor::Controller::WithDBIC' sub base_chain :PathPart('event') Chained('/') CaptureArgs(1) {} sub delete :PathPart('delete') Chained('item') A my ( $self, $c ) = @_; $c->stash->{event}->delete; }
  • 101. $ cpanm -n Starman ... $ starman curs_app.psgi 2012/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
  • 102. Más Catalyst IRC #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
  • 103. Manual Ejercicios https://metacpan.org/module/Catalyst::Manual Añadir un metodo (API) que deje ver datos de UN usuario en JSON: /user/1/json Extra: vista json que devuelva array de usuarios (sin repetir codigo) Añadir una vista que liste los eventos (Creados en la práctica anterior) Crear una acción (solo para admins), un formulario y su plantilla para crear un evento y otra para editarlo.