In ideal circumstances, we should use PHP 8.0 (the latest version as of writing this) for all our sites and update it as soon as a new version is released. However, developers will often need to work with previous PHP versions, such as when creating a public plugin for WordPress or working with legacy code which impedes upgrading the webserver’s environment.

In these situations, we could give up hope of using the latest PHP code. But there is a better alternative: we can still write our source code with PHP 8.0 and transpile it to a previous PHP version — even to PHP 7.1.

In this guide, we’ll teach you everything you need to know about transpiling PHP code.

What Is Transpiling?

Transpiling converts source code from a programming language into an equivalent source code of the same or a different programming language.

Transpiling is not a new concept within web development: client-side developers will quite likely be familiar with Babel, a transpiler for JavaScript code.

Babel converts JavaScript code from the modern ECMAScript 2015+ version into a legacy version compatible with older browsers. For instance, given an ES2015 arrow function:

[2, 4, 6].map((n) => n * 2);

…Babel will convert it into its ES5 version:

[2, 4, 6].map(function(n) {
  return n * 2;
});

What Is Transpiling PHP?

What is potentially new within web development is the possibility of transpiling server-side code, in particular PHP.

Transpiling PHP works the same way as transpiling JavaScript: source code from a modern PHP version is converted into an equivalent code for an older PHP version.

Following the same example as before, an arrow function from PHP 7.4:

$nums = array_map(fn($n) => $n * 2, [2, 4, 6]);

…can be transpiled into its equivalent PHP 7.3 version:

$nums = array_map(
  function ($n) {
    return $n * 2;
  },
  [2, 4, 6]
);

Arrow functions can be transpiled because they are syntactic sugar, i.e. a new syntax to produce an existing behavior. This is the low-hanging fruit.

However, there are also new features that create a new behavior, and as such, there will be no equivalent code for previous versions of PHP. That’s the case with union types, introduced in PHP 8.0:

function someFunction(float|int $param): string|float|int|null
{
  // ...
}

In these situations, transpiling can still be done as long as the new feature is required for development but not for production. Then, we can simply remove the feature altogether from the transpiled code without serious consequences.

One such example is union types. This feature is used to check that there is no mismatch between the input type and its provided value, which helps prevent bugs. If there is a conflict with types, there will be an error already in development, and we should catch it and fix it before the code reaches production.

Hence, we can afford to remove the feature from the code for production:

function someFunction($param)
{
  // ...
}

If the error still happens in production, the thrown error message will be less precise than if we had union types. However, this potential disadvantage is outweighed by being able to use union types in the first place.

Advantages of Transpiling PHP Code

Transpiling enables one to code an application using the latest version of PHP and produce a release that also works in environments running older versions of PHP.

This can be particularly useful for developers creating products for legacy content management systems (CMS). WordPress, for instance, still officially supports PHP 5.6 (even though it recommends PHP 7.4+). The percentage of WordPress sites running PHP versions 5.6 to 7.2 — which are all End-of-Life (EOL), meaning they’re not receiving security updates anymore — stands at a sizable 34.8%, and those running on any PHP version other than 8.0 stands at a whopping 99.5%:

WordPress usage by version
WordPress usage stats by version. Image source: WordPress

Consequently, WordPress themes and plugins targeted at a global audience will quite likely be coded with an old version of PHP to increase their possible reach. Thanks to transpiling, these could be coded using PHP 8.0, and still be released for an older PHP version, thus targeting as many users as possible.

Indeed, any application that needs to support any PHP version other than the most recent one (even within the range of the presently supported PHP versions) can benefit.

This is the case with Drupal, which requires PHP 7.3. Thanks to transpiling, developers can create publicly available Drupal modules using PHP 8.0, and release them with PHP 7.3.

Another example is when creating custom code for clients who cannot run PHP 8.0 in their environments due to one reason or another. Nevertheless, thanks to transpiling, developers can still code their deliverables using PHP 8.0 and run them on those legacy environments.

When to Transpile PHP

PHP code can always be transpiled unless it contains some PHP feature that has no equivalent in the previous version of PHP.

That’s possibly the case with attributes, introduced in PHP 8.0:

#[SomeAttr]
function someFunc() {}

#[AnotherAttr]
class SomeClass {}

