XF 2.1 Showing custom index page with specified categories


What I'm trying to do is add a custom Page (which I've added a menu tab for) which shows a forum index with only specific categories listed. These categories will have "Display in the node list" disabled so they don't show up on the main forum index, only on the custom page.

I'm of course trying to set it up using a PHP Callback. Since I want to be able to specify which categories to show within the page's template HTML, rather than having to edit the PHP file to update which categories are shown, I opted to use callback references within the template HTML rather than in the PHP Callback field for the Page.

So, for example, within the template HTML to show 3 categories with the specified node IDs:
<xf:callback class="Brettflan\ShowContent" method="getCategoryHtml" params="[11]"></xf:callback>
<xf:callback class="Brettflan\ShowContent" method="getCategoryHtml" params="[17]"></xf:callback>
<xf:callback class="Brettflan\ShowContent" method="getCategoryHtml" params="[24]"></xf:callback>

I looked around for someone having tried something similar, but the closest I could find was this for XF 1.5, to show a Forum within a Page:
Not quite what I'm after, and not working in XF 2.1 anyway, but it gave me some points of reference at least.

So after spending hours mostly digging around through XF's code and the online documentation, among other efforts, this is the mess I've currently reached trying to work out the proper method to get the properly templated (with nodes and other content potentially hidden based on the viewer's permissions) output, largely debug code which has been commented out:
    public static function getCategoryHtml($contents, $params)
//        $req = new Request(new InputFilterer);
        $req = \XF::app()->request();
        $req->set('node_id', $params[0]);
//        $controller = \XF::app()->controller('XF:Category', $req);
        $controller = new CatShow(\XF::app(), \XF::app()->request());
        $parBag = new ParameterBag(['node_id' => $params[0]]);

        $controller->preDispatch('index', $parBag);

//        echo '<pre>'; var_dump(get_class_methods($controller)); echo '</pre>'; return '';
        $controllerResponse = $controller->actionIndex($parBag);
//        $controllerResponse = $controller->{'actionIndex'}($parBag);
        /* Set the param called 'visitor' in the controller response. This param is used in the thread_list template. */
        $visitor = \XF::visitor();
        $controllerResponse->setParam('visitor', $visitor->toArray());
//        echo '<pre>'; var_dump($controllerResponse->getParams()); echo '</pre>';
//        echo '<pre>'; print_r(get_class_methods($controllerResponse)); echo '</pre>'; return '';
//        return '';
//  ----------

//        $theNode = \XF::finder('XF:Category')->where('node_id', $params[0])->fetchOne();
        $theNode = \XF::finder('XF:Node')->where('node_id', $params[0])->fetchOne();
//        $data = $this->getTemplateData($theNode);

        $nodeRepo = \XF::repository('XF:Node');
        $childNodes = $nodeRepo->findChildren($theNode, false)->fetch();

//        echo '<pre>'; var_dump($childNodes->toArray()[9]->getNodeListExtras()); echo '</pre>';
//        echo '<pre>'; var_dump($childNodes->toArray()); echo '</pre>';
//        return '';

        $dat['node'] = $theNode;
        $dat['extras'] = $theNode->getNodeListExtras();
        $dat['children'] = $childNodes->toArray();
        $dat['childExtras'] = [];
        $dat['depth'] = 1;
//        var_dump(\XF::app()->templater()->renderMacro('public:node_list_category', 'depth1', $dat));
        return \XF::app()->templater()->renderMacro('public:node_list_category', 'depth1', $dat);
//        return '';

The upper part was mostly trying to work out how to do it similar to that guide for showing a forum in a page for XF 1.5, and the bottom is mostly from my other efforts. The upper part stalled out.
The bottom part does partly work at this point, in that the category header is shown, but the main problem I'm then facing is that I can't find the right way to get the proper data for 'children' and 'childExtras'. As for 'children', it seems that should be an array of the child nodes (read as [$id] => $child), with each $child being in the form of an \XF\Tree. At the moment I can't figure out how I can get that info.

The template errors from the above code (again, it does at least show the templated category header, but not the forums or other info below that):
  • Template public:forum_list: Accessed unknown getter 'record' on XF:Node[9] (src/XF/Mvc/Entity/Entity.php:190)
  • Template public:forum_list: Accessed unknown getter 'children' on XF:Node[9] (src/XF/Mvc/Entity/Entity.php:190)
  • Template public:forum_list: Cannot call method getNodeTemplateRenderer on a non-object (NULL) (src/XF/Template/Templater.php:970)
  • Template public:forum_list: Illegal string offset 'macro' (internal_data/code_cache/templates/l1/s1/public/forum_list.php:22)
  • Template public:forum_list: Illegal string offset 'template' (internal_data/code_cache/templates/l1/s1/public/forum_list.php:32)

Also, CatShow definition for reference, used to get around assertCanonicalUrl() causing failure and being unnecessary for this:
use \XF\Pub\Controller\Category;

class CatShow extends Category
    public function assertCanonicalUrl($linkUrl) { return; }

Anyone have a better idea of how to do this in XF 2.1, or any other thoughts? I'm about at my wit's end here.
Last edited:


OK, all it needed was for me to take a bit of a break, and then come back and look through the code a bit again... specifically, I realized the controller response from the upper test code in fact contained the child tree and child extras. That was easy, and should have been staring me in the face already. So:
        $dat['children'] = $controllerResponse->getParams()['nodeTree'];
        $dat['childExtras'] = $controllerResponse->getParams()['nodeExtras'];

That seems to work, displaying the fully templated category and child boards, just like the index. Awesome! I just need to go back through and clean it up quite a bit, and do a security pass mostly to make sure boards and categories won't show when the visitor shouldn't be able to see them.


OK, for reference, this is the final method after a little spit and polish. I found that specifying the visitor wasn't even necessary, it still properly hides forums that the visitor doesn't have permission to see.

    // call using this XF template syntax, can go in a Page Node or similar  (replace $catID with a category ID #):
    //        <xf:callback class="Brettflan\ShowContent" method="getCategoryHtml" params="[$catID]"></xf:callback>
    public static function getCategoryHtml($contents, $params)
      // make sure proper integer param was passed
        if ( !isset($params[0]) || !( is_int($params[0]) || ctype_digit($params[0]) ) )
            throw new \InvalidArgumentException("getCategoryHtml: you must pass an integer representing the node ID for the target category.");

        // get Index response from Category controller, for specified category node index
        $controller = new CatShow(\XF::app(), \XF::app()->request());
        $parBag = new \XF\Mvc\ParameterBag(['node_id' => $params[0]]);
        $response = $controller->actionIndex($parBag);

        // prepare needed data from response
        $resParams = $response->getParams();
        $catNode = $resParams['category']->Node;
        $data = [
            'node' => $catNode,
            'extras' => $catNode->getNodeListExtras(),
            'children' => $resParams['nodeTree'],
            'childExtras' => $resParams['nodeExtras'],
            'depth' => 1,

        // pass data to templater for "node_list_category" template's "depth1" macro, and return result
        return \XF::app()->templater()->renderMacro('public:node_list_category', 'depth1', $data);

And elsewhere in the same PHP file, the CatShow definition:
// variant of Category disabling assertCanonicalUrl check, since it interferes with getting Category Index data
class CatShow extends \XF\Pub\Controller\Category
  public function assertCanonicalUrl($linkUrl) { return; }

Works as intended. 😁