This is the documentation for concrete5 version 5.6 and earlier. View Current Documentation

Let's get recursive

Introduction

Let's try a little experiment together that will break your site (so just don't do it on a production site please, I repeat DO NOT ATTEMPT THIS ON A PRODUCTION SITE, you've been warned).

  • Edit a page and add a page list block to the "Main" Area
  • Set the block to display all pages (including the page you are on)
  • Set the block view template to "Blog Index"
  • congrats'! you just broke your page

... wait, what ?? .... O_O ?

OK, don't panic, first : I'll explain how to repair that page :

  • go to the dashboard, then full site map
  • locate the page in the sitemap
  • go to "Versions" (in the menu)
  • select last version and remove it
  • here you go again.

phew, catastrophe just got avoided, but hey what the hell just happened ?!!!?

Let's explain a bit, by setting the page list to display the list using the Blog Index template, you asked it to render the content of the "Main" Area, which contains .... the page list.

In short, if you are on the page Foo :

  1. page list lists pages,
  2. page list displays all pages before Foo
  3. then extract the main area of page Foo, which contains the page list : > 1. page list lists pages, > 2. page list displays all pages before Foo > 3. then extract the main area of page Foo, which contains the page list : > > 1. page list lists pages, > > 2. page list displays all pages before Foo > > 3. then extract the main area of page Foo, which contains the page list : > > ...

You get the idea ... to some point where PHP will just kill the running script, preventing it from outputting the headers and other important c5 parts.

This is the problem we gonna solve in this howto

The naive (but non very aesthetic) yet working solution:

in your controller class, add a static counter :

public static $runningBlocks = 0;

then in the view() method add this :

public function view()
{
    self::$runningBlocks ++;
    if ( $runningBlocks > 100 )
    {
        Log::addEntry( t("Block not rendering to prevent recursions issues"), error );
        $this->set( "recursionLimit", true );
     }
    ....

finally in the template add this at the beginning of the file :

if ( $recursionLimit ) return;

Let's explain :

  1. we use a counter to keep tracks on how many blocks we rendered in that page
  2. if we go over an arbitrary high number (here 100), then we conclude something went wrong
  3. in that case, we inform the template to stop rendering (and we also log an error)
  4. from the template we check that the $recursionLimit variable is not set to continue.

It's not very clean, but at least it does not break. You will render a large number of times the block before it gets to understand that something terribly wrong is happening, and stops it from happening again.

Ugly, yet stable and efficient, … but still ugly. Well, let's see if we can do better :

The recursive counter

The problem with the latter method is that we still get stuck a while in recursion before we do something. We can decrease the time by decreasing the number, but still we need to keep that pretty high to avoid problems with users that would put several instances of the block in the same page (which would be totally legit btw).

Let's see if we can just prevent the recursion to happen :

in your controller class add this :

protected static $_runningList = array();

protected function _hashView( $view )
{
    $c = $view->getCollectionObject();
    $cID   = $c->getCollectionID();
    $cType = $c->getCollectionTypeHandle();
    $key = "${cType}_$cID";
    return $key;
}

public function startRender( $view ) { self::$_runningList[] = $this->_hashView($view); }
public function endRender( $view )
{
    $key = $this->_hashView( $view );

    $idx = array_search($key, self::$_runningList );
    if ( $idx != -1 ) return;
    unset( self::$_runningList[$idx] );
}
public function isRunning( $view ) { return in_array( $this->_hashView( $view ), self::$_runningList ); }

then in your view template, add this :

if ( $controller->isRunning( $this ) )
{
    Log::addEntry( t( "Recursion detected in cID %s", Page::getCurrentPage()->getCollectionID() ), "error" );
    return;
}
$controller->startRender( $this );

then at the end of the view template :

$controller->endRender( $this );

and that's it !

Hum ? not convinced ?, OK let's explain :

  1. using a static variable in the controller we keep track of running views
  2. the block is rendered within a BlockView class, which allows you to get the collection ID of the collection currently rendering the block
  3. at the start of the view we check if we are currently running, and if so, we exit (logging the error, but preventing the recursion to happen)
  4. at the start of the view again, we record the start of the template rendering
  5. at the end we notify the controller that we exitted properly.

This way whatever the number of identical blocks in the same page, it works fine, as long as they are not trying to render each other recursively (which is what this method shield us against anyway).

Happy coding !

Loading Conversation