man Catalyst::Manual::Cookbook () - Cooking with Catalyst

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 CWdie() call in the CWend action.

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

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

    sub end : Private {  
        my ( $self, $c ) = @_;  
        die "forced debug" if $c->req->params->{dump_info};  
    }

Then just add to your query string CW"&dump_info=1", or the like, to force debug output.

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.

The recommended way is to use Catalyst::Helper::Controller::Scaffold.

Just install this module, and to scaffold a Class::DBI Model class, do the following:

./script/myapp_create controller <name> Scaffold <CDBI::Class>Scaffolding

File uploads

Single file upload with Catalyst

To implement uploads in Catalyst, you need to have a HTML form similar 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 CWenctype="multipart/form-data" in the form.

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 a few changes:

The form should have this basic structure:

    <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>

And in the 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';
    }

CWfor 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: CWdieing 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 CW$! in CW$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(1),
   surname   varchar(1),
   password  varchar(1),
   email     varchar(1),
   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 CW$c->req->params->{password} are html form parameters from a login form. If login succeeds, then CW$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 CW$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 CW$c->req->{user} where needed.

To log out a user, just call CW$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(1),
   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->res->output("Your account has the role 'admin.'");
        } else {
            $c->res->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('/user/login');
        }
    }

Pay attention to CW$c->req->action(undef). This is needed because of the way CW$c->forward works - CWforward to CWlogin gets called, but after that Catalyst will still execute the action defined in the URI (e.g. if you tried to go to CW/add, then first 'begin' will forward to 'login', but after that 'add' will nonetheless be executed). So CW$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 CWmod_fastcgi and there is Perl support in the CWFCGI module. To convert a CGI Catalyst application to FastCGI one needs to initialize an CWFCGI::Request object and loop while the CWAccept 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 CWMyApp->run outputs a complete HTTP response including the status line (e.g.: "CWHTTP/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 CWmod_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>

CWmod_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 CWFCGI module and <http://www.fastcgi.com/>.

Serving static content

Serving static content in Catalyst can be somewhat tricky; this recipe shows one possible solution. Using this recipe will serve all static content through Catalyst when developing with the built-in HTTP::Daemon server, and will make it easy to use Apache to serve the content when your app goes into production.

Static content is best served from a single directory within your root directory. Having many different directories such as CWroot/css and CWroot/images requires more code to manage, because you must separately identify each static directoryif you decide to add a CWroot/js directory, you'll need to change your code to account for it. In contrast, keeping all static directories as subdirectories of a main CWroot/static directory makes things much easier to manager. Here's an example of a typical root directory structure:

    root/
    root/content.tt
    root/controller/stuff.tt
    root/header.tt
    root/static/
    root/static/css/main.css
    root/static/images/logo.jpg
    root/static/js/code.js

All static content lives under CWroot/static with everything else being Template Toolkit files. Now you can identify the static content by matching CWstatic from within Catalyst.

Serving with HTTP::Daemon (myapp_server.pl)

To serve these files under the standalone server, we first must load the Static plugin. Install Catalyst::Plugin::Static if it's not already installed.

In your main application class (MyApp.pm), load the plugin:

    use Catalyst qw/-Debug FormValidator Static OtherPlugin/;

You will also need to make sure your end method does not forward static content to the view, perhaps like this:

    sub end : Private {
        my ( $self, $c ) = @_;

        $c->forward( 'MyApp::V::TT' ) 
          unless ( $c->res->body || !$c->stash->{template} );
    }

This code will only forward to the view if a template has been previously defined by a controller and if there is not already data in CW$c->res->body.

Next, create a controller to handle requests for the /static path. Use the Helper to save time. This command will create a stub controller as CWlib/MyApp/C/Static.pm.

    $ script/myapp_create.pl controller Static

Edit the file and add the following methods:

    # serve all files under /static as static files
    sub default : Path('/static') {
        my ( $self, $c ) = @_;

        # Optional, allow the browser to cache the content
        $c->res->headers->header( 'Cache-Control' => 'max-age=86400' );

        $c->serve_static; # from Catalyst::Plugin::Static
    }

    # also handle requests for /favicon.ico
    sub favicon : Path('/favicon.ico') {
        my ( $self, $c ) = @_;

        $c->serve_static;
    }

You can also define a different icon for the browser to use instead of favicon.ico by using this in your HTML header:

    <link rel="icon" href="/static/myapp.ico" type="image/x-icon" />

Common problems

The Static plugin makes use of the CWshared-mime-info package to automatically determine MIME types. This package is notoriously difficult to install, especially on win32 and OS X. For OS X the easiest path might be to install Fink, then use CWapt-get install shared-mime-info. Restart the server, and everything should be fine.

Make sure you are using the latest version (>= 0.16) for best results. If you are having errors serving CSS files, or if they get served as text/plain instead of text/css, you may have an outdated shared-mime-info version. You may also wish to simply use the following code in your Static controller:

    if ($c->req->path =~ /css$/i) {
        $c->serve_static( "text/css" );
    } else {
        $c->serve_static;
    }

Serving with Apache

When using Apache, you can completely bypass Catalyst and the Static controller by intercepting requests for the CWroot/static path at the server level. All that is required is to define a DocumentRoot and add a separate Location block for your static content. Here is a complete config for this application under mod_perl 1.x:

    <Perl>
        use lib qw(/var/www/MyApp/lib);
    </Perl>
    PerlModule MyApp

    <VirtualHost *>
        ServerName myapp.example.com
        DocumentRoot /var/www/MyApp/root
        <Location />
            SetHandler perl-script
            PerlHandler MyApp
        </Location>
        <LocationMatch "/(static|favicon.ico)">
            SetHandler default-handler
        </LocationMatch>
    </VirtualHost>

And here's a simpler example that'll get you started:

    Alias /static/ "/my/static/files/"
    <Location "/static">
        SetHandler none
    </Location>

Forwarding with arguments

Sometimes you want to pass along arguments when forwarding to another action. As of version 5.30, arguments can be passed in the call to CWforward; in earlier versions, you can manually set the arguments in the Catalyst Request object:

  # version 5.30 and later:
  $c->forward('/wherever', [qw/arg1 arg2 arg3/]);

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

(See Catalyst::Manual::Intro#Flow_Control for more information on passing arguments via CWforward.)

Configure your application

You configure your application with the CWconfig method in your application class. This can be hard-coded, or brought in from a separate configuration file.

Using YAML

YAML is a method for creating flexible and readable configuration files. It's a great way to keep your Catalyst application configuration in one easy-to-understand location.

In your application class (e.g. CWlib/MyApp.pm):

  use YAML;
  # application setup
  __PACKAGE__->config( YAML::LoadFile(__PACKAGE__->config->{'home'} . '/myapp.yml') );
  __PACKAGE__->setup;

Now create CWmyapp.yml in your application home:

  --- #YAML:1.0
  # DO NOT USE TABS FOR INDENTATION OR label/value SEPARATION!!!
  name:     MyApp

  # authentication; perldoc Catalyst::Plugin::Authentication::CDBI
  authentication:
    user_class:           'MyApp::M::MyDB::Customer'
    user_field:           'username'
    password_field:       'password'
    password_hash:        'md5'
    role_class:           'MyApp::M::MyDB::Role'
    user_role_class:      'MyApp::M::MyDB::PersonRole'
    user_role_user_field: 'person'

  # session; perldoc Catalyst::Plugin::Session::FastMmap
  session:
    expires:        '3600'
    rewrite:        '0'
    storage:        '/tmp/myapp.session'

  # emails; perldoc Catalyst::Plugin::Email
  # this passes options as an array :(
  email:
    - SMTP
    - localhost

This is equivalent to:

  # configure base package
  __PACKAGE__->config( name => MyApp );
  # configure authentication
  __PACKAGE__->config->{authentication} = {
    user_class => 'MyApp::M::MyDB::Customer',
    ...
  };
  # configure sessions
  __PACKAGE__->config->{session} = {
    expires => 3600,
    ...
  };
  # configure email sending
  __PACKAGE__->config->{email} = [qw/SMTP localhost/];

See also YAML.

Using existing CDBI (etc.) classes with Catalyst

Many people have existing Model classes that they would like to use with Catalyst (or, conversely, they want to write Catalyst models that can be used outside of Catalyst, e.g. in a cron job). It's trivial to write a simple component in Catalyst that slurps in an outside Model:

    package MyApp::M::Catalog;
    use base qw/Catalyst::Base Some::Other::CDBI::Module::Catalog/;
    1;

and that's it! Now CWSome::Other::CDBI::Module::Catalog is part of your Cat app as CWMyApp::M::Catalog.

Delivering a Custom Error Page

By default, Catalyst will display its own error page whenever it encounters an error in your application. When running under CW-Debug mode, the error page is a useful screen including the error message and a full Data::Dumper output of the CW$c context object. When not in CW-Debug, users see a simple Please come back later screen.

To use a custom error page, use a special CWend method to short-circuit the error processing. The following is an example; you might want to adjust it further depending on the needs of your application (for example, any calls to CWfillform will probably need to go into this CWend method; see Catalyst::Plugin::FillInForm).

    sub end : Private {
        my ( $self, $c ) = @_;

        if ( scalar @{ $c->error } ) {
            $c->stash->{errors}   = $c->error;
            $c->stash->{template} = 'errors.tt';
            $c->forward('MyApp::View::TT');
            $c->{error} = [];
        }

        return 1 if $c->response->status =~ /^3\d\d$/;
        return 1 if $c->response->body;

        unless ( $c->response->content_type ) {
            $c->response->content_type('text/html; charset=utf-8');
        }

        $c->forward('MyApp::View::TT');
    }

You can manually set errors in your code to trigger this page by calling

    $c->error( 'You broke me!' );

Require user logins

It's often useful to restrict access to your application to a set of registered users, forcing everyone else to the login page until they're signed in.

To implement this in your application make sure you have a customer table with username and password fields and a corresponding Model class in your Catalyst application, then make the following changes:

lib/MyApp.pm

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

  __PACKAGE__->config->{authentication} = {
    'user_class'        => 'ScratchPad::M::MyDB::Customer',
    'user_field'        => 'username',
    'password_field'    => 'password',
    'password_hash'     => '',
  };

  sub auto : Private {
    my ($self, $c) = @_;
    my $login_path = 'user/login';

    # allow people to actually reach the login page!
    if ($c->req->path eq $login_path) {
      return 1;
    }

    # if we have a user ... we're OK
    if ( $c->req->user ) {
      $c->session->{'authed_user'} =
        MyApp::M::MyDB::Customer->retrieve(
          'username' => $c->req->user
        );
    }

    # otherwise they're not logged in
    else {
      # force the login screen to be shown
      $c->res->redirect($c->req->base . $login_path);
    }

    # continue with the processing chain
    return 1;
  }

lib/MyApp/C/User.pm

  sub login : Path('/user/login') {
    my ($self, $c) = @_;

    # default template
    $c->stash->{'template'} = "user/login.tt";
    # default form message
    $c->stash->{'message'} = 'Please enter your username and password';

    if ( $c->req->param('username') ) {
      # try to log the user in
      $c->session_login(
        $c->req->param('username'),
        $c->req->param('password'),
      );

      # if we have a user we're logged in
      if ( $c->req->user ) {
        $c->res->redirect('/some/page');
      }

      # otherwise we failed to login, try again!
      else {
        $c->stash->{'message'} = 
           'Unable to authenticate the login details supplied';
      }
    }
  }

  sub logout : Path('/user/logout') {
    my ($self, $c) = @_;
    # logout the session, and remove information we've stashed
    $c->session_logout;
    delete $c->session->{'authed_user'};

    # do the 'default' action
    $c->res->redirect($c->req->base);
}

root/base/user/login.tt

 [% INCLUDE header.tt %]
 <form action="/user/login" method="POST" name="login_form">
    [% message %]<br />
    <label for="username">username:</label><br />
    <input type="text" id="username" name="username" /><br />

    <label for="password">password:</label><br />
    <input type="password" id="password" name="password" /><br />

    <input type="submit" value="log in" name="form_submit" />
  </form>
  [% INCLUDE footer.tt %]

AUTHOR

Sebastian Riedel, CWsri@oook.de Danijel Milicevic, CWme@danijel.de Viljo Marrandi, CWvilts@yahoo.com Marcus Ramberg, CWmramberg@cpan.org Jesse Sheidlower, CWjester@panix.com Andy Grundman, CWandy@hybridized.org Chisel Wright, CWpause@herlpacker.co.uk

COPYRIGHT

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