INTRO This file documents my tribulations as I tried to get a first Reaction site up and running. The background I come from is this: I'm an experienced Perl developer, but I haven't programmed Perl on a regular basis over the past few year; I've made toy Catalyst and Moose apps, but nothing really serious; I've also made a couple small DBIC apps but I've never liked RDBMS and would much rather use CouchDB, or something similar; and finally, I've made a first pass at a simple Reaction app but quite frankly I got lost. Reaction is clearly a powerful framework with very interesting features, but it makes use of a fair number of concepts and terminology that may be new to many (at least in part), has its fair share of conventions, and little to none of that is documented (not to mention the fact that it's a moving target, which makes some of the documentation ). Altogether, this makes for a fairly steep learning curve. This is my attempt to leave a trail of breadcrumbs like le Petit Poucet did (or Hansel and Gretel, depending on where you got your fairy tales from). Please note that at times the level of detail may seem silly. This is intentional, as I believe that it's easier to skip over something you already know than to guess something you don't. GETTING STARTED Presumably you've gotten the Reaction SVN checked out and installed alongside all its dependencies. Now let's start building the app. My app is a simple blog/CMS thingie for my website berjon.com, so it's called "Berjon". Reaction apps are Catalyst apps, so we can reuse the catalyst.pl creation script to get a skeleton up. Reaction uses a slightly different conventional structure so we'll be tweaking the generated files. Since you want your web frontend to be orthogonal to the rest of the app, we'll tell Catalyst that the app is called Berjon::Web so that everything Catalyst goes under the Berjon::Web while the bits that are generic and could have another frontend will just be under Berjon. Finally, I like to use the short names M, V, and C instead of their longer versions — that's a personal preference which you need not mirror (but which I'll keep using throughout this document). lamia-[13:12]:code2$ catalyst.pl -short Berjon::Web created "Berjon-Web" created "Berjon-Web/script" created "Berjon-Web/lib" created "Berjon-Web/root" created "Berjon-Web/root/static" created "Berjon-Web/root/static/images" created "Berjon-Web/t" created "Berjon-Web/lib/Berjon/Web" created "Berjon-Web/lib/Berjon/Web/M" created "Berjon-Web/lib/Berjon/Web/V" created "Berjon-Web/lib/Berjon/Web/C" created "Berjon-Web/berjon_web.yml" created "Berjon-Web/lib/Berjon/Web.pm" created "Berjon-Web/lib/Berjon/Web/C/Root.pm" created "Berjon-Web/README" created "Berjon-Web/Changes" created "Berjon-Web/t/01app.t" created "Berjon-Web/t/02pod.t" created "Berjon-Web/t/03podcoverage.t" created "Berjon-Web/root/static/images/catalyst_logo.png" created "Berjon-Web/root/static/images/btn_120x50_built.png" created "Berjon-Web/root/static/images/btn_120x50_built_shadow.png" created "Berjon-Web/root/static/images/btn_120x50_powered.png" created "Berjon-Web/root/static/images/btn_120x50_powered_shadow.png" created "Berjon-Web/root/static/images/btn_88x31_built.png" created "Berjon-Web/root/static/images/btn_88x31_built_shadow.png" created "Berjon-Web/root/static/images/btn_88x31_powered.png" created "Berjon-Web/root/static/images/btn_88x31_powered_shadow.png" created "Berjon-Web/root/favicon.ico" created "Berjon-Web/Makefile.PL" created "Berjon-Web/script/berjon_web_cgi.pl" created "Berjon-Web/script/berjon_web_fastcgi.pl" created "Berjon-Web/script/berjon_web_server.pl" created "Berjon-Web/script/berjon_web_test.pl" created "Berjon-Web/script/berjon_web_create.pl" As you can see, a lot of the above files are in static/. You could keep it (and use it) but that's one bit that is typically done differently in Reaction, so we'll kill that directory. We're going to replace it with a share directory, containing a directory for skins, and in it a directory named after the skin we intend to create (here 'berjon'). Every skin directory then contains two subdirectories called layout and web, respectively for templates and static resources. lamia-[13:12]:code2$ cd Berjon-Web/ lamia-[13:39]:Berjon-Web$ rm -Rf root/ lamia-[13:39]:Berjon-Web$ mkdir share lamia-[13:39]:Berjon-Web$ mkdir share/skin lamia-[13:39]:Berjon-Web$ mkdir share/skin/berjon lamia-[13:39]:Berjon-Web$ mkdir share/skin/berjon/layout lamia-[13:39]:Berjon-Web$ mkdir share/skin/berjon/web Let's take a look at our entry point module. package Berjon::Web; use strict; use warnings; use Catalyst::Runtime '5.70'; use Catalyst qw/ -Debug ConfigLoader Static::Simple I18N /; our $VERSION = '0.01'; __PACKAGE__->config( name => 'Berjon::Web', 'V::Site' => { skin_name => 'berjon', }, ); __PACKAGE__->setup; 1; All we've done here is remove all the comments and load the additional I18N plugin which Reaction silently expects to be present. We've also given a skin_name parameter to the Site view module matching the name of the skin for this site (I know that you're supposed to use an external file for such configuration but the default is in YAML which I find hard to read so I'll get around to this later). Since at the very least one needs to display something, we'll set up a trivial Reaction view, like so: package Berjon::Web::V::Site; use Reaction::Class; use aliased 'Reaction::UI::View::TT'; class Site is TT, which { }; 1; At this point not much is needed, we're simply inheriting from Reaction's TT view. Let's now take a look at our root controller: package Berjon::Web::C::Root; use base 'Reaction::UI::Controller::Root'; use Reaction::Class; use aliased 'Reaction::UI::ViewPort'; use aliased 'Reaction::UI::ViewPort::SiteLayout'; __PACKAGE__->config( view_name => 'Site', window_title => 'Berjon.com', namespace => '', ); sub base :Chained('/') :PathPart('') :CaptureArgs(0) { my ($self, $c) = @_; $self->push_viewport( SiteLayout, title => 'SiteLayout Title for Berjon.com', static_base_uri => "${\$c->uri_for('/static')}", ); } sub root :Chained('base') :PathPart('') :Args(0) { } 1; Amongst the noteworthy bits here, you'll see that we're inheriting from Reaction's Root controller which is charged with setting up a Window that will hold all the Viewports. We have a chained base => root action that does nothing other than set up a SiteLayout viewport. The SiteLayout viewport is good for creating the global look and feel of the entire application. We just give it a title and the base URI for static content, and off we go. Later on we'll be adding more functionality here, but for the time being that'll do. And the final piece that's needed for the minimal Reaction application is the layout. Let's turn to the share/skin directory and put our layouts in place. First, inside the skin directory itself create a defaults.conf file containing this: widget_search_path Reaction::UI::Widget I'll be downright honest here and admit that this is cargo-culting on my part — I don't know if this is necessary of if Reaction can find its own Widgets no matter what. I'm including this file because I've all other examples use it and because Reaction sometimes has rather obscure error messages — if not including this can create mischief down the line, I'd rather avoid it. Nevertheless, I hope this can be done away with as it seems rather useless (or rather feels like a trivial default value). Then, still inside the share/skin directory create a link to the default set of layout-sets that Reaction sets up when it is installed. Mine was in /Library/Perl/5.8.6/auto/Reaction/skin/default, just adapt to your system's @INC and you'll find it. These default contain frequently used pieces of templating (such as an XHTML page structure to boot) which makes it quite useful, but unfortunately Reaction only search the local share/skin directory instead of iterating through several, hence the link: lamia-[18:06]:skin$ ln -s /Library/Perl/5.8.8/auto/Reaction/skin/default UPDATE: it turns out that this is a bug with File::ShareDir that's being worked on. You can either use this workaround, ln -s the auto/Reaction into the arch-specific dir (mst), or wait for the fix. Then inside the specific skin directory (here share/skin/berjon) add a file called skin.conf containing: extends default widget_search_path Berjon::Web::V::Site::Widget The widget_search_path we've seen before, it tells Reaction to search for widget code below a given class path. The previous line is simply an indication that this skin extends the default skin. Doing this is not a necessity as skins can be created entirely from scratch, but it is convenient as it allows us to reuse from another skin, to use Reaction's built-in widgets, or to just modify the small parts which one intends to change. Finally we will create a site_layout.tt file inside our skin (in berjon/layout), filling it with just: =extends NEXT =for layout inner =cut The "=extends NEXT" means that it's an extension of the default site_layout layout (itself selected automatically because the Viewport class name is SiteLayout). Since it's a wrapper layout, it expects to be provided with some content to fill it coming from an inner viewport. But we haven't defined that yet, so we override its definition of "inner" to contain nothing (if you don't do that it'll look for an inner viewport, not find it, and promptly blow up). We're now ready to start our Reaction application that does exactly nothing! While inside the application directory, launch it the usual way for Catalyst apps: lamia-[15:11]:Berjon-Web$ ./script/berjon_web_server.pl -p 3005 -rr '\.yml$|\.yaml$|\.pm$|\.tt$' -r Point your browser to http://localhost:3005/ (or whichever matches the way in which you started the app) and you should see an empty page. If not, you'll get an error (or your app will crash). That's it, we've created a blank Reaction app! ========================================================================================================= FIRST REAL STEPS So let's look back at what we want to achieve: a simple way of serving content based on the URI path, and wrapped inside a sexy template. Nothing we haven't all done a zillion times, but a good first step on which to build more. The first part will have nothing to do with Reaction: we will simply define a set of classes that will take a path and return objects representing the corresponding content. Those objects will be of two types: pages, that will get wrapped in the template and shown, and resources, that represent content that should be served directly rather than wrapped (typically images in this use case). If the path doesn't match, we'll also need to return an error. We won't go into the details of this class as it is very classic. Suffice it to say that it is called Berjon::ContentTree, and that it has a contentForPath method that takes an array representing path components. If it cannot find the content it will return undef, otherwise it'll return either a Berjon::Page or a Berjon::Resource object, both of which have a few simple methods to access their content. In Reaction lingo this set of classes are what is known as the Domain Model. As its name indicates, this is tasked with encapsulating the business logic for a given model. It knows nothing of the Web application or of its' needs. [Note: I may be confused as to the exact scope of DM vs IM in Reaction, or at least the SC site code doesn't seem to place the limit where I would place it if Shadowcat::Site::Shadowcat::ContentTree's usage of Reaction::InterfaceModel::ObjectClass is anything to go by. Also, our ContentTree manipulates URI parameters, that can't really be good — it should get a path and separate params. I can see an argument for the IM not being just the Model class but something more elaborate that's brought in by it. The following is written as if the Model were the IM since that's what the Controller ties to a VP.] Now we shall move on to building our Interface Model, or IM. The IM is the part that produces a view on the DM that is most usable to the Web application. It has the advantage that since it presents a Façade over the DM, you can change the latter (say, from disk-based storage to RDBM to CouchDB to neural shunts into kitten brains) easily without having to change anything other than the IM in your Web application. Since our DM is trivial, our IM — which is also the Catalyst Model — will be equally trivial, simply providing a dumb wrapper: package Berjon::Web::M::ContentTree; use aliased 'Berjon::ContentTree'; sub COMPONENT { return ContentTree->new; } 1; As you can see there isn't much in the way of smarts wasted here. The COMPONENT method is the one that is called when a Catalyst component in instantiated (when $c->model('ContentTree') is called). We're all set where modelling is concerned, so let's turn to the controller: we need a controller to call in this Interface Model, and bind it to a viewport that will know what to do with it. It also needs to handle pages and resources differently. Here goes: package Berjon::Web::C::Content; use strict; use warnings; use base 'Reaction::UI::Controller'; use aliased 'Berjon::Web::ViewPort::PageContent'; use Carp qw(confess); sub content :Chained('/base') :PathPart('content') :Args { my ($self, $c, @args) = @_; my $cnt = $c->model('ContentTree')->contentForPath(@args); if ($cnt) { if ($cnt->type eq 'page') { $self->push_viewport(PageContent, content => $cnt); } elsif ($cnt->type eq 'resource') { $c->res->content_type($cnt->mimeType); $c->res->content_length($cnt->fileSize); $c->res->body($cnt->content); } else { confess "Unable to handle content object of type " . $cnt->type; } } else { $c->detach('/error_404'); } } 1; Our controller, Berjon::Web::C::Content, inherits from Reaction::UI::Controller (and not Reaction::UI::Controller::Root since it is not the root of the application). It features and action that matches "/content" and is chained to the base action in root (the one that pushes the SiteLayout viewport). Anything after "/content" will be taken as the arguments that will allow us to select the content to show. Any step in that path that contains an = sign will be taken as a parameter. Currently only the 'lang' parameter is recognised, selecting a given language for multilingual content (any other parameter is silently ignored). As you can see there are three options. If the content exists and is a page, a Berjon::Web::ViewPort::PageContent viewport is pushed onto the stack, together with a 'content' parameter that is the IM for this data. Take note of this usage as it is the quintessential Reaction approach. If the content is a resource then it is simply sent to the browser. That is typical Catalyst code without anything Reaction-specific. Finally, if not content is found we detach in favour of the "/error_404" action. That action is provided to us automatically by the root Reaction controller. That's all pretty simple, the viewport isn't more complex: package Berjon::Web::ViewPort::PageContent; use Reaction::Class; use aliased 'Reaction::UI::ViewPort'; use aliased 'Berjon::Page'; class PageContent is ViewPort, which { has 'content' => ( isa => Page, is => 'ro', required => 1, handles => { title => 'title' }, ); }; 1; Basically it has a content field that is configured within the push_viewport call that the controller made. Since the viewport is called "PageContent", we can simply have a page_content.tt template and it will get picked up automatically. For now it doesn't need much content: =for layout widget