I did something similar, but in a different way. The end result is that I can define my redirects and aliases in configuration files. For example:
'/about',
'/articles/grips-guide' => '/articles/grips',
'/articles/grips/the-grips' => '/articles/grips/holds',
'/articles/grips/holds/basic' => '/articles/grips/holds/forehand',
];
The indentation is "cosmetic". My convention is that, when a whole folder is redirected, I define that first. If any pages have also changed, these redirects are defined afterwards, using the new folder URL.
Instead of replacing the router, I created a links.correct
filter that runs for all requests:
// At the top of routes.php
Route::when( '*', 'links.correct' );
This runs my LinkResolver
filter:
link = App::make('LinkResolver');
$this->link->resolve( Request::rawUrl() );
// Store the results in the container for later
// Maybe this should be changed to something more
// maintainable / explicit!
App::getFacadeRoot()->BBappUrl = $this->link->url;
App::getFacadeRoot()->BBappView = $this->link->view;
// Enforce SSL
if ( !Request::secure() )
{
$link->status = 301;
}
// For redirection after login, etc.
if ( $this->allowedForRedirectIntended() )
{
Session::put( 'url.intended', $this->link->url );
}
if ( $this->link->status !== 200 )
{
return Redirect::secure( $this->link->url, $this->link->status );
}
}
private function allowedForRedirectIntended()
{
// We don't want login page to wipe out the intended page
$disallowedPages = [
'/login',
'/logout',
];
// We don't want validation errors to wipe out the intended page
$disallowedStatuses = [ 302 ];
return !in_array($this->link->url, $disallowedPages)
&& !in_array($this->link->status, $disallowedStatuses);
}
}
(Note that I did extend the Request
class, purely for dealing with multiple slashes.)
The "heavy lifting" is done by a separate LinkResolver
class:
assembleConfigData();
}
public function resolve($path)
{
$this->url = $path;
$this->view = $path;
// Remove query string, unless we're on a page that needs it
if ( !$this->pageRequiresQueryString() )
{
$this->removeQuery();
}
// Special case for English homepage, i.e. '/' route
if ( $this->url === '/' )
{
return;
}
// Is this an old link with a .php extension?
$this->removeExtension();
// Are there any extra slashes?
$this->removeSlashes();
// Has this page or folder been moved / renamed?
$this->resolveRedirects();
// Is this folder an alias? E.g. '/de' maps to '/languages/de'
$this->resolveAliases();
}
private function removeQuery()
{
// Redirect if there was a query string
if ( $this->getQuery() )
{
$this->url = $this->getQuery()['base'];
$this->view = $this->getQuery()['base'];
$this->status = 301;
}
}
private function getQuery()
{
$parts = explode('?', $this->url, 2);
if ( isset($parts[1]) )
{
return [ 'base' => $parts[0], 'query' => '?' . $parts[1] ];
}
return false;
}
private function removeExtension()
{
if ( ends_with($this->url, '.php') )
{
$this->url = substr($this->url, 0, -4);
$this->status = 301;
}
}
private function removeSlashes()
{
// Replace multiple slashes with a single slash, i.e. '//' --> '/'
while ( strpos($this->url, '//') !== false )
{
$this->url = str_replace('//', '/', $this->url);
$this->status = 301;
}
// Remove trailing slash, even if there's still a query string (Paypal!)
$query = '';
if ( $this->getQuery() )
{
$query = $this->getQuery()['query'];
$this->url = $this->getQuery()['base'];
}
if ( ends_with($this->url, '/') )
{
$this->url = substr( $this->url, 0, -1 );
$this->status = 301;
}
$this->url .= $query;
}
private function resolveRedirects()
{
foreach ($this->redirects as $old => $new)
{
if ( starts_with($this->url, $old) )
{
$this->url = str_replace_once( $old, $new, $this->url );
$this->status = 301;
// Check for any later fragments of the URL that match
$this->resolveRedirects( $this->url );
}
}
}
private function resolveAliases()
{
foreach ($this->aliases as $alias => $real)
{
if ( starts_with($this->url, $alias) )
{
$this->view = str_replace_once($alias, $real, $this->url);
}
}
}
private function assembleConfigData()
{
$this->languages = Config::get('links/languages');
$this->redirects = Config::get('links/redirects');
$this->aliases = Config::get('links/aliases');
// Copy redirects & aliases for languages: all languages share English
// URL patterns. Also add the base URL of '/languages/' as an alias
foreach($this->languages as $lang)
{
// Alias '/french' to '/languages/french'
$this->aliases['/' . $lang] = '/languages/' . $lang;
// Reuse redirect patterns from English
foreach($this->redirects as $old => $new)
{
$this->redirects['/' . $lang . $old] = '/' . $lang . $new;
}
// Reuse alias patterns from English
foreach($this->aliases as $alias => $real)
{
$this->aliases['/' . $lang . $alias] = '/' . $lang . $real;
}
}
// Force aliases to be used, i.e. redirect from real path to alias
foreach($this->aliases as $alias => $real)
{
$this->redirects[$real] = $alias;
}
}
private function pageRequiresQueryString()
{
$queryPages = Config::get('links/queryStringPages');
if ( in_array( $this->getQuery()['base'], $queryPages) )
{
return true;
}
return false;
}
}
What this essentially does is take my URL logic out of .htaccess
, where it should never belong. .htaccess
is a server configuration override file. Cramming tonnes of redirects in there is horrendously messy.
Instead, I want my redirection logic contained in my PHP code, where it is easily testable, and where I can use separate configuration files for each concern.