Location: PHPKode > scripts > PSPCooker > pspcooker/class_pspcooker.php
<?php

/*

PSPCooker is a new, JSP-like template engine for PHP
http://yapbb.sourceforge.net/PSPCooker/
Copyright (C) 2001  Arno van der Kolk

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

//=====================================================================================================================
// PHP3 support
//=====================================================================================================================
	if (!function_exists("in_array"))
	{
		function in_array($needle, $haystack)
		{
			for($i = 0; $i < count($haystack) && $haystack[$i] != $needle; $i++);
			return ($i != count($haystack));
		}
	}

//=====================================================================================================================
// Some static initializers:
//=====================================================================================================================
	$GLOBALS["pspcooker_include_bits"] = array();


//=====================================================================================================================
// Description: A new JSP-like template engine for PHP (BETA VERSION v.1)
// Note that, at present, this class REQUIRES PHP4.0.2 or better to run properly
//=====================================================================================================================
class PSPCooker
{

/**
 *
 * Public interface:
 * -----------------
 *
 * - PSPCooker($storage_directory = "/tmp/", $repository = "pspcooker_repository.php")
 *	 constructor (with target location for data files and repository filename)
 *
 * - load_file($handle, $filename)
 *	 assigns the contents of file with name $filename to this template handle
 *
 * - register($handle, $variable, $value = "\0")
 *	 register a variable (string or 2-dimensional array) to this template handle
 *   (optionally, you can immediately register the value as well)
 *
 * - execute($handle, $exitAfterEcho = false)
 *	 parse and display the template
 *
 *
 * Common use:
 * -----------
 *
 * $template = new PSPCooker("/tmp/");
 * $template->load_file("sampleHandle", "/home/www/templates/sample.txt");
 * $template->load_file("sampleHeader", "/home/www/templates/header.txt");
 * $template->register("sampleHandle", "nameOfVariable");
 * $template->register("sampleHandle", "nameOfArray");
 * $template->register("sampleHandle", "nameOfVariable", "withAStaticValue");
 * $template->register("sampleHandle", "nameOfArray", Array(Array("key1" => "value1"), Array("key2" => "value2"), Array("key3" => "value3")));
 * $template->execute("sampleHandle", true);
 *
 *
 * Structure of template files:
 * ----------------------------
 *
 * - Fields are displayed in this manner:
 *
 *     {fieldName}
 *
 *     Note that a variable must already be registerd under this name. Else this field will not be parsed.
 *
 * - Comments can be added (much like the regular <!-- and -->):
 *
 *     {-- this is a comment and won't be seen --}
 *
 * - Conditional blocks are build like this:
 *
 *     <CONDITTION NAME="varName"> text </CONDITTION NAME="varName">
 *
 *     $varName can be any kind of variable. A simple if($varName) statement will be used to evaluate it.
 *
 * - To negate a condition, simply add an exclamation mark to the front of the varName:
 *
 *     <CONDITTION NAME="!varName"> text </CONDITTION NAME="!varName">
 *
 *     Again, $varName can be any kind of variable. A simple if(!$varName) statement will be used to evaluate it.
 *
 * - A loop construct looks as follows:
 *
 *     <LOOP NAME="arrayName"> text and arrayName.someField </LOOP NAME="arrayName">
 *
 *     $arrayName must be a globally accessible array in the form of
 *     Array(Array("field1" => "value1"), Array("field2" => "value2"), Array("field3" => "value3"))
 *     So, it's a 2-dimensional array with a counter as the first index, and fields as the second index.
 *
 * - Loop fields can simply be displayed this way:
 *
 *     <LOOP NAME="arrayName">
 *       {arrayName.field}
 *     </LOOP NAME="arrayName">
 *
 * - Conditions based on fields of an array can also be used in a loop:
 *
 *     <LOOP NAME="arrayName">
 *       text
 *
 *       <CONDITION NAME="arrayName.someBooleanField">
 *         $arrayName[$index]['someBooleanField'] evaluates to TRUE
 *       </CONDITION NAME="arrayName.someBooleanField">
 *
 *       <CONDITION NAME="!arrayName.someBooleanField">
 *         $arrayName[$index]['someBooleanField'] evaluates to FALSE
 *       </CONDITION NAME="!arrayName.someBooleanField">
 *
 *       more text
 *     </LOOP NAME="arrayName">
 *
 * - You can specify an alternate string in case the array for a loop is empty:
 *
 *     <LOOP NAME="arrayName">
 *
 *       <NOITEMS>
 *         Nothing found!
 *       </NOITEMS>
 *
 *       Value is: {arrayName.field}
 *
 *     </LOOP NAME="arrayName">
 *
 * - To include other sections:
 *
 *     <INCLUDE HANDLE="handleName">
 *
 *
 * Notes:
 * ------
 *
 * - If the string '<include handle="sampleHeader">' would occur in 'sample.txt' in the
 *   above example, it would automatically be replaced by the contents of 'header.txt'.
 *
 * - Any registered variables are automatically registered to included sections as well.
 *
 * - PHP must be able to write in the specified storage directory.
 *
 *
 * To do:
 * ------
 *
 * - Since < PHP4.0.2 does not support the limit value in preg_replace,
 *   this will have to be emulated some how (line 569).
 *
 * - In order to ultimately make this run on PHP3 as well, the bits that
 *   use pointers (variable assignments using &$variable) will have to be
 *   altered as well. Note that additional corrections may have to be performed.
 *
 * - When using subloops, performance may degrade as these fragments cannot fully be 'compiled'.
 *
 *     <LOOP NAME="loopA">
 *
 *       Text and {loopA.anotherField}
 *
 *       <LOOP NAME="loop{loopA.aField}">
 *
 *         Some field: {loop{loopA.aField}.someField}
 *
 *       </LOOP NAME="loop{loopA.aField}">
 *
 *     </LOOP NAME="loopA">
 *
 */