In the earlier example using arrow functions, the code could be transpiled because arrow functions are syntactic sugar. Attributes, in contrast, create completely new behavior. This behavior could also be reproduced with PHP 7.4 and below, but only by manually coding it, i.e. not automatically based on a tool or process (AI could provide a solution, but we’re not there yet).

Attributes intended for development use, such as #[Deprecated], can be removed the same way that union types are removed. But attributes that modify the application’s behavior in production cannot be removed, and they cannot be directly transpiled either.

As of today, no transpiler can take code with PHP 8.0 attributes and automatically produce its equivalent PHP 7.4 code. Consequently, if your PHP code needs to use attributes, then transpiling it will be difficult or unfeasible.

PHP Features Which Can Be Transpiled

These are the features from PHP 7.1 and above which can currently be transpiled. If your code only uses these features, you can enjoy the certainty that your transpiled application will work. Otherwise, you’ll need to assess if the transpiled code will produce failures.

PHP Version Features
7.1 Everything
7.2 object type
parameter type widening
PREG_UNMATCHED_AS_NULL flag in preg_match
7.3 Reference assignments in list() / array destructuring (Except inside foreach#4376)
Flexible Heredoc and Nowdoc syntax
Trailing commas in functions calls
set(raw)cookie accepts $option argument
7.4 Typed properties
Arrow functions
Null coalescing assignment operator
Unpacking inside arrays
Numeric literal separator
strip_tags() with array of tag names
covariant return types and contravariant param types
8.0 Union types
mixed pseudo type
static return type
::class magic constant on objects
match expressions
catch exceptions only by type
Null-safe operator
Class constructor property promotion
Trailing commas in parameter lists and closure use lists

PHP Transpilers

Currently, there is one tool for transpiling PHP code: Rector.

Rector is a PHP reconstructor tool, which converts PHP code based on programmable rules. We input the source code and the set of rules to run, and Rector will transform the code.

Rector is operated via command line, installed in the project via Composer. When executed, Rector will output a “diff” (additions in green, removals in red) of the code before and after conversion:

"diff" output from Rector
“diff” output from Rector

Which Version of PHP to Transpile to

To transpile code across PHP versions, the corresponding rules must be created.

Today, the Rector library includes most of the rules for transpiling code within the range of PHP 8.0 to 7.1. Hence, we can reliably transpile our PHP code as far down as version 7.1.

There are also rules for transpiling from PHP 7.1 to 7.0 and from 7.0 to 5.6, but these are not exhaustive. Work is underway to complete them, so we may eventually transpile PHP code down to version 5.6.

Transpiling vs Backporting

Backporting is similar to transpiling, but simpler. Backporting code does not necessarily rely on new features from a language. Instead, the same functionality can be provided to an older version of the language simply by copying/pasting/adapting the corresponding code from the new version of the language.

For instance, the function str_contains was introduced in PHP 8.0. The same function for PHP 7.4 and below can be easily 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;
    }
  }
}

Because backporting is simpler than transpiling, we should opt for this solution whenever backporting does the job.

Concerning the range between PHP 8.0 to 7.1, we can use Symfony‘s polyfill libraries:

These libraries backport the following functions, classes, constants, and interfaces:

PHP Version Features
7.2 Functions:

Constants:

7.3 Functions:

Exceptions:

7.4 Functions:
8.0 Interfaces:
  • Stringable

Classes:

  • ValueError
  • UnhandledMatchError

Constants:

  • FILTER_VALIDATE_BOOL

Functions:

Examples of Transpiled PHP

Let’s inspect a few examples of transpiled PHP code, and a few packages which are being fully transpiled.

PHP Code

The match expression was introduced in PHP 8.0. This source code:

function getFieldValue(string $fieldName): ?string
{
  return match($fieldName) {
    'foo' => 'foofoo',
    'bar' => 'barbar',
    'baz' => 'bazbaz',
    default => null,
  };
}

…will be transpiled to its equivalent PHP 7.4 version, using the switch operator:

function getFieldValue(string $fieldName): ?string
{
  switch ($fieldName) {
    case 'foo':
      return 'foofoo';
    case 'bar':
      return 'barbar';
    case 'baz':
      return 'bazbaz';
    default:
      return null;
  }
}

The nullsafe operator was also introduced in PHP 8.0:

public function getValue(TypeResolverInterface $typeResolver): ?string
{
  return $this->getResolver($typeResolver)?->getValue();
}

The transpiled code needs to assign the value of the operation to a new variable first, as to avoid executing the operation twice:

