template engine - nested blocks

PHP programming forum. Ask questions or help people concerning PHP code. Don't understand a function? Need help implementing a class? Don't understand a class? Here is where to ask. Remember to do your homework!

Moderator: General Moderators

User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post by s.dot »

OK, now I'm onto the issue of replacing blocks with content. Given the code on page one, I'm able to capture each unique block's template code content into an array.. ready for REPLACING.

My head's spinning so far.

I looked through phpbb's template code, and I like their idea of using "." to separate block names.. instead of using a multi-dimensional array for the blocks. This way, all of the block names would be on the first level of the main array.

So, say I had this code:

Code: Select all

//assign categories
$arr['category'][] = array('name' => 'cat1');
$arr['category'][] = array('name' => 'cat2');
$arr['category'][] = array('name' => 'cat3');

//assign category forums
$arr['category.forum_row'][] = array('foo' => 'foo', 'bar' => 'bar');
$arr['category.forum_row'][] = array('foo' => 'foo2', 'bar' => 'bar2');
$arr['category.forum_row'][] = array('foo' => 'foo3', 'bar' => 'bar3');
Assigning them in my template class is no problem. It's the looping through and replacing that is giving me a headache. I've come up with this psuedo-psuedo code. That works some-what in my head. However, if you read the comments, I haven't really a clue.

Code: Select all

//Misc. class data
//private $_blocks = array();  //holds block data
//private $_blockCodes = array(); //holds each unique block's code in the format of blockname => blockcode
//$tplCode = file_get_contents($templateFile);

foreach ($this->_blocks AS $blockName => $contents)
{
    if (strpos('.', $blockName)
    {
        //nested block
        $blockNames = explode('.', $blockName);
        $blockCount = count($blockNames);
        
        //iterate through each block, building and evaluating the nesting, then replace variables
        //in that code with the values for each variable

        //maybe this is wrong.  maybe I need to nest in reverse?
        //something... my head is spinning ;(

        for ($i=0; $i<$blockCount; $i++)
        {
            $thisNest = array_slice($blockNames, 0, $i, true);

            //build nest
            $nestStr = '$this->_blocks';
            foreach ($thisNest AS $nest)
            {
                $nestStr .= '[\'' . $nest . '\']';
            }

            //evaluate the string
            eval($nestStr);

            //get block name
            $thisBlock = $blockNames[$i];

            //get block template code
            $blockCode = $this->_blockCodes[$thisBlock];

            //parse block of code into string
            foreach ($nestStr AS $var => $value)
            {
                $code = str_replace('{' . $var . '}', $value, $blockCode);
            }

            //place the evaluated block of code back into the template
            $tplCode = str_replace($this->_blockCodes[$thisBlock], $code, $tplCode);
        }
    } else
    {
        //rootlevel block
        //replace contents with vars/values
        foreach ($contents AS $var => $value)
        {
            $code = str_replace('{' . $var . '}', $value, $code);
        }

        //place the evaluated block of code back into the template
        $tplCode = str_replace($this->_blockCodes[$blockName], $code, $tplCode);
    }
}
I'm not sure if this is even in the right direction. I think it will traverse the blocks in order, placing them back inside the template code in order.. but I'm not sure. I need to build a testing environment.. which I'll do in the morning because I'm way too tired right now.

I've spent a couple hours straightening out my thoughts in notepad.. so.. comments, suggestions?
Am I overcomplicating things?
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
John Cartwright
Site Admin
Posts: 11470
Joined: Tue Dec 23, 2003 2:10 am
Location: Toronto
Contact:

Post by John Cartwright »

You are doing great, keep going :) A couple notes I want to mention,

1. You should never roll over and use eval because it is easier ;) I've taken the liberty of demonstrating how this done be done without eval below. It still needs some tweeking but I've left that out for you to implement properly.

Code: Select all

