So I am working on converting a previously-standalone Yii 2 app into a Craft module and am having some issues with routing. I wish to keep my old URL structure as much as possible. As such, I created a plugin and register routes. However, I've noticed an issue: When I call Url::to(), I get back URLs that seem to rely on the actionTrigger parameter, even if I explicitly defined routes in my plugin.

Here are the routes as I have them in my plugin:

class CustomerPortal extends \craft\base\Plugin
    public function init()
            function(\craft\events\RegisterUrlRulesEvent $event) {
                $event->rules['portal/<action:[\w-+]>'] = 'customer-portal/portal/<action>';

If I make the call in a controller to redirect to a route, it builds the URL using an action param, not the rules we defined. For example:

class UserController extends \craft\web\Controller
    public function actionRedirect()
        echo Url::to(['checkout');
        // I'd expect to see '/checkout', but instead we get 
        // '/actions/customer-portal/user/checkout' since we aren't
        // applying either of our rules.

    public function actionCheckout()

It seems that the rules are checked when parsing, but not during URL creation, and if you come in with an action prefix, the rules aren't parsed then, but the raw action is attempted.

Am I missing something or is this a bug and either way, can anyone think of a way to work around it?

Edit: Figured out the issue with routes not resolving I had originally, it turns out in the end, my issues was that the regex in the rules wasn't accounting for the fact that many of routes have dashes in them (for example: /user/billing-info). The default Yii docs say to use <action:\w+>, but that doesn't match the dash character in the URL. Using <action:[\w-]+> works perfectly and makes this all work.

That said, I'm still not getting Url::to or anything else that references UrlManager to work properly since UrlManager seems to add the action param to the URL before checkout to see if we match existing routes.

  • Craft checks for your custom routes before adding an action param. You just have to declare the route correctly. Oct 21, 2017 at 22:10

2 Answers 2



I'm so sorry - I messed it up... I rechecked the craft code and I was all the way wrong with the first answer. I hope you had no trouble because of me.

The correct answer is this one....

Event::on(UrlManager::class, UrlManager::EVENT_REGISTER_SITE_URL_RULES, function(RegisterUrlRulesEvent $event) {
    $event->rules['redirect/<action:\w+>'] = 'customer-portal/portal/';
    $event->rules['checkout']              = 'customer-portal/portal/checkout';

and in your controller

public function actionIndex(){
    $request = Craft::$app->getRequest();
    $method = $request->getParam('action');
    return $this->redirect($method);

In order to have access to the backend function you need to make it "public" so you have to include a protected $allowAnonymous = true; or an array with all functions protected $allowAnonymous = ['functionOne', 'functionTwo']; Your Controller could look like this

class UserController extends Controller {
    protected $allowAnonymous = true;

    // pretend the request is /redirect/foo and we have created the rule 
    // $event->rules['foo']                    = 'customer-portal/user/foo'; and
    // $event->rules['redirect/<action:\w+>']  = 'customer-portal/user/redirect';
    public function actionRedirect(){
        $request = Craft::$app->getRequest();
        $method = $request->getParam('action');
        return $this->redirect($method);

    public function actionFoo(){


Old Post... ignore this

It's not a bug, that's the way how craft handles it. Unlike in other cms plugins in craft are not supposed to handle entire frontend page requests in order to render the entire page. You can access actions from controllers only via the /action url. Everything else is handled by entries/elements

So you'll not be able to achieve what you want since craft tries to access an entry when you don't include an action url. All frontend request without an action url need to be related to an entry/element or a category. So when you try to access /redirect craft will search for an entry with the slug redirect

What you want is the following:

  1. creating an entry with the slug "redirect"
  2. $entry = Craft::$app-> //your logic to fetch this specific entry
  3. $this->redirect($entry->url)

When you want to create a plugin for other people to use it, you have to create a settings page, include a relation field to that and let other people link an entry to the redirect -> you can fetch this entry from your settings and redirect to it.

The other thing... $event->rules['portal/<action>'] = 'customer-portal/portal/<action>'; is wrong.. Take a look at this documentation
$event->rules['portal/<action:\w+>'] = 'customer-portal/portal/handle-redirect'; and in your portalController

public function actionHandleRedirect(){
    $request = Craft::$app->getRequest();
    $function = "action" . $request->getParam('action');

But I would suggest you not to do that and use the craft way to handle these things


The example given by Robin didn't work for me. The action param needs to be a param of the actionIndex function:

public function actionIndex($action){
  • Indeed they changed the behavior about 5 months ago. Thanks for noting, I forgot about it Apr 24, 2018 at 8:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.