├── LICENSE ├── Patchwork.php ├── README.md ├── box.json ├── composer.json └── src ├── CallRerouting.php ├── CallRerouting ├── Decorator.php └── Handle.php ├── CodeManipulation.php ├── CodeManipulation ├── Actions │ ├── Arguments.php │ ├── CallRerouting.php │ ├── CodeManipulation.php │ ├── ConflictPrevention.php │ ├── Generic.php │ ├── Namespaces.php │ ├── RedefinitionOfInternals.php │ ├── RedefinitionOfLanguageConstructs.php │ └── RedefinitionOfNew.php ├── Source.php └── Stream.php ├── Config.php ├── Console.php ├── Exceptions.php ├── Redefinitions └── LanguageConstructs.php ├── Stack.php └── Utils.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2018 Ignas Rudaitis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Patchwork.php: -------------------------------------------------------------------------------- 1 | 5 | * @copyright 2010-2023 Ignas Rudaitis 6 | * @license http://www.opensource.org/licenses/mit-license.html 7 | */ 8 | namespace Patchwork; 9 | 10 | if (function_exists('Patchwork\replace')) { 11 | return; 12 | } 13 | 14 | require_once __DIR__ . '/src/Exceptions.php'; 15 | require_once __DIR__ . '/src/CallRerouting.php'; 16 | require_once __DIR__ . '/src/CodeManipulation.php'; 17 | require_once __DIR__ . '/src/Utils.php'; 18 | require_once __DIR__ . '/src/Stack.php'; 19 | require_once __DIR__ . '/src/Config.php'; 20 | 21 | function redefine($subject, callable $content) 22 | { 23 | $handle = null; 24 | foreach (array_slice(func_get_args(), 1) as $content) { 25 | $handle = CallRerouting\connect($subject, $content, $handle); 26 | } 27 | $handle->silence(); 28 | return $handle; 29 | } 30 | 31 | function relay(?array $args = null) 32 | { 33 | return CallRerouting\relay($args); 34 | } 35 | 36 | function fallBack() 37 | { 38 | throw new Exceptions\NoResult; 39 | } 40 | 41 | function restore(CallRerouting\Handle $handle) 42 | { 43 | $handle->expire(); 44 | } 45 | 46 | function restoreAll() 47 | { 48 | CallRerouting\disconnectAll(); 49 | } 50 | 51 | function silence(CallRerouting\Handle $handle) 52 | { 53 | $handle->silence(); 54 | } 55 | 56 | function assertEventuallyDefined(CallRerouting\Handle $handle) 57 | { 58 | $handle->unsilence(); 59 | } 60 | 61 | function getClass() 62 | { 63 | return Stack\top('class'); 64 | } 65 | 66 | function getCalledClass() 67 | { 68 | return Stack\topCalledClass(); 69 | } 70 | 71 | function getFunction() 72 | { 73 | return Stack\top('function'); 74 | } 75 | 76 | function getMethod() 77 | { 78 | return getClass() . '::' . getFunction(); 79 | } 80 | 81 | function configure() 82 | { 83 | Config\locate(); 84 | } 85 | 86 | function hasMissed($callable) 87 | { 88 | return Utils\callableWasMissed($callable); 89 | } 90 | 91 | function always($value) 92 | { 93 | return function() use ($value) { 94 | return $value; 95 | }; 96 | } 97 | 98 | Utils\alias('Patchwork', [ 99 | 'redefine' => ['replace', 'replaceLater'], 100 | 'relay' => 'callOriginal', 101 | 'fallBack' => 'pass', 102 | 'restore' => 'undo', 103 | 'restoreAll' => 'undoAll', 104 | ]); 105 | 106 | configure(); 107 | 108 | Utils\markMissedCallables(); 109 | 110 | CodeManipulation\Stream::discoverOtherWrapper(); 111 | CodeManipulation\Stream::wrap(); 112 | 113 | CodeManipulation\register([ 114 | CodeManipulation\Actions\CodeManipulation\propagateThroughEval(), 115 | CodeManipulation\Actions\CallRerouting\injectCallInterceptionCode(), 116 | CodeManipulation\Actions\RedefinitionOfInternals\spliceNamedFunctionCalls(), 117 | CodeManipulation\Actions\RedefinitionOfInternals\spliceDynamicCalls(), 118 | CodeManipulation\Actions\RedefinitionOfNew\spliceAllInstantiations, 119 | CodeManipulation\Actions\RedefinitionOfNew\publicizeConstructors, 120 | CodeManipulation\Actions\ConflictPrevention\preventImportingOtherCopiesOfPatchwork(), 121 | ]); 122 | 123 | CodeManipulation\onImport([ 124 | CodeManipulation\Actions\CallRerouting\markPreprocessedFiles(), 125 | ]); 126 | 127 | Utils\clearOpcodeCaches(); 128 | 129 | register_shutdown_function('Patchwork\Utils\clearOpcodeCaches'); 130 | 131 | CallRerouting\createStubsForInternals(); 132 | CallRerouting\connectDefaultInternals(); 133 | 134 | require __DIR__ . '/src/Redefinitions/LanguageConstructs.php'; 135 | 136 | CodeManipulation\register([ 137 | CodeManipulation\Actions\RedefinitionOfLanguageConstructs\spliceAllConfiguredLanguageConstructs(), 138 | CodeManipulation\Actions\CallRerouting\injectQueueDeploymentCode(), 139 | CodeManipulation\Actions\CodeManipulation\injectStreamWrapperReinstatementCode(), 140 | ]); 141 | 142 | if (Utils\wasRunAsConsoleApp()) { 143 | require __DIR__ . '/src/Console.php'; 144 | } 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Patchwork 2 | 3 | Patchwork implements the redefinition ([monkey-patching](https://en.wikipedia.org/wiki/Monkey_patch)) of functions and methods in PHP. This includes both user-defined and internal callables, which can be functions, class methods, or instance methods. In addition, [many](https://github.com/antecedent/patchwork/blob/master/src/Redefinitions/LanguageConstructs.php) function-like constructs, such as `exit` or `include`, are supported in an analogous way. 4 | 5 | Internally, Patchwork uses a [stream wrapper](http://php.net/manual/en/class.streamwrapper.php) on `file://`. In the case of user-defined functions and methods, it is used to inject a simple interceptor snippet to the beginning of every such callable. For the remaining types of callables, various other strategies are applied. 6 | 7 | ## Example: a DIY profiler 8 | 9 | ```php 10 | use function Patchwork\{redefine, relay, getMethod}; 11 | 12 | $profiling = fopen('profiling.csv', 'w'); 13 | 14 | redefine('App\*', function(...$args) use ($profiling) { 15 | $begin = microtime(true); 16 | relay(); # calls the original definition 17 | $end = microtime(true); 18 | fputcsv($profiling, [getMethod(), $end - $begin]); 19 | }); 20 | ``` 21 | 22 | ## Notes 23 | 24 | * *Method redefinition* is the internally preferred metaphor for Patchwork's behavior. 25 | * `restoreAll()` and `restore($handle)` end the lifetime of, respectively, all redefinitions, or only one of them, where `$handle = redefine(...)`. 26 | * Closure `$this` is automatically re-bound to the enclosing class of the method being redefined. 27 | * The behavior of `__CLASS__`, `static::class` etc. inside redefinitions disregards the metaphor. `getClass()`, `getCalledClass()`, `getMethod()` and `getFunction()` from the `Patchwork` namespace should be used instead. 28 | 29 | ## Testing-related uses 30 | 31 | Patchwork can be used to stub static methods, which, however, is a controversial practice. 32 | 33 | It should be applied prudently, that is, only after making oneself familiar with its pitfalls and temptations in other programming languages. For instance, in Javascript, Ruby, Python and some others, the native support for monkey-patching has made its testing-related uses more commonplace than in PHP. 34 | 35 | Tests that use monkey-patching are often no longer *unit* tests, because they become sensitive to details of implementation, not only those of interface: for example, such a test might no longer pass after switching from `time()` to `DateTime`. 36 | 37 | That being said, they still have their place where the only economically viable alternative is having no tests at all. 38 | 39 | ## Other use cases 40 | 41 | Patchwork is not suggested for [AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming) and other kinds of production usage. Its impact on the application's performance is highly likely to be prohibitively large. Additionally, while no _particular_ Patchwork-related security risks are either known or anticipated, please keep in mind that Patchwork was never developed with production environments in mind. 42 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "base-path": null, 3 | "output": "patchwork.phar", 4 | "check-requirements": false, 5 | "compactors": [ 6 | "KevinGH\\Box\\Compactor\\Php" 7 | ], 8 | "main": "Patchwork.php", 9 | "directories": [ 10 | "src" 11 | ], 12 | "files": [ 13 | "Patchwork.php", 14 | "LICENSE" 15 | ], 16 | "dump-autoload": false 17 | } 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "antecedent/patchwork", 3 | "homepage": "https://antecedent.github.io/patchwork/", 4 | "description": "Method redefinition (monkey-patching) functionality for PHP.", 5 | "keywords": ["testing", "redefinition", "runkit", "monkeypatching", "interception", "aop", "aspect"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Ignas Rudaitis", 10 | "email": "ignas.rudaitis@gmail.com" 11 | } 12 | ], 13 | "minimum-stability": "stable", 14 | "require": { 15 | "php": ">=7.1.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": ">=4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CallRerouting.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CallRerouting; 10 | 11 | require __DIR__ . '/CallRerouting/Handle.php'; 12 | require __DIR__ . '/CallRerouting/Decorator.php'; 13 | 14 | use Patchwork\Utils; 15 | use Patchwork\Stack; 16 | use Patchwork\Config; 17 | use Patchwork\Exceptions; 18 | use Patchwork\CodeManipulation; 19 | use Patchwork\CodeManipulation\Actions\RedefinitionOfLanguageConstructs; 20 | use Patchwork\CodeManipulation\Actions\RedefinitionOfNew; 21 | 22 | const INTERNAL_REDEFINITION_NAMESPACE = 'Patchwork\Redefinitions'; 23 | const EVALUATED_CODE_FILE_NAME_SUFFIX = '/\(\d+\) : eval\(\)\'d code$/'; 24 | const INSTANTIATOR_NAMESPACE = 'Patchwork\Instantiators'; 25 | const INSTANTIATOR_DEFAULT_ARGUMENT = 'Patchwork\CallRerouting\INSTANTIATOR_DEFAULT_ARGUMENT'; 26 | 27 | const INTERNAL_STUB_CODE = ' 28 | namespace @ns_for_redefinitions; 29 | function @name(@signature) { 30 | $__pwArgs = \array_slice(\debug_backtrace()[0]["args"], 1); 31 | if (!empty($__pwNamespace) && \function_exists($__pwNamespace . "\\\\@name")) { 32 | return \call_user_func_array($__pwNamespace . "\\\\@name", $__pwArgs); 33 | } 34 | @interceptor; 35 | return \call_user_func_array("@name", $__pwArgs); 36 | } 37 | '; 38 | 39 | const INSTANTIATOR_CODE = ' 40 | namespace @namespace; 41 | class @instantiator { 42 | function instantiate(@parameters) { 43 | $__pwArgs = \debug_backtrace()[0]["args"]; 44 | foreach ($__pwArgs as $__pwOffset => $__pwValue) { 45 | if ($__pwValue === \Patchwork\CallRerouting\INSTANTIATOR_DEFAULT_ARGUMENT) { 46 | unset($__pwArgs[$__pwOffset]); 47 | } 48 | } 49 | switch (count($__pwArgs)) { 50 | case 0: 51 | return new \@class; 52 | case 1: 53 | return new \@class($__pwArgs[0]); 54 | case 2: 55 | return new \@class($__pwArgs[0], $__pwArgs[1]); 56 | case 3: 57 | return new \@class($__pwArgs[0], $__pwArgs[1], $__pwArgs[2]); 58 | case 4: 59 | return new \@class($__pwArgs[0], $__pwArgs[1], $__pwArgs[2], $__pwArgs[3]); 60 | case 5: 61 | return new \@class($__pwArgs[0], $__pwArgs[1], $__pwArgs[2], $__pwArgs[3], $__pwArgs[4]); 62 | default: 63 | $__pwReflector = new \ReflectionClass(\'@class\'); 64 | return $__pwReflector->newInstanceArgs($__pwArgs); 65 | } 66 | } 67 | } 68 | '; 69 | 70 | function connect($source, callable $target, ?Handle $handle = null, $partOfWildcard = false) 71 | { 72 | $source = translateIfLanguageConstruct($source); 73 | $handle = $handle ?: new Handle; 74 | list($class, $method) = Utils\interpretCallable($source); 75 | if (constitutesWildcard($source)) { 76 | return applyWildcard($source, $target, $handle); 77 | } 78 | if (Utils\isOwnName($class) || Utils\isOwnName($method)) { 79 | return $handle; 80 | } 81 | validate($source, $partOfWildcard); 82 | if (empty($class)) { 83 | if (Utils\callableDefined($source) && (new \ReflectionFunction($method))->isInternal()) { 84 | $stub = INTERNAL_REDEFINITION_NAMESPACE . '\\' . $source; 85 | return connect($stub, $target, $handle, $partOfWildcard); 86 | } 87 | $handle = connectFunction($method, $target, $handle); 88 | } else { 89 | if (Utils\callableDefined($source)) { 90 | if ($method === 'new') { 91 | $handle = connectInstantiation($class, $target, $handle); 92 | } elseif ((new \ReflectionMethod($class, $method))->isUserDefined()) { 93 | $handle = connectMethod($source, $target, $handle); 94 | } else { 95 | throw new InternalMethodsNotSupported($source); 96 | } 97 | } else { 98 | $handle = queueConnection($source, $target, $handle); 99 | } 100 | } 101 | attachExistenceAssertion($handle, $source); 102 | return $handle; 103 | } 104 | 105 | function constitutesWildcard($source) 106 | { 107 | $source = Utils\interpretCallable($source); 108 | $source = Utils\callableToString($source); 109 | return strcspn($source, '*{,}') != strlen($source); 110 | } 111 | 112 | function applyWildcard($wildcard, callable $target, ?Handle $handle = null) 113 | { 114 | $handle = $handle ?: new Handle; 115 | list($class, $method, $instance) = Utils\interpretCallable($wildcard); 116 | if (!empty($instance)) { 117 | foreach (Utils\matchWildcard($method, get_class_methods($instance)) as $item) { 118 | if (!$handle->hasTag($item)) { 119 | connect([$instance, $item], $target, $handle); 120 | $handle->tag($item); 121 | } 122 | } 123 | return $handle; 124 | } 125 | 126 | $callables = Utils\matchWildcard($wildcard, Utils\getRedefinableCallables()); 127 | foreach ($callables as $callable) { 128 | if (!inPreprocessedFile($callable) || $handle->hasTag($callable)) { 129 | continue; 130 | } 131 | if (function_exists($callable)) { 132 | # Restore lower/upper case distinction 133 | $callable = (new \ReflectionFunction($callable))->getName(); 134 | } 135 | connect($callable, $target, $handle, true); 136 | $handle->tag($callable); 137 | } 138 | if (!isset($class) || !class_exists($class, false)) { 139 | queueConnection($wildcard, $target, $handle); 140 | } 141 | return $handle; 142 | } 143 | 144 | function attachExistenceAssertion(Handle $handle, $function) 145 | { 146 | $handle->addExpirationHandler(function() use ($function) { 147 | if (!Utils\callableDefined($function)) { 148 | # Not using exceptions because this might happen during PHP shutdown 149 | $message = '%s() was never defined during the lifetime of its redefinition'; 150 | trigger_error(sprintf($message, Utils\callableToString($function)), E_USER_WARNING); 151 | } 152 | }); 153 | } 154 | 155 | function validate($function, $partOfWildcard = false) 156 | { 157 | list($class, $method) = Utils\interpretCallable($function); 158 | if (!Utils\callableDefined($function) || $method === 'new') { 159 | return; 160 | } 161 | $reflection = Utils\reflectCallable($function); 162 | $name = Utils\callableToString($function); 163 | if ($reflection->isInternal() && !in_array($name, Config\getRedefinableInternals())) { 164 | throw new Exceptions\NotUserDefined($function); 165 | } 166 | if (!$reflection->isInternal() && !inPreprocessedFile($function) && !$partOfWildcard) { 167 | throw new Exceptions\DefinedTooEarly($function); 168 | } 169 | } 170 | 171 | function inPreprocessedFile($callable) 172 | { 173 | if (Utils\isOwnName(Utils\callableToString($callable))) { 174 | return false; 175 | } 176 | $file = Utils\reflectCallable($callable)->getFileName(); 177 | $evaluated = preg_match(EVALUATED_CODE_FILE_NAME_SUFFIX, $file); 178 | return $evaluated || !empty(State::$preprocessedFiles[$file]); 179 | } 180 | 181 | function connectFunction($function, callable $target, ?Handle $handle = null) 182 | { 183 | $handle = $handle ?: new Handle; 184 | $routes = &State::$routes[null][$function]; 185 | $offset = Utils\append($routes, [$target, $handle]); 186 | $handle->addReference($routes[$offset]); 187 | return $handle; 188 | } 189 | 190 | function queueConnection($source, callable $target, ?Handle $handle = null) 191 | { 192 | $handle = $handle ?: new Handle; 193 | $offset = Utils\append(State::$queue, [$source, $target, $handle]); 194 | $handle->addReference(State::$queue[$offset]); 195 | return $handle; 196 | } 197 | 198 | function deployQueue() 199 | { 200 | foreach (State::$queue as $offset => $item) { 201 | if (empty($item)) { 202 | unset(State::$queue[$offset]); 203 | continue; 204 | } 205 | list($source, $target, $handle) = $item; 206 | if (Utils\callableDefined($source) || constitutesWildcard($source)) { 207 | connect($source, $target, $handle); 208 | unset(State::$queue[$offset]); 209 | } 210 | } 211 | } 212 | 213 | function connectMethod($function, callable $target, ?Handle $handle = null) 214 | { 215 | $handle = $handle ?: new Handle; 216 | list($class, $method, $instance) = Utils\interpretCallable($function); 217 | $target = new Decorator($target); 218 | $target->superclass = $class; 219 | $target->method = $method; 220 | $target->instance = $instance; 221 | $reflection = Utils\reflectCallable($function); 222 | $declaringClass = $reflection->getDeclaringClass(); 223 | $class = $declaringClass->getName(); 224 | $aliases = $declaringClass->getTraitAliases(); 225 | if (isset($aliases[$method])) { 226 | list($trait, $method) = explode('::', $aliases[$method]); 227 | } 228 | $routes = &State::$routes[$class][$method]; 229 | $offset = Utils\append($routes, [$target, $handle]); 230 | $handle->addReference($routes[$offset]); 231 | return $handle; 232 | } 233 | 234 | function connectInstantiation($class, callable $target, ?Handle $handle = null) 235 | { 236 | if (!Config\isNewKeywordRedefinable()) { 237 | throw new Exceptions\NewKeywordNotRedefinable; 238 | } 239 | $handle = $handle ?: new Handle; 240 | $class = strtr($class, ['\\' => '__']); 241 | $routes = &State::$routes["Patchwork\\Instantiators\\$class"]['instantiate']; 242 | $offset = Utils\append($routes, [$target, $handle]); 243 | $handle->addReference($routes[$offset]); 244 | return $handle; 245 | } 246 | 247 | function disconnectAll() 248 | { 249 | foreach (State::$routes as $class => $routesByClass) { 250 | foreach ($routesByClass as $method => $routes) { 251 | foreach ($routes as $route) { 252 | list($callback, $handle) = $route; 253 | if ($handle !== null) { 254 | $handle->expire(); 255 | } 256 | } 257 | } 258 | } 259 | State::$routes = []; 260 | connectDefaultInternals(); 261 | } 262 | 263 | function dispatchTo(callable $target) 264 | { 265 | return call_user_func_array($target, Stack\top('args')); 266 | } 267 | 268 | function dispatch($class, $calledClass, $method, $frame, &$result, ?array $args = null) 269 | { 270 | $trace = debug_backtrace(); 271 | $isInternalStub = strpos($method, INTERNAL_REDEFINITION_NAMESPACE) === 0; 272 | $isLanguageConstructStub = strpos($method, RedefinitionOfLanguageConstructs\LANGUAGE_CONSTRUCT_PREFIX) === 0; 273 | $isInstantiator = strpos($method, INSTANTIATOR_NAMESPACE) === 0; 274 | if ($isInternalStub && !$isLanguageConstructStub && $args === null) { 275 | # Mind the namespace-of-origin argument 276 | $args = array_reverse($trace)[$frame - 1]['args']; 277 | array_shift($args); 278 | } 279 | if ($isInstantiator) { 280 | $args = $args ?: array_reverse($trace)[$frame - 1]['args']; 281 | foreach ($args as $offset => $value) { 282 | if ($value === INSTANTIATOR_DEFAULT_ARGUMENT) { 283 | unset($args[$offset]); 284 | } 285 | } 286 | } 287 | $success = false; 288 | Stack\pushFor($frame, $calledClass, function() use ($class, $method, &$result, &$success) { 289 | foreach (getRoutesFor($class, $method) as $offset => $route) { 290 | if (empty($route)) { 291 | unset(State::$routes[$class][$method][$offset]); 292 | continue; 293 | } 294 | State::$routeStack[] = [$class, $method, $offset]; 295 | try { 296 | $result = dispatchTo(reset($route)); 297 | $success = true; 298 | } catch (Exceptions\NoResult $e) { 299 | array_pop(State::$routeStack); 300 | continue; 301 | } 302 | array_pop(State::$routeStack); 303 | if ($success) { 304 | break; 305 | } 306 | } 307 | }, $args); 308 | return $success; 309 | } 310 | 311 | function relay(?array $args = null) 312 | { 313 | list($class, $method, $offset) = end(State::$routeStack); 314 | $route = &State::$routes[$class][$method][$offset]; 315 | $backup = $route; 316 | $route = ['Patchwork\fallBack', new Handle]; 317 | $top = Stack\top(); 318 | if ($args === null) { 319 | $args = $top['args']; 320 | } 321 | $isInternalStub = strpos($method, INTERNAL_REDEFINITION_NAMESPACE) === 0; 322 | $isLanguageConstructStub = strpos($method, RedefinitionOfLanguageConstructs\LANGUAGE_CONSTRUCT_PREFIX) === 0; 323 | if ($isInternalStub && !$isLanguageConstructStub) { 324 | array_unshift($args, ''); 325 | } 326 | try { 327 | if (isset($top['class'])) { 328 | $reflection = new \ReflectionMethod(Stack\topCalledClass(), $top['function']); 329 | $reflection->setAccessible(true); 330 | $result = $reflection->invokeArgs(Stack\top('object'), $args); 331 | } else { 332 | $result = call_user_func_array($top['function'], $args); 333 | } 334 | } catch (\Exception $e) { 335 | $exception = $e; 336 | } 337 | $route = $backup; 338 | if (isset($exception)) { 339 | throw $exception; 340 | } 341 | return $result; 342 | } 343 | 344 | /** 345 | * @deprecated 2.2.0 346 | */ 347 | function connectOnHHVM($function, Handle $handle) 348 | { 349 | fb_intercept($function, function($name, $obj, $args, $data, &$done) { 350 | deployQueue(); 351 | list($class, $method) = Utils\interpretCallable($name); 352 | $calledClass = null; 353 | if (is_string($obj)) { 354 | $calledClass = $obj; 355 | } elseif (is_object($obj)) { 356 | $calledClass = get_class($obj); 357 | } 358 | $frame = count(debug_backtrace(0)) - 1; 359 | $result = null; 360 | $done = dispatch($class, $calledClass, $method, $frame, $result, $args); 361 | return $result; 362 | }); 363 | $handle->addExpirationHandler(getHHVMExpirationHandler($function)); 364 | } 365 | 366 | /** 367 | * @deprecated 2.2.0 368 | */ 369 | function getHHVMExpirationHandler($function) 370 | { 371 | return function() use ($function) { 372 | list($class, $method) = Utils\interpretCallable($function); 373 | $empty = true; 374 | foreach (getRoutesFor($class, $method) as $offset => $route) { 375 | if (!empty($route)) { 376 | $empty = false; 377 | break; 378 | } else { 379 | unset(State::$routes[$class][$method][$offset]); 380 | } 381 | } 382 | if ($empty) { 383 | fb_intercept($function, null); 384 | } 385 | }; 386 | } 387 | 388 | function getRoutesFor($class, $method) 389 | { 390 | if (!isset(State::$routes[$class][$method])) { 391 | return []; 392 | } 393 | return array_reverse(State::$routes[$class][$method], true); 394 | } 395 | 396 | function dispatchDynamic($callable, array $arguments) 397 | { 398 | list($class, $method) = Utils\interpretCallable($callable); 399 | $translation = INTERNAL_REDEFINITION_NAMESPACE . '\\' . $method; 400 | if ($class === null && function_exists($translation)) { 401 | $callable = $translation; 402 | # Mind the namespace-of-origin argument 403 | array_unshift($arguments, ''); 404 | } 405 | return call_user_func_array($callable, $arguments); 406 | } 407 | 408 | function createStubsForInternals() 409 | { 410 | $namespace = INTERNAL_REDEFINITION_NAMESPACE; 411 | foreach (Config\getRedefinableInternals() as $name) { 412 | if (function_exists($namespace . '\\' . $name)) { 413 | continue; 414 | } 415 | $signature = ['$__pwNamespace']; 416 | foreach ((new \ReflectionFunction($name))->getParameters() as $offset => $argument) { 417 | $formal = ''; 418 | if ($argument->isPassedByReference()) { 419 | $formal .= '&'; 420 | } 421 | $formal .= '$' . $argument->getName(); 422 | $isVariadic = is_callable([$argument, 'isVariadic']) ? $argument->isVariadic() : false; 423 | if ($argument->isOptional() || $isVariadic || ($name === 'define' && $offset === 2)) { 424 | continue; 425 | } 426 | $signature[] = $formal; 427 | } 428 | $refs = sprintf('[%s]', join(', ', $signature)); 429 | $interceptor = sprintf( 430 | str_replace( 431 | '$__pwRefOffset = 0;', 432 | '$__pwRefOffset = 1;', 433 | \Patchwork\CodeManipulation\Actions\CallRerouting\CALL_INTERCEPTION_CODE 434 | ), 435 | $refs 436 | ); 437 | eval(strtr(INTERNAL_STUB_CODE, [ 438 | '@name' => $name, 439 | '@signature' => join(', ', $signature), 440 | '@interceptor' => $interceptor, 441 | '@ns_for_redefinitions' => INTERNAL_REDEFINITION_NAMESPACE, 442 | ])); 443 | } 444 | } 445 | 446 | /** 447 | * This is needed, for instance, to intercept the time() call in call_user_func('time'). 448 | * 449 | * For that to happen, we require that if at least one internal function is redefinable, then 450 | * call_user_func, preg_replace_callback and other callback-taking internal functions also be 451 | * redefinable: see Patchwork\Config. 452 | * 453 | * Here, we go through the callback-taking internals and add argument-inspecting patches 454 | * (redefinitions) to them. 455 | * 456 | * The patches are then expected to find the "nested" internal calls, such as the 'time' argument 457 | * in call_user_func('time'), and invoke their respective redefinitions, if any. 458 | */ 459 | function connectDefaultInternals() 460 | { 461 | # call_user_func() etc. are not a problem if no other internal functions are redefined 462 | if (Config\getRedefinableInternals() === []) { 463 | return; 464 | } 465 | foreach (Config\getDefaultRedefinableInternals() as $function) { 466 | # Which arguments are callbacks? Store their offsets in the following array. 467 | $offsets = []; 468 | foreach ((new \ReflectionFunction($function))->getParameters() as $offset => $argument) { 469 | $name = $argument->getName(); 470 | if (strpos($name, 'call') !== false || strpos($name, 'func') !== false) { 471 | $offsets[] = $offset; 472 | } 473 | } 474 | connect($function, function() use ($function, $offsets) { 475 | # This is the argument-inspecting patch. 476 | $args = Stack\top('args'); 477 | $caller = Stack\all()[1]; 478 | foreach ($offsets as $offset) { 479 | # Callback absent 480 | if (!isset($args[$offset])) { 481 | continue; 482 | } 483 | $callable = $args[$offset]; 484 | # Callback is a closure => definitely not internal 485 | if ($callable instanceof \Closure) { 486 | continue; 487 | } 488 | list($class, $method, $instance) = Utils\interpretCallable($callable); 489 | if (empty($class)) { 490 | # Callback is global function, which might be internal too. 491 | $args[$offset] = function() use ($callable) { 492 | return dispatchDynamic($callable, func_get_args()); 493 | }; 494 | } 495 | # Callback involves a class => not internal either, since the only internals that 496 | # Patchwork can handle as of 2.0 are global functions. 497 | # However, we must handle all kinds of opaque access here too, such as self:: and 498 | # private methods, because we're actually patching a stub (see INTERNAL_STUB_CODE) 499 | # and not directly call_user_func itself (or usort, or any other of those). 500 | # We must compensate for scope that is lost, and that callback-taking functions 501 | # can make use of. 502 | if (!empty($class)) { 503 | if ($class === 'self' || $class === 'static' || $class === 'parent') { 504 | # We do not discriminate between early and late static binding here: FIXME. 505 | $actualClass = $caller['class']; 506 | if ($class === 'parent') { 507 | $actualClass = get_parent_class($actualClass); 508 | } 509 | $class = $actualClass; 510 | } 511 | 512 | # When calling a parent constructor, the reference to the object being 513 | # constructed needs to be extracted from the stack info. 514 | # Also turned out to be necessary to solve this, without any parent 515 | # constructors involved: https://github.com/antecedent/patchwork/issues/99 516 | if (is_null($instance) && isset($caller['object'])) { 517 | $instance = $caller['object']; 518 | } 519 | try { 520 | $reflection = new \ReflectionMethod($class, $method); 521 | $reflection->setAccessible(true); 522 | $args[$offset] = function() use ($reflection, $instance) { 523 | return $reflection->invokeArgs($instance, func_get_args()); 524 | }; 525 | } catch (\ReflectionException $e) { 526 | # If it's an invalid callable, then just prevent the unexpected propagation 527 | # of ReflectionExceptions. 528 | } 529 | } 530 | } 531 | # Give the inspected arguments back to the *original* definition of the 532 | # callback-taking function, e.g. \array_map(). This works given that the 533 | # present patch is the innermost. 534 | return call_user_func_array($function, $args); 535 | }); 536 | } 537 | } 538 | 539 | /** 540 | * @since 2.0.5 541 | * 542 | * As of version 2.0.5, this is used to accommodate language constructs 543 | * (echo, eval, exit and others) within the concept of callable. 544 | */ 545 | function translateIfLanguageConstruct($callable) 546 | { 547 | if (!is_string($callable)) { 548 | return $callable; 549 | } 550 | if (in_array($callable, Config\getRedefinableLanguageConstructs())) { 551 | return RedefinitionOfLanguageConstructs\LANGUAGE_CONSTRUCT_PREFIX . $callable; 552 | } elseif (in_array($callable, Config\getSupportedLanguageConstructs())) { 553 | throw new Exceptions\NotUserDefined($callable); 554 | } else { 555 | return $callable; 556 | } 557 | } 558 | 559 | function resolveClassToInstantiate($class, $calledClass) 560 | { 561 | $pieces = explode('\\', $class); 562 | $last = array_pop($pieces); 563 | if (in_array($last, ['self', 'static', 'parent'])) { 564 | $frame = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]; 565 | if ($last == 'self') { 566 | $class = $frame['class']; 567 | } elseif ($last == 'parent') { 568 | $class = get_parent_class($frame['class']); 569 | } elseif ($last == 'static') { 570 | $class = $calledClass; 571 | } 572 | } 573 | return ltrim($class, '\\'); 574 | } 575 | 576 | function getInstantiator($class, $calledClass) 577 | { 578 | $namespace = INSTANTIATOR_NAMESPACE; 579 | $class = resolveClassToInstantiate($class, $calledClass); 580 | $adaptedName = strtr($class, ['\\' => '__']); 581 | if (!class_exists("$namespace\\$adaptedName")) { 582 | $constructor = (new \ReflectionClass($class))->getConstructor(); 583 | list($parameters, $arguments) = Utils\getParameterAndArgumentLists($constructor); 584 | $code = strtr(INSTANTIATOR_CODE, [ 585 | '@namespace' => INSTANTIATOR_NAMESPACE, 586 | '@instantiator' => $adaptedName, 587 | '@class' => $class, 588 | '@parameters' => $parameters, 589 | ]); 590 | RedefinitionOfNew\suspendFor(function() use ($code) { 591 | eval(CodeManipulation\transformForEval($code)); 592 | }); 593 | } 594 | $instantiator = "$namespace\\$adaptedName"; 595 | return new $instantiator; 596 | } 597 | 598 | class State 599 | { 600 | static $routes = []; 601 | static $queue = []; 602 | static $preprocessedFiles = []; 603 | static $routeStack = []; 604 | } 605 | -------------------------------------------------------------------------------- /src/CallRerouting/Decorator.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CallRerouting; 10 | 11 | use Patchwork; 12 | use Patchwork\Stack; 13 | 14 | class Decorator 15 | { 16 | public $superclass; 17 | public $instance; 18 | public $method; 19 | 20 | private $patch; 21 | 22 | public function __construct($patch) 23 | { 24 | $this->patch = $patch; 25 | } 26 | 27 | public function __invoke() 28 | { 29 | $top = Stack\top(); 30 | $superclassMatches = $this->superclassMatches(); 31 | $instanceMatches = $this->instanceMatches($top); 32 | $methodMatches = $this->methodMatches($top); 33 | if ($superclassMatches && $instanceMatches && $methodMatches) { 34 | $patch = $this->patch; 35 | if (isset($top["object"]) && $patch instanceof \Closure) { 36 | $patch = $patch->bindTo($top["object"], $this->superclass); 37 | } 38 | return dispatchTo($patch); 39 | } 40 | Patchwork\fallBack(); 41 | } 42 | 43 | private function superclassMatches() 44 | { 45 | return $this->superclass === null || 46 | Stack\topCalledClass() === $this->superclass || 47 | is_subclass_of(Stack\topCalledClass(), $this->superclass); 48 | } 49 | 50 | private function instanceMatches(array $top) 51 | { 52 | return $this->instance === null || 53 | (isset($top["object"]) && $top["object"] === $this->instance); 54 | } 55 | 56 | private function methodMatches(array $top) 57 | { 58 | return $this->method === null || 59 | $this->method === 'new' || 60 | $top["function"] === $this->method; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/CallRerouting/Handle.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CallRerouting; 10 | 11 | class Handle 12 | { 13 | private $references = []; 14 | private $expirationHandlers = []; 15 | private $silenced = false; 16 | private $tags = []; 17 | 18 | public function __destruct() 19 | { 20 | $this->expire(); 21 | } 22 | 23 | public function tag($tag) 24 | { 25 | $this->tags[] = $tag; 26 | } 27 | 28 | public function hasTag($tag) 29 | { 30 | return in_array($tag, $this->tags); 31 | } 32 | 33 | public function addReference(&$reference) 34 | { 35 | $this->references[] = &$reference; 36 | } 37 | 38 | public function expire() 39 | { 40 | foreach ($this->references as &$reference) { 41 | $reference = null; 42 | } 43 | if (!$this->silenced) { 44 | foreach ($this->expirationHandlers as $expirationHandler) { 45 | $expirationHandler(); 46 | } 47 | } 48 | $this->expirationHandlers = []; 49 | } 50 | 51 | public function addExpirationHandler(callable $expirationHandler) 52 | { 53 | $this->expirationHandlers[] = $expirationHandler; 54 | } 55 | 56 | public function silence() 57 | { 58 | $this->silenced = true; 59 | } 60 | 61 | public function unsilence() 62 | { 63 | $this->silenced = false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/CodeManipulation.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2023 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation; 10 | 11 | require __DIR__ . '/CodeManipulation/Source.php'; 12 | require __DIR__ . '/CodeManipulation/Stream.php'; 13 | require __DIR__ . '/CodeManipulation/Actions/Generic.php'; 14 | require __DIR__ . '/CodeManipulation/Actions/CallRerouting.php'; 15 | require __DIR__ . '/CodeManipulation/Actions/CodeManipulation.php'; 16 | require __DIR__ . '/CodeManipulation/Actions/Namespaces.php'; 17 | require __DIR__ . '/CodeManipulation/Actions/RedefinitionOfInternals.php'; 18 | require __DIR__ . '/CodeManipulation/Actions/RedefinitionOfLanguageConstructs.php'; 19 | require __DIR__ . '/CodeManipulation/Actions/ConflictPrevention.php'; 20 | require __DIR__ . '/CodeManipulation/Actions/RedefinitionOfNew.php'; 21 | require __DIR__ . '/CodeManipulation/Actions/Arguments.php'; 22 | 23 | use Patchwork\Exceptions; 24 | use Patchwork\Config; 25 | 26 | const OUTPUT_DESTINATION = 'php://memory'; 27 | const OUTPUT_ACCESS_MODE = 'rb+'; 28 | 29 | function transform(Source $s) 30 | { 31 | foreach (State::$actions as $action) { 32 | $action($s); 33 | } 34 | } 35 | 36 | function transformString($code) 37 | { 38 | $source = new Source($code); 39 | transform($source); 40 | return (string) $source; 41 | } 42 | 43 | function transformForEval($code) 44 | { 45 | $prefix = "file), 'w', false); 95 | Stream::fwrite($handle, $source); 96 | Stream::fclose($handle); 97 | } 98 | 99 | function availableCached($file) 100 | { 101 | if (!cacheEnabled()) { 102 | return false; 103 | } 104 | $cached = getCachedPath($file); 105 | return file_exists($cached) && 106 | filemtime($file) <= filemtime($cached) && 107 | Config\getTimestamp() <= filemtime($cached); 108 | } 109 | 110 | function internalToCache($file) 111 | { 112 | if (!cacheEnabled()) { 113 | return false; 114 | } 115 | return strpos($file, Config\getCachePath() . '/') === 0 116 | || strpos($file, Config\getCachePath() . DIRECTORY_SEPARATOR) === 0; 117 | } 118 | 119 | 120 | function getContents($file) 121 | { 122 | $handle = Stream::fopen($file, 'r', true); 123 | if ($handle === false) { 124 | return false; 125 | } 126 | $contents = ''; 127 | while (!Stream::feof($handle)) { 128 | $contents .= Stream::fread($handle, 8192); 129 | } 130 | Stream::fclose($handle); 131 | return $contents; 132 | } 133 | 134 | function transformAndOpen($file) 135 | { 136 | foreach (State::$importListeners as $listener) { 137 | $listener($file); 138 | } 139 | if (!internalToCache($file) && availableCached($file)) { 140 | return Stream::fopen(getCachedPath($file), 'r', false); 141 | } 142 | $code = getContents($file); 143 | if ($code === false) { 144 | return false; 145 | } 146 | $source = new Source($code); 147 | $source->file = $file; 148 | transform($source); 149 | if (!internalToCache($file) && cacheEnabled()) { 150 | storeInCache($source); 151 | return transformAndOpen($file); 152 | } 153 | $resource = fopen(OUTPUT_DESTINATION, OUTPUT_ACCESS_MODE); 154 | if ($resource) { 155 | fwrite($resource, $source); 156 | rewind($resource); 157 | } 158 | return $resource; 159 | } 160 | 161 | function prime($file) 162 | { 163 | Stream::fclose(transformAndOpen($file)); 164 | } 165 | 166 | function shouldTransform($file) 167 | { 168 | return !Config\isBlacklisted($file) || Config\isWhitelisted($file); 169 | } 170 | 171 | function register($actions) 172 | { 173 | State::$actions = array_merge(State::$actions, (array) $actions); 174 | } 175 | 176 | function onImport($listeners) 177 | { 178 | State::$importListeners = array_merge(State::$importListeners, (array) $listeners); 179 | } 180 | 181 | class State 182 | { 183 | static $actions = []; 184 | static $importListeners = []; 185 | static $cacheIndex = []; 186 | static $cacheIndexFile; 187 | } 188 | -------------------------------------------------------------------------------- /src/CodeManipulation/Actions/Arguments.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2021 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation\Actions\Arguments; 10 | 11 | use Patchwork\CodeManipulation\Source; 12 | use Patchwork\CodeManipulation\Actions\Generic; 13 | 14 | /** 15 | * @since 2.1.13 16 | */ 17 | function readNames(Source $s, $pos) 18 | { 19 | $result = []; 20 | $pos++; 21 | while (!$s->is(Generic\RIGHT_ROUND, $pos)) { 22 | if ($s->is([Generic\LEFT_ROUND, Generic\LEFT_SQUARE, Generic\LEFT_CURLY], $pos)) { 23 | $pos = $s->match($pos); 24 | } else { 25 | if ($s->is(T_VARIABLE, $pos)) { 26 | $result[] = $s->read($pos); 27 | } elseif ($s->is(Generic\ELLIPSIS, $pos)) { 28 | $pos = $s->skip(Source::junk(), $pos); 29 | $result[] = '...' . $s->read($pos); 30 | } 31 | $pos++; 32 | } 33 | } 34 | return $result; 35 | } 36 | 37 | /** 38 | * @since 2.1.13 39 | */ 40 | function constructReferenceArray(array $names) 41 | { 42 | $names = array_map(function($name) { 43 | if ($name[0] === '.') { 44 | return '], ' . substr($name, 3) . ', ['; 45 | } 46 | return '&' . $name; 47 | }, $names); 48 | return 'array_merge([' . join(', ', $names) . '])'; 49 | } -------------------------------------------------------------------------------- /src/CodeManipulation/Actions/CallRerouting.php: -------------------------------------------------------------------------------- 1 | 5 | * @link http://patchwork2.org/ 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation\Actions\CallRerouting; 10 | 11 | use Patchwork\CodeManipulation\Actions\Generic; 12 | use Patchwork\CallRerouting; 13 | use Patchwork\Utils; 14 | 15 | const CALL_INTERCEPTION_CODE = ' 16 | $__pwClosureName = __NAMESPACE__ ? __NAMESPACE__ . "\\\\{closure}" : "\\\\{closure}"; 17 | $__pwClass = (__CLASS__ && __FUNCTION__ !== $__pwClosureName) ? __CLASS__ : null; 18 | if (!empty(\Patchwork\CallRerouting\State::$routes[$__pwClass][__FUNCTION__])) { 19 | $__pwCalledClass = $__pwClass ? \get_called_class() : null; 20 | $__pwFrame = \count(\debug_backtrace(0)); 21 | $__pwRefs = %s; 22 | $__pwRefOffset = 0; 23 | if (\Patchwork\CallRerouting\dispatch($__pwClass, $__pwCalledClass, __FUNCTION__, $__pwFrame, $__pwResult, \array_merge(\array_slice($__pwRefs, $__pwRefOffset, \func_num_args()), \array_slice(\func_get_args(), \count($__pwRefs))))) { 24 | return $__pwResult; 25 | } 26 | } 27 | unset($__pwClass, $__pwCalledClass, $__pwResult, $__pwClosureName, $__pwFrame, $__pwRefs, $__pwRefOffset); 28 | '; 29 | 30 | const CALL_INTERCEPTION_CODE_VOID_TYPED = ' 31 | $__pwClosureName = __NAMESPACE__ ? __NAMESPACE__ . "\\\\{closure}" : "\\\\{closure}"; 32 | $__pwClass = (__CLASS__ && __FUNCTION__ !== $__pwClosureName) ? __CLASS__ : null; 33 | if (!empty(\Patchwork\CallRerouting\State::$routes[$__pwClass][__FUNCTION__])) { 34 | $__pwCalledClass = $__pwClass ? \get_called_class() : null; 35 | $__pwFrame = \count(\debug_backtrace(0)); 36 | $__pwRefs = %s; 37 | $__pwRefOffset = 0; 38 | if (\Patchwork\CallRerouting\dispatch($__pwClass, $__pwCalledClass, __FUNCTION__, $__pwFrame, $__pwResult, \array_merge(\array_slice($__pwRefs, $__pwRefOffset, \func_num_args()), \array_slice(\func_get_args(), \count($__pwRefs))))) { 39 | if ($__pwResult !== null) { 40 | throw new \Patchwork\Exceptions\NonNullToVoid; 41 | } 42 | return; 43 | } 44 | } 45 | unset($__pwClass, $__pwCalledClass, $__pwResult, $__pwClosureName, $__pwFrame, $__pwRefOffset); 46 | '; 47 | 48 | const CALL_INTERCEPTION_CODE_NEVER_TYPED = ' 49 | $__pwClosureName = __NAMESPACE__ ? __NAMESPACE__ . "\\\\{closure}" : "\\\\{closure}"; 50 | $__pwClass = (__CLASS__ && __FUNCTION__ !== $__pwClosureName) ? __CLASS__ : null; 51 | if (!empty(\Patchwork\CallRerouting\State::$routes[$__pwClass][__FUNCTION__])) { 52 | $__pwCalledClass = $__pwClass ? \get_called_class() : null; 53 | $__pwFrame = \count(\debug_backtrace(0)); 54 | $__pwRefs = %s; 55 | $__pwRefOffset = 0; 56 | if (\Patchwork\CallRerouting\dispatch($__pwClass, $__pwCalledClass, __FUNCTION__, $__pwFrame, $__pwResult, \array_merge(\array_slice($__pwRefs, $__pwRefOffset, \func_num_args()), \array_slice(\func_get_args(), \count($__pwRefs))))) { 57 | throw new \Patchwork\Exceptions\ReturnFromNever; 58 | } 59 | } 60 | unset($__pwClass, $__pwCalledClass, $__pwResult, $__pwClosureName, $__pwFrame, $__pwRefOffset); 61 | '; 62 | 63 | const QUEUE_DEPLOYMENT_CODE = '\Patchwork\CallRerouting\deployQueue()'; 64 | 65 | function markPreprocessedFiles() 66 | { 67 | return Generic\markPreprocessedFiles(CallRerouting\State::$preprocessedFiles); 68 | } 69 | 70 | function injectCallInterceptionCode() 71 | { 72 | return Generic\prependCodeToFunctions( 73 | Utils\condense(CALL_INTERCEPTION_CODE), 74 | array( 75 | 'void' => Utils\condense(CALL_INTERCEPTION_CODE_VOID_TYPED), 76 | 'never' => Utils\condense(CALL_INTERCEPTION_CODE_NEVER_TYPED), 77 | ), 78 | true 79 | ); 80 | } 81 | 82 | function injectQueueDeploymentCode() 83 | { 84 | return Generic\chain(array( 85 | Generic\injectFalseExpressionAtBeginnings(QUEUE_DEPLOYMENT_CODE), 86 | Generic\injectCodeAfterClassDefinitions(QUEUE_DEPLOYMENT_CODE . ';'), 87 | )); 88 | } 89 | -------------------------------------------------------------------------------- /src/CodeManipulation/Actions/CodeManipulation.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2023 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation\Actions\CodeManipulation; 10 | 11 | use Patchwork\CodeManipulation\Actions\Generic; 12 | use Patchwork\CodeManipulation\Source; 13 | 14 | const EVAL_ARGUMENT_WRAPPER = '\Patchwork\CodeManipulation\transformForEval'; 15 | 16 | const STREAM_WRAPPER_REINSTATEMENT_CODE = '\Patchwork\CodeManipulation\Stream::reinstateWrapper();'; 17 | 18 | function propagateThroughEval() 19 | { 20 | return Generic\wrapUnaryConstructArguments(T_EVAL, EVAL_ARGUMENT_WRAPPER); 21 | } 22 | 23 | function injectStreamWrapperReinstatementCode() 24 | { 25 | return Generic\injectCodeAtEnd(STREAM_WRAPPER_REINSTATEMENT_CODE); 26 | } 27 | 28 | function flush() 29 | { 30 | return function(Source $s) { 31 | $s->flush(); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/CodeManipulation/Actions/ConflictPrevention.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation\Actions\ConflictPrevention; 10 | 11 | use Patchwork\CodeManipulation\Source; 12 | 13 | /** 14 | * @since 2.0.1 15 | * 16 | * Serves to avoid "Cannot redeclare Patchwork\redefine()" errors. 17 | */ 18 | function preventImportingOtherCopiesOfPatchwork() 19 | { 20 | return function(Source $s) { 21 | $namespaceKeyword = $s->next(T_NAMESPACE, -1); 22 | if ($namespaceKeyword === INF || $namespaceKeyword < 2) { 23 | return; 24 | } 25 | if ($s->read($namespaceKeyword, 4) == 'namespace Patchwork;') { 26 | $pattern = '/@copyright\s+2010(-\d+)? Ignas Rudaitis/'; 27 | if (preg_match($pattern, $s->read($namespaceKeyword - 2))) { 28 | # Clear the file completely (in memory) 29 | $s->splice('', 0, count($s->tokens)); 30 | } 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/CodeManipulation/Actions/Generic.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation\Actions\Generic; 10 | 11 | use Patchwork\CodeManipulation\Actions\Arguments; 12 | use Patchwork\CodeManipulation\Source; 13 | use Patchwork\Utils; 14 | 15 | const LEFT_ROUND = '('; 16 | const RIGHT_ROUND = ')'; 17 | const LEFT_CURLY = '{'; 18 | const RIGHT_CURLY = '}'; 19 | const LEFT_SQUARE = '['; 20 | const RIGHT_SQUARE = ']'; 21 | const SEMICOLON = ';'; 22 | 23 | foreach (['NAME_FULLY_QUALIFIED', 'NAME_QUALIFIED', 'NAME_RELATIVE', 'ELLIPSIS', 'ATTRIBUTE', 'READONLY'] as $constant) { 24 | if (defined('T_' . $constant)) { 25 | define(__NAMESPACE__ . '\\' . $constant, constant('T_' . $constant)); 26 | } else { 27 | define(__NAMESPACE__ . '\\' . $constant, -1); 28 | } 29 | } 30 | 31 | function markPreprocessedFiles(&$target) 32 | { 33 | return function($file) use (&$target) { 34 | $target[$file] = true; 35 | }; 36 | } 37 | 38 | function prependCodeToFunctions($code, $typedVariants = array(), $fillArgRefs = false) 39 | { 40 | if (!is_array($typedVariants)) { 41 | $typedVariants = array( 42 | 'void' => $typedVariants, 43 | ); 44 | } 45 | return function(Source $s) use ($code, $typedVariants, $fillArgRefs) { 46 | foreach ($s->all(T_FUNCTION) as $function) { 47 | # Skip "use function" 48 | $previous = $s->skipBack(Source::junk(), $function); 49 | if ($s->is(T_USE, $previous)) { 50 | continue; 51 | } 52 | $returnType = getDeclaredReturnType($s, $function); 53 | $argRefs = null; 54 | if ($fillArgRefs) { 55 | $parenthesis = $s->next(LEFT_ROUND, $function); 56 | $args = Arguments\readNames($s, $parenthesis); 57 | $argRefs = Arguments\constructReferenceArray($args); 58 | } 59 | $bracket = $s->next(LEFT_CURLY, $function); 60 | # Skip generators 61 | $yield = $s->next(T_YIELD, $bracket); 62 | if ($yield < $s->match($bracket)) { 63 | continue; 64 | } 65 | $semicolon = $s->next(SEMICOLON, $function); 66 | if ($bracket < $semicolon) { 67 | $variant = $returnType && isset($typedVariants[$returnType]) ? $typedVariants[$returnType] : $code; 68 | if ($fillArgRefs) { 69 | $variant = sprintf($variant, $argRefs); 70 | } 71 | $s->splice($variant, $bracket + 1); 72 | } 73 | } 74 | }; 75 | } 76 | 77 | function getDeclaredReturnType(Source $s, $function) 78 | { 79 | $parenthesis = $s->next(LEFT_ROUND, $function); 80 | $next = $s->skip(Source::junk(), $s->match($parenthesis)); 81 | if ($s->is(T_USE, $next)) { 82 | $next = $s->skip(Source::junk(), $s->match($s->next(LEFT_ROUND, $next))); 83 | } 84 | if ($s->is(':', $next)) { 85 | return $s->read($s->skip(Source::junk(), $next), 1); 86 | } 87 | return false; 88 | } 89 | 90 | function wrapUnaryConstructArguments($construct, $wrapper) 91 | { 92 | return function(Source $s) use ($construct, $wrapper) { 93 | foreach ($s->all($construct) as $match) { 94 | $pos = $s->next(LEFT_ROUND, $match); 95 | $s->splice($wrapper . LEFT_ROUND, $pos + 1); 96 | $s->splice(RIGHT_ROUND, $s->match($pos)); 97 | } 98 | }; 99 | } 100 | 101 | function injectFalseExpressionAtBeginnings($expression) 102 | { 103 | return function(Source $s) use ($expression) { 104 | $openingTags = $s->all(T_OPEN_TAG); 105 | $openingTagsWithEcho = $s->all(T_OPEN_TAG_WITH_ECHO); 106 | if (empty($openingTags) && empty($openingTagsWithEcho)) { 107 | return; 108 | } 109 | if (!empty($openingTags) && 110 | (empty($openingTagsWithEcho) || reset($openingTags) < reset($openingTagsWithEcho))) { 111 | $pos = reset($openingTags); 112 | # Skip initial declare() statements 113 | while ($s->read($s->skip(Source::junk(), $pos)) === 'declare') { 114 | $pos = $s->next(SEMICOLON, $pos); 115 | } 116 | # Enter first namespace 117 | $namespaceKeyword = $s->next(T_NAMESPACE, $pos); 118 | if ($namespaceKeyword !== INF) { 119 | $semicolon = $s->next(SEMICOLON, $namespaceKeyword); 120 | $leftBracket = $s->next(LEFT_CURLY, $namespaceKeyword); 121 | $pos = min($semicolon, $leftBracket); 122 | } 123 | $s->splice(' ' . $expression . ';', $pos + 1); 124 | } else { 125 | $openingTag = reset($openingTagsWithEcho); 126 | $closingTag = $s->next(T_CLOSE_TAG, $openingTag); 127 | $semicolon = $s->next(SEMICOLON, $openingTag); 128 | $s->splice(' (' . $expression . ') ?: (', $openingTag + 1); 129 | $s->splice(') ', min($closingTag, $semicolon)); 130 | } 131 | }; 132 | } 133 | 134 | function injectCodeAfterClassDefinitions($code) 135 | { 136 | return function(Source $s) use ($code) { 137 | foreach ($s->all(T_CLASS) as $match) { 138 | if ($s->is([LEFT_ROUND, LEFT_CURLY, T_EXTENDS, T_IMPLEMENTS], $s->skip(Source::junk(), $match))) { 139 | # Not a proper class definition: anonymous class (with or without attribute) 140 | continue; 141 | } 142 | if ($s->is(T_DOUBLE_COLON, $s->skipBack(Source::junk(), $match))) { 143 | # Not a proper class definition: ::class syntax 144 | continue; 145 | } 146 | $leftBracket = $s->next(LEFT_CURLY, $match); 147 | if ($leftBracket === INF) { 148 | continue; 149 | } 150 | $rightBracket = $s->match($leftBracket); 151 | if ($rightBracket === INF) { 152 | continue; 153 | } 154 | $s->splice($code, $rightBracket + 1); 155 | } 156 | }; 157 | } 158 | 159 | function injectCodeAtEnd($code) 160 | { 161 | return function(Source $s) use ($code) { 162 | $openTags = $s->all(T_OPEN_TAG); 163 | $lastOpenTag = end($openTags); 164 | $closeTag = $s->next(T_CLOSE_TAG, $lastOpenTag); 165 | $namespaceKeyword = $s->next(T_NAMESPACE, 0); 166 | $extraSemicolon = ';'; 167 | if ($namespaceKeyword !== INF) { 168 | $semicolon = $s->next(SEMICOLON, $namespaceKeyword); 169 | $leftBracket = $s->next(LEFT_CURLY, $namespaceKeyword); 170 | if ($leftBracket < $semicolon) { 171 | $code = "namespace { $code }"; 172 | $extraSemicolon = ''; 173 | } 174 | } 175 | if ($closeTag !== INF) { 176 | $s->splice("tokens) - 1, 0, Source::APPEND); 177 | } else { 178 | $s->splice($extraSemicolon . $code, count($s->tokens) - 1, 0, Source::APPEND); 179 | } 180 | }; 181 | } 182 | 183 | function chain(array $callbacks) 184 | { 185 | return function(Source $s) use ($callbacks) { 186 | foreach ($callbacks as $callback) { 187 | $callback($s); 188 | } 189 | }; 190 | } 191 | -------------------------------------------------------------------------------- /src/CodeManipulation/Actions/Namespaces.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation\Actions\Namespaces; 10 | 11 | use Patchwork\CodeManipulation\Source; 12 | use Patchwork\CodeManipulation\Actions\Generic; 13 | 14 | /** 15 | * @since 2.1.0 16 | */ 17 | function resolveName(Source $s, $pos, $type = 'class') 18 | { 19 | $name = scanQualifiedName($s, $pos); 20 | $pieces = explode('\\', $name); 21 | if ($pieces[0] === '') { 22 | return $name; 23 | } 24 | $uses = collectUseDeclarations($s, $pos); 25 | if (isset($uses[$type][$name])) { 26 | return '\\' . ltrim($uses[$type][$name], ' \\'); 27 | } 28 | if (isset($uses['class'][$pieces[0]])) { 29 | $name = '\\' . ltrim($uses['class'][$pieces[0]] . '\\' . join('\\', array_slice($pieces, 1)), '\\'); 30 | } else { 31 | $name = '\\' . ltrim(getNamespaceAt($s, $pos) . '\\' . $name, '\\'); 32 | } 33 | return $name; 34 | } 35 | 36 | /** 37 | * @since 2.1.0 38 | */ 39 | function getNamespaceAt(Source $s, $pos) 40 | { 41 | foreach (collectNamespaceBoundaries($s) as $namespace => $boundaryPairs) { 42 | foreach ($boundaryPairs as $boundaries) { 43 | list($begin, $end) = $boundaries; 44 | if ($begin <= $pos && $pos <= $end) { 45 | return $namespace; 46 | } 47 | } 48 | } 49 | return ''; 50 | } 51 | 52 | function collectNamespaceBoundaries(Source $s) 53 | { 54 | return $s->cache([], function() { 55 | if (!$this->has(T_NAMESPACE)) { 56 | return ['' => [[0, INF]]]; 57 | } 58 | $result = []; 59 | foreach ($this->all(T_NAMESPACE) as $keyword) { 60 | if ($this->next(';', $keyword) < $this->next(Generic\LEFT_CURLY, $keyword)) { 61 | return [scanQualifiedName($this, $keyword + 1) => [[0, INF]]]; 62 | } 63 | $begin = $this->next(Generic\LEFT_CURLY, $keyword) + 1; 64 | $end = $this->match($begin - 1) - 1; 65 | $name = scanQualifiedName($this, $keyword + 1); 66 | if (!isset($result[$name])) { 67 | $result[$name] = []; 68 | } 69 | $result[$name][] = [$begin, $end]; 70 | } 71 | return $result; 72 | }); 73 | } 74 | 75 | function collectUseDeclarations(Source $s, $begin) 76 | { 77 | foreach (collectNamespaceBoundaries($s) as $boundaryPairs) { 78 | foreach ($boundaryPairs as $boundaries) { 79 | list($leftBoundary, $rightBoundary) = $boundaries; 80 | if ($leftBoundary <= $begin && $begin <= $rightBoundary) { 81 | $begin = $leftBoundary; 82 | break; 83 | } 84 | } 85 | } 86 | return $s->cache([$begin], function($begin) { 87 | $result = ['class' => [], 'function' => [], 'const' => []]; 88 | # only tokens that are siblings bracket-wise are considered, 89 | # so trait-use instances are not an issue 90 | foreach ($this->siblings(T_USE, $begin) as $keyword) { 91 | # skip if closure-use 92 | $next = $this->skip(Source::junk(), $keyword); 93 | if ($this->is(Generic\LEFT_ROUND, $next)) { 94 | continue; 95 | } 96 | parseUseDeclaration($this, $next, $result); 97 | } 98 | return $result; 99 | }); 100 | } 101 | 102 | function parseUseDeclaration(Source $s, $pos, array &$aliases, $prefix = '', $type = 'class') 103 | { 104 | $lastPart = null; 105 | $whole = $prefix; 106 | while (true) { 107 | switch ($s->tokens[$pos][Source::TYPE_OFFSET]) { 108 | case T_FUNCTION: 109 | $type = 'function'; 110 | break; 111 | case T_CONST: 112 | $type = 'const'; 113 | break; 114 | case T_NS_SEPARATOR: 115 | if (!empty($whole)) { 116 | $whole .= '\\'; 117 | } 118 | break; 119 | case T_STRING: 120 | case Generic\NAME_FULLY_QUALIFIED: 121 | case Generic\NAME_QUALIFIED: 122 | case Generic\NAME_RELATIVE: 123 | $update = $s->tokens[$pos][Source::STRING_OFFSET]; 124 | $parts = explode('\\', $update); 125 | $whole .= $update; 126 | $lastPart = end($parts); 127 | break; 128 | case T_AS: 129 | $pos = $s->skip(Source::junk(), $pos); 130 | $aliases[$type][$s->tokens[$pos][Source::STRING_OFFSET]] = $whole; 131 | $lastPart = null; 132 | $whole = $prefix; 133 | break; 134 | case ',': 135 | if ($lastPart !== null) { 136 | $aliases[$type][$lastPart] = $whole; 137 | } 138 | $lastPart = null; 139 | $whole = $prefix; 140 | $type = 'class'; 141 | break; 142 | case Generic\LEFT_CURLY: 143 | parseUseDeclaration($s, $pos + 1, $aliases, $prefix . '\\', $type); 144 | break; 145 | case T_WHITESPACE: 146 | case T_COMMENT: 147 | case T_DOC_COMMENT: 148 | break; 149 | default: 150 | if ($lastPart !== null) { 151 | $aliases[$type][$lastPart] = $whole; 152 | } 153 | return; 154 | } 155 | $pos++; 156 | } 157 | } 158 | 159 | function scanQualifiedName(Source $s, $begin) 160 | { 161 | $result = ''; 162 | while (true) { 163 | switch ($s->tokens[$begin][Source::TYPE_OFFSET]) { 164 | case T_NS_SEPARATOR: 165 | if (!empty($result)) { 166 | $result .= '\\'; 167 | } 168 | # fall through 169 | case T_STRING: 170 | case Generic\NAME_FULLY_QUALIFIED: 171 | case Generic\NAME_QUALIFIED: 172 | case Generic\NAME_RELATIVE: 173 | case T_STATIC: 174 | $result .= $s->tokens[$begin][Source::STRING_OFFSET]; 175 | break; 176 | case T_WHITESPACE: 177 | case T_COMMENT: 178 | case T_DOC_COMMENT: 179 | break; 180 | default: 181 | return str_replace('\\\\', '\\', $result); 182 | } 183 | $begin++; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/CodeManipulation/Actions/RedefinitionOfInternals.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation\Actions\RedefinitionOfInternals; 10 | 11 | use Patchwork\Config; 12 | use Patchwork\CallRerouting; 13 | use Patchwork\CodeManipulation\Source; 14 | use Patchwork\CodeManipulation\Actions\Generic; 15 | use Patchwork\CodeManipulation\Actions\Namespaces; 16 | 17 | const DYNAMIC_CALL_REPLACEMENT = '\Patchwork\CallRerouting\dispatchDynamic(%s, \Patchwork\Utils\args(%s))'; 18 | 19 | function spliceNamedFunctionCalls() 20 | { 21 | if (Config\getRedefinableInternals() === []) { 22 | return function() {}; 23 | } 24 | $names = []; 25 | foreach (Config\getRedefinableInternals() as $name) { 26 | $names[strtolower($name)] = true; 27 | } 28 | return function(Source $s) use ($names) { 29 | foreach (Namespaces\collectNamespaceBoundaries($s) as $namespace => $boundaryList) { 30 | foreach ($boundaryList as $boundaries) { 31 | list($begin, $end) = $boundaries; 32 | $aliases = Namespaces\collectUseDeclarations($s, $begin)['function']; 33 | # Receive all aliases, leave only those for redefinable internals 34 | foreach ($aliases as $alias => $qualified) { 35 | if (!isset($names[$qualified])) { 36 | unset($aliases[$alias]); 37 | } else { 38 | $aliases[strtolower($alias)] = strtolower($qualified); 39 | } 40 | } 41 | spliceNamedCallsWithin($s, $begin, $end, $names, $aliases); 42 | } 43 | } 44 | }; 45 | } 46 | 47 | function spliceNamedCallsWithin(Source $s, $begin, $end, array $names, array $aliases) 48 | { 49 | foreach ($s->within([T_STRING, Generic\NAME_FULLY_QUALIFIED, Generic\NAME_QUALIFIED, Generic\NAME_RELATIVE], $begin, $end) as $string) { 50 | $original = strtolower($s->read($string)); 51 | if ($original[0] == '\\') { 52 | $original = substr($original, 1); 53 | } 54 | if (isset($names[$original]) || isset($aliases[$original])) { 55 | $previous = $s->skipBack(Source::junk(), $string); 56 | $hadBackslash = false; 57 | if ($s->is(T_NS_SEPARATOR, $previous) || $s->is(Generic\NAME_FULLY_QUALIFIED, $string)) { 58 | if (!isset($names[$original])) { 59 | # use-aliased name cannot have a leading backslash 60 | continue; 61 | } 62 | if ($s->is(T_NS_SEPARATOR, $previous)) { 63 | $s->splice('', $previous, 1); 64 | $previous = $s->skipBack(Source::junk(), $previous); 65 | } 66 | $hadBackslash = true; 67 | } 68 | if ($s->is([T_FUNCTION, T_OBJECT_OPERATOR, T_DOUBLE_COLON, T_STRING, T_NEW, Generic\NAME_FULLY_QUALIFIED, Generic\NAME_QUALIFIED, Generic\NAME_RELATIVE], $previous)) { 69 | continue; 70 | } 71 | $next = $s->skip(Source::junk(), $string); 72 | if (!$s->is(Generic\LEFT_ROUND, $next)) { 73 | continue; 74 | } 75 | if (isset($aliases[$original])) { 76 | $original = $aliases[$original]; 77 | } 78 | $secondNext = $s->skip(Source::junk(), $next); 79 | $splice = '\\' . CallRerouting\INTERNAL_REDEFINITION_NAMESPACE . '\\'; 80 | $splice .= $original . Generic\LEFT_ROUND; 81 | # prepend a namespace-of-origin argument to handle cases like Acme\time() vs time() 82 | $splice .= !$hadBackslash ? '__NAMESPACE__' : '""'; 83 | if (!$s->is(Generic\RIGHT_ROUND, $secondNext)) { 84 | # right parenthesis doesn't follow immediately => there are arguments 85 | $splice .= ', '; 86 | } 87 | $s->splice($splice, $string, $secondNext - $string); 88 | } 89 | } 90 | } 91 | 92 | function spliceDynamicCalls() 93 | { 94 | if (Config\getRedefinableInternals() === []) { 95 | return function() {}; 96 | } 97 | return function(Source $s) { 98 | spliceDynamicCallsWithin($s, 0, count($s->tokens) - 1); 99 | }; 100 | } 101 | 102 | function spliceDynamicCallsWithin(Source $s, $first, $last) 103 | { 104 | $pos = $first; 105 | $anchor = INF; 106 | $suppress = false; 107 | while ($pos <= $last) { 108 | switch ($s->tokens[$pos][Source::TYPE_OFFSET]) { 109 | case '$': 110 | case T_VARIABLE: 111 | $anchor = min($pos, $anchor); 112 | break; 113 | case Generic\LEFT_ROUND: 114 | if ($anchor !== INF && !$suppress) { 115 | $callable = $s->read($anchor, $pos - $anchor); 116 | $arguments = $s->read($pos + 1, $s->match($pos) - $pos - 1); 117 | $pos = $s->match($pos); 118 | $replacement = sprintf(DYNAMIC_CALL_REPLACEMENT, $callable, $arguments); 119 | $s->splice($replacement, $anchor, $pos - $anchor + 1); 120 | } 121 | break; 122 | case Generic\LEFT_SQUARE: 123 | case Generic\LEFT_CURLY: 124 | spliceDynamicCallsWithin($s, $pos + 1, $s->match($pos) - 1); 125 | $pos = $s->match($pos); 126 | break; 127 | case T_WHITESPACE: 128 | case T_COMMENT: 129 | case T_DOC_COMMENT: 130 | break; 131 | case T_OBJECT_OPERATOR: 132 | case T_DOUBLE_COLON: 133 | case T_NEW: 134 | $suppress = true; 135 | break; 136 | default: 137 | $suppress = false; 138 | $anchor = INF; 139 | } 140 | $pos++; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/CodeManipulation/Actions/RedefinitionOfLanguageConstructs.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation\Actions\RedefinitionOfLanguageConstructs; 10 | 11 | use Patchwork\CodeManipulation\Source; 12 | use Patchwork\CodeManipulation\Actions\Generic; 13 | use Patchwork\Exceptions; 14 | use Patchwork\Config; 15 | 16 | const LANGUAGE_CONSTRUCT_PREFIX = 'Patchwork\Redefinitions\LanguageConstructs\_'; 17 | 18 | /** 19 | * @since 2.0.5 20 | */ 21 | function spliceAllConfiguredLanguageConstructs() 22 | { 23 | $mapping = getMappingOfConstructs(); 24 | $used = []; 25 | $actions = []; 26 | foreach (Config\getRedefinableLanguageConstructs() as $construct) { 27 | if (isset($used[$mapping[$construct]])) { 28 | continue; 29 | } 30 | $used[$mapping[$construct]] = true; 31 | $actions[] = spliceLanguageConstruct($mapping[$construct]); 32 | } 33 | return Generic\chain($actions); 34 | } 35 | 36 | function getMappingOfConstructs() 37 | { 38 | return [ 39 | 'echo' => T_ECHO, 40 | 'print' => T_PRINT, 41 | 'eval' => T_EVAL, 42 | 'die' => T_EXIT, 43 | 'exit' => T_EXIT, 44 | 'isset' => T_ISSET, 45 | 'unset' => T_UNSET, 46 | 'empty' => T_EMPTY, 47 | 'require' => T_REQUIRE, 48 | 'require_once' => T_REQUIRE_ONCE, 49 | 'include' => T_INCLUDE, 50 | 'include_once' => T_INCLUDE_ONCE, 51 | 'clone' => T_CLONE, 52 | ]; 53 | } 54 | 55 | function getInnerTokens() 56 | { 57 | return [ 58 | '$', 59 | ',', 60 | '"', 61 | T_START_HEREDOC, 62 | T_END_HEREDOC, 63 | T_OBJECT_OPERATOR, 64 | T_DOUBLE_COLON, 65 | T_NS_SEPARATOR, 66 | T_STRING, 67 | T_LNUMBER, 68 | T_DNUMBER, 69 | T_WHITESPACE, 70 | T_CONSTANT_ENCAPSED_STRING, 71 | T_COMMENT, 72 | T_DOC_COMMENT, 73 | T_VARIABLE, 74 | T_ENCAPSED_AND_WHITESPACE, 75 | Generic\NAME_FULLY_QUALIFIED, 76 | Generic\NAME_QUALIFIED, 77 | Generic\NAME_RELATIVE, 78 | ]; 79 | } 80 | 81 | function getBracketTokens() 82 | { 83 | return [ 84 | Generic\LEFT_ROUND, 85 | Generic\LEFT_SQUARE, 86 | Generic\LEFT_CURLY, 87 | T_CURLY_OPEN, 88 | T_DOLLAR_OPEN_CURLY_BRACES, 89 | Generic\ATTRIBUTE, 90 | ]; 91 | } 92 | 93 | function spliceLanguageConstruct($token) 94 | { 95 | return function(Source $s) use ($token) { 96 | foreach ($s->all($token) as $pos) { 97 | $s->splice('\\' . LANGUAGE_CONSTRUCT_PREFIX, $pos, 0, Source::PREPEND); 98 | if (lacksParentheses($s, $pos)) { 99 | addParentheses($s, $pos); 100 | } 101 | } 102 | }; 103 | } 104 | 105 | function lacksParentheses(Source $s, $pos) 106 | { 107 | if ($s->is(T_ECHO, $pos)) { 108 | return true; 109 | } 110 | $next = $s->skip(Source::junk(), $pos); 111 | return !$s->is(Generic\LEFT_ROUND, $next); 112 | } 113 | 114 | function addParentheses(Source $s, $pos) 115 | { 116 | $pos = $s->skip(Source::junk(), $pos); 117 | $s->splice(Generic\LEFT_ROUND, $pos, 0, Source::PREPEND); 118 | while ($pos < count($s->tokens)) { 119 | if ($s->is(getInnerTokens(), $pos)) { 120 | $pos++; 121 | } elseif ($s->is(getBracketTokens(), $pos)) { 122 | $pos = $s->match($pos) + 1; 123 | } else { 124 | break; 125 | } 126 | } 127 | if ($s->is(Source::junk(), $pos)) { 128 | $pos = $s->skipBack(Source::junk(), $pos); 129 | } 130 | $s->splice(Generic\RIGHT_ROUND, $pos, 0, Source::APPEND); 131 | } 132 | -------------------------------------------------------------------------------- /src/CodeManipulation/Actions/RedefinitionOfNew.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation\Actions\RedefinitionOfNew; 10 | 11 | use Patchwork\CodeManipulation\Source; 12 | use Patchwork\CodeManipulation\Actions\Generic; 13 | use Patchwork\CodeManipulation\Actions\Namespaces; 14 | use Patchwork\Config; 15 | 16 | const STATIC_INSTANTIATION_REPLACEMENT = '\Patchwork\CallRerouting\getInstantiator(\'%s\', %s)->instantiate(%s)'; 17 | const DYNAMIC_INSTANTIATION_REPLACEMENT = '\Patchwork\CallRerouting\getInstantiator(%s, %s)->instantiate(%s)'; 18 | const CALLED_CLASS = '((__CLASS__ && __FUNCTION__ !== (__NAMESPACE__ ? __NAMESPACE__ . "\\\\{closure}" : "\\\\{closure}")) ? \get_called_class() : null)'; 19 | 20 | const spliceAllInstantiations = 'Patchwork\CodeManipulation\Actions\RedefinitionOfNew\spliceAllInstantiations'; 21 | const publicizeConstructors = 'Patchwork\CodeManipulation\Actions\RedefinitionOfNew\publicizeConstructors'; 22 | 23 | /** 24 | * @since 2.1.0 25 | */ 26 | function spliceAllInstantiations(Source $s) 27 | { 28 | if (!State::$enabled || !Config\isNewKeywordRedefinable()) { 29 | return; 30 | } 31 | foreach ($s->all(T_NEW) as $new) { 32 | $begin = $s->skip(Source::junk(), $new); 33 | if ($s->is([T_CLASS, Generic\READONLY, Generic\ATTRIBUTE], $begin)) { 34 | # Anonymous class 35 | continue; 36 | } 37 | $end = scanInnerTokens($s, $begin, $dynamic); 38 | $afterEnd = $s->skip(Source::junk(), $end); 39 | list($argsOpen, $argsClose) = [null, null]; 40 | if ($s->is(Generic\LEFT_ROUND, $afterEnd)) { 41 | list($argsOpen, $argsClose) = [$afterEnd, $s->match($afterEnd)]; 42 | } 43 | spliceInstantiation($s, $new, $begin, $end, $argsOpen, $argsClose, $dynamic); 44 | if (hasExtraParentheses($s, $new)) { 45 | removeExtraParentheses($s, $new); 46 | } 47 | } 48 | } 49 | 50 | function publicizeConstructors(Source $s) 51 | { 52 | if (!Config\isNewKeywordRedefinable()) { 53 | return; 54 | } 55 | foreach ($s->all([T_PRIVATE, T_PROTECTED]) as $first) { 56 | $second = $s->skip(Source::junk(), $first); 57 | $third = $s->skip(Source::junk(), $second); 58 | if ($s->is(T_FUNCTION, $second) && $s->read($third, 1) === '__construct') { 59 | $s->splice('public', $first, 1); 60 | } 61 | } 62 | } 63 | 64 | function spliceInstantiation(Source $s, $new, $begin, $end, $argsOpen, $argsClose, $dynamic) 65 | { 66 | $class = $s->read($begin, $end - $begin + 1); 67 | $args = ''; 68 | $length = $end - $new + 1; 69 | if ($argsOpen !== null) { 70 | $args = $s->read($argsOpen + 1, $argsClose - $argsOpen - 1); 71 | $length = $argsClose - $new + 1; 72 | } 73 | $replacement = DYNAMIC_INSTANTIATION_REPLACEMENT; 74 | if (!$dynamic) { 75 | $class = Namespaces\resolveName($s, $begin); 76 | $replacement = STATIC_INSTANTIATION_REPLACEMENT; 77 | } 78 | $s->splice(sprintf($replacement, $class, CALLED_CLASS, $args), $new, $length); 79 | } 80 | 81 | function getInnerTokens() 82 | { 83 | return [ 84 | '$', 85 | T_OBJECT_OPERATOR, 86 | T_DOUBLE_COLON, 87 | T_NS_SEPARATOR, 88 | T_STRING, 89 | T_LNUMBER, 90 | T_DNUMBER, 91 | T_WHITESPACE, 92 | T_CONSTANT_ENCAPSED_STRING, 93 | T_COMMENT, 94 | T_DOC_COMMENT, 95 | T_VARIABLE, 96 | T_ENCAPSED_AND_WHITESPACE, 97 | T_STATIC, 98 | Generic\NAME_FULLY_QUALIFIED, 99 | Generic\NAME_QUALIFIED, 100 | Generic\NAME_RELATIVE, 101 | ]; 102 | } 103 | 104 | function getBracketTokens() 105 | { 106 | return [ 107 | Generic\LEFT_SQUARE, 108 | Generic\LEFT_CURLY, 109 | T_CURLY_OPEN, 110 | T_DOLLAR_OPEN_CURLY_BRACES, 111 | Generic\ATTRIBUTE, 112 | ]; 113 | } 114 | 115 | function getDynamicTokens() 116 | { 117 | return [ 118 | '$', 119 | T_OBJECT_OPERATOR, 120 | T_DOUBLE_COLON, 121 | T_LNUMBER, 122 | T_DNUMBER, 123 | T_CONSTANT_ENCAPSED_STRING, 124 | T_VARIABLE, 125 | T_ENCAPSED_AND_WHITESPACE, 126 | ]; 127 | } 128 | 129 | function scanInnerTokens(Source $s, $begin, &$dynamic = null) 130 | { 131 | $dynamic = false; 132 | $pos = $begin; 133 | while ($s->is(getInnerTokens(), $pos) || $s->is(getBracketTokens(), $pos)) { 134 | if ($s->is(getBracketTokens(), $pos)) { 135 | $dynamic = true; 136 | $pos = $s->match($pos) + 1; 137 | } else { 138 | if ($s->is(getDynamicTokens(), $pos)) { 139 | $dynamic = true; 140 | } 141 | $pos++; 142 | } 143 | } 144 | return $pos - 1; 145 | } 146 | 147 | function hasExtraParentheses(Source $s, $new) 148 | { 149 | $doNotRemoveAfter = [ 150 | T_STRING, 151 | T_STATIC, 152 | T_VARIABLE, 153 | T_FOREACH, 154 | T_FOR, 155 | T_IF, 156 | T_ELSEIF, 157 | T_WHILE, 158 | T_ARRAY, 159 | T_PRINT, 160 | T_ECHO, 161 | T_CLASS, 162 | Generic\NAME_FULLY_QUALIFIED, 163 | Generic\NAME_QUALIFIED, 164 | Generic\NAME_RELATIVE, 165 | Generic\RIGHT_ROUND, 166 | Generic\RIGHT_SQUARE, 167 | ]; 168 | $left = $s->skipBack(Source::junk(), $new); 169 | if (!$s->is(Generic\LEFT_ROUND, $left)) { 170 | return false; 171 | } 172 | $beforeLeft = $s->skipBack(Source::junk(), $left); 173 | return !$s->is($doNotRemoveAfter, $beforeLeft); 174 | } 175 | 176 | function removeExtraParentheses(Source $s, $new) 177 | { 178 | $left = $s->skipBack(Source::junk(), $new); 179 | $s->splice('', $left, 1); 180 | $s->splice('', $s->match($left), 1); 181 | } 182 | 183 | function suspendFor(callable $function) 184 | { 185 | State::$enabled = false; 186 | $exception = null; 187 | try { 188 | $function(); 189 | } catch (\Exception $e) { 190 | $exception = $e; 191 | } 192 | State::$enabled = true; 193 | if ($exception) { 194 | throw $exception; 195 | } 196 | } 197 | 198 | class State 199 | { 200 | static $enabled = true; 201 | } 202 | -------------------------------------------------------------------------------- /src/CodeManipulation/Source.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation; 10 | 11 | use Patchwork\CodeManipulation\Actions\Generic; 12 | use Patchwork\Utils; 13 | 14 | class Source 15 | { 16 | const TYPE_OFFSET = 0; 17 | const STRING_OFFSET = 1; 18 | 19 | const PREPEND = 'PREPEND'; 20 | const APPEND = 'APPEND'; 21 | const OVERWRITE = 'OVERWRITE'; 22 | 23 | const ANY = null; 24 | 25 | public $tokens; 26 | public $tokensByType; 27 | public $splices; 28 | public $spliceLengths; 29 | public $code; 30 | public $file; 31 | public $matchingBrackets; 32 | public $levels; 33 | public $levelBeginnings; 34 | public $levelEndings; 35 | public $tokensByLevel; 36 | public $tokensByLevelAndType; 37 | public $cache; 38 | 39 | function __construct($string) 40 | { 41 | $this->code = $string; 42 | $this->initialize(); 43 | } 44 | 45 | function initialize() 46 | { 47 | $this->tokens = Utils\tokenize($this->code); 48 | $this->tokens[] = [T_WHITESPACE, ""]; 49 | $this->indexTokensByType(); 50 | $this->collectBracketMatchings(); 51 | $this->collectLevelInfo(); 52 | $this->splices = []; 53 | $this->spliceLengths = []; 54 | $this->cache = []; 55 | } 56 | 57 | function indexTokensByType() 58 | { 59 | $this->tokensByType = []; 60 | foreach ($this->tokens as $offset => $token) { 61 | $this->tokensByType[$token[self::TYPE_OFFSET]][] = $offset; 62 | } 63 | } 64 | 65 | function collectBracketMatchings() 66 | { 67 | $this->matchingBrackets = []; 68 | $stack = []; 69 | foreach ($this->tokens as $offset => $token) { 70 | $type = $token[self::TYPE_OFFSET]; 71 | switch ($type) { 72 | case '(': 73 | case '[': 74 | case '{': 75 | case T_CURLY_OPEN: 76 | case T_DOLLAR_OPEN_CURLY_BRACES: 77 | case Generic\ATTRIBUTE: 78 | $stack[] = $offset; 79 | break; 80 | case ')': 81 | case ']': 82 | case '}': 83 | $top = array_pop($stack); 84 | $this->matchingBrackets[$top] = $offset; 85 | $this->matchingBrackets[$offset] = $top; 86 | break; 87 | } 88 | } 89 | } 90 | 91 | function collectLevelInfo() 92 | { 93 | $level = 0; 94 | $this->levels = []; 95 | $this->tokensByLevel = []; 96 | $this->levelBeginnings = []; 97 | $this->levelEndings = []; 98 | $this->tokensByLevelAndType = []; 99 | foreach ($this->tokens as $offset => $token) { 100 | $type = $token[self::TYPE_OFFSET]; 101 | switch ($type) { 102 | case '(': 103 | case '[': 104 | case '{': 105 | case T_CURLY_OPEN: 106 | case T_DOLLAR_OPEN_CURLY_BRACES: 107 | case Generic\ATTRIBUTE: 108 | $level++; 109 | Utils\appendUnder($this->levelBeginnings, $level, $offset); 110 | break; 111 | case ')': 112 | case ']': 113 | case '}': 114 | Utils\appendUnder($this->levelEndings, $level, $offset); 115 | $level--; 116 | } 117 | $this->levels[$offset] = $level; 118 | Utils\appendUnder($this->tokensByLevel, $level, $offset); 119 | Utils\appendUnder($this->tokensByLevelAndType, [$level, $type], $offset); 120 | } 121 | Utils\appendUnder($this->levelBeginnings, 0, 0); 122 | Utils\appendUnder($this->levelEndings, 0, count($this->tokens) - 1); 123 | } 124 | 125 | function has($types) 126 | { 127 | foreach ((array) $types as $type) { 128 | if ($this->all($type) !== []) { 129 | return true; 130 | } 131 | } 132 | return false; 133 | } 134 | 135 | function is($types, $offset) 136 | { 137 | foreach ((array) $types as $type) { 138 | if ($this->tokens[$offset][self::TYPE_OFFSET] === $type) { 139 | return true; 140 | } 141 | } 142 | return false; 143 | } 144 | 145 | function skip($types, $offset, $direction = 1) 146 | { 147 | $offset += $direction; 148 | $types = (array) $types; 149 | while ($offset < count($this->tokens) && $offset >= 0) { 150 | if (!in_array($this->tokens[$offset][self::TYPE_OFFSET], $types)) { 151 | return $offset; 152 | } 153 | $offset += $direction; 154 | } 155 | return ($direction > 0) ? INF : -1; 156 | } 157 | 158 | function skipBack($types, $offset) 159 | { 160 | return $this->skip($types, $offset, -1); 161 | } 162 | 163 | function within($types, $low, $high) 164 | { 165 | $result = []; 166 | foreach ((array) $types as $type) { 167 | $candidates = isset($this->tokensByType[$type]) ? $this->tokensByType[$type] : []; 168 | $result = array_merge(Utils\allWithinRange($candidates, $low, $high), $result); 169 | } 170 | return $result; 171 | } 172 | 173 | function read($offset, $count = 1) 174 | { 175 | $result = ''; 176 | $pos = $offset; 177 | while ($pos < $offset + $count) { 178 | if (isset($this->tokens[$pos][self::STRING_OFFSET])) { 179 | $result .= $this->tokens[$pos][self::STRING_OFFSET]; 180 | } else { 181 | $result .= $this->tokens[$pos]; 182 | } 183 | $pos++; 184 | } 185 | return $result; 186 | } 187 | 188 | function siblings($types, $offset) 189 | { 190 | $level = $this->levels[$offset]; 191 | $begin = Utils\lastNotGreaterThan(Utils\access($this->levelBeginnings, $level, []), $offset); 192 | $end = Utils\firstGreaterThan(Utils\access($this->levelEndings, $level, []), $offset); 193 | if ($types === self::ANY) { 194 | return Utils\allWithinRange($this->tokensByLevel[$level], $begin, $end); 195 | } else { 196 | $result = []; 197 | foreach ((array) $types as $type) { 198 | $candidates = Utils\access($this->tokensByLevelAndType, [$level, $type], []); 199 | $result = array_merge(Utils\allWithinRange($candidates, $begin, $end), $result); 200 | } 201 | return $result; 202 | } 203 | } 204 | 205 | function next($types, $offset) 206 | { 207 | if (!is_array($types)) { 208 | $candidates = Utils\access($this->tokensByType, $types, []); 209 | return Utils\firstGreaterThan($candidates, $offset); 210 | } 211 | $result = INF; 212 | foreach ($types as $type) { 213 | $result = min($this->next($type, $offset), $result); 214 | } 215 | return $result; 216 | } 217 | 218 | function all($types) 219 | { 220 | if (!is_array($types)) { 221 | return Utils\access($this->tokensByType, $types, []); 222 | } 223 | $result = []; 224 | foreach ($types as $type) { 225 | $result = array_merge($result, $this->all($type)); 226 | } 227 | sort($result); 228 | return $result; 229 | } 230 | 231 | function match($offset) 232 | { 233 | $offset = (string) $offset; 234 | return isset($this->matchingBrackets[$offset]) ? $this->matchingBrackets[$offset] : INF; 235 | } 236 | 237 | function splice($splice, $offset, $length = 0, $policy = self::OVERWRITE) 238 | { 239 | if ($policy === self::OVERWRITE) { 240 | $this->splices[$offset] = $splice; 241 | } elseif ($policy === self::PREPEND || $policy === self::APPEND) { 242 | if (!isset($this->splices[$offset])) { 243 | $this->splices[$offset] = ''; 244 | } 245 | if ($policy === self::PREPEND) { 246 | $this->splices[$offset] = $splice . $this->splices[$offset]; 247 | } elseif ($policy === self::APPEND) { 248 | $this->splices[$offset] .= $splice; 249 | } 250 | } 251 | if (!isset($this->spliceLengths[$offset])) { 252 | $this->spliceLengths[$offset] = 0; 253 | } 254 | $this->spliceLengths[$offset] = max($length, $this->spliceLengths[$offset]); 255 | $this->code = null; 256 | } 257 | 258 | function createCodeFromTokens() 259 | { 260 | $splices = $this->splices; 261 | $code = ""; 262 | $count = count($this->tokens); 263 | for ($offset = 0; $offset < $count; $offset++) { 264 | if (isset($splices[$offset])) { 265 | $code .= $splices[$offset]; 266 | unset($splices[$offset]); 267 | $offset += $this->spliceLengths[$offset] - 1; 268 | } else { 269 | $t = $this->tokens[$offset]; 270 | $code .= isset($t[self::STRING_OFFSET]) ? $t[self::STRING_OFFSET] : $t; 271 | } 272 | } 273 | $this->code = $code; 274 | } 275 | 276 | static function junk() 277 | { 278 | return [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT]; 279 | } 280 | 281 | function __toString() 282 | { 283 | if ($this->code === null) { 284 | $this->createCodeFromTokens(); 285 | } 286 | return (string) $this->code; 287 | } 288 | 289 | function flush() 290 | { 291 | $this->initialize(Utils\tokenize($this)); 292 | } 293 | 294 | /** 295 | * @since 2.1.0 296 | */ 297 | function cache(array $args, \Closure $function) 298 | { 299 | $found = true; 300 | $trace = debug_backtrace()[1]; 301 | $location = $trace['file'] . ':' . $trace['line']; 302 | $result = &$this->cache; 303 | foreach (array_merge([$location], $args) as $step) { 304 | if (!is_scalar($step)) { 305 | throw new \LogicException; 306 | } 307 | if (!isset($result[$step])) { 308 | $result[$step] = []; 309 | $found = false; 310 | } 311 | $result = &$result[$step]; 312 | } 313 | if (!$found) { 314 | $result = call_user_func_array($function->bindTo($this), $args); 315 | } 316 | return $result; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/CodeManipulation/Stream.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2023 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\CodeManipulation; 10 | 11 | use Patchwork\Utils; 12 | 13 | class Stream 14 | { 15 | const STREAM_OPEN_FOR_INCLUDE = 128; 16 | const STAT_MTIME_NUMERIC_OFFSET = 9; 17 | const STAT_MTIME_ASSOC_OFFSET = 'mtime'; 18 | 19 | protected static $protocols = ['file', 'phar']; 20 | protected static $otherWrapperClass; 21 | 22 | public $context; 23 | public $resource; 24 | 25 | public static function discoverOtherWrapper() 26 | { 27 | $handle = fopen(__FILE__, 'r'); 28 | $meta = stream_get_meta_data($handle); 29 | if ($meta && isset($meta['wrapper_data']) && is_object($meta['wrapper_data']) && !($meta['wrapper_data'] instanceof self)) { 30 | static::$otherWrapperClass = get_class($meta['wrapper_data']); 31 | } 32 | } 33 | 34 | public static function wrap() 35 | { 36 | foreach (static::$protocols as $protocol) { 37 | stream_wrapper_unregister($protocol); 38 | stream_wrapper_register($protocol, get_called_class()); 39 | } 40 | } 41 | 42 | public static function unwrap() 43 | { 44 | foreach (static::$protocols as $protocol) { 45 | set_error_handler(function() {}); 46 | stream_wrapper_restore($protocol); 47 | restore_error_handler(); 48 | } 49 | } 50 | 51 | public static function reinstateWrapper() 52 | { 53 | static::discoverOtherWrapper(); 54 | static::unwrap(); 55 | static::wrap(); 56 | } 57 | 58 | public function stream_open($path, $mode, $options, &$openedPath) 59 | { 60 | $including = (bool) ($options & self::STREAM_OPEN_FOR_INCLUDE); 61 | 62 | // `parse_ini_file()` also sets STREAM_OPEN_FOR_INCLUDE. 63 | if ($including) { 64 | $frame = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; 65 | if (empty($frame['class']) && $frame['function'] === 'parse_ini_file') { 66 | $including = false; 67 | } 68 | } 69 | 70 | if ($including && shouldTransform($path)) { 71 | $this->resource = transformAndOpen($path); 72 | return $this->resource !== false; 73 | } 74 | 75 | $this->resource = static::fopen($path, $mode, $options, $this->context); 76 | return $this->resource !== false; 77 | } 78 | 79 | public static function getOtherWrapper($context) 80 | { 81 | if (isset(static::$otherWrapperClass)) { 82 | $class = static::$otherWrapperClass; 83 | $otherWrapper = new $class; 84 | if ($context !== null) { 85 | $otherWrapper->context = $context; 86 | } 87 | return $otherWrapper; 88 | } 89 | } 90 | 91 | public static function alternate(callable $internal, $resource, $wrapped, array $args = [], array $extraArgs = [], $context = null, $shouldReturnResource = false) 92 | { 93 | $shouldAddResourceArg = true; 94 | if ($resource === null) { 95 | $resource = static::getOtherWrapper($context); 96 | $shouldAddResourceArg = false; 97 | } 98 | if (is_object($resource)) { 99 | $args = array_merge($args, $extraArgs); 100 | $ladder = function() use ($resource, $wrapped, $args) { 101 | switch (count($args)) { 102 | case 0: 103 | return $resource->$wrapped(); 104 | case 1: 105 | return $resource->$wrapped($args[0]); 106 | case 2: 107 | return $resource->$wrapped($args[0], $args[1]); 108 | default: 109 | return call_user_func_array([$resource, $wrapped], $args); 110 | } 111 | }; 112 | $result = $ladder(); 113 | static::unwrap(); 114 | static::wrap(); 115 | } else { 116 | if ($shouldAddResourceArg) { 117 | array_unshift($args, $resource); 118 | } 119 | if ($context !== null) { 120 | $args[] = $context; 121 | } 122 | $result = static::bypass(function() use ($internal, $args) { 123 | switch (count($args)) { 124 | case 0: 125 | return $internal(); 126 | case 1: 127 | return $internal($args[0]); 128 | case 2: 129 | return $internal($args[0], $args[1]); 130 | default: 131 | return call_user_func_array($internal, $args); 132 | } 133 | }); 134 | } 135 | if ($shouldReturnResource) { 136 | return ($result !== false) ? $resource : false; 137 | } 138 | return $result; 139 | } 140 | 141 | public static function fopen($path, $mode, $options, $context = null) 142 | { 143 | $otherWrapper = static::getOtherWrapper($context); 144 | if ($otherWrapper !== null) { 145 | $openedPath = null; 146 | $result = $otherWrapper->stream_open($path, $mode, $options, $openedPath); 147 | return $result !== false ? $otherWrapper : false; 148 | } 149 | return static::bypass(function() use ($path, $mode, $options, $context) { 150 | if ($context === null) { 151 | return fopen($path, $mode, $options); 152 | } 153 | return fopen($path, $mode, $options, $context); 154 | }); 155 | } 156 | 157 | public function stream_close() 158 | { 159 | return static::fclose($this->resource); 160 | } 161 | 162 | public static function fclose($resource) 163 | { 164 | return static::alternate('fclose', $resource, 'stream_close'); 165 | } 166 | 167 | public static function fread($resource, $count) 168 | { 169 | return static::alternate('fread', $resource, 'stream_read', [$count]); 170 | } 171 | 172 | public static function feof($resource) 173 | { 174 | return static::alternate('feof', $resource, 'stream_eof'); 175 | } 176 | 177 | public function stream_eof() 178 | { 179 | return static::feof($this->resource); 180 | } 181 | 182 | public function stream_flush() 183 | { 184 | return static::alternate('fflush', $this->resource, 'stream_flush'); 185 | } 186 | 187 | public function stream_read($count) 188 | { 189 | return static::fread($this->resource, $count); 190 | } 191 | 192 | public function stream_seek($offset, $whence = SEEK_SET) 193 | { 194 | if (is_object($this->resource)) { 195 | return $this->resource->stream_seek($offset, $whence); 196 | } 197 | return fseek($this->resource, $offset, $whence) === 0; 198 | } 199 | 200 | public function stream_stat() 201 | { 202 | if (is_object($this->resource)) { 203 | return $this->resource->stream_stat(); 204 | } 205 | $result = fstat($this->resource); 206 | if ($result) { 207 | $result[self::STAT_MTIME_ASSOC_OFFSET]++; 208 | $result[self::STAT_MTIME_NUMERIC_OFFSET]++; 209 | } 210 | return $result; 211 | } 212 | 213 | public function stream_tell() 214 | { 215 | return static::alternate('ftell', $this->resource, 'stream_tell'); 216 | } 217 | 218 | public static function bypass(callable $action) 219 | { 220 | static::unwrap(); 221 | $result = $action(); 222 | static::wrap(); 223 | return $result; 224 | } 225 | 226 | public function url_stat($path, $flags) 227 | { 228 | $internal = function($path, $flags) { 229 | $func = ($flags & STREAM_URL_STAT_LINK) ? 'lstat' : 'stat'; 230 | clearstatcache(); 231 | if ($flags & STREAM_URL_STAT_QUIET) { 232 | set_error_handler(function() {}); 233 | try { 234 | $result = call_user_func($func, $path); 235 | } catch (\Exception $e) { 236 | $result = null; 237 | } 238 | restore_error_handler(); 239 | } else { 240 | $result = call_user_func($func, $path); 241 | } 242 | clearstatcache(); 243 | if ($result) { 244 | $result[self::STAT_MTIME_ASSOC_OFFSET]++; 245 | $result[self::STAT_MTIME_NUMERIC_OFFSET]++; 246 | } 247 | return $result; 248 | }; 249 | return static::alternate($internal, null, __FUNCTION__, [$path, $flags], [], $this->context); 250 | } 251 | 252 | public function dir_closedir() 253 | { 254 | return static::alternate('closedir', $this->resource, 'dir_closedir') ?: true; 255 | } 256 | 257 | public function dir_opendir($path, $options) 258 | { 259 | $this->resource = static::alternate('opendir', null, __FUNCTION__, [$path], [$options], $this->context); 260 | return $this->resource !== false; 261 | } 262 | 263 | public function dir_readdir() 264 | { 265 | return static::alternate('readdir', $this->resource, __FUNCTION__); 266 | } 267 | 268 | public function dir_rewinddir() 269 | { 270 | return static::alternate('rewinddir', $this->resource, __FUNCTION__); 271 | } 272 | 273 | public function mkdir($path, $mode, $options) 274 | { 275 | return static::alternate('mkdir', null, __FUNCTION__, [$path, $mode, $options], [], $this->context); 276 | } 277 | 278 | public function rename($pathFrom, $pathTo) 279 | { 280 | return static::alternate('rename', null, __FUNCTION__, [$pathFrom, $pathTo], [], $this->context); 281 | } 282 | 283 | public function rmdir($path, $options) 284 | { 285 | return static::alternate('rmdir', null, __FUNCTION__, [$path], [$options], $this->context); 286 | } 287 | 288 | public function stream_cast($castAs) 289 | { 290 | return static::alternate(function() { 291 | return $this->resource; 292 | }, null, __FUNCTION__, [$castAs]); 293 | } 294 | 295 | public function stream_lock($operation) 296 | { 297 | if ($operation === '0' || $operation === 0) { 298 | $operation = LOCK_EX; 299 | } 300 | return static::alternate('flock', $this->resource, __FUNCTION__, [$operation]); 301 | } 302 | 303 | public function stream_set_option($option, $arg1, $arg2) 304 | { 305 | $internal = function($option, $arg1, $arg2) { 306 | switch ($option) { 307 | case STREAM_OPTION_BLOCKING: 308 | return stream_set_blocking($this->resource, $arg1); 309 | case STREAM_OPTION_READ_TIMEOUT: 310 | return stream_set_timeout($this->resource, $arg1, $arg2); 311 | case STREAM_OPTION_WRITE_BUFFER: 312 | return stream_set_write_buffer($this->resource, $arg1); 313 | case STREAM_OPTION_READ_BUFFER: 314 | return stream_set_read_buffer($this->resource, $arg1); 315 | } 316 | }; 317 | return static::alternate($internal, $this->resource, __FUNCTION__, [$option, $arg1, $arg2]); 318 | } 319 | 320 | public function stream_write($data) 321 | { 322 | return static::fwrite($this->resource, $data); 323 | } 324 | 325 | public static function fwrite($resource, $data) 326 | { 327 | return static::alternate('fwrite', $resource, 'stream_write', [$data]); 328 | } 329 | 330 | public function unlink($path) 331 | { 332 | return static::alternate('unlink', $this->resource, __FUNCTION__, [$path], [], $this->context); 333 | } 334 | 335 | public function stream_metadata($path, $option, $value) 336 | { 337 | $internal = function($path, $option, $value) { 338 | switch ($option) { 339 | case STREAM_META_TOUCH: 340 | if (empty($value)) { 341 | return touch($path); 342 | } else { 343 | return touch($path, $value[0], $value[1]); 344 | } 345 | case STREAM_META_OWNER_NAME: 346 | case STREAM_META_OWNER: 347 | return chown($path, $value); 348 | case STREAM_META_GROUP_NAME: 349 | case STREAM_META_GROUP: 350 | return chgrp($path, $value); 351 | case STREAM_META_ACCESS: 352 | return chmod($path, $value); 353 | } 354 | }; 355 | return static::alternate($internal, null, __FUNCTION__, [$path, $option, $value]); 356 | } 357 | 358 | public function stream_truncate($newSize) 359 | { 360 | return static::alternate('ftruncate', $this->resource, __FUNCTION__, [$newSize]); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\Config; 10 | 11 | use Patchwork\Utils; 12 | use Patchwork\Exceptions; 13 | use Patchwork\CodeManipulation\Actions\RedefinitionOfLanguageConstructs; 14 | 15 | const FILE_NAME = 'patchwork.json'; 16 | 17 | function locate() 18 | { 19 | $alreadyChecked = []; 20 | $paths = array_map('dirname', get_included_files()); 21 | $paths[] = dirname($_SERVER['PHP_SELF']); 22 | $paths[] = getcwd(); 23 | foreach ($paths as $path) { 24 | while (dirname($path) !== $path) { 25 | $file = $path . DIRECTORY_SEPARATOR . FILE_NAME; 26 | if (!isset($alreadyChecked[$file]) && is_file($file)) { 27 | read($file); 28 | State::$timestamp = max(filemtime($file), State::$timestamp); 29 | } 30 | $alreadyChecked[$file] = true; 31 | $path = dirname($path); 32 | } 33 | } 34 | } 35 | 36 | function read($file) 37 | { 38 | $data = json_decode(file_get_contents($file), true); 39 | if (json_last_error() !== JSON_ERROR_NONE) { 40 | $message = json_last_error_msg(); 41 | throw new Exceptions\ConfigMalformed($file, $message); 42 | } 43 | set($data, $file); 44 | } 45 | 46 | function set(array $data, $file) 47 | { 48 | $keys = array_keys($data); 49 | $list = ['blacklist', 'whitelist', 'cache-path', 'redefinable-internals', 'new-keyword-redefinable']; 50 | $unknown = array_diff($keys, $list); 51 | if ($unknown != []) { 52 | throw new Exceptions\ConfigKeyNotRecognized(reset($unknown), $list, $file); 53 | } 54 | $root = dirname($file); 55 | setBlacklist(get($data, 'blacklist'), $root); 56 | setWhitelist(get($data, 'whitelist'), $root); 57 | setCachePath(get($data, 'cache-path'), $root); 58 | setRedefinableInternals(get($data, 'redefinable-internals'), $root); 59 | setNewKeywordRedefinability(get($data, 'new-keyword-redefinable'), $root); 60 | } 61 | 62 | function get(array $data, $key) 63 | { 64 | return isset($data[$key]) ? $data[$key] : null; 65 | } 66 | 67 | function setBlacklist($data, $root) 68 | { 69 | merge(State::$blacklist, resolvePaths($data, $root)); 70 | } 71 | 72 | function isListed($path, array $list) 73 | { 74 | $path = rtrim($path, '\\/'); 75 | foreach ($list as $item) { 76 | if (!is_string($item)) { 77 | $item = chr($item); 78 | } 79 | if (strpos($path, $item) === 0) { 80 | return true; 81 | } 82 | } 83 | return false; 84 | } 85 | 86 | function isBlacklisted($path) 87 | { 88 | return isListed($path, State::$blacklist); 89 | } 90 | 91 | function setWhitelist($data, $root) 92 | { 93 | merge(State::$whitelist, resolvePaths($data, $root)); 94 | } 95 | 96 | function isWhitelisted($path) 97 | { 98 | return isListed($path, State::$whitelist); 99 | } 100 | 101 | function setCachePath($data, $root) 102 | { 103 | if ($data === null) { 104 | return; 105 | } 106 | $path = resolvePath($data, $root); 107 | if (State::$cachePath !== null && State::$cachePath !== $path) { 108 | throw new Exceptions\CachePathConflict(State::$cachePath, $path); 109 | } 110 | State::$cachePath = $path; 111 | } 112 | 113 | function getDefaultRedefinableInternals() 114 | { 115 | return [ 116 | 'preg_replace_callback', 117 | 'spl_autoload_register', 118 | 'iterator_apply', 119 | 'header_register_callback', 120 | 'call_user_func', 121 | 'call_user_func_array', 122 | 'forward_static_call', 123 | 'forward_static_call_array', 124 | 'register_shutdown_function', 125 | 'register_tick_function', 126 | 'unregister_tick_function', 127 | 'ob_start', 128 | 'usort', 129 | 'uasort', 130 | 'uksort', 131 | 'array_reduce', 132 | 'array_intersect_ukey', 133 | 'array_uintersect', 134 | 'array_uintersect_assoc', 135 | 'array_intersect_uassoc', 136 | 'array_uintersect_uassoc', 137 | 'array_uintersect_uassoc', 138 | 'array_diff_ukey', 139 | 'array_udiff', 140 | 'array_udiff_assoc', 141 | 'array_diff_uassoc', 142 | 'array_udiff_uassoc', 143 | 'array_udiff_uassoc', 144 | 'array_filter', 145 | 'array_map', 146 | 'libxml_set_external_entity_loader', 147 | ]; 148 | } 149 | 150 | function getRedefinableInternals() 151 | { 152 | if (!empty(State::$redefinableInternals)) { 153 | return array_merge(State::$redefinableInternals, getDefaultRedefinableInternals()); 154 | } 155 | return []; 156 | } 157 | 158 | function setRedefinableInternals($names) 159 | { 160 | merge(State::$redefinableInternals, $names); 161 | $constructs = array_intersect(State::$redefinableInternals, getSupportedLanguageConstructs()); 162 | State::$redefinableLanguageConstructs = array_merge(State::$redefinableLanguageConstructs, $constructs); 163 | State::$redefinableInternals = array_diff(State::$redefinableInternals, $constructs); 164 | } 165 | 166 | function setNewKeywordRedefinability($value) 167 | { 168 | State::$newKeywordRedefinable = State::$newKeywordRedefinable || $value; 169 | } 170 | 171 | function getRedefinableLanguageConstructs() 172 | { 173 | return State::$redefinableLanguageConstructs; 174 | } 175 | 176 | function getSupportedLanguageConstructs() 177 | { 178 | return array_keys(RedefinitionOfLanguageConstructs\getMappingOfConstructs()); 179 | } 180 | 181 | function isNewKeywordRedefinable() 182 | { 183 | return State::$newKeywordRedefinable; 184 | } 185 | 186 | function getCachePath() 187 | { 188 | return State::$cachePath; 189 | } 190 | 191 | function resolvePath($path, $root) 192 | { 193 | if ($path === null) { 194 | return null; 195 | } 196 | if (file_exists($path) && realpath($path) === $path) { 197 | return $path; 198 | } 199 | return realpath($root . '/' . $path); 200 | } 201 | 202 | function resolvePaths($paths, $root) 203 | { 204 | if ($paths === null) { 205 | return []; 206 | } 207 | $result = []; 208 | foreach ((array) $paths as $path) { 209 | $result[] = resolvePath($path, $root); 210 | } 211 | return $result; 212 | } 213 | 214 | function merge(array &$target, $source) 215 | { 216 | $target = array_merge($target, (array) $source); 217 | } 218 | 219 | function getTimestamp() 220 | { 221 | return State::$timestamp; 222 | } 223 | 224 | class State 225 | { 226 | static $blacklist = []; 227 | static $whitelist = []; 228 | static $cachePath; 229 | static $redefinableInternals = []; 230 | static $redefinableLanguageConstructs = []; 231 | static $newKeywordRedefinable = false; 232 | static $timestamp = 0; 233 | } 234 | -------------------------------------------------------------------------------- /src/Console.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\Console; 10 | 11 | use Patchwork\CodeManipulation as CM; 12 | 13 | error_reporting(E_ALL); 14 | 15 | $argc > 2 && $argv[1] == 'prime' 16 | or exit("\nUsage: php patchwork.phar prime DIR1 DIR2 ... DIRn\n" . 17 | " (to recursively prime all PHP files under given directories)\n\n"); 18 | 19 | try { 20 | CM\cacheEnabled() 21 | or exit("\nError: no cache location set.\n\n"); 22 | } catch (Patchwork\Exceptions\CachePathUnavailable $e) { 23 | exit("\nError: " . $e->getMessage() . "\n\n"); 24 | } 25 | 26 | echo "\nCounting files...\n"; 27 | 28 | $files = []; 29 | 30 | foreach (array_slice($argv, 2) as $path) { 31 | $path = realpath($path); 32 | foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path)) as $file) { 33 | if (substr($file, -4) == '.php' && !CM\internalToCache($file) && !CM\availableCached($file)) { 34 | $files[] = $file; 35 | } 36 | } 37 | } 38 | 39 | $count = count($files); 40 | 41 | $count > 0 or exit("\nNothing to do.\n\n"); 42 | 43 | echo "\nPriming ($count files total):\n"; 44 | 45 | const CONSOLE_WIDTH = 80; 46 | 47 | $progress = 0; 48 | 49 | for ($i = 0; $i < $count; $i++) { 50 | CM\prime($files[$i]->getRealPath()); 51 | while ((int) (($i + 1) / $count * CONSOLE_WIDTH) > $progress) { 52 | echo '.'; 53 | $progress++; 54 | } 55 | } 56 | 57 | echo "\n\n"; 58 | -------------------------------------------------------------------------------- /src/Exceptions.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2023 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\Exceptions; 10 | 11 | use Patchwork\Utils; 12 | 13 | abstract class Exception extends \Exception 14 | { 15 | } 16 | 17 | class NoResult extends Exception 18 | { 19 | } 20 | 21 | class StackEmpty extends Exception 22 | { 23 | protected $message = "There are no calls in the dispatch stack"; 24 | } 25 | 26 | abstract class CallbackException extends Exception 27 | { 28 | function __construct($callback) 29 | { 30 | parent::__construct(sprintf($this->message, Utils\callableToString($callback))); 31 | } 32 | } 33 | 34 | class NotUserDefined extends CallbackException 35 | { 36 | protected $message = 'Please include {"redefinable-internals": ["%s"]} in your patchwork.json.'; 37 | } 38 | 39 | class DefinedTooEarly extends CallbackException 40 | { 41 | 42 | function __construct($callback) 43 | { 44 | $this->message = "The file that defines %s() was included earlier than Patchwork. " . 45 | "Please reverse this order to be able to redefine the function in question."; 46 | parent::__construct($callback); 47 | } 48 | } 49 | 50 | class InternalMethodsNotSupported extends CallbackException 51 | { 52 | protected $message = "Methods of internal classes (such as %s) are not yet redefinable in Patchwork 2.1."; 53 | } 54 | 55 | /** 56 | * @deprecated 2.2.0 57 | */ 58 | class InternalsNotSupportedOnHHVM extends CallbackException 59 | { 60 | protected $message = "As of version 2.1, Patchwork cannot redefine internal functions and methods (such as %s) on HHVM."; 61 | } 62 | 63 | class CachePathUnavailable extends Exception 64 | { 65 | function __construct($location) 66 | { 67 | parent::__construct(sprintf( 68 | "The specified cache path is nonexistent or read-only: %s", 69 | $location 70 | )); 71 | } 72 | } 73 | 74 | class ConfigException extends Exception 75 | { 76 | } 77 | 78 | class ConfigMalformed extends ConfigException 79 | { 80 | function __construct($file, $message) 81 | { 82 | parent::__construct(sprintf( 83 | 'The configuration file %s is malformed: %s', 84 | $file, 85 | $message 86 | )); 87 | } 88 | } 89 | 90 | class ConfigKeyNotRecognized extends ConfigException 91 | { 92 | function __construct($key, $list, $file) 93 | { 94 | parent::__construct(sprintf( 95 | "The key '%s' in the configuration file %s was not recognized. " . 96 | "You might have meant one of these: %s", 97 | $key, 98 | $file, 99 | join(', ', $list) 100 | )); 101 | } 102 | } 103 | 104 | class CachePathConflict extends ConfigException 105 | { 106 | function __construct($first, $second) 107 | { 108 | parent::__construct(sprintf( 109 | "Detected configuration files provide conflicting cache paths: %s and %s", 110 | $first, 111 | $second 112 | )); 113 | } 114 | } 115 | 116 | class NewKeywordNotRedefinable extends ConfigException 117 | { 118 | protected $message = 'Please set {"new-keyword-redefinable": true} to redefine instantiations'; 119 | } 120 | 121 | class NonNullToVoid extends Exception 122 | { 123 | protected $message = 'A redefinition of a void-typed callable attempted to return a non-null result'; 124 | } 125 | 126 | class ReturnFromNever extends Exception 127 | { 128 | protected $message = 'A redefinition of a never-typed callable attempted to return'; 129 | } 130 | -------------------------------------------------------------------------------- /src/Redefinitions/LanguageConstructs.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\Redefinitions\LanguageConstructs; 10 | 11 | function _echo($string) 12 | { 13 | foreach (func_get_args() as $argument) { 14 | echo $argument; 15 | } 16 | } 17 | 18 | function _print($string) 19 | { 20 | return print($string); 21 | } 22 | 23 | function _eval($code) 24 | { 25 | return eval($code); 26 | } 27 | 28 | function _die($message = null) 29 | { 30 | die($message); 31 | } 32 | 33 | function _exit($message = null) 34 | { 35 | exit($message); 36 | } 37 | 38 | function _isset(&$lvalue) 39 | { 40 | return isset($lvalue); 41 | } 42 | 43 | function _unset(&$lvalue) 44 | { 45 | unset($lvalue); 46 | } 47 | 48 | function _empty(&$lvalue) 49 | { 50 | return empty($lvalue); 51 | } 52 | 53 | function _require($path) 54 | { 55 | return require($path); 56 | } 57 | 58 | function _require_once($path) 59 | { 60 | return require_once($path); 61 | } 62 | 63 | function _include($path) 64 | { 65 | return include($path); 66 | } 67 | 68 | function _include_once($path) 69 | { 70 | return include_once($path); 71 | } 72 | 73 | function _clone($object) 74 | { 75 | return clone $object; 76 | } 77 | -------------------------------------------------------------------------------- /src/Stack.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\Stack; 10 | 11 | use Patchwork\Exceptions; 12 | 13 | function push($offset, $calledClass, ?array $argsOverride = null) 14 | { 15 | State::$items[] = [$offset, $calledClass, $argsOverride]; 16 | } 17 | 18 | function pop() 19 | { 20 | array_pop(State::$items); 21 | } 22 | 23 | function pushFor($offset, $calledClass, $callback, ?array $argsOverride = null) 24 | { 25 | push($offset, $calledClass, $argsOverride); 26 | try { 27 | $callback(); 28 | } catch (\Exception $e) { 29 | $exception = $e; 30 | } 31 | pop(); 32 | if (isset($exception)) { 33 | throw $exception; 34 | } 35 | } 36 | 37 | function top($property = null) 38 | { 39 | $all = all(); 40 | $frame = reset($all); 41 | $argsOverride = topArgsOverride(); 42 | if ($argsOverride !== null) { 43 | $frame["args"] = $argsOverride; 44 | } 45 | if ($property) { 46 | return isset($frame[$property]) ? $frame[$property] : null; 47 | } 48 | return $frame; 49 | } 50 | 51 | function topOffset() 52 | { 53 | if (empty(State::$items)) { 54 | throw new Exceptions\StackEmpty; 55 | } 56 | list($offset, $calledClass) = end(State::$items); 57 | return $offset; 58 | } 59 | 60 | function topCalledClass() 61 | { 62 | if (empty(State::$items)) { 63 | throw new Exceptions\StackEmpty; 64 | } 65 | list($offset, $calledClass) = end(State::$items); 66 | return $calledClass; 67 | } 68 | 69 | function topArgsOverride() 70 | { 71 | if (empty(State::$items)) { 72 | throw new Exceptions\StackEmpty; 73 | } 74 | list($offset, $calledClass, $argsOverride) = end(State::$items); 75 | return $argsOverride; 76 | } 77 | 78 | function all() 79 | { 80 | $backtrace = debug_backtrace(); 81 | return array_slice($backtrace, count($backtrace) - topOffset()); 82 | } 83 | 84 | function allCalledClasses() 85 | { 86 | return array_map(function($item) { 87 | list($offset, $calledClass) = $item; 88 | return $calledClass; 89 | }, State::$items); 90 | } 91 | 92 | class State 93 | { 94 | static $items = []; 95 | } 96 | -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2010-2018 Ignas Rudaitis 7 | * @license http://www.opensource.org/licenses/mit-license.html 8 | */ 9 | namespace Patchwork\Utils; 10 | 11 | use Patchwork\Config; 12 | use Patchwork\CallRerouting; 13 | use Patchwork\CodeManipulation; 14 | 15 | const ALIASING_CODE = ' 16 | namespace %s; 17 | function %s() { 18 | return call_user_func_array("%s", func_get_args()); 19 | } 20 | '; 21 | 22 | function clearOpcodeCaches() 23 | { 24 | if (function_exists('opcache_reset')) { 25 | opcache_reset(); 26 | } 27 | if (ini_get('wincache.ocenabled')) { 28 | wincache_refresh_if_changed(); 29 | } 30 | if (ini_get('apc.enabled') && function_exists('apc_clear_cache')) { 31 | apc_clear_cache(); 32 | } 33 | } 34 | 35 | /** 36 | * @deprecated 2.2.0 37 | */ 38 | function generatorsSupported() 39 | { 40 | return version_compare(PHP_VERSION, "5.5", ">="); 41 | } 42 | 43 | /** 44 | * @deprecated 2.2.0 45 | */ 46 | function runningOnHHVM() 47 | { 48 | return defined("HHVM_VERSION"); 49 | } 50 | 51 | function condense($string) 52 | { 53 | return preg_replace('/\s+/', ' ', $string); 54 | } 55 | 56 | function indexOfFirstGreaterThan(array $array, $value) 57 | { 58 | $low = 0; 59 | $high = count($array) - 1; 60 | if (empty($array) || $array[$high] <= $value) { 61 | return -1; 62 | } 63 | while ($low < $high) { 64 | $mid = (int)(($low + $high) / 2); 65 | if ($array[$mid] <= $value) { 66 | $low = $mid + 1; 67 | } else { 68 | $high = $mid; 69 | } 70 | } 71 | return $low; 72 | } 73 | 74 | function indexOfLastNotGreaterThan(array $array, $value) 75 | { 76 | if (empty($array)) { 77 | return -1; 78 | } 79 | $result = indexOfFirstGreaterThan($array, $value); 80 | if ($result === -1) { 81 | $result = count($array) - 1; 82 | } 83 | while ($array[$result] > $value) { 84 | $result--; 85 | } 86 | return $result; 87 | } 88 | 89 | function firstGreaterThan(array $array, $value, $default = INF) 90 | { 91 | $index = indexOfFirstGreaterThan($array, $value); 92 | return ($index !== -1) ? $array[$index] : $default; 93 | } 94 | 95 | function lastNotGreaterThan(array $array, $value, $default = INF) 96 | { 97 | $index = indexOfLastNotGreaterThan($array, $value); 98 | return ($index !== -1) ? $array[$index] : $default; 99 | } 100 | 101 | function allWithinRange(array $array, $low, $high) 102 | { 103 | $low--; 104 | $high++; 105 | $index = indexOfFirstGreaterThan($array, $low); 106 | if ($index === -1) { 107 | return []; 108 | } 109 | $result = []; 110 | while ($index < count($array) && $array[$index] < $high) { 111 | $result[] = $array[$index]; 112 | $index++; 113 | } 114 | return $result; 115 | } 116 | 117 | function interpretCallable($callback) 118 | { 119 | if (is_object($callback)) { 120 | return interpretCallable([$callback, "__invoke"]); 121 | } 122 | if (is_array($callback)) { 123 | list($class, $method) = $callback; 124 | $instance = null; 125 | if (is_object($class)) { 126 | $instance = $class; 127 | $class = get_class($class); 128 | } 129 | $class = isset($class) ? ltrim($class, "\\") : ''; 130 | return [$class, $method, $instance]; 131 | } 132 | if (substr($callback, 0, 4) === 'new ') { 133 | return [ltrim(substr($callback, 4)), 'new', null]; 134 | } 135 | $callback = ltrim($callback, "\\"); 136 | if (strpos($callback, "::")) { 137 | list($class, $method) = explode("::", $callback); 138 | return [$class, $method, null]; 139 | } 140 | return [null, $callback, null]; 141 | } 142 | 143 | function callableDefined($callable, $shouldAutoload = false) 144 | { 145 | list($class, $method, $instance) = interpretCallable($callable); 146 | if ($instance !== null) { 147 | return true; 148 | } 149 | if (isset($class)) { 150 | return classOrTraitExists($class, $shouldAutoload) && 151 | (method_exists($class, $method) || $method === 'new'); 152 | } 153 | return function_exists($method); 154 | } 155 | 156 | function classOrTraitExists($classOrTrait, $shouldAutoload = true) 157 | { 158 | return class_exists($classOrTrait, $shouldAutoload) 159 | || trait_exists($classOrTrait, $shouldAutoload); 160 | } 161 | 162 | function append(&$array, $value) 163 | { 164 | $array[] = $value; 165 | end($array); 166 | return key($array); 167 | } 168 | 169 | function appendUnder(&$array, $path, $value) 170 | { 171 | foreach ((array) $path as $key) { 172 | if (!isset($array[$key])) { 173 | $array[$key] = []; 174 | } 175 | $array = &$array[$key]; 176 | } 177 | return append($array, $value); 178 | } 179 | 180 | function access($array, $path, $default = null) 181 | { 182 | foreach ((array) $path as $key) { 183 | if (!isset($array[$key])) { 184 | return $default; 185 | } 186 | $array = $array[$key]; 187 | } 188 | return $array; 189 | } 190 | 191 | function normalizePath($path) 192 | { 193 | return rtrim(strtr($path, "\\", "/"), "/"); 194 | } 195 | 196 | function reflectCallable($callback) 197 | { 198 | if ($callback instanceof \Closure) { 199 | return new \ReflectionFunction($callback); 200 | } 201 | list($class, $method) = interpretCallable($callback); 202 | if (isset($class)) { 203 | return new \ReflectionMethod($class, $method); 204 | } 205 | return new \ReflectionFunction($method); 206 | } 207 | 208 | function callableToString($callback) 209 | { 210 | list($class, $method) = interpretCallable($callback); 211 | if (isset($class)) { 212 | return $class . "::" . $method; 213 | } 214 | return $method; 215 | } 216 | 217 | function alias($namespace, array $mapping) 218 | { 219 | foreach ($mapping as $original => $aliases) { 220 | $original = ltrim(str_replace('\\', '\\\\', $namespace) . '\\\\' . $original, '\\'); 221 | foreach ((array) $aliases as $alias) { 222 | eval(sprintf(ALIASING_CODE, $namespace, $alias, $original)); 223 | } 224 | } 225 | } 226 | 227 | function getUserDefinedCallables() 228 | { 229 | return array_merge(get_defined_functions()['user'], getUserDefinedMethods()); 230 | } 231 | 232 | function getRedefinableCallables() 233 | { 234 | return array_merge(getUserDefinedCallables(), Config\getRedefinableInternals()); 235 | } 236 | 237 | function getUserDefinedMethods() 238 | { 239 | static $result = []; 240 | static $classCount = 0; 241 | static $traitCount = 0; 242 | $classes = getUserDefinedClasses(); 243 | $traits = getUserDefinedTraits(); 244 | $newClasses = array_slice($classes, $classCount); 245 | $newTraits = array_slice($traits, $traitCount); 246 | foreach (array_merge($newClasses, $newTraits) as $newClass) { 247 | foreach (get_class_methods($newClass) as $method) { 248 | $result[] = $newClass . '::' . $method; 249 | } 250 | } 251 | $classCount = count($classes); 252 | $traitCount = count($traits); 253 | return $result; 254 | } 255 | 256 | function getUserDefinedClasses() 257 | { 258 | static $classCutoff; 259 | $classes = get_declared_classes(); 260 | if (!isset($classCutoff)) { 261 | $classCutoff = count($classes); 262 | for ($i = 0; $i < count($classes); $i++) { 263 | if ((new \ReflectionClass($classes[$i]))->isUserDefined()) { 264 | $classCutoff = $i; 265 | break; 266 | } 267 | } 268 | } 269 | return array_slice($classes, $classCutoff); 270 | } 271 | 272 | function getUserDefinedTraits() 273 | { 274 | static $traitCutoff; 275 | $traits = get_declared_traits(); 276 | if (!isset($traitCutoff)) { 277 | $traitCutoff = count($traits); 278 | for ($i = 0; $i < count($traits); $i++) { 279 | $methods = get_class_methods($traits[$i]); 280 | if (empty($methods)) { 281 | continue; 282 | } 283 | list($first) = $methods; 284 | if ((new \ReflectionMethod($traits[$i], $first))->isUserDefined()) { 285 | $traitCutoff = $i; 286 | break; 287 | } 288 | } 289 | } 290 | return array_slice($traits, $traitCutoff); 291 | } 292 | 293 | function matchWildcard($wildcard, array $subjects) 294 | { 295 | $table = ['*' => '.*', '{' => '(', '}' => ')', ' ' => '', '\\' => '\\\\']; 296 | $pattern = '/' . strtr($wildcard, $table) . '/i'; 297 | return preg_grep($pattern, $subjects); 298 | } 299 | 300 | function wildcardMatches($wildcard, $subject) 301 | { 302 | return matchWildcard($wildcard, [$subject]) == [$subject]; 303 | } 304 | 305 | function isOwnName($name) 306 | { 307 | return stripos((string) $name, 'Patchwork\\') === 0 308 | && stripos((string) $name, CallRerouting\INTERNAL_REDEFINITION_NAMESPACE . '\\') !== 0; 309 | } 310 | 311 | function isForeignName($name) 312 | { 313 | return !isOwnName($name); 314 | } 315 | 316 | function markMissedCallables() 317 | { 318 | State::$missedCallables = array_map('strtolower', getUserDefinedCallables()); 319 | } 320 | 321 | function getMissedCallables() 322 | { 323 | return State::$missedCallables; 324 | } 325 | 326 | function callableWasMissed($name) 327 | { 328 | return in_array(strtolower($name), getMissedCallables()); 329 | } 330 | 331 | function endsWith($haystack, $needle) 332 | { 333 | if (strlen($haystack) === strlen($needle)) { 334 | return $haystack === $needle; 335 | } 336 | if (strlen($haystack) < strlen($needle)) { 337 | return false; 338 | } 339 | return substr($haystack, -strlen($needle)) === $needle; 340 | } 341 | 342 | function wasRunAsConsoleApp() 343 | { 344 | global $argv; 345 | return isset($argv) && ( 346 | endsWith($argv[0], 'patchwork.phar') || endsWith($argv[0], 'Patchwork.php') 347 | ); 348 | } 349 | 350 | function getParameterAndArgumentLists(?\ReflectionMethod $reflection = null) 351 | { 352 | $parameters = []; 353 | $arguments = []; 354 | if ($reflection) { 355 | foreach ($reflection->getParameters() as $p) { 356 | $parameter = '$' . $p->name; 357 | if ($p->isOptional()) { 358 | try { 359 | $value = var_export($p->getDefaultValue(), true); 360 | } catch (\ReflectionException $e) { 361 | $value = var_export(CallRerouting\INSTANTIATOR_DEFAULT_ARGUMENT, true); 362 | } 363 | $parameter .= ' = ' . $value; 364 | } 365 | $parameters[] = $parameter; 366 | $arguments[] = '$' . $p->name; 367 | } 368 | } 369 | return [join(', ' , $parameters), join(', ', $arguments)]; 370 | } 371 | 372 | function args() 373 | { 374 | return func_get_args(); 375 | } 376 | 377 | function tokenize($string) 378 | { 379 | if (defined('TOKEN_PARSE')) { 380 | return token_get_all($string, TOKEN_PARSE); 381 | } 382 | return token_get_all($string); 383 | } 384 | 385 | class State 386 | { 387 | static $missedCallables = []; 388 | } 389 | --------------------------------------------------------------------------------