PHP Code Transformer is a PHP library that allows you to modify and transform the source code of a loaded PHP class.
composer require okapi/code-transformer
- Create a Kernel
- Create a Transformer
- Target Class
- Initialize the Kernel
- Target Class (transformed)
- Result
- Limitations
- How it works
- Testing
<?php
use Okapi\CodeTransformer\CodeTransformerKernel;
// Extend from the "CodeTransformerKernel" class
class Kernel extends CodeTransformerKernel
{
// Define a list of transformer classes
protected array $transformers = [
StringTransformer::class,
UnPrivateTransformer::class,
];
// Define the settings of the kernel from the "protected" properties
// The directory where the transformed source code will be stored
protected ?string $cacheDir = __DIR__ . '/var/cache';
// The cache file mode
protected ?int $cacheFileMode = 0777;
}
// String Transformer
<?php
use Okapi\CodeTransformer\Transformer;
use Okapi\CodeTransformer\Transformer\Code;
// Extend from the "Transformer" class
class StringTransformer extends Transformer
{
// Define the target class(es)
public function getTargetClass(): string|array
{
// You can specify a single class or an array of classes
// You can also use wildcards, see https://github.com/okapi-web/php-wildcards
return MyTargetClass::class;
}
// The "transform" method will be called when the target class is loaded
// Here you can modify the source code of the target class(es)
public function transform(Code $code): void
{
// I recommend using the Microsoft\PhpParser library to parse the source
// code. It's already included in the dependencies of this package and
// the "$code->getSourceFileNode()" property contains the parsed source code.
// But you can also use any other library or manually parse the source
// code with basic PHP string functions and "$code->getOriginalSource()"
$sourceFileNode = $code->getSourceFileNode();
// Iterate over all nodes
foreach ($sourceFileNode->getDescendantNodes() as $node) {
// Find 'Hello World!' string
if ($node instanceof StringLiteral
&& $node->getStringContentsText() === 'Hello World!'
) {
// Replace it with 'Hello from Code Transformer!'
// Edit method accepts a Token or Node class
$code->edit(
$node->children,
"'Hello from Code Transformer!'",
);
// You can also manually edit the source code
$code->editAt(
$node->getStartPosition() + 1,
$node->getWidth() - 2,
"Hello from Code Transformer!",
);
// Append a new line of code
$code->append('$iAmAppended = true;');
}
}
}
}
// UnPrivate Transformer
<?php
namespace Okapi\CodeTransformer\Tests\Stubs\Transformer;
use Microsoft\PhpParser\TokenKind;
use Okapi\CodeTransformer\Transformer;
use Okapi\CodeTransformer\Transformer\Code;
// Replace all "private" keywords with "public"
class UnPrivateTransformer extends Transformer
{
public function getTargetClass(): string|array
{
return MyTargetClass::class;
}
public function transform(Code $code): void
{
$sourceFileNode = $code->getSourceFileNode();
// Iterate over all tokens
foreach ($sourceFileNode->getDescendantTokens() as $token) {
// Find "private" keyword
if ($token->kind === TokenKind::PrivateKeyword) {
// Replace it with "public"
$code->edit($token, 'public');
}
}
}
}
<?php
class MyTargetClass
{
private string $myPrivateProperty = "You can't get me!";
private function myPrivateMethod(): void
{
echo 'Hello World!';
}
}
// Initialize the kernel early in the application lifecycle
// Preferably after the autoloader is registered
<?php
use MyKernel;
require_once __DIR__ . '/vendor/autoload.php';
// Initialize the Code Transformer Kernel
$kernel = MyKernel::init();
<?php
class MyTargetClass
{
public string $myPrivateProperty = "You can't get me!";
public function myPrivateMethod(): void
{
echo 'Hello from Code Transformer!';
}
}
$iAmAppended = true;
<?php
// Just use your classes as usual
$myTargetClass = new MyTargetClass();
$myTargetClass->myPrivateProperty; // You can't get me!
$myTargetClass->myPrivateMethod(); // Hello from Code Transformer!
- Normally xdebug will point to the original source code, not the transformed one. The problem with this is if you add or remove a line of code, xdebug will point to the wrong line, so try to keep the number of lines the same as the original source code.
-
The
CodeTransformerKernel
registers multiple services-
The
TransformerManager
service stores the list of transformers and their configuration -
The
CacheStateManager
service manages the cache state -
The
StreamFilter
service registers a PHP Stream Filter which allows to modify the source code before it is loaded by PHP -
The
AutoloadInterceptor
service overloads the Composer autoloader, which handles the loading of classes
-
-
The
AutoloadInterceptor
service intercepts the loading of a class -
The
TransformerMatcher
matches the class name with the list of transformer target classes -
If the class is matched, query the cache state to see if the transformed source code is already cached
-
Check if the cache is valid:
- Modification time of the caching process is less than the modification time of the source file or the transformers
- Check if the cache file, the source file and the transformers exist
- Check if the number of transformers is the same as the number of transformers in the cache
-
If the cache is valid, load the transformed source code from the cache
-
If not, return a stream filter path to the
AutoloadInterceptor
service
-
-
The
StreamFilter
modifies the source code by applying the matching transformers- If the modified source code is different from the original source code, cache the transformed source code
- If not, cache it anyway, but without a cached source file path, so that the transformation process is not repeated
- Run
composer run-script test
or - Run
composer run-script test-coverage
Give a β if this project helped you!
- Big thanks to lisachenko for their pioneering work on the Go! Aspect-Oriented Framework for PHP. This project drew inspiration from their innovative approach and served as a foundation for this project.
Copyright Β© 2023 Valentin Wotschel.
This project is MIT licensed.