Implementing a Filesystem model

In this final example, we'll build an entire model from scratch without even the help of a model base class like Catalyst::Model::DBI. Before you do this for your own application, you should check the CPAN to see if anyone's done anything similar already. There are currently about fifty ready-to-use model base classes that abstract data sources, such as LDAP servers, RSS readers, shopping carts, search engines, subversion, e-mail folders, web services, and even YouTube. Expanding upon one of these classes will usually be easier than writing everything yourself.

For this example, we'll create a very simple blog application. To post the blog, you just write some text and put it in a file whose name is the title you want on the post. We'll write a Filesystem model from scratch to provide the application with the blog posts.

Let's start by creating the application's skeleton:

$ catalyst.pl Blog

After that, we'll create our Filesystem model:

$ cd Blog
$ perl script/blog_create.pl model Filesystem

We'll also use plain TT for the View:

$ perl script/blog_create.pl view TT TT

Let's continue by creating a template for displaying the most recent blog posts called root/recent.tt:

<html>
<head><title>Recent blog posts</title></head>
<body>
<h1>Blog</h1>
[% FOREACH post = posts %]
<h2>[% post.title | html %]</h2>
<i>Written on [% post.created | html %]</i> [% post.body %]
[% END %]
</body>
</html>

Finally, let's replace the default index action with one that gets the posts from the model and then displays them inside the recent.tt template. In Blog::Controller::Root, we'll replace default with the following code:

sub default : Path Args {
my ( $self, $c ) = @_;
$c->stash->{template} = 'recent.tt';
#$c->stash->{posts} = [$c->model('Filesystem')
# ->get_recent_posts()];
}

Note that we've commented out the line where we get the posts; as we haven't implemented the get_recent_posts method yet, you should be able to start the application now and see the beginnings of a blog when you visit http://localhost:3000/.

All that's left to do is implement the model. This model will take a two-tiered approach. The actual Catalyst model will find all "posts" and will create a Blog::Model::Filesystem::Post object for each. These objects will do most of the work such as reading the file and so on. Let's start by creating the Post class by writing lib/Blog/Model/Filesystem/Post.pm:

# Post.pm
package Blog::Model::Filesystem::Post;
use strict; use warnings; use Carp;
use File::Basename;
use File::Slurp qw(read_file);
use File::CreationTime qw(creation_time);
use base 'Class::Accessor';
PACKAGE ->mk_ro_accessors(qw(filename));
sub new {
my $class = shift;
my $filename = shift;
croak "Must specify a filename" unless $filename;
my $self = {};
$self->{filename} = $filename;
bless $self, $class;
return $self;
}
sub title {
my $self = shift;
my $filename = $self->filename;
my $title = basename($filename);
$title =~ s/[.](w+)$//; # strip off .extensions return $title;
}
sub body {
my $self = shift;
return read_file($self->filename);
}
sub created {
my $self = shift;
return creation_time($self->filename);
}
sub modified {
my $self = shift;
return (stat $self->filename)[9]; # 9 is mtime
}
1;

This is a pretty standard Perl class. The new method takes a filename and creates an instance of this class based on the filename. The rest of the methods access the file and return the desired information. Because of the way we've designed the class, it will be extremely simple to add more information to each blog post in the future. We'll just create another method, and the information will be easily available to the Controller and the template.

Now we need to create the actual Catalyst model that will find blog posts and return instances of the Post object. To start with, we'll just need to add the get_recent_posts method in the following manner:

package Blog::Model::Filesystem;
use strict;
use warnings;
use base 'Catalyst::Model';
use Carp;
use File::Spec;
use Blog::Model::Filesystem::Post;
PACKAGE ->mk_accessors('base'),
sub get_recent_posts {
my $self = shift;
my $base = $self->base;
my @articles;
opendir my $dir, $base or
croak "Problem opening $base: $!";
while(my $file = readdir $dir){
next if $file =~ /^[.]/; # skip hidden files
my $filename = File::Spec->catfile($base, $file);
next if -d $filename;
push @articles,
Blog::Model::Filesystem::Post->new($filename);
}
closedir $dir;
@articles = reverse sort {$a->created <=> $b->created}
@articles;
return @articles if @articles < 5;
return @articles[0..4]; # top 5 otherwise
}

The mk_accessors line is especially important—this will allow you to specify a base attribute in the config file, which will then be available in the rest of the methods as $self->base. Here's the config file, blog.yml:

---
name: Blog
Model::Filesystem:
base: /tmp/test

Now all you need to do is remove the comment from the line in Root.pm and then add some HTML files to /tmp/test. When you start your server, you should see the five most recent posts displayed!

If you're interested in taking this idea further, check out the Angerwhale blogging system, available from the CPAN. It uses a similar filesystem-based model, but one that has many more features.

As Catalyst is now based on Moose, it might be a good idea to implement models using it. There are already specialized Moose roles for components on CPAN. In the next chapter, you will learn more about Moose and its application.

Tweaking the model

With the core functionality in place, we can now dig a bit deeper into the model and add some more features. The first one is a sort of "user interface" improvement. Instead of making the user type out the Model::Filesystem part in the config file, it would be nice to just specify base and have that take the effect in the same way. We can achieve this by reading the value of $c->config->{base} into $c->config->{Model::Filesystem}->{base} just before Catalyst creates an instance of the class. This is done by overriding the COMPONENT method in the model.

