XF 2.1 Create a new url pattern for some threads without using route filters


I'm doing some development on a huge forum with about 3.5 million threads.
I have an extended class for XF:Thread in an addon that is adding a new field is_faq which is a boolean.

I have about 6000 urls to convert to /faq/... instead of /threads/...
First, in my extended class, just before the parent action, I was adding route filters if it where not existing for threads having 1 in my is_faq field and it was working.

Some flagged threads where well redirected to /faq/... instead of /threads/... .

But my problem is that when I reached about 5000 urls in route filter table, my Xenforo forum became dramatically slow because for each urls built in the page, it was doing 5000 preg_replace and about 10 million on a page then.

So as this is not possible this way, I want to avoid using route filters, maybe creating a new route which would take care about my DB field is_faq.

Is that possible ? (so that my forum listings would build well /faq/... links for the flagged threads and without using route filters. )

Or another suggestion would be to rework/improve the Router::applyRouteFilterToUrl method in order to avoid this CPU load due to the number of preg_replace done for 1 link.
Last edited:
Here is my solution:
  • Create a new addon MyNewAddon (for example)
  • Create a Listener.php with a xfThreadStructure method and add it in the admin panel in the Development tab in Code event listeners mapped to your addon:

# File: Listener.php
namespace MyNewAddon;

use XF\Mvc\Entity\Entity;

class Listener
    public static function xfThreadStructure(\XF\Mvc\Entity\Manager $em, \XF\Mvc\Entity\Structure &$structure)
        $structure->columns['is_faq'] = ['type' => Entity::INT, 'changeLog' => false, 'default' => 0];


  • Create an extended Thread class and add it in the admin panel in the Development tab in Class extensions:
# File: XF/Pub/Controller/Thread.php

namespace MyNewAddon\XF\Pub\Controller;

use XF\Mvc\FormAction;
use XF\Mvc\ParameterBag;
use XF\Mvc\Reply\AbstractReply;

class Thread extends XFCP_Thread
    protected $isFaq = 0;

    public function actionIndex(ParameterBag $params) {
        // FAQ threads.
        $thread = $this->assertViewableThread($params->thread_id, $this->getThreadViewExtraWith());
        $this->isFaq = $thread->is_faq;
        return parent::actionIndex($params);

    public function assertCanonicalUrl($linkUrl) {
        $basePath = $this->request->getBasePath();
        $requestUri = $this->request->getRequestUri();

        $routeBase = ltrim(substr($requestUri, strlen($basePath)), '/');
        if(property_exists($this, 'isFaq') && $this->isFaq && (strpos($routeBase, 'faq/') === 0 || strpos($routeBase, 'threads/') === 0)) {
            // we have an FAQ.
            $linkUrl = str_replace('/threads', '/faq', $linkUrl);

        return parent::assertCanonicalUrl($linkUrl);

Then either you can add the field to MySQL DB or add a Setup.php field to your addon to add this field on install:
ALTER TABLE `xf_thread` ADD `is_faq` INT(1) NOT NULL DEFAULT '0';

So if you want your thread to work on /faq/.... instead of /threads/.... patterns, you just need to flag it to 1 in the DB.
Top Bottom