public function getValue(TypeResolverInterface $typeResolver): ?string
{
  return ($val = $this->getResolver($typeResolver)) ? $val->getValue() : null;
}

The constructor property promotion feature, also introduced in PHP 8.0, allows developers to write less code:

class QueryResolver
{
  function __construct(protected QueryFormatter $queryFormatter)
  {
  }
}

When transpiling it for PHP 7.4, the full piece of code is produced:

 class QueryResolver
 {
  protected QueryFormatter $queryFormatter;

  function __construct(QueryFormatter $queryFormatter)
  {
    $this->queryFormatter = $queryFormatter;
  }
}

The transpiled code above contains typed properties, which were introduced in PHP 7.4. Transpiling that code down to PHP 7.3 replaces them with docblocks:

 class QueryResolver
 {
  /**
   * @var QueryFormatter
   */
  protected $queryFormatter;

  function __construct(QueryFormatter $queryFormatter)
  {
    $this->queryFormatter = $queryFormatter;
  }
}

PHP Packages

The following libraries are being transpiled for production:

Library/description Code/notes
Rector
PHP reconstructor tool that makes transpiling possible
Source code
Transpiled code
Notes
Easy Coding Standards
Tool to have PHP code adhere to a set of rules
Source code
Transpiled code
Notes
GraphQL API for WordPress
Plugin providing a GraphQL server for WordPress
Source code
Transpiled code
Notes

Pros and Cons of Transpiling PHP

The benefit of transpiling PHP has already been described: it allows the source code to use PHP 8.0 (i.e. the latest version of PHP), which will be transformed to a lower version for PHP for production to run in a legacy application or environment.

This effectively allows us to become better developers, producing code with higher quality. This is because our source code can use PHP 8.0’s union types, PHP 7.4’s typed properties, and the different types and pseudo-types added to each new version of PHP (mixed from PHP 8.0, object from PHP 7.2), among other modern features of PHP.

Using these features, we can better catch bugs during development and write code that’s easier to read.

Now, let’s take a look at the drawbacks.

It Must Be Coded And Maintained

Rector can transpile code automatically, but the process will likely require some manual input to make it work with our specific setup.

Third-Party Libraries Must Also Be Transpiled

This becomes an issue whenever transpiling them produces errors since we must then delve into their source code to find out the possible reason. If the issue can be fixed and the project is open source, we will need to submit a pull request. If the library is not open source, we may hit a roadblock.

Rector Does Not Inform Us When The Code Cannot Be Transpiled

If the source code contains PHP 8.0 attributes or any other feature that cannot be transpiled, we cannot proceed. However, Rector will not check out this condition, so we need to do it manually. This may not be a big problem concerning our own source code since we are already familiar with it, but it could become an obstacle concerning third-party dependencies.

Debugging Information Uses The Transpiled Code, Not The Source Code

When the application produces an error message with a stack trace in production, the line number will point to the transpiled code. We need to convert back from transpiled to original code to find the corresponding line number in the source code.

The Transpiled Code Must Also Be Prefixed

Our transpiled project and some other library also installed in the production environment could use the same third-party dependency. This third-party dependency will be transpiled for our project and keep its original source code for the other library. Hence, the transpiled version must be prefixed via PHP-Scoper, Strauss, or some other tool to avoid potential conflicts.

Transpiling Must Take Place During Continuous Integration (CI)

Because the transpiled code will naturally override the source code, we should not run the transpiling process on our development computers, or we’ll risk creating side effects. Running the process during a CI run is more suitable (more on this below).

How to Transpile PHP

First, we need to install Rector in our project for development:

composer require rector/rector --dev

We then create a rector.php configuration file in the root directory of the project containing the required sets of rules. To downgrade code from PHP 8.0 to 7.1, we use this config:

use Rector\Set\ValueObject\DowngradeSetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->import(DowngradeSetList::PHP_80);
    $containerConfigurator->import(DowngradeSetList::PHP_74);
    $containerConfigurator->import(DowngradeSetList::PHP_73);
    $containerConfigurator->import(DowngradeSetList::PHP_72);
};

To make sure the process executes as expected, we can run Rector’s process command in dry mode, passing the location(s) to process (in this case, all files under the folder src/):

vendor/bin/rector process src --dry-run

To perform the transpiling, we run Rector’s process command, which will modify the files within their existing location:

