PHP Classes
Icontem

File: pClosure.php


  Search   All class groups All class groups   Latest entries Latest entries   Top 10 charts Top 10 charts   Newsletter Newsletter   Blog Blog   Forums Forums   Help FAQ Help FAQ  
  Login   Register  
Recommend this page to a friend! ReTweet ReTweet Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Sam Shull  >  pClosure  >  pClosure.php  
File: pClosure.php
Role: Class source
Content type: text/plain
Description: The class and two interface definitions.
Class: pClosure
Create closure functions for any PHP 5 version
 

Contents

Class file image Download
<?php
/**
 *	Create a closure and optionally include variables from any other scope into the execution scope, 
 *	also enables execution within a different scope @see pClosure_test.php
 *	and supports type hinted arguments of classes and PHP default values (like object, string, int)
 *
 *
 *	@author Sam Shull <sam.shull@jhspecialty.com>
 *	@version 0.1
 *
 *	@copyright Copyright (c) 2009 Sam Shull <sam.shull@jhspeicalty.com>
 *	@license <http://www.opensource.org/licenses/mit-license.html>
 *
 *	Permission is hereby granted, free of charge, to any person obtaining a copy
 *	of this software and associated documentation files (the "Software"), to deal
 *	in the Software without restriction, including without limitation the rights
 *	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *	copies of the Software, and to permit persons to whom the Software is
 *	furnished to do so, subject to the following conditions:
 *	
 *	The above copyright notice and this permission notice shall be included in
 *	all copies or substantial portions of the Software.
 *	
 *	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *	THE SOFTWARE.
 *
 *
 *	CHANGES:
 *
 */
 
/**
 *	Just a function to simplify the creation of a closure
 *
 *	@param string $args
 *	@param string $code
 *	@param array $additional=array()
 *	@return pClosure
 */
function pClosure ($args, $code, array $additional=array())
{
	$trace = debug_backtrace();
		
	$instance = new pClosure($args, $code, $additional);
	
	//changes the backtrace info that the object was created in
	//so it is easier to figure out where the setup came from
	$instance->trace = $trace;
	
	return $instance;
}

/**
 *	
 *
 *
 */
