Docendo discimus

$self->explain

  • Calendar

    November 2017
    M T W T F S S
    « Sep    
     12345
    6789101112
    13141516171819
    20212223242526
    27282930  
  • Archives

  • Recent Posts

  • Bling

Posts Tagged ‘model’

Catalyst is easy – third movement

Posted by brunorc on June 27, 2009

Wherever you go to read about Catalyst you will find it is an MVC framework. But we are still digging in the same file, aren’t we? Everything the application does is stuffed into one controller file. Now we are going to change it and isolate the model.

Model is responsible for providing the data. Very often it will be some database wrapping module, but data can be fetched from everywhere: filesystem, mail queue, IMAP, LDAP, Web Service, image generator/transformator, version control system, RSS feed… However, model shouldn’t be identified with the data itself. All the facilities – like formatting or consistency checks – should be done inside the model, not in the application. Then the model can be reused while writing e.g. maintenance scripts. Actually, Catalyst model classes should only act as a glue code between the application and the real model, which shouldn’t be related with Catalyst.

In our case the data consists of one list, but recently we isolated some code used for adding new items. We can put this code in the separate module and use all model-related facilities of Catalyst. Of course, we need a model for this:

script/helloworld_create.pl model Simple

and the stub of the model will be created in the file lib/helloworld/Model/Simple.pm. The file itself is not very interesting, since all the necessary code is inherited from Catalyst::Model class.

We would like our model to access the “raw data” (in this case – the list), to return all items, to add a new one and maybe to do the search. Let’s start with =cutting the documentation under the description and putting some stubs:

package helloworld::Model::Simple;

use strict;
use warnings;
use parent 'Catalyst::Model';

=head1 NAME

helloworld::Model::Simple - Catalyst Model

=head1 DESCRIPTION

Catalyst Model.

=cut

my @raw_data = qw/beer bacon pierogi/;

sub get_all {
  my ( $self ) = @_;

  return @raw_data;
}

sub add_new {
  my ( $self, @newitems ) = @_;

  push @raw_data, @newitems;
}

Notice that methods of the model don’t have access to the Catalyst context object, because, in fact, they shouldn’t have it.

Now we can check how it works. Until now, the code in the index action looked like this:

sub index :Path :Args(0) {
    my ( $self, $c ) = @_;

    my $list = '';

    foreach my $item ( @list ) {
        $list .= "<li>$item</li>\n";
    }
# (...)

Let’s change line 6 of this example:

    foreach my $item ( $c->model('Simple')->get_all ) {

Similarly, we can change this line of the _add action:

    push @items, @new_items;

to this:

    $c->model('Simple')->add_new(@new_items);

Why bother? Let’s assume that there’s a need to have a unique list of items. It can be achieved easily, converting the array into the hash, without changing a single line in the controller. This is the code of the new version of the model:

my %raw_data = map { $_ => 1 } qw/beer bacon pierogi/;

sub get_all {
  my ( $self ) = @_;

  return keys %raw_data;
}

sub add_new {
  my ( $self, @newitems ) = @_;

  $raw_data{$_} = 1 foreach @newitems;
}

Now it is possible to move the code from the private action to the model. However, there’s no $c variable received by model methods. And since there’s no context, there will be no possibility to populate the stash directly. No problem at all:

$c->stash->{add_result} = $c->model('Simple')->add_new($whatever);

But how to stuff the message and count in one variable? Oh, and probably we would like to notify user – just in case – that he tried to add the value already present in the list. Returning the hash reference allows one to pass anything necessary. This may be not the best option, but at least it gives some reason to mention complex data structures.

After this, the controller will look like this:

sub api_add :Local :Args(1) {
    my ( $self, $c, $newitem ) = @_;

    if ( $c->req->method eq 'POST' ) {
        $c->stash->{add_result} = $c->model('Simple')->add_new($newitem);

        $c->res->body( $c->stash->{add_result}->{count} ? "OK" : "ER" );
    }
    else {
        $c->res->status(405);
        $c->res->body('ER');
    }
}

sub add :Local :Args(0) {
    my ( $self, $c ) = @_;

    my $optional;

    if ( $c->req->params->{newitem} ) {
        $c->stash->{add_result} = $c->model('Simple')->add_new( $c->req->params->{newitem} );

        if ( $c->req->params->{addit} eq 'Add it!' and $c->stash->{add_result}->{count} ) {
            my $count = $c->stash->{add_result}->{count};
            $optional = "<h4>Thank you for adding $count items!</h4>";
        }

        if ( $c->stash->{add_result}->{message} ) {
            $optional .= '<h4>' . $c->stash->{add_result}->{message} . '</h4>';
        }
    }
    # template part skipped
}

All the logic is now embedded into the model class:

sub add_new {
  my ( $self, $newitem ) = @_;

  my $result = {};

  if ( $newitem =~ s/_//g ) {
      $result->{message} = 'Removed illegal underscore(s). ';
  }

  my @newitems = split / /, $newitem;

  my @okitems = grep { not exists $raw_data{$_} } @newitems;
  
  $raw_data{$_} = 1 foreach @okitems;

  $result->{count} = @okitems;

  $result->{message} .= 'Skipped ' . ( @newitems - @okitems ) . ' doubles'
    if scalar @newitems != scalar @okitems;

  return $result;
}

but why to put the result in the stash? At the moment it is used only internally, but in fact, stash is the way to propagate the information from Model into View. The application has no view yet, but soon it will change. Generating HTML inside the code is clumsy and once we get tired of it, Catalyst is eager to relieve us of this ugliness.

Advertisements

Posted in Catalyst for intimidated | Tagged: , , , , , , | 1 Comment »