00004 Integrating Multiple (unlimited) Nested Category Levels

A. Adding Category Levels

Below is a focused recipe to replace the current two-level collapse toggles (“parent-categories” and “child-categories”) with an arbitrary number of “Category Level” toggles (up to 9). You’ll end up with checkboxes like:

  • Spaces I am a member of
  • Category Level 1
  • Category Level 2
  • Category Level 9
  • Unclassified spaces
  • Spaces I’m not a member of

1) Define your max levels and dynamic options

File

protected/modules/abcdirectory/models/Configuration.php

Patch

  1. Add at the top of the class (after existing constants):

    /**
    * Maximum number of nested category levels to expose for collapse toggles.
    */
    public const MAX_CATEGORY_LEVELS = 9;
  2. Replace your existing getSpaceBrowserCollapsedSectionOptions() entirely with:

    public static function getSpaceBrowserCollapsedSectionOptions(): array
    {
       $options = [
           self::SPACE_BROWSER_COLLAPSED_SECTION_MY_SPACES         => Yii::t('AbcdirectoryModule.config', 'Spaces I am a member of'),
       ];
    
       // dynamically add “Category Level 1” … “Category Level 9”
       for ($lvl = 1; $lvl <= self::MAX_CATEGORY_LEVELS; $lvl++) {
           $key   = 'category-level-' . $lvl;
           $label = Yii::t('AbcdirectoryModule.config', 'Category Level {n}', ['n' => $lvl]);
           $options[$key] = $label;
       }
    
       // keep your other two:
       $options[self::SPACE_BROWSER_COLLAPSED_SECTION_UNCLASSIFIED_SPACES] = Yii::t('AbcdirectoryModule.config', 'Unclassified spaces');
       $options[self::SPACE_BROWSER_COLLAPSED_SECTION_OTHER_SPACES]      = Yii::t('AbcdirectoryModule.config', 'Spaces I\'m not a member of');
    
       return $options;
    }
  3. Remove the old constants for parent‐ and child‐categories:

    - public const SPACE_BROWSER_COLLAPSED_SECTION_PARENT_CATEGORIES = 'parent-categories';
    - public const SPACE_BROWSER_COLLAPSED_SECTION_CHILD_CATEGORIES  = 'child-categories';

2) Wire the new options into your Settings form

Your config form already has:

<?= $form->field($model, 'spaceBrowserCollapsedSections')
         ->checkboxList(Configuration::getSpaceBrowserCollapsedSectionOptions()) ?>

so once you regenerate the option list (step 1), you’ll see nine “Category Level” checkboxes instead of the old two.


3) Make your front-end collapse logic respect levels

Wherever you currently collapse/expand “parent-categories” and “child-categories” in your browser view (SpaceBrowser.php or similar), replace that with a loop over levels:

$config = Yii::$app->getModule('abcdirectory')->getConfiguration();
$collapsed = $config->spaceBrowserCollapsedSections;

// for level i:
if (in_array("category-level-{$i}", $collapsed, true)) {
    // render level-$i section collapsed
}

Repeat that for each depth you support (1…9). If you discover you need more than 9, just bump MAX_CATEGORY_LEVELS.


4) Rebuild & Clear Cache