$this->_blocks = array_reverse($this->_blocks);
foreach ($this->_blocks AS $blockName => $contents) 
{
	$pieces = explode('.', $this->blockName);
	if (count($pieces)) 
	{
		$stack = array_shift($pieces);
		if (array_key_exists($stack, $this->block)) 
		{
			foreach ($pieces as $key) 
			{
				if (!is_array($stack) || !array_key_exists($key, $stack)) 
				{
					continue;	
				} 	
				
				$stack = $stack[$key];
			}				
		}
	}
}
2. You might want to consider using the offsets of the string from the $matches to make the string replacement, instead of matching the entire strings. substr_replace() comes to mind

3. Yes, you will need to reverse the order of the array (like I have done above). This is to ensure that the nested elements get parsed first.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post by s.dot »

Thanks for that JCart!

I figured that this would be one of those times where eval() was an acceptable function to use. Can you explain to me why it's not? (or do we just avoid eval() at all costs?)

I've yet to see a template engine without using eval() (though, admittedly, i've only looked at smarty, template lite, and phpbb's template class.. and those do a lot more than I'm doing). So if I hammer the replacing out without using eval(), it'd be the first template engine I've seen that doesn't evaluate! That's kind of exciting.

Kudos to you, too. Those of you who can wrap your head around this code that's been driving me nuts for days are amazing. I guess that shows me that I'm quite a bit away from being up there with you guys in terms of coding. :)
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
John Cartwright
Site Admin
Posts: 11470
Joined: Tue Dec 23, 2003 2:10 am
Location: Toronto
Contact:

Post by John Cartwright »

Actually had a little error in the code

Code: Select all

$this->_blocks = array_reverse($this->_blocks);
foreach ($this->_blocks AS $blockName => $contents)
{
   $pieces = explode('.', $this->blockName);
	$stack = array_shift($pieces);

	if (count($pieces) && array_key_exists($stack, $this->_blocks))
	{
		foreach ($pieces as $key)
		{
			if (!is_array($stack) || !array_key_exists($key, $stack))
			{
				continue;   
			}  
			
			$stack = $stack[$key];
		}       
	}
}
As long as you carefully control and sanitize the php code entered into eval it is a reasonable solution, however when a method is inherently insecure I'd always look for alternatives.
Last edited by John Cartwright on Sat Nov 24, 2007 3:59 pm, edited 2 times in total.
User avatar
RobertGonzalez
Site Administrator
Posts: 14293
Joined: Tue Sep 09, 2003 6:04 pm
Location: Fremont, CA, USA

Post by RobertGonzalez »

Have you looked at the phpBB /includes/template.php file? It does almost exactly what you are doing.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post by s.dot »

Yes, it is my inspiration for using the period as a block concatenator. I'm looking at it for ideas, but want to maintain my own code to keep it tight and original :D Plus.. it's pretty messy.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
RobertGonzalez
Site Administrator
Posts: 14293
Joined: Tue Sep 09, 2003 6:04 pm
Location: Fremont, CA, USA

Post by RobertGonzalez »

Cool. It just looked like your code was very similar to it.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post by s.dot »

bah humbug. I've been stuck on this for days.

JCart, the code you posted will never work because array_shift() returns the first element of the array (which is always a string). Therefore the !is_array() check will always evaluate, and continue; will always be called.

I've tried tweaking your code, concatenating the stacks to get nested block order.

Given this template:

Code: Select all

<table width="100%" cellspacing="2" cellpadding="2" border="0" style="border: solid 1px #000;">
	<!-- Start Category -->
	<tr>
		<td colspan="4" class="cat_name">{cat_name}</td>
	</tr>
	<tr>
		<td width="60%" class="titletext">{L_FORUM}</td>
		<td width="10%" class="titletext">{L_TOPICS}</td>
		<td width="10%" class="titletext">{L_POSTS}</td>
		<td width="20%" class="titletext">{L_LAST_POST}</td>
	</tr>
	<!-- Start Forum Row -->
	<tr>
		<td colspan="4" class="forum_row">{forum_name}</td>
	</tr>
	<!-- Start Devnet -->
	<p>I love devnet! {adjective}</p>
	<!-- End Devnet -->
	
	<!-- Start Crap -->
	<p>{crap}</p>
	<!-- End Crap -->
	
	<!-- End Forum Row -->
	<tr>
		<td colspan="4">&nbsp;</td>
	</tr>
	<!-- End Category -->