//=====================================================================================================================
// Internal variables only!
//=====================================================================================================================
	var $classname = "PSPCooker";		// for serialize
	var $_repository;					// path to repository file
	var $_files = array();				// contains texts of template-handles
	var $_is_parsed = array();			// is text of templates parsed
	var $_storage_directory = "";		// path to data directory
	var $_evals = array();				// execute these evals before creating template PHP
	var $_template_files = array();		// path to original template file (used to retrieve modification date)
	var $_check = false;				// check handles for validity?
	var $_start = '{';					// open tag (not yet used)
	var $_end = '}';					// close tag (not yet used)
	var $_included_handles = array();	// store names to includes

//=====================================================================================================================
// Constructor (specify data storage directory and repository filename)
//=====================================================================================================================
	function PSPCooker($store = "/tmp/", $repository = "pspcooker_repository.php")
	{
/*	// some directory checks (not really needed though)
		$patterns = array("^/(.*)", "\.\.+/");
		$replacements = array("\./\\1", "\./");
		$this->_storage_directory = ereg_replace($patterns, $replacements, $storage_directory);
*/
		$this->_storage_directory = $store;
		$this->_repository = $this->_storage_directory . $repository;
	}

//=====================================================================================================================
// Reads an entire file (static function actually)
//=====================================================================================================================
	function _getFile($fileName)
	{
		$error = FALSE;
		$tmp = "";
		$fh = @fopen($fileName,"r") or $error = TRUE;
		if (!$error)
		{
			$tmp = fread($fh, filesize($fileName));
			fclose($fh);
		}
		unset($error);
		unset($fh);
		return $tmp;
	}

//=====================================================================================================================
// Register variables/loops (and optionally accept actual values)
//=====================================================================================================================
	function register($handle, $variable, $value = "\0")									// accepts simple strings as well as arrays[index][fields]
	{
		if (is_long(strpos($variable, ",")))
			$variable = explode(",", $variable);

		if (is_array($variable))
		{
			for (reset($variable); list(,$var) = each($variable);)
				$this->register($handle, trim($var));
			return;
		}

		if ($this->_check) if (!preg_match("!\w+!", $variable)) die("<P>ERROR: Invalid variable: '$variable'</P>");

		if ($value == "\0")
		{
			$isArray = is_array($GLOBALS[$variable]);
			$value = "\$GLOBALS[\"$variable\"]";											// make sure actual value will be retrieved
		}
		else
		{
			// NOT TESTED MUCH:

			$value = (($isArray = is_array($value)) ? $value : addslashes($value));
			$value = "\"$value\"";

/*			// old redundant code:
			if ($isArray)
				$value = "\"$value\"";														// pass the array as value (it works magically)
			else
				$value = '"' . addslashes($value) . '"';*/
		}

		// the variable '$instance' will be used in 'execute()' to point to the instance
		$this->_evals[$handle][] = '$instance->register' . ($isArray ? "_loop" : "") . "(\"$variable\", $value);";
	}

