PHP 8.0 will be released at the end of this year. Is it possible to introduce it immediately into our projects? Or would we be unable to do it because, for instance, it uses a framework or CMS with legacy code?
This concern affects every PHP-based project — whether based on Laravel, Symfony, Drupal, vanilla PHP, or whatnot — but it is particularly pressing for WordPress, and its community is currently attempting to find a solution.
In its upcoming new release this December, WordPress should upgrade its minimum required PHP version from 5.6 to 7.1. However, it’s been decided to temporarily cancel the PHP version bump, because almost 24 percent of installations still run on either PHP 5.6 or 7.0:
Under these circumstances, it has been proposed to start having a fixed schedule for upgrading the minimum version, offering a compromise between upgrading to new PHP versions while providing security patches for older versions:
Whether this fixed schedule is approved or not, the situation looks dire for developers who want to use the latest improvements to PHP. Themes and plugins are not bounded by the PHP requirements by WordPress, so they could already require version 7.1 or higher. However, doing so limits their potential reach.
For instance, only 10.7 percent of installations currently run on PHP 7.4, and we can expect even less will immediately run on PHP 8.0 after being released. These numbers make it very difficult to introduce typed properties or union types into the codebase, among other valuable features.
This comment by a developer conveys some sense of despair:
So effectively this means that we cannot use PHP 8 syntax in themes/plugins if we want to support all WordPress versions until December 2023, three years after it has been released. This is very disappointing.
Is there anything that can be done to improve the situation today? Or do we have to wait three years to be able to use PHP 8 code for our WordPress themes and plugins? (By which time it will have reached its end of life!)
A transpiler is “a type of translator that takes the source code of a program written in a programming language as its input and produces an equivalent source code in the same or a different programming language.”
An exemplary model for transpiling is Babel, the toolchain that allows us to convert ECMAScript 2015+ code into a backwards-compatible version of JavaScript. Thanks to Babel, developers can use new JavaScript language features to convert their source code into versions of JavaScript that can be executed on older browsers.
For instance, Babel converts an ES2015 arrow function to its ES5 equivalent:
// Babel Input: ES2015 arrow function [1, 2, 3].map((n) => n + 1); // Babel Output: ES5 equivalent [1, 2, 3].map(function(n) { return n + 1; });
Following the lead of ES2015, PHP 7.4 has also introduced arrow functions as syntactic sugar over anonymous functions, which have been supported since PHP 5.3:
// PHP 7.4: arrow function $nums = array_map(fn($n) => $n + 1, [1, 2, 3]); // PHP 5.3: anonymous function $nums = array_map( function ($n) { return $n + 1; }, [1, 2, 3] );
With a transpiling tool for PHP, we could write PHP 7.4 arrow functions and convert them into the equivalent anonymous functions, which can run on any version of PHP starting from 5.3.
This would enable developers to use features from PHP 7.4 for their WordPress themes and plugins while still allowing users running older versions (such as PHP 7.1) to also install their software.
Another benefit of transpiling is to have access to the newer versions of libraries used for development.
That is the case with PHPUnit, the framework for testing. As it stands today with PHP 5.6, WordPress cannot go above PHPUnit’s version 7.x, with the consequence that test suites cannot be tested against PHP 8.
Coding with either PHP 7.3+ (or PHP 7.1+), and then transpiling the code for production, would enable us to upgrade to PHPUnit’s versions 9.x (or 8.x) and modernize the test suites.
Features introduced in a new PHP version can be roughly categorized as follows:
The arrow function introduced in PHP 7.4, demonstrated above, is an example of a new syntax for an already-existing feature. Converting the syntax from the new to the old version will execute the same functionality; hence, these features can be transpiled, and the resulting code will have no shortcomings.
Let’s analyze the other cases.
Typed properties (introduced in PHP 7.4) and union types (introduced in PHP 8.0) introduce new syntax for brand-new features:
class User { // Typed properties private int $id; private string $name; private bool $isAdmin; // Union types (in params and return declaration) public function getID(string|int $domain): string|int { if ($this->isAdmin) { return $domain . $this->name; } return $domain . $this->id; } }
These features cannot be directly reproduced in previous PHP versions. The closest we can come to them in the transpiled code is to remove them completely and use docblock tags to describe their nature:
class User { /** @var int */ private $id; /** @var string */ private $name; /** @var bool */ private $isAdmin; /** * @param string|int $domain * @return string|int */ public function getID($domain) { if ($this->isAdmin) { return $domain . $this->name; } return $domain . $this->id; } }
For code containing these two features, its transpiled code will compile in PHP 7.3 and below, but the new features will be absent.
However, more likely than not, their absence won’t matter: these features are mostly useful during development to validate the correctness of our code (aided by additional tools, such as PHPUnit for testing and PHPStan for static analysis). If our code has errors and it fails in production, it would fail with or without these new features; at most, the error message will be different.
Thus, the imperfect transformation of the code is still enough to satisfy our needs, and this code can be transpiled for production.
New features that have no equivalent in previous versions and are needed on runtime (in production) cannot be removed or else the application will behave differently.
An example is the WeakReference
class introduced in PHP 7.4, which enables destroying an object for which we still hold a reference:
$obj = new stdClass; $weakref = WeakReference::create($obj); var_dump($weakref->get()); unset($obj); var_dump($weakref->get());
This will print:
object(stdClass)#1 (0) { } NULL
Using PHP 7.3, the object would not be destroyed unless all references to it are removed:
$obj = new stdClass; $array = [$obj]; var_dump($array); unset($obj); var_dump($array);
This will print:
array(1) { [0]=> object(stdClass)#412 (0) { } } array(1) { [0]=> object(stdClass)#412 (0) { } }
Hence, we need to find out if the new behavior is acceptable or not. For instance, an application running transpiled WeakReference
classes may consume more memory, and that may be acceptable, but if our logic needs to assert that an object is null
after unsetting it, then it will fail.
Finally, there is the case for newly implemented functionality: functions, classes, interfaces, constants, and exceptions.
There is no need to transpile them; a much simpler solution is to backport them, i.e., provide their same implementation for lower PHP versions.
For instance, function str_contains
introduced in PHP 8.0 can be implemented like this:
if (!defined('PHP_VERSION_ID') || (defined('PHP_VERSION_ID') && PHP_VERSION_ID < 80000)) { if (!function_exists('str_contains')) { /** * Checks if a string contains another * * @param string $haystack The string to search in * @param string $needle The string to search * @return boolean Returns TRUE if the needle was found in haystack, FALSE otherwise. */ function str_contains(string $haystack, string $needle): bool { return strpos($haystack, $needle) !== false; } } }
Conveniently, we don’t even need to implement the backporting code since these are already available as polyfill libraries by Symfony:
It’s time to switch from theory to practice and start transpiling our PHP code.
Rector is a reconstructor tool, which does instant upgrades and refactoring of code. It is based on the popular PHP Parser library.
Rector executes this sequence of operations:
From this sequence, we will only be concerned with the second step: supplying Rector with transformation rules.
A rule has as its objective the transformation of a node from the AST, from A
to B
. To describe this operation, we use the diff format applied on the end result: deletions (belonging to state A
) are shown in red, and additions (belonging to state B
) are shown in green.
For instance, this is the diff for rule Downgrade Null Coalescing Operator, which replaces the ??=
operator introduced in PHP 7.4:
function run(array $options)
{
- $options['limit'] ??= 10;
+ $options['limit'] = $array['limit'] ?? 10;
// do something
// ...
}
Rector has almost 600 currently available rules that can be applied. However, most of them are for modernizing code (e.g., from PHP 7.1 to PHP 7.4), which is the opposite of our goal.
The rules we can use are those under the “downgrade” sets:
Each of the rules in these sets converts the code from the mentioned version into the equivalent code from the version right before it. Then, everything under DowngradePhp80
converts code from PHP 8.0 to 7.4.
Adding them up, there are currently 16 of these rules, which to some degree enable us to convert code from PHP 8.0 down to PHP 7.0.
The remaining transformations we’ll need to unlock access to all new features between PHP 8.0 and PHP 7.0 have already been documented. Everyone is welcome to contribute to the open-source project and implement any of these rules.
After installing Rector, we must create the file rector.php
(by default at the root of the project) defining the sets of rules to execute, and we run it by executing the following in the command line:
vendor/bin/rector process src
Please notice that the source code — in this case, located under src/
— will be overridden with the transformation, so downgrading code must be integrated with continuous integration to produce a new asset (for instance, during deployment).
To preview the transformations without applying them, run the command with --dry-run
:
vendor/bin/rector process src --dry-run
Let’s see how to configure rector.php
. To downgrade code from PHP 7.4 to 7.1, we must execute sets downgrade-php74
and downgrade-php72
(currently there is no set implemented for PHP 7.3):
<?php declare(strict_types=1); use Rector\Core\Configuration\Option; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Rector\Set\ValueObject\SetList; return static function (ContainerConfigurator $containerConfigurator): void { // get parameters $parameters = $containerConfigurator->parameters(); // paths to refactor; solid alternative to CLI arguments $parameters->set(Option::PATHS, [ __DIR__ . '/src', ]); // here we can define, what sets of rules will be applied $parameters->set(Option::SETS, [ SetList::DOWNGRADE_PHP74, SetList::DOWNGRADE_PHP72, ]); // is your PHP version different from the one your refactor to? [default: your PHP version] $parameters->set(Option::PHP_VERSION_FEATURES, '7.1'); };
Running the command with --dry-run
shows the results in diff format (deletions in red, additions in green):
The end result is code that was written using features from PHP 7.4, but was transformed to code that can be deployed to PHP 7.1.
How do we make a compromise between developers’ desire to access the latest tools and language features and to improve the quality of their code with the need to target a broad user base by making software that can be installed in as many environments as possible?
Transpiling is a solution. It is not a novel concept: if we make websites, we are most likely already using Babel to transpile JavaScript code even if we are unaware of it, as it may be integrated into some framework.
What we possibly didn’t realize is that there is a tool to transpile PHP code called Rector. With this tool, we can write code containing PHP 8.0 features and deploy it to an environment running a lower version of PHP, all the way down to PHP 7.0. That is wonderful.
Happy transpiling!
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 nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
4 Replies to "Transpiling PHP code from 8.0 to 7.x via Rector"
Awesome, thanks for the post! And you’re totally right, I’m a WordPress plug-in and theme developer and I’ve been wanting to use php 7 since day one. The only thing that worries me is testing. How will we know the production code works as expected? Will we have to write two sets of tests for each codebase?
My idea is to run some PHP tool to analyze the downgraded code, using PHP 7.1. If the transpiling failed and PHP 7.2 was left behind, then this process will fail.
I describe this strategy here: https://graphql-api.com/blog/graphql-api-for-wp-is-now-scoped-thanks-to-php-scoper/#heading-testing (his blog post is about scoping the code, but either scoping or downgrading, the idea is the same).
As for what library to use, you can still use Rector, but since it requires PHP 7.3+, you must use its downgraded to PHP 7.1 version from here: https://github.com/rectorphp/rector-php71
It’s me again! What’s your experience with named arguments? Have you been able to compile named arguments to positional arguments? The DowngradeNamedArgumentRector seems to only downgrade arguments from class methods. And only form the classes defined in the source code. I’m guessing it’s not possible to downgrade named arguments when using bult in functions or third party APIs like WordPress, right?
So my understanding is that the classes and functions need to be loaded in memory at the time Rector is running. Any ideas on this? It seems an extremely hard thing to do considering WordPress mixes initialization logic with function definitions. Which seems like a shame, because if there’s an API we could benefit from using named arguments it’s the main WordPress Core.