Handling URL Path Prefixes
==========================
The Problem
-----------
Suppose you want your site URLs to be prefixed with the language the pages are delivered in, but you don't necessarily want to set up all your controllers to be aware of that prefix (via chaining or some other mechanism).
For example, you might want URL paths that look like "__`/en`__`/user/view`" and "__`/fr`__`/user/view`".
A Solution
----------
At the beginning of the request cycle, the engine uses `$c->prepare_path` to handle the incoming request URI path.   In MyApp.pm, you can subclass `$c->prepare_path` to modify the way the path processing is done.
Dispatching to controller actions is based on `$c->request->path`, so we will need to strip the language prefix from that path.   The trick is to keep `$c->request->uri`, `$c->request->base`, and `$c->request->path` consistent with each other.   For example, if we originally have:
$c->request->uri : http://localhost:3000/en/user/view
$c->request->base : http://localhost:3000/
$c->request->path : en/user/view
we will want to end up with:
$c->request->uri : http://localhost:3000/en/user/view
$c->request->base : http://localhost:3000/en/
$c->request->path : user/view
In other words, we always want:
$c->request->uri eq $c->request->base . $c->request->path
Here is an example:
package MyApp;
use Catalyst::Runtime '5.70';
use base 'Catalyst';
use Catalyst qw/-Debug/;
__PACKAGE__->setup();
# This could be pulled in from config or database:
#
my %valid_languages = map { $_ => 1 } qw(en fr es);
sub prepare_path
{
my $c = shift;
$c->NEXT::prepare_path(@_);
my @path_chunks = split m[/], $c->request->path, -1;
# Ignore paths that don't start with a valid language:
return unless @path_chunks && $valid_languages{$path_chunks[0]};
$self->_dump_paths($c) if $c->debug;
_dump_paths($c) if $c->debug;
# Found a language, pull it off the beginning of the path:
my $language = shift @path_chunks;
# Create modified request path from any remaining path chunks:
my $path = join('/', @path_chunks) || '/';
# Stuff modified request path back into request:
$c->request->path($path);
# Modify the path part of the request base
# to include the path prefix:
my $base = $c->request->base;
$base->path($base->path . "$language/");
$self->_dump_paths($c) if $c->debug;
_dump_paths($c) if $c->debug;
$c->stash->{language} = $language; # save language for later
return;
}
sub _dump_paths
{
my ($self, $c) = @_;
my ($c) = @_;
my $indent = '.' x length($c->request->base);
$c->log->debug('Paths:',
"\t\$c->request->uri: " . $c->request->uri,
"\t\$c->request->base: " . $c->request->base,
"\t\$c->request->path: $indent" . $c->request->path
);
}
A nice side effect is that tweaking `$c->request->base` to include the language means that the language will be automatically included when you call `$c->uri_for` later on.
In the above example, request paths that do not begin with a valid language are passed through unchanged.   But suppose for sake of consistency you'd like to make those requests look as if they had a default language prefix.   In that case, if we originally have:
$c->request->uri : http://localhost:3000/user/view
$c->request->base : http://localhost:3000/
$c->request->path : user/view
we might want to end up with:
$c->request->uri : http://localhost:3000/en/user/view
$c->request->base : http://localhost:3000/en/
$c->request->path : user/view
Note that in this situation we need to modify `$c->request->uri` instead of `$c->request->path`:
sub prepare_path
{
my $c = shift;
$c->NEXT::prepare_path(@_);
my $language = 'en'; # default
my @path_chunks = split m[/], $c->request->path, -1;
if (@path_chunks && $valid_languages{$path_chunks[0]})
{
# Found a language, pull it off the beginning of the path:
$language = shift @path_chunks;
# Create modified request path from any remaining path chunks:
my $path = join('/', @path_chunks) || '/';
# Stuff modified request path back into request:
$c->request->path($path);
}
else
{
# Modify the path part of the URI to look as if it had a language prefix:
$c->request->uri->path("$language/" . $c->request->path);
}
# Modify the path part of the request base
# to include the path prefix:
my $base = $c->request->base;
$base->path($base->path . "$language/");
$c->stash->{language} = $language; # save language for later
return;
}