class pClosure
{
	/**
	 *	A PCRE pattern for getting the arguments names,
	 *	type hinting and default values
	 *
	 *	@const string
	 */
	const ARGUMENT_PATTERN = '/(?P<type>[\w\\\\]+)?\s*&?\s*(?# 									match the type and a reference
								)\$(?P<symbol>\w+)(?# 											get the symbol
								)(?P<default>\s*=\s*(?# 										look for a default value if one is provided
									)(?P<quote>[\'"])?(?# 										look for a quote [\'"]
										)(?(quote)(?# 											if it begins with a quote
											)(?P<string>([^\\4]*?)(\\\\\\4[^\\4]*?)*?)\\4|(?# 	handle a string with escapes
											)(?P<other>array\s*\(\s*\)|[^\s*,]+){1}))?/i'; 	  //otherwise it should be a normal default value
	
	/**
	 *	A PCRE pattern for making an argument string compliant with PHP standard arguments
	 *
	 *	@const string
	 */
	const LEGALIZE_ARGUMENTS = '/(?![\$\\\\:])\b(bool|boolean|string|int|integer|real|float|double|object)\b\s*(&)?\s*\$/i'; 
	
	/**
	 *	Type cast identifier for an argument that defaults to null and has no type hinting
	 *
	 *	@const string
	 */
	const ANY = '--any--'; 
	
	/**
	 *	Type cast identifier for an argument that does not default to null and has no type hinting
	 *
	 *	@const string
	 */
	const REQUIRED = '--required--'; 
	
	/**
	 *	Used to identify an argument as being NULL by default
	 *
	 *	@const string
	 */
	const ARGUMENT_DEFAULT_NULL = '--argument-default-null--';
	
	/**
	 *	A PCRE pattern for replacing calls to func_get_args within executed code
	 *
	 *	@const string
	 */
	const FUNC_GET_ARGS = '/([=\s])func_get_args\s*\(\s*\)\s*;/i';
											
	/**
	 *	Static storage of closure instances
	 *	
	 *	@access protected
	 *	@var array
	 */
	protected static $instances = array();
	
	/**
	 *	The string used to initialize the closure
	 *
	 *	@var string
	 */
	private $originalArgumentString;
	
	/**
	 *	The results of debug_backtrace at the __construct call
	 *
	 *	@var array
	 */
	public $trace;
	
	/**
	 *	Contains a formatted associative array of 
	 *	the closures desired arguments, type hints and defaults
	 *
	 *	@access protected
	 *	@var array
	 */
	protected $_args;
	
	/**
	 *	The code that the closure executes
	 *	
	 *	@access protected
	 *	@var string
	 */
	protected $_code;
	
	/**
	 *	Additional arguments supplied to the closure
	 *	works like the 'use' parameter of PHP 5.3 closures
	 *	must be an associative array with key representing the
	 *	name of the parameter in the execution scope
	 *
	 *	@access protected
	 *	@var array
	 */
	protected $_additional;
	
	/**
	 *	Properties that you set on the object
	 *	preventing overwrite of values
	 *
	 *	@access protected
	 *	@var array
	 */
	protected $_other_properties = array();
	
	/**
	 *	Create a new callable instance of a closure
	 *
	 *	@param string $args
	 *	@param string $code
	 *	@param array $additional = array()
	 *	@return callable
	 */
	public static function createClosure ($args, $code, array $additional = array())
	{
		$trace = debug_backtrace();
		
		$instance = count(self::$instances);
		
		self::$instances[$instance] = new self($args, $code, $additional);
		
		//changes the backtrace info that the object was created in
		//so it is easier to figure out where the setup came from
		self::$instances[$instance]->trace = $trace;
		
		//remove PHP default values from the type castings in the arguments
		return create_function(preg_replace(self::LEGALIZE_ARGUMENTS, '\\2$', $args), 
					'$__instance__ = pClosure::getInstance('.$instance.');
					$__args__ = array();
					
					foreach ($__instance__->arguments as $__name__ => $__value__)
					{
						$__parts__ = explode(":", $__name__);
						$__realName__ = $__parts__[1];
						
						//maintain references
						if (isset($$__realName__))
						{
							$__args__[$__realName__] =& $$__realName__;
						}
						else
						{
							$__args__[$__realName__] = null;
						}
					}
					
					return $__instance__->_execute($__args__);');
	}
	
	/**
	 *	Get a pre-registered instance of pClosure
	 *
	 *	@param integer $instance
	 *	@return pClosure
	 */
	public static function getInstance ($instance)
	{
		if (!is_numeric($instance))
		{
			throw new InvalidArgumentException('pClosure::getInstance expects 1 parameter '.
							'and it must be an integer, "' . gettype($instance).'" given');
		}
		
		return self::$instances[$instance];
	}
	
	/**
	 *	
	 *
	 *	@param string $args
	 *	@param string $code
	 *	@param array $additional = array()
	 */
	public function __construct ($args, $code, array $additional = array())
	{
		$this->originalArgumentString = (string)$args;
		//track the backtrace stack that the object was created in
		//so it is easier to figure out where the setup came from
		//for error reporting purposes
		$this->trace = debug_backtrace();
		
		$this->_code = (string)$code;
		$this->_args = $this->formatArguments((string)$args);
		$this->_additional =& $additional;
	}
	
	/**
	 *	
	 *
	 *	@return string
	 */
	public function __toString ()
	{
		return "function ({$this->originalArgumentString})\n{\n{$this->_code}\n}";
	}
	
	/**
	 *	
	 *
	 *	@param ...args
	 *	@return mixed
	 */
	public function __invoke ()
	{
		$args = array();
		$arguments = func_get_args();
		/*$i = 0;
					
		foreach ($this->arguments as $name => $value)
		{
			$parts = explode(":", $name);
			$realName = $parts[1];
			
			//maintain references
			if (isset($arguments[$i]))
			{
				$args[$realName] =& $arguments[$i];
			}
			else
			{
				$args[$realName] = null;
			}
			
			++$i;
		}*/
		
		return $this->_execute($arguments);//$args);
	}
	
	/**
	 *	formats an argument string into an associative array
	 *	with type hinting added to the key along with the name
	 *
	 *	**Additionally supports type hinting of default PHP values
	 *
	 *	@access protected
	 *	@param string $args
	 *	@return array
	 */
	protected function formatArguments ($args)
	{
		$matches = null;
		
		$newArgs = array();
		
		//match each argument in the string
		if ($count = preg_match_all(self::ARGUMENT_PATTERN, $args, $matches, PREG_PATTERN_ORDER))
		{
			//loop over them
			for ($i=0;$i < $count; ++$i)
			{
				$default = null;
				
				//if the default value was a string
				if ($matches['quote'][$i])
				{
					$default = $matches['string'][$i];
				}
				//else if there was a default value
				elseif ($matches['default'][$i])
				{
					$value = preg_replace('/\s+/', '', strtolower(trim($matches['other'][$i])));
					
					switch ($value)
					{
						//a default null is a special case
						case 'null':	$default = self::ARGUMENT_DEFAULT_NULL; break;
						case 'array()':	$default = array(); break;
						case 'true':	$default = true; break;
						case 'false':	$default = false; break;
						default:
						{
							//if the first character is a number,
							//or the first character is a . (period)
							//or the first character is a - (hyphen)
							//it is a float or integer
							if (is_numeric($value[0]) || $value[0] == '.' || $value[0] == '-')
							{
								//figure out if it is a float or integer
								$default = (floatval($value) == intval($value) ? intval($value) : floatval($value));
								break;
							}
							
							//otherwise it must be a constant or a class constant
							$default = eval('return ' . trim($matches['other'][$i]) . ';');
							/* - didnt account for \NAMESPACE_CONSTANT
							$default = strstr($value, '::') ? 
										//if it is a class constant
										eval('return ' . trim($matches['other'][$i]) . ';') : 
										//otherwise it must be a global constant
										constant(trim($matches['other'][$i]));
							*/
							break;
						}
					}
				}
				
				//the name will also contain type hinting
				$name = (
							$matches['type'][$i] ? 
							$matches['type'][$i] : 
							(
								is_null($default) ? 
								self::REQUIRED : 
								self::ANY
							)
						) . ':' . $matches['symbol'][$i];
				
				$newArgs[ $name ] = ($matches['default'][$i] ? $default : null);
			}
		}
		
		return $newArgs;
	}
	
	/**
	 *	Takes an indexxed array of values and 
	 *	returns an associative array of name value pairs
	 *	that represent the arguments for extracting into a execution scope
	 *
	 *	**Additionally supports type hinting of default PHP values
	 *
	 *	@param array $args
	 *	@return array
	 */
	public function &prepareArguments (array $args)
	{
		$newArgs = array();
		$length = count($args);
		$i = 0;
		
		foreach ($this->_args as $name => $default)
		{
			$arg = ($i < $length ? $args[$i] : $default);
			
			//if the argument has a type hint - which it should
			if (strstr($name, ':'))
			{
				$parts = explode(':', $name);
				
				$type = strtolower($parts[0]);
				
				if ($type == self::ANY)
				{
					//go on
				}
				elseif ($type == self::REQUIRED)
				{
					if ($i >= $length)
					{
						throw new InvalidArgumentException("Argument number {$i} of pClosure::_execute requires an argument, '" .
											gettype($arg).
										"' given and defined in '{$this->trace[0]['file']}' on line {$this->trace[0]['line']} : runtime-created closure",
										E_USER_ERROR);
					}
				}
				//support argument type hinting for callables
				elseif ($type == 'callable')
				{
					//if the arg is of the type pClosure change to make it qualify as callable
					if (is_a($arg, 'pClosure'))
					{
						$arg = array($arg, '_execute');
					}
					elseif ($arg != self::ARGUMENT_DEFAULT_NULL && !is_callable($arg))
					{
						throw new InvalidArgumentException("Argument number {$i} of pClosure::_execute expects 'callable', '" .
											gettype($arg).
										"' given and defined in '{$this->trace[0]['file']}' on line {$this->trace[0]['line']} : runtime-created closure",
										E_USER_ERROR);
					}
				}
				//support argument type hinting for other PHP types
				elseif (strstr('|bool|boolean|string|int|integer|real|float|double|array|object|', "|{$type}|"))
				{
					if ($type == 'real' || $type == 'float')
					{
						$type = 'double';
					}
					
					if ($type == 'int')
					{
						$type = 'integer';
					}
					
					if ($type == 'bool')
					{
						$type = 'boolean';
					}
					
					if ($arg != self::ARGUMENT_DEFAULT_NULL && strtolower(gettype($arg)) != $type)
					{
						throw new InvalidArgumentException("Argument number {$i} of pClosure::_execute expects '{$type}', '" .
										gettype($arg).
									"' given and defined in '{$this->trace[0]['file']}' on line {$this->trace[0]['line']} : runtime-created closure",
									E_USER_ERROR);
					}
				}
				//type checking objects
				//if the argument is not of the desired type and not a default null value
				//hacked for the is_a deprecated error
				elseif (
					(
						!is_object($arg) ||  
						(
							get_class($arg) != $parts[0] &&
							is_subclass_of($arg, $parts[0])
						)
					) && (
						$default != self::ARGUMENT_DEFAULT_NULL ||
						$arg !== null
					)
				)
				{
					throw new InvalidArgumentException("Argument number {$i} of pClosure::_execute expects object of the type '{$parts[0]}', '" .
									gettype($arg) .
									"' given and defined in '{$this->trace[0]['file']}' on line {$this->trace[0]['line']} : runtime-created closure",
									E_USER_ERROR);
				}
				
				$name = $parts[1];
			}
			
			$arg = $arg == self::ARGUMENT_DEFAULT_NULL ? null : $arg;
			
			//if the arg has been set to the default value
			if ($arg === $default || $arg === null)
			{
				$newArgs[$name] = $arg;
			}
			//else maintain a reference to the original argument
			else
			{
				$newArgs[$name] =& $args[$i];
			}
			
			++$i;
		}
		
		return $newArgs;
	}
	
	/**
	 *
	 *
	 *
	 */
	public function __get ($name)
	{
		switch ($name)
		{
			case 'code': 				return $this->_code;
			case 'args':
			case 'arguments':			return $this->_args;
			case 'additional':			return $this->_additional;
			case 'other_properties': 	return $this->_other_properties;
		}
		
		return isset($this->_other_properties[$name]) ? $this->_other_properties[$name] : null;
	}
	
	/**
	 *
	 *
	 *
	 */
	public function __set ($name, $value)
	{
		$this->_other_properties[$name] = $value;
	}
	
	/**
	 *
	 *
	 *
	 */
	public function __isset ($name)
	{
		return strstr('|other_properties|code|args|arguments|additional|', "|{$name}|") || isset($this->_other_properties[$name]);
	}
	
	/**
	 *
	 *
	 *
	 */
	public function __unset ($name)
	{
		unset($this->_other_properties[$name]);
	}
	
	/**
	 *	A call to execute the closure
	 *
	 *	**I didn't use __invoke because it has special meaning as of PHP 5.3
	 *	**In order to minimize symbol table collisions I have tried to name
	 *	**local variables in the execution scope using magic style naming conventions
	 *
	 *	@param array $__args__
	 *	@return mixed
	 */
	public function _execute (array $__args__)
	{
		$__returnValue__ = null;
		
		$__arguments__ = array();
		
		foreach ($__args__ as $__name__ => $__arg__)
		{
			$__arguments__[count($__arguments__)] =& $__args__[$__name__];
		}
		
		$__preparedArguments__ = $this->prepareArguments($__arguments__);
		
		if (is_null($__preparedArguments__))
		{
			return;
		}
		
		if ($__preparedArguments__)
		{
			//extract them into the current execution scope with references
			extract($__preparedArguments__, EXTR_OVERWRITE | EXTR_REFS);
		}
		
		//if the closure was created with additional parameters
		if ($this->additional)
		{
			//extract them into the current execution scope with references
			extract($this->additional, EXTR_OVERWRITE | EXTR_REFS);
		}
		
		//if other parameters were added
		if ($this->_other_properties)
		{
			//extract them into the current execution scope with references
			extract($this->_other_properties, EXTR_OVERWRITE | EXTR_REFS);
		}
		
		try{
			//print $this->code;
			//evaluate the code in this execution scope
			$__returnValue__ = eval(preg_replace(self::FUNC_GET_ARGS, '\\1$__arguments__;', $this->code) . ' return null;');
		}
		catch (Exception $e)
		{
			$etype = get_class($e);
			//throw a new Exception, that contains backtrace info
			throw new $etype(
						$e->getMessage() . 
							" and defined in '{$this->trace[0]['file']}' on line {$this->trace[0]['line']} : runtime-created closure", 
						$e->getCode()
			);
		}
		
		return $__returnValue__;
	}
	
	/**
	 *	A call to execute the closure 
	 *
	 *	Warning: A call to this function will cause the loss of references
	 *			 even with pass-by-reference enabled, func_get_args() returns values
	 *
	 *	@param pClosureContext | pClosureStaticContext $context
	 *	@param ...$args
	 *	@return mixed
	 */
	public function call ($context)
	{
		if (!(
				$context instanceof pClosureContext || 
				is_subclass_of($context, 'pClosureStaticContext')
			)
		)
		{
			throw new InvalidArgumentException('pClosure::call requires that the first argument be an instance of pClosureContext, or '.
												'a string name for a class that implements pClosureStaticContext, "'.gettype($context).'" given');
		}
		
		$args = func_get_args();
		array_shift($args);
		$newArgs = $this->prepareArguments($args);
		
		//if the closure was created with additional parameters
		if ($this->_additional)
		{
			foreach ($this->additional as $name => $value)
			{
				$newArgs[$name] =& $this->_additional[$name];
			}
		}
		
		//if other parameters were added
		if ($this->_other_properties)
		{
			foreach ($this->_other_properties as $name => $value)
			{
				$newArgs[$name] =& $this->_other_properties[$name];
			}
		}
		
		return $context instanceof pClosureContext ?
				$context->callClosure($this, $newArgs) : 
				call_user_func(array(is_string($context) ? $context : get_class($context), 'callStaticClosure'), $this, $newArgs);
	}
	
	/**
	 *	A call to execute the closure 
	 *
	 *	Good news this call can preserve references
	 *
	 *	@param pClosureContext | pClosureStaticContext $context
	 *	@param array $args
	 *	@return mixed
	 */
	public function apply ($context, array $args)
	{
		if (!(
				$context instanceof pClosureContext || 
				is_subclass_of($context, 'pClosureStaticContext')
			)
		)
		{
			throw new InvalidArgumentException('pClosure::apply requires that the first argument be an instance of pClosureContext, or '.
												'a string name for a class that implements pClosureStaticContext, "'.gettype($context).'" given');
		}
		
		$i = 0;
		$newArgs = array();
		
		//this is to maintain references while ensuring that
		//prepareAruments gets an indexed array 
		foreach ($args as $n => $value)
		{
			$newArgs[$i++] =& $args[$n];
		}
		
		$newArgs = $this->prepareArguments($newArgs);
		
		//if the closure was created with additional parameters
		if ($this->_additional)
		{
			foreach ($this->_additional as $name => $value)
			{
				$newArgs[$name] =& $this->_additional[$name];
			}
		}
		
		//if other parameters were added
		if ($this->_other_properties)
		{
			foreach ($this->_other_properties as $name => $value)
			{
				$newArgs[$name] =& $this->_other_properties[$name];
			}
		}
		
		return $context instanceof pClosureContext ?
				$context->callClosure($this, $newArgs) : 
				call_user_func(array(is_string($context) ? $context : get_class($context), 'callStaticClosure'), $this, $newArgs);
	}
}

/**
 *	Classes that implement this interface provide a way to evaluate
 *	the code of a closure along with the given arguments extracted into
 *	the local execution scope
 *	
 *	@author Sam Shull
 */
interface pClosureContext
{
	/**
	 *	
	 *
	 *	@param pClosure $__closure__
	 *	@param array $args - an associative array containing 
	 *							name => value pairs that can be 
	 *							easily extracted into the execution 
	 *							scope with references to the original 
	 *							arguments, the additional parameters of
	 *							the closure, and any post-creation variables
	 *							attached to the closure
	 *	@return mixed
	 */
	public function callClosure (pClosure $__closure__, array $__args__);
	
	/*
	
	Example implementation for reference
	
	public function callClosure(pClosure $__closure__, array $__args__)
	{
		$__returnValue__ = null;
		
		extract($__args__, EXTR_OVERWRITE | EXTR_REFS);
		
		try{
			//evaluate the code in this context
			$__returnValue__ = eval(preg_replace(pClosure::FUNC_GET_ARGS, '\\1$__args__;', $__closure__->code) . ' return null;');
		}
		catch (Exception $e)
		{
			$etype = get_class($e);
			//throw a new Exception, that contains backtrace info
			throw new $etype(
						$e->getMessage() . 
							" and defined in '{$__closure__->trace[0]['file']}' on line {$__closure__->trace[0]['line']} : runtime-created closure", 
						$e->getCode()
			);
		}
		
		return $__returnValue__;
	}
	
	*/
}

/**
 *	Classes that implement this interface provide a way to evaluate
 *	the code of a closure along with the given arguments extracted into
 *	the local execution scope
 *	
 *	@author Sam Shull
 */
interface pClosureStaticContext
{
	/**
	 *	
	 *
	 *	@param pClosure $__closure__
	 *	@param array $args - an associative array containing 
	 *							name => value pairs that can be 
	 *							easily extracted into the execution 
	 *							scope with references to the original 
	 *							arguments, the additional parameters of
	 *							the closure, and any post-creation variables
	 *							attached to the closure
	 *	@return mixed
	 */
	public static function callStaticClosure (pClosure $__closure__, array $__args__);
	
	/*
	
	Example implementation for reference
	
	public static function callStaticClosure(pClosure $__closure__, array $__args__)
	{
		$__returnValue__ = null;
		
		extract($__args__, EXTR_OVERWRITE | EXTR_REFS);
		
		try{
			//evaluate the code in this context
			$__returnValue__ = eval(preg_replace(pClosure::FUNC_GET_ARGS, '\\1$__args__;', $__closure__->code) . ' return null;');
		}
		catch (Exception $e)
		{
			$etype = get_class($e);
			//throw a new Exception, that contains backtrace info
			throw new $etype(
						$e->getMessage() . 
							" and defined in '{$__closure__->trace[0]['file']}' on line {$__closure__->trace[0]['line']} : runtime-created closure", 
						$e->getCode()
			);
		}
		
		return $__returnValue__;
	}
	
	*/
}

?>

 
  Advertise on this site Advertise on this site   Site map Site map   Statistics Statistics   Site tips Site tips   Privacy policy Privacy policy   Contact Contact  

For more information send a message to :
info at phpclasses dot org.
Copyright (c) Icontem 1999-2009 PHP Classes - PHP Class Scripts
  PHP Book Reviews - Reviews of books and other products