The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Catalyst::Manual::Cookbook - Cooking with Catalyst

DESCRIPTION

Yummy code like your mum used to bake!

RECIPES

Force debug screen

You can force Catalyst to display the debug screen at the end of the request by placing a die() call in the end action.

     sub end : Private {
         my ( $self, $c ) = @_;
         die "forced debug";
     }

If you're tired of removing and adding this all the time, you can easily add a condition. For example:

  die "force debug" if $c->req->params->{dump_info};

Disable statistics

Just add this line to your application class if you don't want those nifty statistics in your debug messages.

    sub Catalyst::Log::info { }

Scaffolding

Scaffolding is very simple with Catalyst. Just use Catalyst::Model::CDBI::CRUD as your base class.

    # lib/MyApp/Model/CDBI.pm
    package MyApp::Model::CDBI;

    use strict;
    use base 'Catalyst::Model::CDBI::CRUD';

    __PACKAGE__->config(
        dsn           => 'dbi:SQLite:/tmp/myapp.db',
        relationships => 1
    );

    1;

    # lib/MyApp.pm
    package MyApp;

    use Catalyst 'FormValidator';

    __PACKAGE__->config(
        name => 'My Application',
        root => '/home/joeuser/myapp/root'
    );

    sub my_table : Global {
        my ( $self, $c ) = @_;
        $c->form( optional => [ MyApp::Model::CDBI::Table->columns ] );
        $c->forward('MyApp::Model::CDBI::Table');
    }

    1;

Modify the $c->form() parameters to match your needs, and don't forget to copy the templates into the template root. Can't find the templates? They were in the CRUD model distribution, so you can do look Catalyst::Model::CDBI::CRUD from the CPAN shell to find them.

Single file upload with Catalyst

To implement uploads in Catalyst you need to have a HTML form similiar to this:

    <form action="/upload" method="post" enctype="multipart/form-data">
      <input type="hidden" name="form_submit" value="yes">
      <input type="file" name="my_file">
      <input type="submit" value="Send">
    </form>

It's very important not to forget enctype="multipart/form-data" in form. Uploads will not work without this.

Catalyst Controller module 'upload' action:

    sub upload : Global {
        my ($self, $c) = @_;

        if ( $c->request->parameters->{form_submit} eq 'yes' ) {

            if ( my $upload = $c->request->upload('my_file') ) {
            
                my $filename = $upload->filename;
                my $target   = "/tmp/upload/$filename";
                
                unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
                    die( "Failed to copy '$filename' to '$target': $!" );
                }
            }
        }
        
        $c->stash->{template} = 'file_upload.html';
    }

Multiple file upload with Catalyst

Code for uploading multiple files from one form needs little changes compared to single file upload.

Form goes like this:

    <form action="/upload" method="post" enctype="multipart/form-data">
      <input type="hidden" name="form_submit" value="yes">
      <input type="file" name="file1" size="50"><br>
      <input type="file" name="file2" size="50"><br>
      <input type="file" name="file3" size="50"><br>
      <input type="submit" value="Send">
    </form>

Controller:

    sub upload : Local {
        my ($self, $c) = @_;

        if ( $c->request->parameters->{form_submit} eq 'yes' ) {

            for my $field ( $c->req->upload ) {

                my $upload   = $c->req->upload($field);
                my $filename = $upload->filename;
                my $target   = "/tmp/upload/$filename";
                
                unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
                    die( "Failed to copy '$filename' to '$target': $!" );
                }
            }
        }

        $c->stash->{template} = 'file_upload.html';
    }

for my $field ($c->req-upload)> loops automatically over all file input fields and gets input names. After that is basic file saving code, just like in single file upload.

Notice: dieing might not be what you want to do, when an error occurs, but it works as an example. A better idea would be to store error $! in $c->stash->{error} and show a custom error template displaying this message.

For more information about uploads and usable methods look at Catalyst::Request::Upload and Catalyst::Request.

Authentication with Catalyst::Plugin::Authentication::CDBI

There are (at least) two ways to implement authentication with this plugin: 1) only checking username and password; 2) checking username, password and the roles the user has

For both variants you'll need the following code in your MyApp package:

    use Catalyst qw/Session::FastMmap Static Authentication::CDBI/;

    MyApp->config( authentication => { user_class => 'MyApp::M::MyApp::Users',
                                       user_field => 'email',
                                       password_field => 'password' });

'user_class' is a Class::DBI class for your users table. 'user_field' tells which field is used for username lookup (might be email, first name, surname etc.). 'password_field' is, well, password field in your table and by default password is stored in plain text. Authentication::CDBI looks for 'user' and 'password' fields in table, if they're not defined in the config.

In PostgreSQL, the users table might be something like:

 CREATE TABLE users (
   user_id   serial,
   name      varchar(100),
   surname   varchar(100),
   password  varchar(100),
   email     varchar(100),
   primary key(user_id)
 );

We'll discuss the first variant for now: 1. user:password login/auth without roles

To log in a user you might use an action like this:

    sub login : Local {
        my ($self, $c) = @_;
        if ($c->req->params->{username}) {
            $c->session_login($c->req->params->{username}, 
                              $c->req->params->{password} );
            if ($c->req->{user}) {
                $c->forward('?restricted_area');
            }
        }
    }