vendor/bin/rector process src

Please notice: if we run rector process in our development computers, the source code will be converted in place, under src/. However, we want to produce the converted code in a different location not to override the source code when downgrading code. For this reason, running the process is most suitable during continuous integration.

Optimizing the Transpiling Process

To generate a transpiled deliverable for production, only the code for production must be converted; code needed only for development can be skipped. That means we can avoid transpiling all tests (for both our project and its dependencies) and all dependencies for development.

Concerning tests, we will already know where the ones for our project are located — for instance, under the folder tests/. We must also find out where the ones for the dependencies are — for example, under their subfolders tests/, test/ and Test/ (for different libraries). Then, we tell Rector to skip processing these folders:

return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::SKIP, [
    // Skip tests
    '*/tests/*',
    '*/test/*',
    '*/Test/*',
  ]);
};

Concerning dependencies, Composer knows which ones are for development (those under entry require-dev in composer.json) and which ones are for production (those under entry require).

To retrieve from Composer the paths of all dependencies for production, we run:

composer info --path --no-dev

This command will produce a list of dependencies with their name and path, like this:

brain/cortex                     /Users/leo/GitHub/leoloso/PoP/vendor/brain/cortex
composer/installers              /Users/leo/GitHub/leoloso/PoP/vendor/composer/installers
composer/semver                  /Users/leo/GitHub/leoloso/PoP/vendor/composer/semver
guzzlehttp/guzzle                /Users/leo/GitHub/leoloso/PoP/vendor/guzzlehttp/guzzle
league/pipeline                  /Users/leo/GitHub/leoloso/PoP/vendor/league/pipeline

We can extract all the paths and feed them into the Rector command, which will then process our project’s src/ folder plus those folders containing all dependencies for production:

$ paths="$(composer info --path --no-dev | cut -d' ' -f2- | sed 's/ //g' | tr '\n' ' ')"
$ vendor/bin/rector process src $paths

A further improvement can prevent Rector from processing those dependencies already using the target PHP version. If a library has been coded with PHP 7.1 (or any version below), then there’s no need to transpile it to PHP 7.1.

To achieve this, we can obtain the list of libraries requiring PHP 7.2 and above and process only those. We will obtain the names of all these libraries via Composer’s why-not command, like this:

composer why-not php "7.1.*" | grep -o "\S*\/\S*"

Because this command does not work with the --no-dev flag, to include only dependencies for production, we first need to remove the dependencies for development and regenerate the autoloader, execute the command, and then add them again:

$ composer install --no-dev
$ packages=$(composer why-not php "7.1.*" | grep -o "\S*\/\S*")
$ composer install

Composer’s info --path command retrieves the path for a package, with this format:

# Executing this command
$ composer info psr/cache --path   
# Produces this response:
psr/cache /Users/leo/GitHub/leoloso/PoP/vendor/psr/cache

We execute this command for all items in our list to obtain all paths to transpile:

for package in $packages
do
  path=$(composer info $package --path | cut -d' ' -f2-)
  paths="$paths $path"
done

Finally, we provide this list to Rector (plus the project’s src/ folder):

vendor/bin/rector process src $paths

Pitfalls to Avoid When Transpiling Code

Transpiling code could be considered an art, often requiring tweaks specific to the project. Let’s see a few problems we may come into.

Chained Rules Are Not Always Processed

A chained rule is when a rule needs to convert the code produced by a previous rule.

For instance, library symfony/cache contains this code:

final class CacheItem implements ItemInterface
{
  public function tag($tags): ItemInterface
  {
    // ...
    return $this;
  }
}

When transpiling from PHP 7.4 to 7.3, function tag must undergo two modifications:

The end result should be this one:

final class CacheItem implements ItemInterface
{
  public function tag($tags)
  {
    // ...
    return $this;
  }
}

However, Rector only outputs the intermediate stage:

final class CacheItem implements ItemInterface
{
  public function tag($tags): self
  {
    // ...
    return $this;
  }
}

The issue is that Rector cannot always control the order in which the rules are applied.

The solution is to identify which chained rules were left unprocessed, and execute a new Rector run to apply them.

To identify the chained rules, we run Rector twice on the source code, like this:

$ vendor/bin/rector process src
$ vendor/bin/rector process src --dry-run

