Adding a prefix to every URL in CakePHP

deceze picture deceze · Nov 25, 2009 · Viewed 15.7k times · Source

What's the cleanest way to add a prefix to every URL in CakePHP, like a language parameter?

http://example.com/en/controller/action
http://example.com/ru/admin/controller/action

It needs to work with "real" prefixes like admin, and ideally the bare URL /controller/action could be redirected to /DEFAULT-LANGUAGE/controller/action.

It's working in a retro-fitted application for me now, but it was kind of a hack, and I need to include the language parameter by hand in most links, which is not good.

So the question is twofold:

  • What's the best way to structure Routes, so the language parameter is implicitly included by default without having to be specified for each newly defined Route?
    • Router::connect('/:controller/:action/*', ...) should implicitly include the prefix.
    • The parameter should be available in $this->params['lang'] or somewhere similar to be evaluated in AppController::beforeFilter().
  • How to get Router::url() to automatically include the prefix in the URL, if not explicitly specified?
    • Router::url(array('controller' => 'foo', 'action' => 'bar')) should return /en/foo/bar
    • Since Controller::redirect(), Form::create() or even Router::url() directly need to have the same behavior, overriding every single function is not really an option. Html::image() for instance should produce a prefix-less URL though.

The following methods seem to call Router::url.

  • Controller::redirect
  • Controller::flash
  • Dispatcher::__extractParams via Object::requestAction
  • Helper::url
  • JsHelper::load_
  • JsHelper::redirect_
  • View::uuid, but only for a hash generation

Out of those it seems the Controller and Helper methods would need to be overridden, I could live without the JsHelper. My idea would be to write a general function in AppController or maybe just in bootstrap.php to handle the parameter insertion. The overridden Controller and Helper methods would use this function, as would I if I wanted to manually call Router::url. Would this be sufficient?

Answer

deceze picture deceze · Dec 10, 2009

This is essentially all the code I implemented to solve this problem in the end (at least I think that's all ;-)):

/config/bootstrap.php

define('DEFAULT_LANGUAGE', 'jpn');

if (!function_exists('router_url_language')) {
    function router_url_language($url) {
        if ($lang = Configure::read('Config.language')) {
            if (is_array($url)) {
                if (!isset($url['language'])) {
                    $url['language'] = $lang;
                }
                if ($url['language'] == DEFAULT_LANGUAGE) {
                    unset($url['language']);
                }
            } else if ($url == '/' && $lang !== DEFAULT_LANGUAGE) {
                $url.= $lang;
            }
        }

        return $url;
    }
}

/config/core.php

Configure::write('Config.language', 'jpn');

/app_helper.php

class AppHelper extends Helper {

    public function url($url = null, $full = false) {
        return parent::url(router_url_language($url), $full);
    }

}

/app_controller.php

class AppController extends Controller {

    public function beforeFilter() {
        if (isset($this->params['language'])) {
            Configure::write('Config.language', $this->params['language']);
        }
    }

    public function redirect($url, $status = null, $exit = true) {
        parent::redirect(router_url_language($url), $status, $exit);
    }

    public function flash($message, $url, $pause = 1) {
        parent::flash($message, router_url_language($url), $pause);
    }

}

/config/routes.php

Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'display'));
Router::connect('/:language/', array('controller' => 'pages', 'action' => 'display', 'home'), array('language' => '[a-z]{3}'));
Router::connect('/:language/pages/*', array('controller' => 'pages', 'action' => 'display'), array('language' => '[a-z]{3}'));
Router::connect('/:language/:controller/:action/*', array(), array('language' => '[a-z]{3}'));

This allows default URLs like /controller/action to use the default language (JPN in my case), and URLs like /eng/controller/action to use an alternative language. This logic can be changed pretty easily in the router_url_language() function.

For this to work I also need to define two routes for each route, one containing the /:language/ parameter and one without. At least I couldn't figure out how to do it another way.