# from HumHub root
chmod +x protected/modules/abcdirectory/resources/less/build.sh
cd protected/modules/abcdirectory/resources/less
./build.sh
cd ../../../..   # back to HumHub root
rm -rf protected/runtime/cache/* protected/runtime/assets/*

Result: your admin “Classified spaces browser” settings now list a checkbox for each of Category Level 1–9, letting you turn on/off collapsing of each depth. The front-end should loop over those selections and collapse exactly the levels you choose.

B. Detailed Loop Explanation

In your run() method you’re currently only passing those three sections (mySpaces, otherSpaces, and the catch-all JS slug map). To drive an arbitrary number of category-level sections, you need to:

  1. Loop over your levels (1 up to Configuration::MAX_CATEGORY_LEVELS)
  2. Render each level’s HTML (you’ll need a helper like renderLevel($lvl))
  3. Merge them into the $params you send to the view, naming them spaceBrowserSectionLevel1, spaceBrowserSectionLevel2, …, spaceBrowserSectionLevel9

Here’s exactly how to patch your run():

public function run()
{
    // … your existing query setup …
    $mySpacesCount   = (int)$memberQuery->count() + (int)$followingQuery->count();
    $otherSpacesCount = (int)$noneQuery->count();

    // collect all the params you already have
    $params = [
        'modal'    => $this->modal,
        'allSpaceSlugNames' => $allSpaceSlugNames,
        'csBrowserId'       => $this->csBrowserId,
        'mySpacesCount'     => $mySpacesCount,
        'otherSpacesCount'  => $otherSpacesCount,
        'showSpaceBrowserSectionMySpacesHeaderCollapse'   => $this->showSpaceBrowserSectionMySpacesHeaderCollapse,
        'showSpaceBrowserSectionOtherSpacesHeaderCollapse'=> $this->showSpaceBrowserSectionOtherSpacesHeaderCollapse,
    ];

    // ─── NEW: inject each Category Level section ───────────────────────
    $config    = Yii::$app->getModule('abcdirectory')->getConfiguration();
    $maxLevels = $config::MAX_CATEGORY_LEVELS;

    for ($lvl = 1; $lvl <= $maxLevels; $lvl++) {
        // assume you have a method that builds the HTML for level $lvl
        $sectionHtml = $this->renderLevel($lvl);

        // only pass it if non-empty (optional)
        if (trim($sectionHtml) !== '') {
            $params["spaceBrowserSectionLevel{$lvl}"] = $sectionHtml;
        }
    }
    // ────────────────────────────────────────────────────────────────

    // merge with any other params you may have
    $allParams = array_merge($this->getParams(), $params);

    return $this->render('spaceBrowser', $allParams);
}

What you need to add:

  1. Right after you build $otherSpacesCount, before you call $this->render(), insert the loop above.

  2. You’ll also need a helper method in your widget, e.g.:

    protected function renderLevel(int $level): string
    {
       // fetch the root category for this depth,
       // generate your <ul><li>…</li></ul> HTML,
       // and return it as a string.
       // Return empty string if no spaces exist at that level.
    }
  3. In your view (spaceBrowser.php), use those spaceBrowserSectionLevelN variables in your for-loop to wrap each in a Collapse::widget.

With these additions, your widget will supply spaceBrowserSectionLevel1spaceBrowserSectionLevel9 (or fewer if empty) to the view, and your view’s for-loop will render each one wrapped in a collapsible panel.

C. Making the Max-Level of categories configurable

Below is exactly what you need to make “Max category levels” a configurable value in your Settings UI instead of a hard-coded constant. We’ll:

  1. Add a new $maxCategoryLevel property to your Configuration model.
  2. Use it in your collapse-options generator in place of MAX_CATEGORY_LEVELS.
  3. Wire it into the Settings form as a number input.

1) Configuration model changes

File

protected/modules/abcdirectory/models/Configuration.php

a) Add the new property with a default

/**
 * Maximum number of nested category levels to expose for collapse toggles.
 * @var int
 */
public $maxCategoryLevel = 9;

Place it just below your existing $getSpaceImageFromCategory and $showSpaceIdColumn declarations.

b) Update your rules() to validate it

Find the rules() method and add:

[['maxCategoryLevel'], 'integer', 'min' => 1, 'max' => 20],

for example:

public function rules(): array
{
    return [
        [['removeDefaultSpaceBrowser', 'removeDefaultSpaceDirectory', 'spaceBreadcrumb', 'getSpaceImageFromCategory', 'showSpaceIdColumn'], 'boolean'],
        [['maxCategoryLevel'], 'integer', 'min' => 1, 'max' => 20],
        [['abcdirectoryBrowserSortOrder'], 'integer', 'max' => 10000],
        // …
    ];
}

c) Load & save it

In loadBySettings(), after you load showSpaceIdColumn add:

$this->maxCategoryLevel = (int)$this->settingsManager->get('maxCategoryLevel', $this->maxCategoryLevel);

In save(), after setting showSpaceIdColumn add:

$this->settingsManager->set('maxCategoryLevel', $this->maxCategoryLevel);

d) Replace the constant usage

Anywhere you used Configuration::MAX_CATEGORY_LEVELS, switch to the instance value:

$config = Yii::$app->getModule('abcdirectory')->getConfiguration();
$maxLevels = $config->maxCategoryLevel;

For example in your widget’s run():

$config    = Yii::$app->getModule('abcdirectory')->getConfiguration();
$maxLevels = $config->maxCategoryLevel;