The first time, we run Rector as expected, to execute the transpiling. The second time, we use the --dry-run flag to discover if there are still changes to be made. If there are, the command will exit with an error code, and the “diff” output will indicate which rule(s) can still be applied. That would mean that the first run was not complete, with some chained rule not being processed.

Running Rector with --dry-run flag
Running Rector with –dry-run flag

Once we’ve identified the unapplied chained rule (or rules), we can then create another Rector config file — for instance, rector-chained-rule.php will execute the missing rule. Instead of processing a full set of rules for all files under src/, this time, we can run the specific missing rule on the specific file where it needs to be applied:

// rector-chained-rule.php
use Rector\Core\Configuration\Option;
use Rector\DowngradePhp74\Rector\ClassMethod\DowngradeSelfTypeDeclarationRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
  $services = $containerConfigurator->services();
  $services->set(DowngradeSelfTypeDeclarationRector::class);

  $parameters = $containerConfigurator->parameters();
  $parameters->set(Option::PATHS, [
    __DIR__ . '/vendor/symfony/cache/CacheItem.php',
  ]);
};

Finally, we tell Rector on its second pass to use the new config file via input --config:

# First pass with all modifications
$ vendor/bin/rector process src

# Second pass to fix a specific problem
$ vendor/bin/rector process --config=rector-chained-rule.php

Composer Dependencies May Be Inconsistent

Libraries could declare a dependency to be slated for development (i.e. under require-dev in composer.json), yet still, reference some code from them for production (such as on some files under src/, not tests/).

Usually, this isn’t a problem because that code may not be loaded on production, so there will never be an error on the application. However, when Rector processes the source code and its dependencies, it validates that all referenced code can be loaded. Rector will throw an error if any file references some piece of code from a non-installed library (because it was declared to be needed for development only).

For instance, class EarlyExpirationHandler from Symfony’s Cache component implements interface MessageHandlerInterface from the Messenger component:

class EarlyExpirationHandler implements MessageHandlerInterface
{
    //...
}

However, symfony/cache declares symfony/messenger to be a dependency for development. Then, when running Rector on a project which depends on symfony/cache, it will throw an error:

[ERROR] Could not process "vendor/symfony/cache/Messenger/EarlyExpirationHandler.php" file, due to:             
  "Analyze error: "Class Symfony\Component\Messenger\Handler\MessageHandlerInterface not found.". Include your files in "$parameters->set(Option::AUTOLOAD_PATHS, [...]);" in "rector.php" config.
  See https://github.com/rectorphp/rector#configuration".   

There are three solutions to this issue:

  1. In the Rector config, skip processing the file that references that piece of code:
return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::SKIP, [
    __DIR__ . '/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php',
  ]);
};
  1. Download the missing library and add its path to be autoloaded by Rector:
return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::AUTOLOAD_PATHS, [
    __DIR__ . '/vendor/symfony/messenger',
  ]);
};
  1. Have your project depend on the missing library for production:
composer require symfony/messenger

Transpiling and Continuous Integration

As mentioned earlier, in our development computers we must use the --dry-run flag when running Rector, or otherwise, the source code will be overridden with the transpiled code. For this reason, it’s more suitable to run the actual transpiling process during continuous integration (CI), where we can spin up temporary runners to execute the process.

An ideal time to execute the transpiling process is when generating the release for our project. For instance, the code below is a workflow for GitHub Actions, which creates the release of a WordPress plugin:

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 }}

This workflow contains a standard procedure to release a WordPress plugin via GitHub Actions. The new addition, to transpile the plugin’s code from PHP 7.4 to 7.1, happens in this step:

      - name: Downgrade code for production (to PHP 7.1)
        run: |
          vendor/bin/rector process
          sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php

Taken together, this workflow now performs the following steps:

  1. Checks out the source code for a WordPress plugin from its repository, written with PHP 7.4
  2. Installs its Composer dependencies
  3. Transpiles its code from PHP 7.4 to 7.1
  4. Modifies the “Requires PHP” entry in the plugin’s main file’s header from "7.4" to "7.1"
  5. Removes the dependencies needed for development
  6. Creates the plugin’s .zip file, excluding all unneeded files
  7. Uploads the .zip file as a release asset (and, in addition, as an artifact to the GitHub Action)

Testing the Transpiled Code

Once the code has been transpiled to PHP 7.1, how do we know that it works well? Or, in other words, how do we know it has been thoroughly converted, and no remnants of higher versions of PHP code were left behind?

