Autonav caching - worked for version 8 but not for version 9 (can't seem to retrieve the stored cache successfully)

Hello! I have a site with enough pages that load times are starting to be noticeably affected by how long it takes the autonav to generate.

On version 8 I have successfully followed this awesome article to cache the autonav: https://www.kalmoya.com/articles/concrete5-speed-boost-autonav-block

However, I’ve got a site on 9.1.1 that just isn’t taking to this method. I’ve double-checked against the official documentation to see if anything in the article seemed outdated, but nothing does (https://documentation.concretecms.org/9-x/developers/security/cache ).

The IsMiss(); check just keeps being true no matter what I do - so instead of loading from cache, it keeps rebuilding to autonav from scratch, despite that fact that I’ve proven through logging that the $datacache variable is indeed saving and returning what looks like valid autonav data. See below for a snapshot of the saved cache data:

I’ve got all my cache setting enabled in the Dashboard. I am not hardcoding the block, it’s in a GlobalArea, and i simply have a view.php in the application/blocks/autonav folder to override it with the custom cache template from the above article.

My theory is that something about the autonav data causes the function to loop through the “miss” condition, as if in version 9.1.1 is not liking something about the data that version 8 was OK with. However I’ve spent hours changing everything I can think of and find on the internet, and no matter what, it still refuses to find the cache and counts it as a “miss”.

Any insight would be appreciated!!

my autonav_caching_engine.php:

<?php namespace Application\Controller;

use Concrete\Core\Controller\Controller;

class AutonavCachingEngine extends Controller
{
    public function getOrSetCachedAutonav($handle, $autonavController, $expire = 3600)
    {

        \Log::addInfo('Cache handle: ' . $handle);
        \Log::addInfo('Expiration: ' . $expire);

        $expensiveCache = $this->app->make('cache/expensive');
        $cacheKey = $handle;
        // $dataCache = $expensiveCache->getItem($cacheKey);
        $dataCache = $expensiveCache->getItem('WebsiteAutonav/' . $cacheKey);
        
        // \Log::addInfo('Datacache handle: ' . print_r($dataCache, true));
        // Log the cache key for debugging
        \Log::info('Checking cache for key: ' . $cacheKey);
        \Log::info('dataCache is null: ' . ($dataCache == null ? "null" : "NOT NULL"));
        //\Log::info('dataCache variable value: ' . print_r(get_class_vars($dataCache), true));
    
        if ($dataCache->isMiss()) {
            \Log::info('Cache miss for key: ' . $cacheKey);
            
            // Rebuild and cache the navigation
        
            $navItems = $autonavController->getNavItems();
            \Log::info('IsArray: ' . (is_array($navItems) ? 'yep' : 'nope'));

            foreach ($navItems as $index => $item) {
                $item->cObj->siteTree = null;
                
                $navItems[$index] = $item;
            }

            // Log the actual data cached
            // \Log::addInfo('Storing navItems in cache: ' . print_r($navItems, true));
            $dataCache->lock();
            $expensiveCache->save($dataCache->set($navItems)->expiresAfter($expire));
            \Log::info('Saved nav items to cache for ' . $cacheKey);
            //$checkCache = $expensiveCache->getItem($cacheKey);
            //\Log::info('Immediate cache check: ' . ($checkCache->isMiss() ? 'Miss' : 'Hit'));
        } 
        else {
            \Log::info('Cache hit for key: ' . $cacheKey);
            $navItems = $dataCache->get();
        }

        return $navItems;
    }

    public function testCache()
{
    $expensiveCache = \Core::make('cache/expensive');
    $testKey = 'test_cache_key';
    
    // Try to get cache
    $dataCacheTest = $expensiveCache->getItem($testKey);

    if (!$dataCacheTest->isMiss()) {
        \Log::info('Cache hit for test key: ' . $testKey);
        return $dataCacheTest->get();
    } else {
        \Log::info('Cache miss for test key: ' . $testKey);
    }

    // Set a cache value
    $expensiveCache->save($dataCacheTest->set('test_data')->expiresAfter(3600 * 24));
    \Log::info('Saved test data to cache for ' . $testKey);
}

public function adjustCachedAutonavForPage($item, $currentPage)
{
    // if the current page ID is the same as this nav item cID it's the current page
    if ($currentPage->getCollectionID() == $item->cID) {
        $item->isCurrent = true;
        $item->inPath = true;
    } else {
        // We know this is not the currnt page so we set our values to false
        $item->isCurrent = false;
        $item->inPath = false;

        // We get the path for a current page
        $cPath = $currentPage->getCollectionPath();

        $itemPagePath = $item->cObj->getCollectionPath();

        if (strncmp($cPath, $itemPagePath, strlen($itemPagePath)) === 0) {
            $item->inPath = true;
        }
    }

    return $item;
}
}

My view.php:

<?php defined('C5_EXECUTE') or die("Access Denied.");

$dataCache = \Concrete\Core\Support\Facade\Application::getFacadeApplication()->make(\Application\Controller\AutonavCachingEngine::class);
$dataCache->testCache();


$navItems = $dataCache->getOrSetCachedAutonav("side_nav", $controller);
$c = Page::getCurrentPage();

foreach ($navItems as $ni) {
    $ni = $dataCache->adjustCachedAutonavForPage($ni, $c);
    $classes = array();

    if ($ni->isCurrent) {
        //class for the page currently being viewed
        $classes[] = 'nav-selected';
    }

    if ($ni->inPath) {
        //class for parent items of the page currently being viewed
        $classes[] = 'nav-path-selected';
    }


    $ni->classes = implode(" ", $classes);
}

//*** Step 2 of 2: Output menu HTML ***/

if (count($navItems) > 0) {
    echo '<ul class="nav royce">'; //opens the top-level menu

    foreach ($navItems as $ni) {
        echo '<li class="' . $ni->classes . '">'; //opens a nav item
        echo '<a href="' . $ni->url . '" target="' . $ni->target . '" class="' . $ni->classes . '">' . h($ni->name) . '</a>';

        if ($ni->hasSubmenu) {
            echo '<ul>'; //opens a dropdown sub-menu
        } else {
            echo '</li>'; //closes a nav item

            echo str_repeat('</ul></li>', $ni->subDepth); //closes dropdown sub-menu(s) and their top-level nav item(s)
        }
    }

    echo '</ul>'; //closes the top-level menu
} elseif (is_object($c) && $c->isEditMode()) {
    ?>
    <div class="ccm-edit-mode-disabled-item"><?=t('Empty Auto-Nav Block.')?></div>
<?php
}

Hello. That’s my article. Thank you for your kind words and for bringing that to my attention. I’ll have a look and will post a fix. Give me a day or 2.

You will be a hero and a legend as far as I’m concerned if you can help! I’ve gone as far as my mediocre-but-sometimes-lucky skills can take me. Thank you!!

:sweat_smile:
So I looked and it seems Concrete is now using var_export() when saving objects to cache. It solves some problems but creates new one.

Here’s the explanation from php.net

When exporting an object, var_export() does not check whether __set_state() is implemented by the object’s class, so re-importing objects will result in an Error exception, if __set_state() is not implemented. Particularly, this affects some internal classes. It is the responsibility of the programmer to verify that only objects will be re-imported, whose class implements __set_state().

So what’s happening is, the data saved contains a Page object ($cObj) and that page object itself contains a PageVersion object. So the data is correctly cached (with the addition of a call to the __set_state() method. But when trying to retrieve it from the cache it’s not retrieved because neither the Page nor the PageVersion objects have a __set_state() method and it throws an error.

The first easiest solution is to replace this line from my code

$item->cObj->siteTree = null;

with

$item->cObj = null;

If you don’t need the page object to get some page attributes for instance, it’s not a problem.

If you do need some pieces of information from the page object, then the best way would be to fetch the information you need and add it to the data to be cached. You’d modify the foreach loop in my code like this

foreach ($navItems as $index => $item) {
    // get the page    
    $page = $item->cObj;
    // get an attribute value and add it to our $item object
    $item->my_attribute = $page->getAttribute('my_attribute');
    // remove the page object
    $item->cObj = null;
    $navItems[$index] = $item;
}

Be careful, when adding a value to our $item object, make sure it’s not a class object. For instance, if your attribute is a Tag attribute, using getAttribute() will return such an object and it will still break. Make sure you’re saving scalar values (strings, numbers, boolean…), arrays or objects of scalar values.

Then in your autonav view you can use $navItem->my_attribute to get the value.

I hope this helps.

I’ll update my article soon to reflect this.

1 Like

I updated the article on my website. It is more complete

2 Likes

Thank you so much!! I will work on this over the next day or two and let you know if I’m successful! You’ve made me believe in the internet again.