</table>
<!-- Start Test -->
<p>{test}</p>
<!-- End Test -->
I think I want to end up with an array like this:

Code: Select all

Array
(
    [Category] => Array
       (
            [Category.Forum Row] => Array
                (
                     [0] => Category.Forum Row.Devnet
                     [1] => Category.Forum Row.Crap
                )
        )
    [Test] => array()
)
Right?
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post by s.dot »

This is the best I can seem to do =/

Code: Select all

//loop through each block name
$stack = array();
foreach ($this->_blocks AS $blockName => $contents)
{
	//if there is no period in the block name, this is a root level block
	//so start a new array indice
	if (!strpos($blockName, '.'))
	{
		$stack[$blockName] = array();
	} else
	{
		//this is a nested block
		$pieces = explode('.', $blockName);
		$count = count($pieces);
	
		//now go through and see if $pieces.$piece.$piece.[etc] exists
		$str = array_shift($pieces);
		
		foreach ($pieces AS $piece)
		{
			if (array_key_exists($str, $stack))
			{
				if (array_key_exists($stack[$str][$piece], $stack[$str]))
				{
					continue;
				} else
				{
					$stack[$str][$piece] = array();
				}
			} else
			{
				$stack[$str] = array();
			}
		}
	}
}
Which gives me

Code: Select all

<pre>Array
(
    [Category] => Array
        (
            [Forum Row] => Array
                (
                )

            [Devnet] => Array
                (
                )

            [Crap] => Array
                (
                )

        )

    [Test] => Array
        (
        )

)
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
John Cartwright
Site Admin
Posts: 11470
Joined: Tue Dec 23, 2003 2:10 am
Location: Toronto
Contact:

Post by John Cartwright »

No more writting snipplets when drinking, I promise! Anyways, heres a slightly revised version that I've left for you to fit to your needs. Hopefully the comments can explain the process better :D

Code: Select all

<?php
function parse($contentBlocks, $templateBlocks) 
{
	$result = array();
	$blocks = array_reverse($contentBlocks);
	foreach ($blocks AS $name => $contents) 
	{	
		//reset the current stack its original data
		$stack = $templateBlocks;
		
		//even if the string does not contain a period, it will convert our name
		//into an array with the element name so there is no need to check for a period
		//within the string
		$pieces = explode('.', $name);
		
		foreach ($pieces as $piece) 
		{
			//if we have finished recursively searching the array before our stack
			//is a string, or if the requested block does not exist, skip the current
			//section
			if (!is_array($stack) || !array_key_exists($piece, $stack)) 
			{
				continue 1;
			}
			
			//reset the stack one step deeper into the array so we can search it
			$stack = $stack[$piece];
		}
		
		//im not sure how you want to control the return array, but heres one format
		//and do whatever formatting you want to do like string replacements, etc etc
		$result[$name][] = $stack;
	}
	
	return $result;
}

$contentBlocks = array();
$contentBlocks['foobar'] = array('woo' => 'hoo');
$contentBlocks['category.forum_row'][] = array('foo' => 'foo', 'bar' => 'bar');

$templateBlocks = array();
$templateBlocks['foobar'] = '{woo}';
$templateBlocks['category']['forum_row'] = '{foo} {bar}';

echo '<pre>';
print_r(parse($contentBlocks, $templateBlocks));
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post by s.dot »

Thanks.

Code: Select all

$this->_blocks = array_reverse($this->_blocks);
foreach ($this->_blocks AS $blockName => $contents)
{
	$stack = $blocks;
	$pieces = explode('.', $blockName);
	
	foreach ($pieces AS $piece)
	{
		if (!is_array($stack) || !array_key_exists($piece, $stack))
		{
			continue 1;
		}

		echo $piece . '<br />';	
		$stack = $stack[$piece];
		//echo $stack . '<br />';
	}
	
	$result[$blockName][] = $stack;
}
For some reason, $piece always turns out to be 'Category', so when I add to the $result[] array, the template block being added is always the category block.

