I have lately been writing a lot about transpiling PHP code (here, here, and here), describing how we can use the latest PHP code for development but release our package/plugin/application for a legacy version, converting our code from anything in between PHP 8.0 and 7.1.
I have myself transpiled my WordPress plugin from PHP 8.0 to 7.1. I’m very pleased with the results since my codebase has improved its quality: I can now use typed properties and union types, something I could not otherwise afford for a public WordPress plugin.
However, I am still not 100 percent happy with it. While solving the original challenge (to be able to use PHP 8.0 when coding for WordPress), transpiling code has created some new problems along the way.
By coding my plugin in PHP 8.0 and then releasing it in PHP 7.1, I’ve come to experience the following three issues:
My plugin, a GraphQL server for WordPress, allows developers to extend the GraphQL schema with their own types by creating an object implementing TypeResolverInterface
. Among others, this interface has function getID
, with this signature:
interface TypeResolverInterface { public function getID(object $resultItem): string|int; }
As we can see, this function uses union types from PHP 8.0 to specify the return type, and the object
param type from PHP 7.2.
When transpiled to PHP 7.1, this method signature is downgraded to this code:
interface TypeResolverInterface { /** * @param $resultItem object * @return string|int */ public function getID($resultItem); }
This method signature is the one released in the plugin.
So what happens when developers want to create an extension for my plugin and deploy it on an application that runs on PHP 8.0? Well, they still need to use PHP 7.1 code for the method signature, i.e., removing the object
param type and string|int
return type; otherwise, PHP will throw an error.
Fortunately, this situation is limited to method signatures. For instance, extensions can still use union types to declare the properties on their classes:
class IcecreamTypeResolver implements IcecreamTypeResolverInterface { // PHP 8.0 code here is allowed private string|int $id = 'vanilla'; /** * PHP 7.1 code in method signature... * * @param $resultItem object * @return string|int */ public function getID($resultItem) { return $this->id; } }
Yet, it is still annoying to have to use PHP 7.1 code when our application requires PHP 8.0. As a plugin provider, forcing my users into this situation feels a bit sad.
(To be clear, I am not creating the situation; the same happens when overriding method signatures for any WordPress plugin that supports PHP 7.1. But it feels different in this case only because I’m starting with PHP 8.0 with the goal of providing a better alternative to my users.)
Because the plugin is released on PHP 7.1, the documentation on extending it must also use PHP 7.1 for the method signatures even though the original source code is on PHP 8.0.
In addition, the documentation cannot point to the repo with the source code on PHP 8.0 or we’d risk visitors copy/pasting a piece of code that will produce PHP errors.
Finally, we developers are normally proud of using the latest version of PHP. But the documentation for the plugin cannot reflect that since it is still based on PHP 7.1.
We could get around these issues by explaining the transpilation process to our visitors, encouraging them to also code their extensions with PHP 8.0 and then transpile it to PHP 7.1. But doing so will increase the cognitive complexity, lowering the chances of their being able to use our software.
Let’s say that the plugin throws an exception, printing this information on some debug.log
file, and we use the stack trace to locate the problem on the source code.
Well, the line where the error happens, shown in the stack trace, will point to the transpiled code, and the line number will most likely will be different in the source code. Hence, there’s a bit of additional work to do in order to convert back from transpiled to original code.
The simplest solution to consider is to generate not one, but two releases:
This is easy to implement since the new release with PHP 8.0 will simply contain the original source code, without any modification.
Having the second plugin using PHP 8.0 code, any developer running a site on PHP 8.0 can use this plugin instead.
This approach has several issues that, I believe, render it impractical.
For a WordPress plugin like mine, we can’t upload both releases to the WordPress.org directory. Thus, we’d have to choose between them, meaning that we’ll end up having the “official” plugin using PHP 7.1 and the “unofficial” one using PHP 8.0.
This complicates matters significantly because while the official plugin can be uploaded to (and downloaded from) the Plugins directory, the unofficial one cannot — unless it is published as a different plugin, which would be a terrible idea. As a result, it would have to be downloaded either from its website or its repo.
In addition, it is recommended to have the official plugin be downloaded only from wordpress.org/plugins so as to not mess with the guidelines:
A stable version of a plugin must be available from its WordPress Plugin Directory page.
The only version of the plugin that WordPress.org distributes is the one in the directory. Though people may develop their code somewhere else, users will be downloading from the directory, not the development environment.
Distributing code via alternate methods, while not keeping the code hosted here up to date, may result in a plugin being removed.
This would effectively mean that our users will need to be aware that there are two different versions of the plugin — one official and one unofficial — and that they are available in two different places.
This situation could become confusing to unsuspecting users, and that’s something I’d rather avoid.
Because the documentation must account for the official plugin, which will contain PHP 7.1 code, then issue “2. Documentation must be provided using PHP 7.1” will still happen.
Transpiling the plugin must be done during our continuous integration process. Since my code is hosted on GitHub, the plugin is generated via GitHub Actions whenever tagging the code and is uploaded as a release asset.
There cannot be two release assets with the same name. Currently, the plugin name is graphql-api.zip
. If I were to also generate and upload the plugin with the PHP 8.0 code, I’d have to call it graphql-api-php80.zip
.
That can lead to a potential problem: anyone is able to download and install the two versions of the plugin in WordPress, and since they have different names, WordPress will effectively install both of them, side by side, under folders graphql-api
and graphql-api-php80
.
If that were to happen, I believe that the installation of the second plugin would fail since having the same method signatures in different PHP versions should produce a PHP error, making WordPress halt the installation. But even then, I wouldn’t want to risk it.
Since the simple solution above is not unblemished, it’s time to iterate.
Instead of releasing the plugin using the transpiled PHP 7.1 code only, include also the source PHP 8.0 code, and decide on runtime, based on the environment, whether to use the code corresponding to one PHP version or the other.
Let’s see how this would work out. My plugin currently ships PHP code in two folders, src
and vendor
, both transpiled to PHP 7.1. With the new approach, it would instead include four folders:
src-php71
: code transpiled to PHP 7.1vendor-php71
: code transpiled to PHP 7.1src
: original code in PHP 8.0vendor
: original code in PHP 8.0The folders must be called src
and vendor
instead of src-php80
and vendor-php80
so that if we have a hardcoded reference to some file under any of those paths, it will still work without any modification.
Loading either the vendor
or vendor-php71
folder would be done like this:
if (PHP_VERSION_ID < 80000) { require_once __DIR__ . '/vendor-php71/autoload.php'; } else { require_once __DIR__ . '/vendor/autoload.php'; }
Loading the src
or src-php71
folder is done through the corresponding autoload_psr4.php
file. The one for PHP 8.0 remains the same:
<?php // autoload_psr4.php @generated by Composer $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( 'GraphQLAPI\\GraphQLAPI\\' => array($baseDir . '/src'), );
But the one transpiled to PHP 7.1, under vendor-php71/composer/autoload_psr4.php
, must change the path to src-php71
:
return array( 'GraphQLAPI\\GraphQLAPI\\' => array($baseDir . '/src-php71'), );
That’s pretty much it. Now, the plugin can ship its code in 2 different PHP versions, and servers running PHP 8.0 can use the PHP 8.0 code.
Let’s see how this approach solves the three issues.
Now the plugin still supports PHP 7.1, but in addition, it supports using native PHP 8.0 code when running PHP 8.0 in the web server. As such, both PHP versions are first-class citizens.
This way, the web server running PHP 8.0 will load the method signatures from the corresponding PHP 8.0 version:
interface TypeResolverInterface { public function getID(object $resultItem): string|int; }
Developers extending the GraphQL schema for their own websites are then able to code their extensions using the PHP 8.0 method signature.
Because PHP 8.0 becomes a first-class citizen, the documentation will demonstrate code using PHP 8.0.
The copy/pasting of source code to documentation can also be done from the original repo. To demonstrate the PHP 7.1 version, we can simply add a link to the corresponding piece of code in the transpiled repo.
If the web server runs PHP 8.0, the stack trace in the debug will rightfully print the line number from the original source code.
If not running PHP 8.0, the issue will still happen, but at least we have improved on it.
If implementing this solution, upgrading the plugin from using PHP 8.0 and 7.1 only to using the whole range of PHP versions in between is very easy.
Why would we want to do this? To improve on solution item “1. Extensions can use method signatures from PHP 7.1” seen above, but enabling developers to use whichever PHP version they are already using for their extensions.
For instance, if running PHP 7.3, the method signature for getID
presented earlier cannot use union types, but it can use the object
param type. So the extension can use this code:
interface TypeResolverInterface { /** * @return string|int */ public function getID(object $resultItem); }
Implementing this upgrade means storing all intermediate downgrade stages within the release, like this:
src-php71
: code transpiled to PHP 7.1vendor-php71
: code transpiled to PHP 7.1src-php72
: code transpiled to PHP 7.2vendor-php72
: code transpiled to PHP 7.2src-php73
: code transpiled to PHP 7.3vendor-php73
: code transpiled to PHP 7.3src-php74
: code transpiled to PHP 7.4vendor-php74
: code transpiled to PHP 7.4src
: original code in PHP 8.0vendor
: original code in PHP 8.0And then, loading one or another version is done like this:
if (PHP_VERSION_ID < 72000) { require_once __DIR__ . '/vendor-php71/autoload.php'; } elseif (PHP_VERSION_ID < 73000) { require_once __DIR__ . '/vendor-php72/autoload.php'; } elseif (PHP_VERSION_ID < 74000) { require_once __DIR__ . '/vendor-php73/autoload.php'; } elseif (PHP_VERSION_ID < 80000) { require_once __DIR__ . '/vendor-php74/autoload.php'; } else { require_once __DIR__ . '/vendor/autoload.php'; }
The most evident problem with this approach is that we will be duplicating the file size of the plugin.
In most situations, though, this will not be a critical concern because these plugins run on the server side, with no effect on the performance of the application whatsoever (such as duplicating the size of a JS or CSS file would do). At most, it will take a bit longer to download the file, and a bit longer to install it in WordPress.
In addition, only PHP code will necessarily be duplicated, but assets (such as CSS/JS files or images) can be kept only under vendor
and src
and removed under vendor-php71
and src-php71
, so the file size of the plugin may be less than double the size.
So no big deal there.
The second problem is more serious: public extensions would also need to be coded with both PHP versions. Depending on the nature of the package/plugin/application, this issue may be a showstopper.
Unfortunately, that’s the case with my plugin, as I explain below.
What happens with those extensions that are publicly available to everyone? What PHP version should they use?
For instance, the GraphQL API plugin allows users to have the GraphQL schema extended to fetch data from any other WordPress plugin. Hence, third-party plugins are able to provide their own extensions (think “WooCommerce for GraphQL API” or “Yoast for GraphQL API”). These extensions could also be uploaded to the WordPress.org Plugin repository for anyone to download and install on their sites.
Now, these extensions will not know in advance what PHP version will be used by the user. And they can’t have the code using one version only (either PHP 7.1 or 8.0) because that will certainly produce PHP errors when the other PHP version is being used. As a consequence, these extensions would also need to include their code in both PHP 7.1 and 8.0.
This is certainly doable from a technical point of view. But otherwise, it is a terrible idea. As much as I love transpiling my code, I can’t force others to do the same. How could I expect an ecosystem to ever flourish around my plugin when imposing such high requirements?
Hence, I decided that, for the GraphQL API, to follow this approach is not worth it.
Let’s review the status so far:
Transpiling code from PHP 8.0 to 7.1 has a few issues:
The first proposed solution, producing two versions of the plugin, does not work well because:
The second proposed solution, including both PHP 7.1 and 8.0 code in the same plugin, may or may not work:
In my case, the GraphQL API is affected by the second proposed solution. Then it’s been a full circle and I’m back where I started — suffering the three problems for which I attempted to find a solution.
Despite this setback, I do not change my positive opinion towards transpiling. Indeed, if I were not transpiling my source code, it’d have to use PHP 7.1 (or possibly PHP 5.6), so I wouldn’t be much better off. (Only the issue about the debugging information not pointing to the source code would be solved.)
I started this article describing the three problems I’ve experienced so far when transpiling my WordPress plugin from PHP 8.0 to 7.1. Then I proposed two solutions, the first of which will not work well.
The second solution will work well, except for packages/plugins/applications that can be extended by third parties. That is the case with my plugin, so I’m back where I started, without a solution to the three problems.
So I’m still not 100 percent happy about transpiling. Only 93 percent.
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.