//=====================================================================================================================
// Loads template
//=====================================================================================================================
	function load_file($handle, $filename)													// possibly postpone the loading of the contents
	{																						// until _parse($handle)
		if ($this->_check) if (!preg_match("!\w+!", $handle)) die("<P>ERROR: Invalid variable: '$handle'</P>");

		if ($exist = file_exists($filename))
		{
			$this->_files[$handle] = addslashes(fread($fp = fopen($filename, 'r'), filesize($filename)));
			$this->_template_files[$handle] = $filename;
			fclose($fp);
		}
		return $exist;
	}

//=====================================================================================================================
// Parse and output text associated with $handle
//=====================================================================================================================
	function execute($handle, $mode = "RUN", $exitAfterEcho = false)
	{
		if ($this->_check) if (!preg_match("!\w+!", $handle)) die("<P>ERROR: Invalid variable: '$handle'</P>");

		if (empty($GLOBALS["cooker_include_bits"][$handle]))								// check if already included in this request
		{																					// this is merely a performance issue
			$filename = $this->_parse($handle);												// because the included file also checks
			include($filename);
		}

		$this->_prepare_instance($handle);													// make sure the instance is ready
		$instance = &$GLOBALS[$handle . "_cooker_instance"];

		for (reset($instance->_included_handles); list(, $var) = each($instance->_included_handles);)
			$this->execute($var, "NOOP", $exitAfterEcho);									// prepare includes

		for ($i = 0; $i < sizeof($this->_evals[$handle]); $i++)								// actually register variables and such
			eval($this->_evals[$handle][$i]);												// to THIS instance

		$tmp = "";																			// select course of action
		if ($mode == "RUN")																	// parse, save and echo
			$instance->run($exitAfterEcho);
		else if ($mode == "GETTEXT")														// parse, save and return text
			$tmp = $instance->get_text();
		else if ($mode == "GETINSTANCE")													// parse, save and return instance
			$tmp = $instance;
		else if ($mode == "NOOP")															// parse and save only
			;
		else																				// error ?
			eval("X");

		return $tmp;
	}

//=====================================================================================================================
// Internal function (called from _execute)
// parse the text associated with $handle
//=====================================================================================================================
	function _parse($handle)
	{
		if ($this->_check) if (!preg_match("!\w+!", $handle)) die("<P>ERROR: Invalid variable: '$handle'</P>");

		$filename = $this->_storage_directory . $handle . "_cooker_class.php";
		$text = &$this->_files[$handle];													// make a pointer to work with

		if (!is_file($this->_repository))													// build new (empty) repository
			$repository = array();
		else																				// retrieve repository
			$repository = explode(',', $this->_getFile($this->_repository));

		$count = count($repository);														// validate repository
		if ($count < 2 || ($count % 2 == 1 && $repository[$count - 1] != ""))
		{
			$repository = array();															// reconstruct repository
			$repository[] = $filename;
			$repository[] = 0;
			$count = 2;	//count($repository);
		}

		$newRepository = array();															// build new (empty) repository
		$updateRepository = false;															// (in case we need to update the current one)
		$isInRepository = FALSE;
		$time = filemtime($this->_template_files[$handle]);

		for ($i = 0; $i < $count - 1; $i += 2)
		{
			if ($repository[$i] == $filename)												// does current page exist in repository?
			{
				$isInRepository = TRUE;
				if ($repository[$i + 1] != $time)											// does it need updating?
				{
					$text = $this->_parse_text($handle, $filename, $text);
					$this->_write_class($handle, $filename);

					$updateRepository = true;												// update repository next
					$repository[$i + 1] = $time;
				}
			}
			$newRepository[] = $repository[$i];
			$newRepository[] = $repository[$i + 1];
		}

		if (!$isInRepository)																// page does not exist in repository
		{
			$text = $this->_parse_text($handle, $filename, $text);
			$this->_write_class($handle, $filename);

			$updateRepository = true;														// update repository next
			$newRepository[] = $filename;
			$newRepository[] = $time;
		}

		if ($updateRepository)																// perform repository update
		{
			$fh = fopen($this->_repository, "w");
			fwrite($fh, implode(',', $newRepository) . ',');
			fclose($fh);
		}

		return $filename;																	// this will be used by execute
	}

