• This forum has been archived. New threads and replies may not be made. All add-ons/resources that are active should be migrated to the Resource Manager. See this thread for more information.

Creating Advanced Sub-Route Controllers

Jaxel

Well-known member
I figured this out the other week, and I've been since refining it, working out small kinks and all that. Now it looks like I've finally completely figured it out... XenForo does not have sub-route prefixes built in, it only does basic route prefixing. In order to do sub-route prefixes, you pretty much have to "trick" the routing system into doing what you want. I now use this prefix system on 4 different mods, and it all works well.

First lets look at a basic route controller without sub-routes...
PHP:
<?php

class EWRporta_Route_Portal implements XenForo_Route_Interface
{
	public function match($routePath, Zend_Controller_Request_Http $request, XenForo_Router $router)
	{
		$action = $router->resolveActionWithStringParam($routePath, $request, 'module_name');
		return $router->getRouteMatch('EWRporta_ControllerPublic_Portal', $action, 'portal', $routePath);
	}

	public function buildLink($originalPrefix, $outputPrefix, $action, $extension, $data, array &$extraParams)
	{
		return XenForo_Link::buildBasicLinkWithStringParam($outputPrefix, $action, $extension, $data, 'module_name');
	}
}
This is very basic, the "match" function will take the string a user is trying to go to, resolve the action of a string parameter, and then forward to the public controller. So if its "/portal/", it will go to the index action. If its "/portal/twitter/", it will go to the index action, and store the variable "twitter" as the variable "module_name". If its "/portal/twitter/edit/", it will instead take the variable to the edit action. If you choose not to resolve the action with a parameter, it will instead read "twitter/edit" as the action, instead of just "edit"; but we wont be getting into that till later.

