PHP Classes

File: pClosure.php

Recommend this page to a friend!
  Classes of Sam S   pClosure   pClosure.php   Download  
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
Author: By
Last change: Typo
Date: 15 years ago
Size: 22,991 bytes
 

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