More

    The Ultimate Guide for Transpiling PHP Code

    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.

      Try a free demo
    

    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.
    body a.novashare-ctt{display:block;background:#00abf0;margin:30px auto;padding:20px 20px 20px 15px;color:#fff;text-decoration:none!important;box-shadow:none!important;-webkit-box-shadow:none!important;-moz-box-shadow:none!important;border:none;border-left:5px solid #00abf0}body a.novashare-ctt:hover{color:#fff;border-left:5px solid #008cc4}body a.novashare-ctt:visited{color:#fff}body a.novashare-ctt *{pointer-events:none}body a.novashare-ctt .novashare-ctt-tweet{display:block;font-size:18px;line-height:27px;margin-bottom:10px}body a.novashare-ctt .novashare-ctt-cta-container{display:block;overflow:hidden}body a.novashare-ctt .novashare-ctt-cta{float:right}body a.novashare-ctt.novashare-ctt-cta-left .novashare-ctt-cta{float:left}body a.novashare-ctt .novashare-ctt-cta-text{font-size:16px;line-height:16px;vertical-align:middle}body a.novashare-ctt .novashare-ctt-cta-icon{margin-left:10px;display:inline-block;vertical-align:middle}body a.novashare-ctt .novashare-ctt-cta-icon svg{vertical-align:middle;height:18px}body a.novashare-ctt.novashare-ctt-simple{background:0 0;padding:10px 0 10px 20px;color:inherit}body a.novashare-ctt.novashare-ctt-simple-alt{background:#f9f9f9;padding:20px;color:#404040}body a.novashare-ctt.novashare-ctt-simple-alt:hover,body a.novashare-ctt.novashare-ctt-simple:hover{border-left:5px solid #008cc4}body a.novashare-ctt.novashare-ctt-simple .novashare-ctt-cta,body a.novashare-ctt.novashare-ctt-simple-alt .novashare-ctt-cta{color:#00abf0}body a.novashare-ctt.novashare-ctt-simple-alt:hover .novashare-ctt-cta,body a.novashare-ctt.novashare-ctt-simple:hover .novashare-ctt-cta{color:#008cc4}In a perfect world, we should be able to use PHP 8.0 on all our sites and update it as soon as a new version is released Click to Tweet
    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 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
    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:

    Polyfill PHP 7.1
    Polyfill PHP 7.2
    Polyfill PHP 7.3
    Polyfill PHP 7.4
    Polyfill PHP 8.0

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

    PHP Version
    Features

    7.2
    Functions:

    spl_object_id
    utf8_encode
    utf8_decode

    Constants:

    PHP_FLOAT_*
    PHP_OS_FAMILY

    7.3
    Functions:

    array_key_first
    array_key_last
    hrtime
    is_countable

    Exceptions:

    JsonException

    7.4
    Functions:

    get_mangled_object_vars
    mb_str_split
    password_algos

    8.0
    Interfaces:

    Stringable

    Classes:

    ValueError
    UnhandledMatchError

    Constants:

    FILTER_VALIDATE_BOOL

    Functions:

    fdiv
    get_debug_type
    preg_last_error_msg
    str_contains
    str_starts_with
    str_ends_with
    get_resource_id

    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.

            Sign Up For the Newsletter        
    
    
    
    
    
    Want to know how we increased our traffic over 1000%?
           Join 20,000+ others who get our weekly newsletter with insider WordPress tips! 
    
    
    Subscribe Now  
    

    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 RectorSetValueObjectDowngradeSetList;
    use SymfonyComponentDependencyInjectionLoaderConfiguratorContainerConfigurator;

    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:

    Need a hosting solution that gives you a competitive edge? Kinsta’s got you covered with incredible speed, state-of-the-art security, and auto-scaling. Check out our plans

    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 return type ItemInterface must be first converted to self, due to rule DowngradeCovariantReturnTypeRector
    The return type self must then be removed, due to rule DowngradeSelfTypeDeclarationRector

    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
    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 RectorCoreConfigurationOption;
    use RectorDowngradePhp74RectorClassMethodDowngradeSelfTypeDeclarationRector;
    use SymfonyComponentDependencyInjectionLoaderConfiguratorContainerConfigurator;

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

    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’,
    ]);
    };

    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’,
    ]);
    };

    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/[email protected]
    – 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/[email protected]
    with:
    name: graphql-api
    path: build/graphql-api.zip
    – name: Upload to release
    uses: JasonEtco/[email protected]
    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:

    Checks out the source code for a WordPress plugin from its repository, written with PHP 7.4
    Installs its Composer dependencies
    Transpiles its code from PHP 7.4 to 7.1
    Modifies the “Requires PHP” entry in the plugin’s main file’s header from “7.4” to “7.1”
    Removes the dependencies needed for development
    Creates the plugin’s .zip file, excluding all unneeded files
    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:

    Check out the source code
    Have the environment run PHP 8.0, so Rector can interpret the source code
    Transpile the code to PHP 7.1
    Install the PHP linter tool
    Switch the environment’s PHP version to 7.1
    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/[email protected]

      - name: Set-up PHP
        uses: shivammathur/[email protected]
        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/[email protected]
        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’;
    }

    Whether you’re creating a public plugin for WordPress or you’re updating legacy code, there are many reasons that using the latest PHP version may be impossible Click to Tweet
    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!The post The Ultimate Guide for Transpiling PHP Code appeared first on Kinsta.

    Recent Articles

    Related Stories

    Stay on op - Ge the daily news in your inbox