for ($lvl = 1; $lvl <= $maxLevels; $lvl++) {
    // …
}

And in getSpaceBrowserCollapsedSectionOptions(), change the loop to:

public static function getSpaceBrowserCollapsedSectionOptions(): array
{
    $config = Yii::$app->getModule('abcdirectory')->getConfiguration();
    $maxLevels = $config->maxCategoryLevel;

    $options = [
        self::SPACE_BROWSER_COLLAPSED_SECTION_MY_SPACES => Yii::t('AbcdirectoryModule.config', 'Spaces I am a member of'),
    ];

    for ($lvl = 1; $lvl <= $maxLevels; $lvl++) {
        $key   = 'category-level-' . $lvl;
        $label = Yii::t('AbcdirectoryModule.config', 'Category Level {n}', ['n' => $lvl]);
        $options[$key] = $label;
    }

    // … unclassified / other …
    return $options;
}

2) Add the input to your Settings form

File

protected/modules/abcdirectory/views/config/index.php

Inside the “Classified spaces browser” collapsible, add right before the spaceBrowserCollapsedSections field:

<?= $form->field($model, 'maxCategoryLevel')
         ->input('number', ['min' => 1, 'max' => 20])
         ->label(Yii::t('AbcdirectoryModule.config', 'Max category levels'))
         ->hint(Yii::t('AbcdirectoryModule.config', 'How many nested category levels to show controls for (e.g., 1–9).')) ?>

Your section might now look like:

<?= $form->beginCollapsibleFields(Yii::t('AbcdirectoryModule.config', 'Classified spaces browser')) ?>
    <?= $form->field($model, 'spaceBrowserMembershipSeparation')->checkbox() ?>
    <?= $form->field($model, 'spaceBrowserCollapsedSections')->checkboxList(Configuration::getSpaceBrowserCollapsedSectionOptions()) ?>

    <?= $form->field($model, 'maxCategoryLevel')
             ->input('number', ['min' => 1, 'max' => 20])
             ->label(Yii::t('AbcdirectoryModule.config', 'Max category levels'))
             ->hint(Yii::t('AbcdirectoryModule.config', 'How many nested category levels to show controls for (e.g., 1–9).')) ?>
<?= $form->endCollapsibleFields() ?>

3) Rebuild & Clear Cache

From your HumHub root:

