Leonardo Losoviz Freelance developer and writer, with an ongoing quest to integrate innovative paradigms into existing PHP frameworks, and unifying all of them into a single mental model.

Transpiling PHP code from 8.0 to 7.x via Rector

7 min read 2076

Transpiling PHP Code via Rector

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:

WordPress PHP Version Stats
September 2020 WordPress usage stats, via wordpress.org/about/stats.

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:

WordPress Fixed Update Schedule
Proposed fixed updated schedule.

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

Babel shows the way

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.

We made a custom demo for .
No really. Click here to check it out.

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.

Upgrading the development toolchain

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.

Evaluating whether new features can be transpiled

Features introduced in a new PHP version can be roughly categorized as follows:

  • New syntax as syntactic sugar for some existing feature
  • New syntax for a brand-new feature
  • Implementation of new functions, classes, interfaces, constants, and exceptions

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.

Making new features available for development

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.

Avoiding features that are needed on runtime

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.

Backporting functionalities

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:

Transpiling PHP code via Rector

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:

  1. Parsing of the PHP code into an AST (short for Abstract Syntax Tree), which enables the manipulation of its structure and content
  2. Applying rules to execute transformations on selected nodes of the AST
  3. Dumping the new AST back to the file, thus storing the transformed PHP code

From this sequence, we will only be concerned with the second step: supplying Rector with transformation rules.

Describing a rule

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

Browsing the list of Rector rules

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.

Running Rector

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

Running Rector with --dry-run
Running Rector with –dry-run.

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.

Conclusion

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!

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    : Full visibility into your web apps

    LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

    In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

    .
    Leonardo Losoviz Freelance developer and writer, with an ongoing quest to integrate innovative paradigms into existing PHP frameworks, and unifying all of them into a single mental model.

    Leave a Reply