//=====================================================================================================================
// Internal function (called from _parse, execute)
// writes out the newly parsed class
//=====================================================================================================================
	function _write_class($handle, $filename)
	{
		if ($this->_check) if (!preg_match("!\w+!", $handle)) die("<P>ERROR: Invalid variable: '$handle'</P>");

		$text = '<' . '?php
/**
 * This file was created by PSPCooker, a new JSP-like template engine for PHP.
 * PSPCooker is availlable at: http://yapbb.sourceforge.net/PSPCooker/
 *
 * Please do not modify
 * Created on: ' . date("D j M Y, h:i:s A") . '
 */


if (!$GLOBALS["cooker_include_bits"]["' . $handle . '"])
{
	$GLOBALS["cooker_include_bits"]["' . $handle . '"] = TRUE;


	class ' . $handle . '_cooker_class extends PSPTemplate
	{
		var $classname = "' . $handle . '_cooker_class";					/* for serialize */

		function ' . $handle . '_cooker_class($store = "./", $headers = false) {				/* constructor */
			$this->_set_headers = $headers;
			$this->_storage_directory = $store;
			$this->_included_handles = array(' . $this->_get_included_handles($handle) . ');
		}

		function _set_text()												/* overwrite parent */
		{
			$this->_text = "' . $this->_files[$handle] . '";
			$this->_text = str_replace("\\' . "'" . '", "' . "'" . '", $this->_text);
		}
	}

}
?' . '>';

		$fh = fopen($filename, "w");									// write page
		fwrite($fh, $text);
		fclose($fh);
		unset($text);
	}

//=====================================================================================================================
// Internal function (called from _write_class, _parse, execute)
// returns a comma-seperated list of all occurances
//=====================================================================================================================
	function _get_included_handles($handle)
	{
		$arr = &$this->_included_handles[$handle];
		if (is_array($arr))
		{
			reset($arr);
			for ($tmp = ""; list(, $var) = each($arr);)
				$tmp .= "\"$var\", ";
			return substr($tmp, 0, -2);
		}
		else
			return "";
	}

//=====================================================================================================================
// Internal function (called from _parse, execute)
// handle loops, includes, comments and conditional blocks
//=====================================================================================================================
	function _parse_text($handle, $filename, $text)
	{
																							// handle loops
		$expression = '!' . quotemeta('<loop name=\"') . '(\w+)' . quotemeta('\">') . '(.*)' . quotemeta('</loop name=\"') . '\\1' . quotemeta('\">') . '!Uis';
		$replacement = '" . ($this->_parse_loop("\\1", "\\2")) . "';
		$text = preg_replace($expression, $replacement, $text);

																							// handle included files
		$expression = '!' . quotemeta('<include handle=\"') . '(.+)' . quotemeta('\">') . '!ie';
		$replacement = '$this->_parse_include($handle, "\\1")';
		$text = preg_replace($expression, $replacement, $text);

																							// strip comments
		$expression = '!' . quotemeta('{--') . '(.+)' . quotemeta('--}') . '!Uis';
		$replacement = "";
		$text = preg_replace($expression, $replacement, $text);

																							// handle conditional blocks
		$expression = '!' . quotemeta('<condition name=\"') . '(\!?)(\w+)' . quotemeta('\">') . '(.*)' . quotemeta('</condition name=\"') . '\\1\\2' . quotemeta('\">') . '!Uis';
		$replacement = '" . ( \\1$GLOBALS["\\2"] ? "\\3" : "" ) . "';
		for ($oldText = !$text; $oldText != $text;)											// make sure all nested blocks are processed too
		{
			$oldText = $text;
			$text = preg_replace($expression, $replacement, $text);
		}

		return $text;
	}

//=====================================================================================================================
// Internal function (called from _parse_text, _parse, _execute)
// prepare a call to an included instance
//=====================================================================================================================
	function _parse_include($mainHandle, $includeHandle)
	{
		$this->_included_handles[$mainHandle][] = $includeHandle;
		return '" . $GLOBALS["' . $includeHandle . '_cooker_instance"]->get_text() . "';
	}

//=====================================================================================================================
// Internal function (called from execute)
// lazy initialization of instances (static function actually)
//=====================================================================================================================
	function _prepare_instance($handle)
	{
		if ($GLOBALS[$handle . "_cooker_instance"] == "")
			eval("\$GLOBALS['" . $handle . "_cooker_instance'] = new " . $handle . '_cooker_class("' . $this->_storage_directory . '", false);');		// instantiate new cooker object
	}
}


//=====================================================================================================================
// Super class for new template pages
//====================================================================================================================
class PSPTemplate
{
	function PSPTemplate($store = "./", $headers = false) {				/* constructor (will be overwritten anyway) */
		$this->_set_headers = $headers;
		$this->_storage_directory = $store;
	}

