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://.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 servicesThe
TransformerManager
service stores the list of transformers and their configurationThe
CacheStateManager
service manages the cache stateThe
StreamFilter
service registers a PHP Stream Filter which allows to modify the source code before it is loaded by PHPThe
AutoloadInterceptor
service overloads the Composer autoloader, which handles the loading of classes
The
AutoloadInterceptor
service intercepts the loading of a classThe
TransformerMatcher
matches the class name with the list of transformer target classesIf 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.