The problem with sub routes would be something like "/media/category/catname.27/edit/". You would want to go to the edit action, with "catname.27" as the parameter, and "category" as a sub-route. Unfortunately, with the default routing structure, "category" is read as the parameter, and "catname.27/edit" is read as the action. So basically what we need to do is parse "/media/category/" as the route (instead of just "/media/" before we bring it to the resolving action step.

So what we need to do is parse the route manually:
PHP:
		$components = explode('/', $routePath);
		$subPrefix = strtolower(array_shift($components));
		$subSplits = explode('.', $subPrefix);
First we take the route path and split it into components separated by "/"; routePath is everything after the initial route; so if we had "yourwebsite.com/media/category/catname.27/edit/", the routePath would be "category/catname.27/edit/". Then we take the first element off the routePath and store it in the "subPrefix" variable. "subSplits" will split the subPrefix variable by "."; I will explain why we do this soon...

PHP:
		$controllerName = '';
		$action = '';
		$intParams = '';
		$strParams = '';
		$slice = false;

		switch ($subPrefix)
		{
			case 'comment':		$controllerName = '_Comment';	$intParams = 'comment_id';		$slice = true;	break;
			case 'playlist':	$controllerName = '_Playlist';	$intParams = 'playlist_id';		$slice = true;	break;
			case 'category':	$controllerName = '_Category';	$intParams = 'category_id';		$slice = true;	break;
			case 'user':		$controllerName = '_User';		$intParams = 'user_id';			$slice = true;	break;
			case 'keyword':		$controllerName = '_Keyword';	$strParams = 'keyword_text';	$slice = true;	break;
			case 'service':		$controllerName = '_Service';	$strParams = 'service_slug';	$slice = true;	break;
			default :
				if (is_numeric(end($subSplits))) { $controllerName = '_Media'; $intParams = 'media_id'; }
		}
Here we run the "subPrefix" through a switch to determine where we would like to send the action to. If all checks on the switch fail, the controller will default as: "EWRmedio_ControllerPublic_Media"; anything not within a subPrefix will go there. If the subPrefix matches any of the cases, it will assign some variables. In this case of "yourwebsite.com/media/category/catname.27/edit/", it matches the word "category".

For matching the word "category", the first thing we do is define a sub controller as "_Category". This way, the controller will go to "EWRmedio_ControllerPublic_Media_Category", instead of the default explained above. The next thing we do is define a possible parameter. In this case, the routing system will look for an integer in "catname.27", and store that in a variable called "category_id". Next we will set "slice" to true; which simply marks that a sub-route was found and sliced out.

You may also notice the if condition in the default case. Normally, if I didn't do this, there would be no resolve action with parameter (as no parameter was defined), and everything without a sub-route would be read as an action. So while "/media/submit/" would be correct with "submit" being the action, "/media/name.1266/edit" would not be correct because we would prefer to get the integer 1266 as a variable, and "edit" as the action, instead of "name.1266/edit".

So by checking the "subSplit" for a number at the end of the split, we are determining if the user is trying to go to a non-sub-route action, or a non-sub-route media ID. Notice that after we check this, we DONT set "slice" to true, since we are not going through a sub-route, so nothing has to be sliced off. However, do make note that we still sent the user to a sub controller "EWRmedio_ControllerPublic_Media_Media". Basically we are tricking the system into thinking "name.1266" is the sub-route, while at the same time storing 1266 in a variable, since technically its not.

This if condition is OPTIONAL and is only required if you wish to send a match variable to a sub-controller instead of the main controller. If you didn't want to use the sub-controller, just replace the entire if condition with "$intParams = 'media_id';". This way it will still check for the integer parameter, but will not set the sub controller. You can easily combine the two controllers; but I separated them just to be neat.

PHP:
		$routePathAction = ($slice ? implode('/', array_slice($components, 0, 2)) : $routePath).'/';
		$routePathAction = str_replace('//', '/', $routePathAction);
This next step is very important. Depending on whether or not a sub-route was found, we need to rebuild the routePath in order to parse it for action resolving. If a sub-route was not found, we simply use the original routePath; however if one was found, we need to rebuild based on the remaining components with the subPrefix shifted off. We limit the slice to only 2 parts because XenForo likes to get a bit overzealous with the action routing.

We then apply a "/" and strip out extras so that a url such as "/media/submit/" and "/media/submit" go to the same place; as there are certain cases where they wont.

PHP:
		if ($strParams)
		{
			$action = $router->resolveActionWithStringParam($routePathAction, $request, $strParams);
		}
		else
		{
			$action = $router->resolveActionWithIntegerParam($routePathAction, $request, $intParams);
		}
Next is simple, we resolve the action depending on what type of parameter the subPrefix switch has determined. When checking for a string parameter, it will always try to find a string, which is why it's checked explicitly. On fail we always try to check for an integer because XenForo automatically handles itself very well when an integer isn't found and reads the lack of integer as an action, instead of a parameter. The parameters in this step can be accessed using basic input filtering in your controller.

PHP:
		$action = $router->resolveActionAsPageNumber($action, $request);
		return $router->getRouteMatch('EWRmedio_ControllerPublic_Media'.$controllerName, $action, 'media', $routePath);
Finally we check for a page number and store it in the "page" input variable. Then we return the final outcome and actions to the appropriate controller.
 
The build function is basically the same, but reversed:
PHP:
    public function buildLink($originalPrefix, $outputPrefix, $action, $extension, $data, array &$extraParams)
    {
        $components = explode('/', $action);
        $subPrefix = strtolower(array_shift($components));

        $intParams = '';
        $strParams = '';
        $title = '';
        $slice = false;

        switch ($subPrefix)
        {
            case 'comment':        $intParams = 'comment_id';                                    $slice = true;    break;
            case 'playlist':    $intParams = 'playlist_id';        $title = 'playlist_name';    $slice = true;    break;
            case 'category':    $intParams = 'category_id';        $title = 'category_name';    $slice = true;    break;
            case 'user':        $intParams = 'user_id';            $title = 'username';        $slice = true;    break;
            case 'keyword':        $strParams = 'keyword_text';                                $slice = true;    break;
            case 'service':        $strParams = 'service_slug';                                $slice = true;    break;
            default:            $intParams = 'media_id';        $title = 'media_title';
        }

        if ($slice)
        {
            $outputPrefix .= '/'.$subPrefix;
            $action = implode('/', $components);
        }

        $action = XenForo_Link::getPageNumberAsAction($action, $extraParams);

        if ($strParams)
        {
            return XenForo_Link::buildBasicLinkWithStringParam($outputPrefix, $action, $extension, $data, $strParams);
        }
        else
        {
            return XenForo_Link::buildBasicLinkWithIntegerParam($outputPrefix, $action, $extension, $data, $intParams, $title);
        }
    }

As you can see... we split up the components, search for the subPrefix and output based on the parameter type. Just about the only thing done differently is that instead of slicing off the sub-prefix like we did in the match function, we are instead attaching the sub-prefix to the initial prefix.
 
Bringing it all together...
PHP:
<?php

class EWRmedio_Route_Media implements XenForo_Route_Interface
{
    public function match($routePath, Zend_Controller_Request_Http $request, XenForo_Router $router)
    {
        $components = explode('/', $routePath);
        $subPrefix = strtolower(array_shift($components));
        $subSplits = explode('.', $subPrefix);

        $controllerName = '';
        $action = '';
        $intParams = '';
        $strParams = '';
        $slice = false;

        switch ($subPrefix)
        {
            case 'comment':        $controllerName = '_Comment';    $intParams = 'comment_id';        $slice = true;    break;
            case 'playlist':    $controllerName = '_Playlist';    $intParams = 'playlist_id';        $slice = true;    break;
            case 'category':    $controllerName = '_Category';    $intParams = 'category_id';        $slice = true;    break;
            case 'user':        $controllerName = '_User';        $intParams = 'user_id';            $slice = true;    break;
            case 'keyword':        $controllerName = '_Keyword';    $strParams = 'keyword_text';    $slice = true;    break;
            case 'service':        $controllerName = '_Service';    $strParams = 'service_slug';    $slice = true;    break;
            default :
                if (is_numeric(end($subSplits))) { $controllerName = '_Media'; $intParams = 'media_id'; }
        }

        $routePathAction = ($slice ? implode('/', array_slice($components, 0, 2)) : $routePath).'/';
        $routePathAction = str_replace('//', '/', $routePathAction);

        if ($strParams)
        {
            $action = $router->resolveActionWithStringParam($routePathAction, $request, $strParams);
        }
        else
        {
            $action = $router->resolveActionWithIntegerParam($routePathAction, $request, $intParams);
        }

        $action = $router->resolveActionAsPageNumber($action, $request);
        return $router->getRouteMatch('EWRmedio_ControllerPublic_Media'.$controllerName, $action, 'media', $routePath);
    }

    public function buildLink($originalPrefix, $outputPrefix, $action, $extension, $data, array &$extraParams)
    {
        $components = explode('/', $action);
        $subPrefix = strtolower(array_shift($components));

        $intParams = '';
        $strParams = '';
        $title = '';
        $slice = false;

        switch ($subPrefix)
        {
            case 'comment':        $intParams = 'comment_id';                                    $slice = true;    break;
            case 'playlist':    $intParams = 'playlist_id';        $title = 'playlist_name';    $slice = true;    break;
            case 'category':    $intParams = 'category_id';        $title = 'category_name';    $slice = true;    break;
            case 'user':        $intParams = 'user_id';            $title = 'username';        $slice = true;    break;
            case 'keyword':        $strParams = 'keyword_text';                                $slice = true;    break;
            case 'service':        $strParams = 'service_slug';                                $slice = true;    break;
            default:            $intParams = 'media_id';        $title = 'media_title';
        }

        if ($slice)
        {
            $outputPrefix .= '/'.$subPrefix;
            $action = implode('/', $components);
        }

        $action = XenForo_Link::getPageNumberAsAction($action, $extraParams);

        if ($strParams)
        {
            return XenForo_Link::buildBasicLinkWithStringParam($outputPrefix, $action, $extension, $data, $strParams);
        }
        else
        {
            return XenForo_Link::buildBasicLinkWithIntegerParam($outputPrefix, $action, $extension, $data, $intParams, $title);
        }
    }
}
 
Top Bottom