If my intent is to write a generic theme which can be (ab)used by many users, I might want to make it as configurable as possible. Some of my audience may only have blocks on the left side, some may only have them on the right and some may have both.

If I'm also trying to maintain consistency in the layout, I may wish to be rigid about the width of the block columns (see Administration -> blocks -> preview) so that the block columns don't vary in width depending on what page I'm viewing. I'm left with little option but to make three separate themes to support the three layouts as there is no look-ahead to know if I'm going to have any blocks in a certain region.

In other words, I want to do something like this:

<?php
class Theme_foo
{
    function header()
    {
        // Start the HTML document here
        // ...
        // Start the main table
        echo '<table><tr>';
        // Check to see if we're going to have any blocks on the left
        if (block_hasAny('left'))
        {
            // Add a fixed-width column for the left blocks
            echo '<td width="150">';
            theme_blocks('left', $this);
            echo '</td>';
        }
        // Start the main column
        echo '<td>';
    }
    function footer()
    {
        // Finish the main column
        echo '</td>';
        // Check to see if we're going to have any blocks on the left
        if (block_hasAny('right'))
        {
            // Add a fixed-width column for the right blocks
            echo '<td width="150">';
            theme_blocks('right', $this);
            echo '</td>';
        }
        // Finish the HTML document here
        // ...
    }
}
?>