chmod +x protected/modules/abcdirectory/resources/less/build.sh
cd protected/modules/abcdirectory/resources/less
./build.sh
cd ../../../..
rm -rf protected/runtime/cache/* protected/runtime/assets/*

Now Max category levels is fully configurable in Settings. Your dynamic loops and option lists will respect whatever number the admin chooses (up to your allowable max).

D. Problem 1: Max Category Levels don't recognize settings.

Solving that all 9 (maxLevel) show even you change to max 5 levels.

You might have the problem that your getSpaceBrowserCollapsedSectionOptions() is still looping up to the old constant instead of the saved maxCategoryLevel. You need to switch that method to read the instance value from your config, not the hard-coded constant. Here’s the exact patch:


1) Update getSpaceBrowserCollapsedSectionOptions()

File

protected/modules/abcdirectory/models/Configuration.php

Replace the entire method (which currently probably looks like this)—

public static function getSpaceBrowserCollapsedSectionOptions(): array
{
    $options = [
        self::SPACE_BROWSER_COLLAPSED_SECTION_MY_SPACES => Yii::t('AbcdirectoryModule.config', 'Spaces I am a member of'),
    ];

    for ($lvl = 1; $lvl <= self::MAX_CATEGORY_LEVELS; $lvl++) {
        $key   = 'category-level-' . $lvl;
        $label = Yii::t('AbcdirectoryModule.config', 'Category Level {n}', ['n' => $lvl]);
        $options[$key] = $label;
    }

    $options[self::SPACE_BROWSER_COLLAPSED_SECTION_UNCLASSIFIED_SPACES] = Yii::t('AbcdirectoryModule.config', 'Unclassified spaces');
    $options[self::SPACE_BROWSER_COLLAPSED_SECTION_OTHER_SPACES]      = Yii::t('AbcdirectoryModule.config', 'Spaces I\'m not a member of');

    return $options;
}

With this version that reads your saved maxCategoryLevel:

public static function getSpaceBrowserCollapsedSectionOptions(): array
{
    // grab the instance so we can read the admin‐saved value
    $config     = Yii::$app->getModule('abcdirectory')->getConfiguration();
    $maxLevels  = (int)$config->maxCategoryLevel;

    $options = [
        self::SPACE_BROWSER_COLLAPSED_SECTION_MY_SPACES => Yii::t('AbcdirectoryModule.config', 'Spaces I am a member of'),
    ];

    for ($lvl = 1; $lvl <= $maxLevels; $lvl++) {
        $key   = 'category-level-' . $lvl;
        $label = Yii::t('AbcdirectoryModule.config', 'Category Level {n}', ['n' => $lvl]);
        $options[$key] = $label;
    }

    $options[self::SPACE_BROWSER_COLLAPSED_SECTION_UNCLASSIFIED_SPACES] = Yii::t('AbcdirectoryModule.config', 'Unclassified spaces');
    $options[self::SPACE_BROWSER_COLLAPSED_SECTION_OTHER_SPACES]      = Yii::t('AbcdirectoryModule.config', 'Spaces I\'m not a member of');

    return $options;
}

2) Remove (or ignore) the old constant

You can leave MAX_CATEGORY_LEVELS defined, but it’s no longer used. If you like, delete it at the top of the class:

-    public const MAX_CATEGORY_LEVELS = 9;

3) Clear caches & reload

From your HumHub root:

# recompile CSS (if you touched any LESS; otherwise skip)
chmod +x protected/modules/abcdirectory/resources/less/build.sh
cd protected/modules/abcdirectory/resources/less && ./build.sh && cd ../../../..

# clear Yii caches & assets
rm -rf protected/runtime/cache/* protected/runtime/assets/*

4) Verify

  1. Go to Settings → Classified spaces browser and set Max category levels to 5 (or any other number).
  2. Save.
  3. Reload the form and confirm you see only 5 checkboxes under Collapsed Sections (you can also inspect the HTML to count them).
  4. Finally, check Admin → Spaces → Classified spaces browser to see collapse toggles for exactly those levels.

With this change, your checkbox-list will always reflect the live maxCategoryLevel stored in settings.

E. Problem 2: Your maxCategoryLevel has been disabled or is missing

Sometimes your maxCategoryLevel property is still commented out, so although you’re saving and loading its value, the instance never actually has that property defined—thus your loop still falls back to self::MAX_CATEGORY_LEVELS. Let’s fix that.


1) Uncomment & Declare the Property

File:

protected/modules/abcdirectory/models/Configuration.php

Find this commented line:

    /**
    * Maximum number of nested category levels to expose for collapse toggles.
    * @var int
    */
    //public $maxCategoryLevel = 9;

Change to:

    /**
     * Maximum number of nested category levels to expose for collapse toggles.
     * @var int
     */
    public $maxCategoryLevel = 9;

2) Add Labels & Hints

Right after your existing attributeLabels() array, append:

    'maxCategoryLevel' => Yii::t('AbcdirectoryModule.config', 'Max category levels'),

And in attributeHints(), append:

    'maxCategoryLevel' => Yii::t('AbcdirectoryModule.config', 'How many nested category levels to show controls for (e.g., 1–9).'),

Your methods should look roughly like:

public function attributeLabels(): array
{
    return [
        // … other labels …
        'getSpaceImageFromCategory' => Yii::t('AbcdirectoryModule.config', 'Use category image.'),
        'maxCategoryLevel'          => Yii::t('AbcdirectoryModule.config', 'Max category levels'),
    ];
}

public function attributeHints(): array
{
    return [
        // … other hints …
        'getSpaceImageFromCategory' => Yii::t('AbcdirectoryModule.config', 'If a space has no header image, use the image of the related category or parent category.'),
        'maxCategoryLevel'          => Yii::t('AbcdirectoryModule.config', 'How many nested category levels to show controls for (e.g., 1–9).'),
    ];
}

3) Clear Cache & Verify

From your HumHub root:

# Recompile your module CSS if needed:
chmod +x protected/modules/abcdirectory/resources/less/build.sh
cd protected/modules/abcdirectory/resources/less && ./build.sh && cd ../../../..

