asmblah / php-code-shift
Installs: 7 911
Dependents: 7
Suggesters: 0
Security: 0
Stars: 1
Watchers: 2
Forks: 0
Open Issues: 0
Type:project
Requires
- php: >=8.1
- nikic/php-parser: ^4.18 || ^5.0
- nytris/nytris: ^0.1
- psr/log: ^1|^2|^3
- symfony/filesystem: ^5.4
Requires (Dev)
- mockery/mockery: 1.6.11
- phpstan/phpstan: ^1.10
- phpstan/phpstan-mockery: ^1.1
- phpunit/phpunit: ^10.2
README
Allows running PHP code to be monkey-patched either ahead of time or on the fly.
Why?
To allow stubbing of built-in functions during testing, for example.
Usage
Install this package with Composer:
$ composer require asmblah/php-code-shift
Hooking built-in functions
runner.php
<?php use Asmblah\PhpCodeShift\CodeShift; use Asmblah\PhpCodeShift\Shifter\Filter\FileFilter; use Asmblah\PhpCodeShift\Shifter\Shift\Shift\FunctionHook\FunctionHookShiftSpec; require_once __DIR__ . '/vendor/autoload.php'; $codeShift = new CodeShift(); $codeShift->shift( new FunctionHookShiftSpec( 'substr', function (callable $originalSubstr) { return function (string $string, int $offset, ?int $length = null) use ($originalSubstr) { return '[substr<' . $originalSubstr($string, $offset, $length) . '>]'; }; } ), new FileFilter(__DIR__ . '/substr_test.php') ); include __DIR__ . '/substr_test.php';
substr_test.php
<?php // NB: substr(...) will be hooked by the shift defined inside runner.php. $myResult = substr('my string', 1, 4) . ' and ' . substr('your string', 1, 2); print $myResult;
The output will be:
[substr<y st>] and [substr<ou>]
Hooking classes
References to a class may be replaced with references to a different class. This only works
for statically-referenced classes, i.e. where it is referenced with a bareword, e.g. new MyClass
.
Dynamic/variable references are not supported, e.g. new $myClassName
as they can only be resolved at runtime.
Any matching types are not replaced - the replacement class must extend the original class or interface in order to pass type checks.
runner.php
<?php use Asmblah\PhpCodeShift\CodeShift; use Asmblah\PhpCodeShift\Shifter\Filter\FileFilter; use Asmblah\PhpCodeShift\Shifter\Shift\Shift\ClassHook\ClassHookShiftSpec; require_once __DIR__ . '/vendor/autoload.php'; $codeShift = new CodeShift(); $codeShift->shift( new ClassHookShiftSpec( 'MyClass', 'MyReplacementClass' ), new FileFilter(__DIR__ . '/class_test.php') ); include __DIR__ . '/class_test.php';
class_test.php
<?php class MyClass { public function getIt(): string { return 'my original string'; } } class MyReplacementClass { public function getIt(): string { return 'my replacement string'; } } // NB: References to MyClass will be hooked by the shift defined inside runner.php. $myObject = new MyClass; print $myObject->getIt();
The output will be:
my replacement string
Static method calls (MyClass::myStaticMethod()
) and class constant fetches (MyClass:MY_CONST
) are also supported.
Custom stream handlers
Filesystem access is intercepted using a stream wrapper for the special file://
and phar://
protocols.
By default, the stream wrapper uses a stream handler (a PHP Code Shift -specific concept)
that for the most part delegates to PHP's native filesystem handling.
A custom stream handler may be registered in its place in order to hook into filesystem operations.
See Nytris Antilag and Nytris Boost for examples of libraries that register custom stream handlers.
Example
<?php declare(strict_types=1); namespace My\App; use Asmblah\PhpCodeShift\CodeShift; use Asmblah\PhpCodeShift\Shifter\Stream\Handler\AbstractStreamHandlerDecorator; use Asmblah\PhpCodeShift\Shifter\Stream\Handler\Registration\RegistrantInterface; use Asmblah\PhpCodeShift\Shifter\Stream\Handler\Registration\Registration; use Asmblah\PhpCodeShift\Shifter\Stream\Handler\Registration\RegistrationInterface; use Asmblah\PhpCodeShift\Shifter\Stream\Handler\StreamHandlerInterface; use Asmblah\PhpCodeShift\Shifter\Stream\Native\StreamWrapperInterface; use Asmblah\PhpCodeShift\Shifter\Stream\StreamWrapperManager; require_once __DIR__ . '/vendor/autoload.php'; class MyStreamHandler extends AbstractStreamHandlerDecorator { public function unlink(StreamWrapperInterface $streamWrapper, string $path): bool { print 'Unlinking path: ' . $path . PHP_EOL; return parent::unlink($streamWrapper, $path); } } class MyRegistrant implements RegistrantInterface { public function registerStreamHandler( StreamHandlerInterface $currentStreamHandler, ?StreamHandlerInterface $previousStreamHandler ): RegistrationInterface { return new Registration( streamHandler: new MyStreamHandler($currentStreamHandler), // Usually, the $previousStreamHandler passed to this method will not be of use, // instead the "current" stream handler should be used as that will become the previous one. previousStreamHandler: $currentStreamHandler ); } } $codeShift = new CodeShift(); $codeShift->install(); StreamWrapperManager::registerStreamHandler(new MyRegistrant()); touch('my_file.txt'); unlink('my_file.txt'); // Will print "Unlinking path: my_file.txt".
Limitations
Functionality is extremely limited at the moment, you may well be better off using one of the alternatives listed in See also below.
- Does not yet support
eval(...)
. FunctionHookShiftType
does not yet support variable function calls.FunctionHookShiftType
does not yet supportcall_user_func(...)
and friends, nor any other functions accepting callable parameters that may refer to functions.
See also
- dg/bypass-finals, which uses the same technique as this library.
- PHP PreProcessor
- Patchwork