<?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__;
}
*/
}
?>
|