I have been a proponent of downgrading PHP code for some time now, since it allows us to use the latest version of PHP and deploy it to environments that, for one or another reason, must still run a legacy PHP version. Downgrading in PHP has been made feasible by Rector, a tool to reconstruct PHP code based on rules.
In my particular case, since I started transpiling my plugin for WordPress, I could start using typed properties, union types, constructor property promotion, arrow functions, and many other modern features, and yet produce the release in PHP 7.1. As a result, my development experience has been greatly improved, yet the plugin is available to 85 percent of WordPress sites (those running PHP 7.1 and above), which is a drastic increase over the 2 percent of WordPress sites running on PHP 8.0.
Hence, I was mildly excited when, several weeks ago, I came across a Reddit post that introduced a new tool specifically dedicated to transpiling PHP code: Phabel. (Its name, as you might have guessed it, comes from mixing “PHP” and “Babel”, the popular transpiler for JS code.)
I felt compelled to try out this new tool and see how it compares against Rector. My intention was not to replace Rector, as I’ve so far been extremely delighted by it, but to compare both approaches, analyze if either tool is superior to the other, and determine the use cases for which each is best.
This article is the result of my exploration.
Phabel is a tool dedicated to downgrading PHP code. In contrast, Rector is a programmable tool for converting PHP code from any state into any other state.
In addition to downgrading, Rector offers rules for modernizing PHP code (such as transforming from PHP 7.1 to 8.1), improving code quality, renaming functions, and several others.
Hence, Phabel could be a replacement for Rector for the specific use case of downgrading PHP code, and nothing else.
Rector and Phabel are very different in how they are executed. Rector relies on Composer to generate a standalone executable under vendor/bin/rector
. Then, we can invoke it whenever we have access to a console or scripting environment, such as on our laptops while doing development, hosting servers when pushing code, webservers when deploying code, and so on.
In order to downgrade the PHP code, a straightforward approach is to invoke Rector in the CI process, upon some desired event. In this example using GitHub Actions, a release asset with the downgraded PHP code is generated when tagging the repo:
name: Generate Installable Plugin and Upload as Release Asset on: release: types: [published] jobs: build: name: Build, Downgrade and Upload Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Downgrade code for production (to PHP 7.1) run: | composer install vendor/bin/rector process sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php - name: Build project for production run: | composer install --no-dev --optimize-autoloader mkdir build - name: Create artifact uses: montudor/[email protected] with: args: zip -X -r build/graphql-api.zip . -x *.git* node_modules/\* .* "*/\.*" CODE_OF_CONDUCT.md CONTRIBUTING.md ISSUE_TEMPLATE.md PULL_REQUEST_TEMPLATE.md rector.php *.dist composer.* dev-helpers** build** - name: Upload artifact uses: actions/upload-artifact@v2 with: name: graphql-api path: build/graphql-api.zip - name: Upload to release uses: JasonEtco/upload-to-release@master with: args: build/graphql-api.zip application/zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
The GitHub Action workflow can also make the downgraded PHP code available by pushing it to an additional “DIST” repo:
- name: Uncompress artifact uses: montudor/[email protected] with: args: unzip -qq build/graphql-api.zip -d build/dist-plugin - id: previous_tag uses: "WyriHaximus/github-action-get-previous-tag@master" - name: Publish to DIST repo uses: symplify/[email protected] env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} with: tag: ${{ steps.previous_tag.outputs.tag }} package-directory: 'build/dist-plugin' split-repository-organization: GraphQLAPI split-repository-name: graphql-api-for-wp-dist
Phabel uses a different strategy, composed of two elements:
vendor/bin/phabel
to tag the repo to be downgradedIn the first stage, whenever tagging the code for the library, we must also execute vendor/bin/phabel publish
to create and push two extra tags: tag.9999
and tag.9998
(for instance, after tagging the repo with 1.0.0
, it will also produce 1.0.0.9999
and 1.0.0.9998
).
tag.9999
is simply a duplicate of tag
, hence it tells Composer it needs the same version of PHP used for development, most likely PHP 8.0.
tag.9998
, instead, replaces the required PHP version in composer.json
with *
, thus telling Composer it can handle any PHP version. It moves all the dependencies from require
to extra
to avoid unsuccessful version constraints from taking effect, allowing Phabel to install and downgrade the dependencies, too.
In the second stage, the users must install the library in their projects via Composer. The library is required as usual, for example, with version constraint ^1.0
. Then, when doing composer install
or composer update
, based on the environment’s PHP version, Composer will decide which version to use. If running PHP 8.0 it will use 1.0.0.9999
; if running PHP 7.3, it will fallback to 1.0.0.9998
.
Finally, if the dependency is resolved via tag 1.0.0.9998
, then Phabel’s Composer script will be automatically triggered right after the dependency is installed, and downgrade its PHP code.
An important implication arises from the two different approaches for downgrading code: while Rector will most likely be executed on the server, Phabel will run on the client!
At first, this may appear to be an advantage for Phabel, because it is simpler:
vendor/
folder (containing all dependencies from the library, which must also be downgraded) to a DIST repoHowever, there are several drawbacks to this approach, which in my opinion make it less appealing than using Rector.
For one, in Rector, the maintainer of the library can fully control the downgrading experience, making sure everything goes well before releasing the downgraded library. In contrast, since Phabel’s downgrading process runs in the client, if the client encounters issues, then Phabel may fail.
Indeed, this happened to me: downgrading my code from PHP 8.0 to 7.1 would take several minutes and, time and again, the Composer script would get terminated before the code would be fully downgraded:
I tried to overcome the issue; I thought it would be related to Composer’s process timeout, which is by default set to 300 seconds, so I increased the timeout in composer.json
:
{ "config": { "process-timeout": 600 } }
But it didn’t work. I spent a couple of hours trying to fix it to no avail. In the end, instead of PHP 7.1, I decided to downgrade my code to PHP 7.3, which needs to execute fewer rules and so would complete before the timeout was due. This is not a practical solution; it was good enough for my exploration of the tool, but not good enough if I needed to use it for production.
For another, when using Phabel, the same library will be downgraded time and again, further consuming processing power. The waiting time is also transferred from the maintainer of the library to every one of the users of the library, which is not optimal.
To put this in perspective, a popular library such as Symfony DependencyInjection has over 5000 projects depending on it. That means that 5000 projects will need to execute the process to downgrade the library; 5000 users will need to wait for the process to execute, and the consumption of energy will be 5000 times bigger than downgrading the library at the origin.
Now, this issue can be solved in Phabel. Indeed, the Phabel library is itself being downgraded using Phabel, and the downgraded versions are all published in the Phabel repo under different branches. But the simplicity of Phabel is all but lost, so it would no longer hold any advantage over using Rector.
Downgrading PHP code is an all-or-nothing proposition: it either works, or it doesn’t. We cannot downgrade just 99% of the code, because the remaining 1% is enough to make the application fail.
Phabel has one project using it: MadelineProto, created by the same author as Phabel’s, so we can be confident that Phabel is good enough to downgrade the PHP features that MadelineProto uses. If your PHP project does not use any additional PHP features, then Phabel may be good to go.
In my case, though, after downgrading my plugin using Phabel, running the application would throw an error:
PHP Fatal error: Class Symfony\\Component\\DependencyInjection\\Exception\\ExceptionInterface cannot implement previously implemented interface Throwable in /app/vendor/phabel.transpiler73:symfony/dependency-injection/Exception/ExceptionInterface.php on line 20
The failing code was this one (happening because ContainerExceptionInterface
already extends from Throwable
):
namespace Symfony\Component\DependencyInjection\Exception; use Psr\Container\ContainerExceptionInterface; interface ExceptionInterface extends ContainerExceptionInterface, \Throwable { }
After I fixed this piece of code, the error went away, and the application ran.
We can safely conclude that it is important for the library to be extensible, so we can provide the missing downgrading functionality. If the functionality is generic (as in the case above), we can attempt to code it and contribute to the repo (after all, open source is made by everyone).
But if the logic is specific to our application, then we should be able to extend Phabel using our own code.
A clear example is when using PHP 8.0 attributes, which must be downgraded to an equivalent functionality, possibly based on annotations. Attributes may be used for some generic goal, such as [#Deprecated]
(to be downgraded as @deprecated
), or may support a custom functionality from the application, for which the downgrade will also be specific to the application.
As of this writing, though, Phabel does not support downgrading attributes and, more importantly, it does not support custom downgrades. As a consequence, if your application uses attributes, or has some application-specific piece of code that needs be downgraded, then you can’t use Phabel.
Rector handles extensibility much better. It already provides a rule to downgrade attributes, which can be configured to handle the low-hanging fruits (such as [#Deprecated]
). If this rule were not sufficient, Rector’s rule-based architecture means we can create and execute our own rules.
In addition, because Rector is usually executed in the CI process, we can also execute downgrade logic in it. In the GitHub Actions workflow demonstrated above, there’s this piece of code:
run: | composer install vendor/bin/rector process sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php
That sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php
is changing the PHP requirement for my WordPress plugin, from PHP 7.4. to 7.1. I could create a Rector rule for this, but there’s really no need, since this solution is much simpler.
After executing the downgrade (and manually fixing the issues), the application would run. However, the downgrade logic unfortunately changed the behavior of the application in a way that would make it work improperly.
My plugin is a GraphQL server for WordPress. When executing a simple GraphQL query that should return a response, I got a validation error instead:
Debugging the downgraded code, I found the following code to be causing the problem:
class IntScalarTypeResolver extends AbstractScalarTypeResolver { public function coerceValue($inputValue) { if (!$inputValue instanceof stdClass) { if (!\is_bool($inputValue)) { if (!(\is_bool($inputValue) || \is_numeric($inputValue) || \is_string($inputValue))) { if (!\is_float($inputValue)) { if (!(\is_bool($inputValue) || \is_numeric($inputValue))) { if (!\is_int($inputValue)) { if (!(\is_bool($inputValue) || \is_numeric($inputValue))) { if (!\is_string($inputValue)) { if (!(\is_string($inputValue) || \is_object($inputValue) && \method_exists($inputValue, '__toString') || (\is_bool($inputValue) || \is_numeric($inputValue)))) { throw new \TypeError(__METHOD__ . '(): Argument #1 ($inputValue) must be of type stdClass|string|int|float|bool, ' . \Phabel\Plugin\TypeHintReplacer::getDebugType($inputValue) . ' given, called in ' . \Phabel\Plugin\TypeHintReplacer::trace()); } else { $inputValue = (string) $inputValue; } } } else { $inputValue = (int) $inputValue; } } } else { $inputValue = (double) $inputValue; } } } else { $inputValue = (bool) $inputValue; } } } // ... } }
What are all those type validations? They were added by Phabel to downgrade the union type in the original function argument to coerceValue
:
function coerceValue(string|int|float|bool|stdClass $inputValue) { // ... }
Whether this logic is buggy or not, I do not know — I didn’t debug deep enough to see where the conflict happens — but, as it stands now, this logic is also unexpectedly casting the type of the variable from int
to string
, which then makes the validation of the GraphQL query fail, since it expects an Int
and it receives a String
.
After manually commenting all those extra lines, in that function and many similar ones throughout the downgraded code, then the application would work well:
Once again, I could perform this editing, manually, because I’m testing the tool. But if I had to use it for production, it would not be practical at all.
Phabel suffers from the issue above due to good intentions: it wants to recreate the same type validation behavior from PHP 8.0 into PHP 7.x. Unfortunately, something along the way did not come out right (hopefully it can be fixed).
Rector does not suffer from this issue because it does not bother to recreate the type validation. This is how Rector downgrades the same piece of code:
/** * @param string|int|float|bool|stdClass $inputValue */ function coerceValue($inputValue) { // ... }
The reason why Rector does not bother to recreate the same functionality is that it doesn’t need to. The downgrade is not expected to be perfect; it only needs to be good enough.
In this particular case, union types can help us prevent bugs in the application during development time, i.e., when we are using PHP 8.0. For production, we can expect bugs not to be there anymore. If they are there, an error will happen in the application nevertheless, whether we recreate the type validation or not; at most, the error message will be different (the original RuntimeException
vs. Phabel’s TypeError
).
As a consequence, Rector is not changing the behavior of the application, at least concerning type validation. Concerning my plugin, it has so far been reliable, and I’m still quite delighted with this tool.
(To be sure: new releases of Rector have introduced unannounced breaking changes every now and then. To counter eventualities and avoid surprises, I started committing my composer.lock
into the repo and using only battle-tested versions of Rector in production. Since doing so I haven’t encountered any issues; no bug in my application has so far made it into production.)
As they stand now, Rector is clearly more reliable than Phabel. This is in part due to its community, which includes dozens of contributors, and a response time to new issues that they can boast about (bugs are normally fixed within days, if not hours).
Phabel still has some way to go before it can realistically become a competitor to Rector. However, the potential is there: even though this review may appear to a large extent negative, I want to stress that I am thoroughly impressed by it. The library is well designed and properly executed, and the code in its repo is very legible (on the down side, there’s not much documentation). And in spite of the drawbacks I mentioned earlier on, I believe that its integration with Composer is a great accomplishment.
Phabel is also promising because it actually does work! In the case where my GraphQL server was failing, and then after some manual fixes it started working, that working code was running PHP 7.3, being a downgrade from the original code in PHP 8.0. Success seems to be within reach!
Notwithstanding Phabel’s current issues, if work on it continues, it can perfectly become a great library. I’ll be watching out for its progress.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]