This action should not go in your MyApp class...if it does, it will conflict with the built-in method of the same name. Instead, put it in a Controller class.

$c->req->params->{username} and $c->req->params->{password} are html form parameters from a login form. If login succeeds, then $c->req->{user} contains the username of the authenticated user.

If you want to remember the user's login status in between further requests, then just use the $c->session_login method. Catalyst will create a session id and session cookie and automatically append session id to all urls. So all you have to do is just check $c->req->{user} where needed.

To log out a user, just call $c->session_logout.

Now let's take a look at the second variant: 2. user:password login/auth with roles

To use roles you need to add the following parameters to MyApp->config in the 'authentication' section:

    role_class      => 'MyApp::M::MyApp::Roles',
    user_role_class => 'MyApp::M::MyApp::UserRoles',
    user_role_user_field => 'user_id',
    user_role_role_field => 'role_id',

Corresponding tables in PostgreSQL could look like this:

 CREATE TABLE roles (
   role_id  serial,
   name     varchar(100),
   primary key(role_id)
 );

 CREATE TABLE user_roles (
   user_role_id  serial,
   user_id       int,
   role_id       int,
   primary key(user_role_id),
   foreign key(user_id) references users(user_id),
   foreign key(role_id) references roles(role_id)
 );

The 'roles' table is a list of role names and the 'user_role' table is used for the user -> role lookup.

Now if a logged-in user wants to see a location which is allowed only for people with an 'admin' role, in your controller you can check it with:

    sub add : Local {
        my ($self, $c) = @_;
        if ($c->roles(qw/admin/)) {
            $c->req->output("Your account has the role 'admin.'");
        } else {
            $c->req->output("You're not allowed to be here.");
        }
    }

One thing you might need is to forward non-authenticated users to a login form if they try to access restricted areas. If you want to do this controller-wide (if you have one controller for your admin section) then it's best to add a user check to a '!begin' action:

    sub begin : Private {
        my ($self, $c) = @_;
        unless ($c->req->{user}) {
            $c->req->action(undef);  ## notice this!!
            $c->forward('?login');
        }
    }

Pay attention to $c->req->action(undef). This is needed because of the way $c->forward works - forward to login gets called, but after that Catalyst will still execute the action defined in the URI (e.g. if you tried to go to /add, then first 'begin' will forward to 'login', but after that 'add' will nonetheless be executed). So $c->req->action(undef) undefines any actions that were to be called and forwards the user where we want him/her to be.

And this is all you need to do.

Pass-through login (and other actions)

An easy way of having assorted actions that occur during the processing of a request that are orthogonal to its actual purpose - logins, silent commands etc. Provide actions for these, but when they're required for something else fill e.g. a form variable __login and have a sub begin like so:

sub begin : Private { my ($self, $c) = @_; foreach my $action (qw/login docommand foo bar whatever/) { if ($c->req->params->{"__${action}"}) { $c->forward($action); } } }

How to use Catalyst without mod_perl

Catalyst applications give optimum performance when run under mod_perl. However sometimes mod_perl is not an option, and running under CGI is just too slow. There's also an alternative to mod_perl that gives reasonable performance named FastCGI.

Using FastCGI

To quote from http://www.fastcgi.com/: "FastCGI is a language independent, scalable, extension to CGI that provides high performance without the limitations of specific server APIs." Web server support is provided for Apache in the form of mod_fastcgi and there is Perl support in the FCGI module. To convert a CGI Catalyst application to FastCGI one needs to initialize an FCGI::Request object and loop while the Accept method returns zero. The following code shows how it is done - and it also works as a normal, single-shot CGI script.

    #!/usr/bin/perl
    use strict;
    use FCGI;
    use MyApp;

    my $request = FCGI::Request();
    while ($request->Accept() >= 0) {
        MyApp->run;
    }

Any initialization code should be included outside the request-accept loop.

There is one little complication, which is that MyApp->run outputs a complete HTTP response including the status line (e.g.: "HTTP/1.1 200"). FastCGI just wants a set of headers, so the sample code captures the output and drops the first line if it is an HTTP status line (note: this may change).

The Apache mod_fastcgi module is provided by a number of Linux distros and is straightforward to compile for most Unix-like systems. The module provides a FastCGI Process Manager, which manages FastCGI scripts. You configure your script as a FastCGI script with the following Apache configuration directives:

    <Location /fcgi-bin>
       AddHandler fastcgi-script fcgi
    </Location>

or:

    <Location /fcgi-bin>
       SetHandler fastcgi-script
       Action fastcgi-script /path/to/fcgi-bin/fcgi-script
    </Location>

mod_fastcgi provides a number of options for controlling the FastCGI scripts spawned; it also allows scripts to be run to handle the authentication, authorization, and access check phases.

For more information see the FastCGI documentation, the FCGI module and http://www.fastcgi.com/.

Forwarding with a parameter

Sometimes you want to pass along arguments when forwarding to another action. This can easily be accomplished like this:

  $c->req->args([qw/arg1 arg2 arg3/]);
  $c->forward('/wherever');

AUTHOR

Sebastian Riedel, sri@oook.de Danijel Milicevic me@danijel.de Viljo Marrandi vilts@yahoo.com Marcus Ramberg mramberg@cpan.org

COPYRIGHT

This program is free software, you can redistribute it and/or modify it under the same terms as Perl itself.