Application-wide access to the context ($c) for the impatient
Overview
MVC is a proven development pattern, and its rules and best practices are certainly worthwhile.
Still, sometimes you just want to get at $c
from the outside of controller code, and
don't want to fuss with passing it around in argument lists.
Isn't there an easier way to just DWIW when I'm in a hurry?
stuff $c in a localized global
Sometimes you just need a global variable. By making use of the
local keyword, global
variables can be isolated to a particular scope. We're going to
use this technique to "localize" a reference to $c
to be globally accessible, but only within
the scope of the request it applies to.
First, we need to declare a package/global variable someplace. Your main app class is a good choice (as shown below), but it could be anywhere.
package MyApp; use Catalyst; # ... our $__ACTIVE_CTX = undef; sub ctx { $__ACTIVE_CTX }
I also like to create a nice, clean accessor method (->ctx
as shown above) to use instead of
accessing the variable directly. This will enable us to get $c from anyplace like this:
my $c = MyApp->ctx;
Which is exactly equivalent to:
my $c = $MyApp::__ACTIVE_CTX;
Now we just need to hook into the right place to capture $c
and localize it into our global
variable.
The Catalyst dispatch()
method provides the obvious choice. It is called immediately after $c
is constructed by prepare()
and is the gatekeeper for each request. All the processing that
happens during a normal request cycle is ultimately contained within the dynamic scope of the
dispatch()
method.
We can wrap this logic very easily with an around modifier and localize our variable:
around 'dispatch' => sub { my ($orig, $c, @args) = @_; local $__ACTIVE_CTX = $c; $c->$orig(@args) };
Now, throughout your application you will be able to access the context object simply by calling
MyApp->ctx
which will work within any code in any class so long as it is called during a
request. This includes Models, but also other general perl packages/modules that you may call out
to, such as DBIC result classes.
Outside of a request
Since Catalyst is a web framework, most code is generally expected to be called during a request,
but not always. There are plenty of other possible scenarios, such as admin scripts, cron jobs, etc
that won't touch the dispatch machinery. For these cases, our ->ctx()
method will simply
return undef
since that is what we set it to when we declared it. By using local
to set the
value, it is isolated to that dynamic scope and remains undef from the perspective of everywhere
else. So, we don't have to worry about stale data, exceptions/interruptions, and so on. When the
request ends and dispatch()
returns, the localized version of the variable goes out of scope and
ceases to exist.
This provides a reasonable degree of confidence that our ->ctx()
method will always return the
correct context object when a request is in progress, and false/undef when it is not.
In your code, you should always handle both cases with conditional logic, even if you only expect to be called during a request. For instance:
if ( my $c = MyApp->ctx ) { # do stuff specific to web request, apply permissions, etc # ... } else { # do stuff specific to non-requests # Maybe die if that should not happen/be allowed # Maybe skip permission logic, logging, etc # ... }
In many cases this can also be as simple as:
my $c = MyApp->ctx or die "Not in a request!";
This is at least better/more descriptive than the "can't call method on undefined value" type exception you'll get if you simply assume you're in a request and blindly try to use $c.
Caveats
This should be filed under the category of "quick and dirty" and is still not recommended to be your first design choice. It is handy for getting stuff working quickly and in a pinch.
Also, note that this should not be expected to work under async scenarios -- only when you are deploying with standard synchronous worker threads.
Author
Henry Van Styn vanstyn@cpan.org