# Clear Yii caches & assets:
rm -rf protected/runtime/cache/* protected/runtime/assets/*

4) Test

  1. Go to Settings → Classified spaces browser.
  2. You should now see Max category levels (number input) plus the correct number of Category Level 1…N checkboxes.
  3. Change Max category levels to 5, save, and reload: only 5 “Category Level” checkboxes should appear.

With the property now declared, your code in getSpaceBrowserCollapsedSectionOptions() will correctly use the admin-saved maxCategoryLevel instead of the old constant.

F. Problem 3: Render problems in Frontend

The “Class …widgets\Configuration not found” error simply means PHP is looking for Configuration in the widgets namespace instead of your models namespace. You have two fixes—either import the correct class at the top, or fully qualify it in your code. Here’s exactly what to change in SpaceBrowser.php.


1) At the very top of SpaceBrowser.php, add:

<?php
namespace humhub\modules\abcdirectory\widgets;

use Yii;
use humhub\modules\abcdirectory\models\Configuration;  // ← add this
use humhub\modules\ui\view\components\Widget;
// … the rest of your imports …

This ensures any un-prefixed Configuration::… refers to the model.


2) Inside renderLevel(), replace:

$categories = Configuration::getInstance()
    ->getCategoryQueryForLevel($level)
    ->all();

with something like:

$config = Yii::$app->getModule('abcdirectory')->getConfiguration();
$categories = $config->getCategoryQueryForLevel($level)->all();

This both avoids a non-existent getInstance() and uses the correct config object.


After these two edits:

  1. Save SpaceBrowser.php.
  2. Clear caches (from your HumHub root):

    rm -rf protected/runtime/cache/* protected/runtime/assets/*
  3. Reload your frontend (/abcdirectory/browse/index) and the widget should no longer crash.

Why this works By importing humhub\modules\abcdirectory\models\Configuration at the top, PHP knows where to find Configuration. Then by fetching your module’s real config via Yii::$app->getModule('abcdirectory')->getConfiguration(), you avoid relying on a missing static getInstance() method and use the same config object you’ve been saving your settings into.

G. Problem 4: Call to undefined method

Perhaps you still have this line at the top of your view:

$config = Configuration::getInstance();  // ← THIS MUST GO

Since your Configuration model has no getInstance(), that call will always fatally error. Let’s remove it and fetch the configuration the same way everywhere else:


  1. Open protected/modules/abcdirectory/widgets/views/spaceBrowser.php

  2. Delete any occurrence of:

    $config = Configuration::getInstance();
  3. Make sure the very top of the file looks like this (one <?php, all your use statements, then setup):

    <?php
    /**
    * Classified Spaces browser
    *
    * @var \humhub\modules\ui\view\components\View $this
    * @var bool                                   $modal
    * @var \humhub\modules\abcdirectory\models\AbcdirectoryCategory $rootCategory
    * @var array<int,string>                      $allSpaceSlugNames
    * @var string                                 $csBrowserId
    * @var string|null                            $spaceBrowserSectionAll
    * @var string|null                            $spaceBrowserSectionMySpaces
    * @var string|null                            $spaceBrowserSectionOtherSpaces
    * @var array                                  $spaceBrowserCollapsedSections
    * @var int                                    $mySpacesCount
    * @var int                                    $otherSpacesCount
    */
    
    use Yii;
    use humhub\libs\Html;
    use humhub\modules\abcdirectory\assets\AssetsSpaceBrowser;
    use humhub\modules\abcdirectory\models\Configuration;
    use humhub\modules\abcdirectory\widgets\Collapse;
    use humhub\modules\space\models\Space;
    use humhub\modules\ui\view\components\View;
    
    AssetsSpaceBrowser::register($this);
    $this->registerJsConfig('abcdirectory.spaceBrowser', ['allSpaceSlugNames' => $allSpaceSlugNames]);
    
    // Fetch the saved config instance
    $config    = Yii::$app->getModule('abcdirectory')->getConfiguration();
    $maxLevels = (int)$config->maxCategoryLevel;
    
    $csbAllSpacesId   = $csBrowserId . '-accordion-all-spaces';
    $csbMySpacesId    = $csBrowserId . '-accordion-my-spaces';
    $csbOtherSpacesId = $csBrowserId . '-accordion-other-spaces';
  4. Save the file.

  5. Clear HumHub’s caches and assets:

    cd /path/to/humhub
    rm -rf protected/runtime/cache/* protected/runtime/assets/*
  6. Reload the browser.

With that removed, your view will no longer attempt the undefined getInstance() call and will instead correctly pull in your maxCategoryLevel setting.