Similar to transpiling the code, we can implement the solution within a CI process. The idea is to set up the runner’s environment with PHP 7.1 and run a linter on the transpiled code. If any piece of code is not compatible with PHP 7.1 (such as a typed property from PHP 7.4 that wasn’t converted), then the linter will throw an error.

A linter for PHP that works well is PHP Parallel Lint. We can install this library as a dependency for development in our project, or have the CI process install it as a standalone Composer project:

composer create-project php-parallel-lint/php-parallel-lint

Whenever the code contains PHP 7.2 and above, PHP Parallel Lint will throw an error like this one:

Run php-parallel-lint/parallel-lint layers/ vendor/ --exclude vendor/symfony/polyfill-ctype/bootstrap80.php --exclude vendor/symfony/polyfill-intl-grapheme/bootstrap80.php --exclude vendor/symfony/polyfill-intl-idn/bootstrap80.php --exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php --exclude vendor/symfony/polyfill-mbstring/bootstrap80.php
PHP 7.1.33 | 10 parallel jobs
............................................................   60/2870 (2 %)
............................................................  120/2870 (4 %)
...
............................................................  660/2870 (22 %)
.............X..............................................  720/2870 (25 %)
............................................................  780/2870 (27 %)
...
............................................................ 2820/2870 (98 %)
..................................................           2870/2870 (100 %)


Checked 2870 files in 15.4 seconds
Syntax error found in 1 file

------------------------------------------------------------
Parse error: layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php:55
    53|     '0.8.0',
    54|     \__('GraphQL API for WordPress', 'graphql-api'),
  > 55| ))) {
    56|     $plugin->setup();
    57| }
Unexpected ')' in layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php on line 55
Error: Process completed with exit code 1.

Let’s add the linter into our CI’s workflow. The steps to execute to transpile code from PHP 8.0 to 7.1 and test it are:

  1. Check out the source code
  2. Have the environment run PHP 8.0, so Rector can interpret the source code
  3. Transpile the code to PHP 7.1
  4. Install the PHP linter tool
  5. Switch the environment’s PHP version to 7.1
  6. Run the linter on the transpiled code

This GitHub Action workflow does the job:

name: Downgrade PHP tests
jobs:
  main:
    name: Downgrade code to PHP 7.1 via Rector, and execute tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set-up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.0
          coverage: none

      - name: Local packages - Downgrade PHP code via Rector
        run: |
          composer install
          vendor/bin/rector process

      # Prepare for testing on PHP 7.1
      - name: Install PHP Parallel Lint
        run: composer create-project php-parallel-lint/php-parallel-lint --ansi

      - name: Switch to PHP 7.1
        uses: shivammathur/setup-php@v2
        with:
          php-version: 7.1
          coverage: none

      # Lint the transpiled code
      - name: Run PHP Parallel Lint on PHP 7.1
        run: php-parallel-lint/parallel-lint src/ vendor/ --exclude vendor/symfony/polyfill-ctype/bootstrap80.php --exclude vendor/symfony/polyfill-intl-grapheme/bootstrap80.php --exclude vendor/symfony/polyfill-intl-idn/bootstrap80.php --exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php --exclude vendor/symfony/polyfill-mbstring/bootstrap80.php

Please notice that several bootstrap80.php files from Symfony’s polyfill libraries (which need not be transpiled) must be excluded from the linter. These files contain PHP 8.0, so the linter would throw errors when processing them. However, excluding these files is safe since they will be loaded on production only when running PHP 8.0 or above:

if (\PHP_VERSION_ID >= 80000) {
  return require __DIR__.'/bootstrap80.php';
}

Summary

This article taught us how to transpile our PHP code, allowing us to use PHP 8.0 in the source code and create a release that works on PHP 7.1. Transpiling is done via Rector, a PHP reconstructor tool.

Transpiling our code makes us better developers since we can better catch bugs in development and produce code that’s naturally easier to read and understand.

Transpiling also enables us to decouple our code with specific PHP requirements from the CMS. We can now do so if we desire to use the latest version of PHP to create a publicly available WordPress plugin or Drupal module without severely restricting our userbase.

Do you have any questions left about transpiling PHP? Let us know in the comments section!

Leonardo Losoviz

Leo writes about innovative web development trends, mostly concerning PHP, WordPress and GraphQL. You can find him at leoloso.com and twitter.com/losoviz.