July 1, 2021
Including both PHP 7.1 and 8.0 code in the same plugin … or not?

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.

Problems with transpiling code

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:

1. Extensions need to code the method signatures with PHP 7.1 even if they require PHP 8.0

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.)

2. Documentation must be provided using PHP 7.1

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.

3. Debugging information uses the transpiled code, not the source code

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.

First proposed solution: Producing two versions of the plugin

The simplest solution to consider is to generate not one, but two releases:

One with the transpiled PHP 7.1 code
One with the original PHP 8.0 code

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.

Issues with producing two versions of the plugin

This approach has several issues that, I believe, render it impractical.

WordPress accepts only one release per plugin

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.

It doesn’t solve the documentation problem

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.

Nothing prevents the plugin from being installed twice

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.

Second proposed solution: Including both PHP 7.1 and 8.0 code in the same plugin

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.1

vendor-php71: code transpiled to PHP 7.1

src: original code in PHP 8.0

vendor: original code in PHP 8.0

The 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.

1. Extensions can use method signatures from PHP 7.1

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.

2. Documentation can be provided using PHP 8.0

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.

3. Debugging information uses the original code, whenever possible

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.

Why only two PHP versions? Targeting the whole range is now possible.

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.1

vendor-php71: code transpiled to PHP 7.1

src-php72: code transpiled to PHP 7.2

vendor-php72: code transpiled to PHP 7.2

src-php73: code transpiled to PHP 7.3

vendor-php73: code transpiled to PHP 7.3

src-php74: code transpiled to PHP 7.4

vendor-php74: code transpiled to PHP 7.4

src: original code in PHP 8.0

vendor: original code in PHP 8.0

And 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’;
}

Issues with including both PHP 7.1 and 8.0 code in the same plugin

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.

Public extensions would also need to include both PHP 8.0 and 7.1 code

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.

What’s the solution, then?

Let’s review the status so far:

Transpiling code from PHP 8.0 to 7.1 has a few issues:

Extensions need to code the method signatures with PHP 7.1 even if they require PHP 8.0
Documentation must be provided using PHP 7.1
Debugging information uses the transpiled code, not the source code

The first proposed solution, producing two versions of the plugin, does not work well because:

WordPress accepts only release per plugin
It doesn’t solve the documentation problem
Nothing prevents the plugin from being installed twice

The second proposed solution, including both PHP 7.1 and 8.0 code in the same plugin, may or may not work:

If the plugin can be extended by third parties, these extensions will also need be transpiled. This will likely increase the barrier of entry, making it not worth it
Otherwise, it should work alright

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.)

Wrapping up

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.

The post Including both PHP 7.1 and 8.0 code in the same plugin … or not? appeared first on LogRocket Blog.

Leave a Reply

Your email address will not be published. Required fields are marked *

Send