	function run($exitScript = false) {									/* echo the text */
		$this->_parse();
		if ($this->_set_headers)
		{
			header("Content-Type: text/html");
			header("Content-Length: " . strlen($this->_text));
		}
		echo $this->_text;
		if ($exitScript) exit;
	}

	function get_text() {												/* returns the file, without echoing it */
		$this->_parse();
		return $this->_text;
	}

	function register($key, $value) {									/* register a var */
		$this->_vars[$key] = $value;
	}

	function register_loop($key, $value) {								/* register a loop */
		$this->_loopvars[$key] = $value;
	}

	function _parse_loop($arr, $loopText) {								/* actually iterate some text */
		$actArr = $this->_loopvars[$arr];
		$result = "";
		if (is_array($actArr)) {
			$count = count($actArr);

			if ($count == 0 || ($count == 1 && count($actArr[0]) == 0))	/* no items in array, display 'noitems' thingy */
			{
				$returnNoItems = true;
			}
			else														/* filter out 'noitems' thingy */
			{
				$expression = "!" . quotemeta('<noitems>') . ".*?" . quotemeta('</noitems>') . "!is";
				$replacement = "";
				$loopText = preg_replace($expression, $replacement, $loopText, 1);
			}

			for ($i = 0; $i < $count; $i++)								/* iterate: */
			{
				$tmp = $loopText;
				reset($actArr);
				while (list($key, $value) = each($actArr[$i]))
					$tmp = str_replace("\{$arr.$key}", $value, $tmp);

																		/* parse conditions */
				$expression = '!' . quotemeta('<condition name="') . '((\!?)(' . $arr . ')\.(\w+))' . quotemeta('">') . '(.*)' . quotemeta('</condition name="') . '\\1' . quotemeta('">') . '!Uise';
//				$expression = "!" . quotemeta('<condition name=\"') . "((\!?)(" . quotemeta($arr) . ")" . quotemeta('.') . '(\w+))' . '\">' . "(.*)" . quotemeta('</condition name=\"') . "\\1" . quotemeta('\">') . "!Uise";
				$replacement = '$this->_parse_loop_condition("\\2", $actArr[' . $i . '], "\\4", "\\5")';
				for ($oldText = !$tmp; $oldText != $tmp;)
				{
					$oldText = $tmp;
					$tmp = preg_replace($expression, $replacement, $tmp);
				}
				$result .= $tmp;
			}
			unset($tmp);
			unset($oldText);
		}
		else
			$returnNoItems = true;										/* not an array, display 'noitems' thingy */

		if ($returnNoItems)
		{
			$expression = "!(.*)" . quotemeta('<noitems>') . "(.*?)" . quotemeta('</noitems>') . "(.*)!is";
			$replacement = "\\2";

//	old:
//			$result = preg_replace($expression, $replacement, $loopText);
//			if ($loopText == $result) $result = "";

			preg_match_all($expression, $loopText, $matches);
			$result = $matches[2][0];
		}
		return $this->_parse_sub_loop($result);
	}

	function _parse_sub_loop($text) {									/* aux. function for _parse_loop */
		$expression = '!' . quotemeta('<loop name="') . '(\w+)' . quotemeta('">') . '(.*)' . quotemeta('</loop name="') . '\\1' . quotemeta('">') . '!Uise';
		$replacement = '$this->_parse_loop("\\1", stripslashes("\\2"))';
		return preg_replace($expression, $replacement, $text);
	}

	function _parse_loop_condition($negate, $array, $field, $text) {	/* aux. function for _parse_loop */
		if ($negate == "!") $array[$field] = !$array[$field];
		return ($array[$field] ? stripslashes($text) : "");
	}

	function _set_text() {												/* this is were the text will be initialized */
		// do nothing (is abstract)
	}

	function _parse() {													/* parse the text */
		if ($this->_parsed) return;
		$this->_parsed = TRUE;
		$this->_set_text();
		for (reset ($this->_vars); list($key, $value) = each($this->_vars);)
			$this->_text = str_replace("\{$key}", $value, $this->_text);
	}

	var $_text;															/* actual text */
	var $_loopvars = array();											/* for keeping track of the loops */
	var $_vars = array();												/* for keeping track of variables */
	var $_set_headers = false;											/* set content type and length before output? */
	var $_parsed = false;												/* has this template already been parsed? */
	var $classname = "PSPTemplate";										/* for serialize */
	var $_storage_directory;											/* here is where the includes can be found */
	var $_included_handles = array();									/* for external use; keep track of included pages */
}

?>
Return current item: PSPCooker