The COMPONENT method is called to set up things such as configuration, right before the new method is called (things like database connections are set up). We can override this in our model, tweak the config, and then call the version of COMPONENT in Catalyst::Model to finish up everything:

sub COMPONENT {
my ($class, $app, $args) = @_;
$args->{base} = $app->config->{base};
return $class->NEXT::COMPONENT($app, $args);
}

With this code in place, we can change the config file to:

---
base: /tmp/test name: Blog

The last feature we'll add is validation of base in the new method. This will check to see if the base directory exists and if it does not, issue an error message and prevent the application from starting:

sub new {
my $class = shift;
my $self = $class->NEXT::new(@_); # get the real self my $base = $self->base;
croak "base $base does not exist" if !-d $base;
return $self;
}

If you're inheriting from a base class, you can control whether or not your code runs before that of your base class, by choosing whether to run your code before or after the NEXT::new() call. NEXT::new() is where the superclasses get a chance to set up themselves, and then control is passed back to you. You should return the result of NEXT::new() from the new method.

Request context inside the model

Generally, your model's configuration won't change as requests run. When Catalyst is started, your model is initialized and it doesn't see the rest of your application again. This means that you can't save the $app you got from COMPONENT and use it to, say, access $c->request or $c->response in the future. It's generally a good idea to avoid touching the request from inside a model anyway (that's what the Controller is for) , but if you absolutely need to, you can get the latest $c by implementing an ACCEPT_CONTEXT method in your model. It's called by Catalyst every time you call $c->model() and is passed $c, and any arguments are passed to $c->model(). In general, it will look something like this:

__PACKAGE__ ->mk_accessors(qw|context|); # at the top
sub ACCEPT_CONTEXT {
my ($self, $c, @args) = @_;
$self->context($c);
return $self;
}

We return $self from ACCEPT_CONTEXT here, but in theory you can return anything. The value is passed directly back to the caller of $c->model(). DBIC::Schema takes advantage of this feature to return individual resultsets instead of the entire schema depending on how $c->model() is invoked.

After you've added an ACCEPT_CONTEXT method like we just did, you can call $self->context() anywhere in your model to get the current request context. There is now a component role on CPAN that can be implemented and does this readily and cleanly, and in a way that the user doesn't have to maintain for themselves in the future. You will learn more about this in the next chapter on Moose.

Maintainable models

When you're writing your own data model for use with Catalyst, you might want to consider making it work without Catalyst first, and then later adding some glue to make it easy to use from within Catalyst. The advantage of this approach is that you can test your model without having a Catalyst application to use it and that you can use your data model class in non-Catalyst applications, most commonly command-line scripts and background processes. DBIx::Class takes this approach; the DBIx::Class::Schema works fine without Catalyst. The DBIC model you create for your application is just a bit of glue to make using the DBIx::Class::Schema from Catalyst convenient.

Let's take a look at how we would build the Filesystem model in this manner. First, we'll move the Blog::Model::Filesystem::Post class to the Blog::Backend::Filesystem::Post namespace. Then, we'll write our post access code in Blog::Backend::Filesystem instead of Blog::Model::Filesystem. The code is exactly the same, except that we'll write our own new method as follows:

package Blog::Backend::Filesystem;
use strict; use warnings; use Carp;
use Blog::Backend::Filesystem::Post;
sub new {
my ($class, $args) = @_; # args is { base => 'path' }
croak 'need a base that exists' if !-d $args->{base};
return bless $args, $class;
}
# then the same as Blog::Model::Filesystem above, substituting
# Blog::Model::Filesystem::Post for
# Blog::Backend::Filesystem::Post.

Now you have a class that you can use to access blog posts from outside of Catalyst. Just instantiate it like my $post_model = Blog::Backend::Filesystem->new({ base => '/var/blog' }) and then use $post_model like you did $c->model('Filesystem').

The final step is to create the glue to bind the backend class to a Catalyst model. Fortunately, Catalyst::Model::Adaptor, a module on CPAN, will do that for us automatically by running the following command:

$ perl script/blog_create.pl model Filesystem Adaptor Blog::Backend:: Filesystem

This command will create a model called Blog::Model::Filesystem that simply returns a Blog::Backend::Filesystem object when you call $c->model('Filesystem'). It works by creating a subclass of Catalyst::Model::Adaptor that will create an instance of your backend class at startup and return it when needed.

One disadvantage is that the configuration format changes slightly:

--- Model::Filesystem:
args:
base: /var/blog

If you want to avoid the unsightly args key, you can override prepare_arguments in the model like this:

package Blog::Model::Filesytem;
# generated code here sub prepare_arguments {
my ($self, $app) = @_;
return { base => $app->{base} };
}

Now the adapted Filesystem model will work exactly like the one we made earlier, but with very little Catalyst-specific code.

If you are writing a model that needs a new backend class created every time you call $c->model or once per request (instead of once per application), you can use the Catalyst::Model::Factory and Catalyst::Model::Factory::PerRequest modules included with Catalyst::Model::Adaptor. They are all used in the same way (as we just saw, substituting Factory or Factory::PerRequest for Adaptor), but integrate your backend class with Catalyst in slightly different ways. For most cases, these models will be all you need.

Other components

Models are just the tip of the iceberg—Views and Controllers work the same way (and implement the same methods) as Models. You can easily create custom Views and Controllers and inherit from them in your application to improve the reusability of your application's code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.197.251