I end up with:

Code: Select all

Array
(
    [test] => test template block
    [category.forum row.crap] => category template block
    [category.forum row.devnet] => category template block
    [category.forum row] => category template block
    [category] => category template block
)
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
John Cartwright
Site Admin
Posts: 11470
Joined: Tue Dec 23, 2003 2:10 am
Location: Toronto
Contact:

Post by John Cartwright »

Just to be certain, where is $blocks defined?
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post by s.dot »

Basically I'm just writing a mock class right now for testing, and I've crammed everything into one function.. but I'll refactor down into more functions once I get it procedurally.

Here's the function

Code: Select all

public function display()
{
	//grab all blocks and position
	preg_match_all("/<!-- (?:Start|End) (.+?) -->/im", $this->_code, $matches, PREG_OFFSET_CAPTURE);
	
	//number of blocks
	$numBlocks = count($matches[0]);
	
	//loop through and match start/end tags, calculating positions of blocks
	$pos = array();
	for ($i=0; $i<$numBlocks; $i++)
	{
		if (strpos($matches[0][$i][0], 'Start'))
		{
			//starting position
			$pos[$matches[1][$i][0]]['Start'] = $matches[0][$i][1] + strlen($matches[0][$i][0]);
		} else
		{
			//ending position
			$pos[$matches[1][$i][0]]['End'] = $matches[0][$i][1];
		}
	}
	
	//capture each block based on starting and ending positions
	foreach ($pos AS $block => $startend)
	{
		$blocks[$block] = substr($this->_code, $startend['Start'], $startend['End']-$startend['Start']);
	}
	echo '<pre>';
	//print_r(array_map('htmlentities', $blocks));
	echo '</pre>';
	
	$this->_blocks = array_reverse($this->_blocks);
	foreach ($this->_blocks AS $blockName => $contents)
	{
		$stack = $blocks;
		$pieces = explode('.', $blockName);
		
		foreach ($pieces AS $piece)
		{
			if (!is_array($stack) || !array_key_exists($piece, $stack))
			{
				continue 1;
			}
				
			$stack = $stack[$piece];
		}
		
		$result[$blockName][] = $stack;
	}
	
	echo '<pre>';
	print_r($result);
	echo '</pre>';
	//echo $this->_code;
}
The output of $blocks is:

Code: Select all

Array
(
    [Category] => 
	<tr>
		<td colspan="4" class="cat_name">{cat_name}</td>
	</tr>
	<tr>
		<td width="60%" class="titletext">{L_FORUM}</td>
		<td width="10%" class="titletext">{L_TOPICS}</td>
		<td width="10%" class="titletext">{L_POSTS}</td>
		<td width="20%" class="titletext">{L_LAST_POST}</td>
	</tr>
	<!-- Start Forum Row -->
	<tr>
		<td colspan="4" class="forum_row">{forum_name}</td>
	</tr>
	<!-- Start Devnet -->
	<p>I love devnet! {adjective}</p>
	<!-- End Devnet -->
	
	<!-- Start Crap -->
	<p>{crap}</p>
	<!-- End Crap -->
	
	<!-- End Forum Row -->
	<tr>
		<td colspan="4">&nbsp;</td>
	</tr>
	
    [Forum Row] => 
	<tr>
		<td colspan="4" class="forum_row">{forum_name}</td>
	</tr>
	<!-- Start Devnet -->
	<p>I love devnet! {adjective}</p>
	<!-- End Devnet -->
	
	<!-- Start Crap -->
	<p>{crap}</p>
	<!-- End Crap -->
	
	
    [Devnet] => 
	<p>I love devnet! {adjective}</p>
	
    [Crap] => 
	<p>{crap}</p>
	
    [Test] => 
<p>{test}</p>

)
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post by s.dot »

Here's my whole mock class, in case that helps any.

Code: Select all

<?php

/**
 * Start template class... this is just a BS mock, but I just need to get the principles down
 */
