Location: PHPKode > scripts > pClosure > pclosure/pClosure.php
<?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 <hide@address.com>
 *	@version 0.1
 *
 *	@copyright Copyright (c) 2009 Sam Shull <hide@address.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__;
	}
	
	*/
}

?>
Return current item: pClosure