Unless I'm really dumb and have missed another way to do this (which is certainly a possibility), I don't know how not waste space with the two fixed-width block columns if one of them is empty (again, since there's no look-ahead).

This may be as easy as adding a function to includes/theme.inc like the following:

function theme_hasBlocks($region)
{
    $result = db_query("SELECT COUNT(*) FROM blocks WHERE (status = '1' OR custom = '1') ".
        ($region != "all" ? "AND region = '%s' " : "") .
        "ORDER BY weight, module", $region == "left" ? 0 : 1);
    // Forgive my ignorance here, as I don't know the format of $result:
    return ($result[0] > 0);
}

Either that or one could have theme_blocks() return a string (but not output anything) so that the caller can figure out what to do with them (just some thoughts)....

Comments

ax’s picture

+1 from me. i needed exactly the same feature for a theme i did (and i guess others will need it, too). the way i did it was to similar to your option 2 ("have theme_blocks() return a string [...]"). more detailed:

- i copied theme_blocks($region, &$theme) from theme.inc as a class method into my theme (MyTheme::theme_blocks($region)) (the &$theme isn't necessary anymore because it's $this) and used $this->blocks() instead of theme_blocks(). so i don't have to mess with the core theme.inc, only with my theme.
- i modified MyTheme::theme_blocks() to accept an optional second parameter $op = "print". without second parameter, it does just the the same as theme_blocks(). with any other second parameter, it implements the "has_block_at_region" functionality and returns TRUE if there are any blocks at the specified region.
- i also modified theme_blocks() so that it doesn't return blocks as boxes ($theme->box()), but as blocks ($this->block()) so that i can layout blocks differently than boxes (of course then my theme needs to implement $this->block())

was that clear? Only local images are allowed.

guess this would be a good solution for drupal core, too.

matt-1’s picture

Well, I did find a way around this. Or rather I used PHP's output buffering to implement the second method in the original report (i.e., outputing all the blocks of a given region to a string and then checking the string to see if one should output it).

/*====================================================================*/ /**
  Returns the blocks of a given region as a string.

  @param string $a_Region the region who's blocks should be returned
  @param string $a_Theme the theme to use for the formatting callbacks
  @return string the blocks as a string on success (null if there were no
  blocks)
*/ /*=====================================================================*/

function theme_blocks_as_string($a_Region, $a_Theme)
{
    ob_start();
    theme_blocks($a_Region, $a_Theme);
    $blocksStr = trim(ob_get_contents());
    ob_end_clean();

    // Return null if there were no blocks in the region
    if ($blocksStr === '')   // Gamble? Only Drupal developers can tell!
    {
        return null;
    }

    return $blocksStr;
}

However, this does seem like a hack unless this method is officially supported in theme.inc. Here's why: if theme_blocks() ever changes the way it handles output (e.g., adds a ``&lt;br /&gt;'' before/after the $theme->box(...) loop), then the conditional may break (and so will the theme). We try to be smart and use trim() to clean it up a little before we pass judgement, but this is not foolproof.

If this happens however (and we are duped into thinking we have blocks when we don't), at least the theme will err on the side of inclusion...it will just look weird to have a big blank column.

Now one can use this function as follows:

$blocks = theme_blocks_as_string('right', $this);

if ($blocks !== null)
{
    // Do your worst!
}
ax’s picture

mine is better! Only local images are allowed. because yours computes and renders all blocks at $region to find out if there are any (not speaking of the uncertainities about theme_blocks() output you mention), mine just checks the result of the sql query in theme_blocks() (SELECT * FROM blocks WHERE region = '$region') to do the same.

even if that wasn't clear from what i wrote before Only local images are allowed.

matt-1’s picture

Actually I did see what you were doing before. I was trying to optimize for fewest number of database accesses to minimize load on a common resource (with Drupal, one can have a bazillion content servers running Apache, but I've yet to figure out a way to use more than one database server...I think this would involve rewriting a fair amount of Drupal's data access code).

I also figured that it would be unlikely for a theme to check for blocks in a given region without the intent to render them (so the rendering/computing isn't done more than once, it's just cached before being sent to the client). The rendering is only done once in both solutions.

If one is running a low-traffic site with a lot of blocks spread among different regions, your solution may be the winner. However, if one is running a very high-traffic site with relatively few blocks, the overhead associated with storing the rendered blocks is most likely less expensive than the database trip (probably around 1k per block). Both will work, it's really a matter of preference:

Method    | Advantages        | Disadvantages
- - - - - + - - - - - - - - - + - - - - - - - - -
ax        | no caching before | requires twice
          | rendering         | the number of
          |                   | database calls to
          |                   | render the blocks
- - - - - + - - - - - - - - - + - - - - - - - - -
matt      | no additional     | caches all block
          | database          | content in a
          | calls necessary   | given region
          |                   | before output

Perhaps an optimization is to cache the results of your query and only update them once every several minutes.

Keep in mind that the above is all hypothesis. I have no empirical evidence to support any of my claims.

Only local images are allowed.

matt-1’s picture

I may have been confused to your original response. Yes, I did reread it and am probably an idiot (in which case, we've both implemented option 2 without extra database calls, but that leaves me confused about your added parameter to theme_blocks() -- do you call it twice?).

If you don't call theme_blocks() more than once per region, then they're equally efficient in the sense that no rendering is done (i.e., the while loop never executes) if there are no blocks returned in the database query. But yours avoids the added overhead from the output buffer.

One question: why do you make theme_blocks() call $theme->block()? Why not just rewrite $theme->box() to do what you want? Is there an object you're returning to stack up and use later when rendering? Just curious....

dries’s picture

Why don't we change the theme_block() function then? We can make it load the blocks in an array with (title, content)-tuples instead of emitting the blocks directly to screen? The themes would have to do something like:

  foreach (block_list() as $title => $content) {
    $theme->box($title, $content);
  }

Feel free to explore this possibility and to submit some patches to frod with.

al’s picture

Priority: Major » Normal

If you're willing, you can also fix this issue with CSS.
Something like (note - I haven't tested this, but it should work):

<div id="left">
<?php theme_blocks('left', $this); ?>
</div>
<div id="main">
<?php // do main stuff ?>
</div>
<div id="right">
<?php theme_blocks('right', $this); ?>
</div>

With CSS:

#left { float: left; }
#left .box { width: 16em; }
#right { float: right; }
#right .box { width: 16em; }
ax’s picture

Component: Theme system » block.module
Assigned: Unassigned » ax

block_list() as suggested by dries is available in drupal cvs since 19 Nov 2003 [1]. block_list($region) [2] returns an array of blocks, so to find out if there are blocks on either side, just check if this array is empty.

[1] http://lists.drupal.org/archives/drupal-cvs/2003-11/msg00414.html
[2] http://drupal.kollm.org/tmp/drupal-phpdoc/block_8module.html#a15