class template
{
	private $_blocks = array();
	private $_code = array();
	
	public function setCode($code)
	{
		$this->_code = $code;
	}
	
	public function assignBlock($blockName, $values)
	{
		$this->_blocks[$blockName][] = $values;
	}
	
	public function display()
	{
		//grab all blocks and position
		preg_match_all("/<!-- (?:Start|End) (.+?) -->/im", $this->_code, $matches, PREG_OFFSET_CAPTURE);
		
		//number of blocks
		$numBlocks = count($matches[0]);
		
		//loop through and match start/end tags, calculating positions of blocks
		$pos = array();
		for ($i=0; $i<$numBlocks; $i++)
		{
			if (strpos($matches[0][$i][0], 'Start'))
			{
				//starting position
				$pos[$matches[1][$i][0]]['Start'] = $matches[0][$i][1] + strlen($matches[0][$i][0]);
			} else
			{
				//ending position
				$pos[$matches[1][$i][0]]['End'] = $matches[0][$i][1];
			}
		}
		
		//capture each block based on starting and ending positions
		foreach ($pos AS $block => $startend)
		{
			$blocks[$block] = substr($this->_code, $startend['Start'], $startend['End']-$startend['Start']);
		}
		
		/******************************************************************************************/
		
		$this->_blocks = array_reverse($this->_blocks);
		foreach ($this->_blocks AS $blockName => $contents)
		{
			$stack = $blocks;
			$pieces = explode('.', $blockName);
			
			foreach ($pieces AS $piece)
			{
				if (!is_array($stack) || !array_key_exists($piece, $stack))
				{
					continue 1;
				}
					
				$stack = $stack[$piece];
			}
			
			$result[$blockName][] = $stack;
		}
		
	//	echo '<pre>';
	//	print_r($result);
	//	echo '</pre>';
		/********************************************************************************************/
	}
		
}

/**
 * Mock template code to assign to our test.  This will never ever be used this way in the real test
 * but it's supplying code for the test to work on
 */
$tplCode = '<table width="100%" cellspacing="2" cellpadding="2" border="0" style="border: solid 1px #000;">
	<!-- Start Category -->
	<tr>
		<td colspan="4" class="cat_name">{cat_name}</td>
	</tr>
	<tr>
		<td width="60%" class="titletext">{L_FORUM}</td>
		<td width="10%" class="titletext">{L_TOPICS}</td>
		<td width="10%" class="titletext">{L_POSTS}</td>
		<td width="20%" class="titletext">{L_LAST_POST}</td>
	</tr>
	<!-- Start Forum Row -->
	<tr>
		<td colspan="4" class="forum_row">{forum_name}</td>
	</tr>
	<!-- Start Devnet -->
	<p>I love devnet! {adjective}</p>
	<!-- End Devnet -->
	
	<!-- Start Crap -->
	<p>{crap}</p>
	<!-- End Crap -->
	
	<!-- End Forum Row -->
	<tr>
		<td colspan="4">&nbsp;</td>
	</tr>
	<!-- End Category -->
</table>
<!-- Start Test -->
<p>{test}</p>
<!-- End Test -->';

/**
 * Instantiate and set code
 */
$tpl = new template();
$tpl->setCode($tplCode);

/**
 * Assign some craptasticular nested block data
 */
$tpl->assignBlock('Category', array('cat_name' => 'cat1'));
$tpl->assignBlock('Category', array('cat_name' => 'cat2'));
$tpl->assignBlock('Category', array('cat_name' => 'cat3'));

for ($i=1;$i<4;$i++)
{
	$tpl->assignBlock('Category.Forum Row', array('name' => 'row' . $i, 'test' => 'bleh!'));
	$tpl->assignBlock('Category.Forum Row.Devnet', array('adjective' => 'super' . $i));
	$tpl->assignBlock('Category.Forum Row.Crap', array('crap' => 'test crap'));
}

$tpl->assignBlock('Test', array('test' => 'this is just a test'));

/**
 * display =[
 */
$tpl->display();
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
Post Reply