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

Phabel vs. Rector: Which is better for transpiling PHP code?

9 min read 2644

Rector vs. Phabel: Which is better for transpiling PHP code?

I have been a proponent of downgrading PHP code for some time now, since it allows us to use the latest version of PHP and deploy it to environments that, for one or another reason, must still run a legacy PHP version. Downgrading in PHP has been made feasible by Rector, a tool to reconstruct PHP code based on rules.

In my particular case, since I started transpiling my plugin for WordPress, I could start using typed properties, union types, constructor property promotion, arrow functions, and many other modern features, and yet produce the release in PHP 7.1. As a result, my development experience has been greatly improved, yet the plugin is available to 85 percent of WordPress sites (those running PHP 7.1 and above), which is a drastic increase over the 2 percent of WordPress sites running on PHP 8.0.

Hence, I was mildly excited when, several weeks ago, I came across a Reddit post that introduced a new tool specifically dedicated to transpiling PHP code: Phabel. (Its name, as you might have guessed it, comes from mixing “PHP” and “Babel”, the popular transpiler for JS code.)

I felt compelled to try out this new tool and see how it compares against Rector. My intention was not to replace Rector, as I’ve so far been extremely delighted by it, but to compare both approaches, analyze if either tool is superior to the other, and determine the use cases for which each is best.

This article is the result of my exploration.

When can these tools be used?

Phabel is a tool dedicated to downgrading PHP code. In contrast, Rector is a programmable tool for converting PHP code from any state into any other state.

In addition to downgrading, Rector offers rules for modernizing PHP code (such as transforming from PHP 7.1 to 8.1), improving code quality, renaming functions, and several others.

Hence, Phabel could be a replacement for Rector for the specific use case of downgrading PHP code, and nothing else.

How Rector downgrades code

Rector and Phabel are very different in how they are executed. Rector relies on Composer to generate a standalone executable under vendor/bin/rector. Then, we can invoke it whenever we have access to a console or scripting environment, such as on our laptops while doing development, hosting servers when pushing code, webservers when deploying code, and so on.

In order to downgrade the PHP code, a straightforward approach is to invoke Rector in the CI process, upon some desired event. In this example using GitHub Actions, a release asset with the downgraded PHP code is generated when tagging the repo:

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

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

The GitHub Action workflow can also make the downgraded PHP code available by pushing it to an additional “DIST” repo:

      - name: Uncompress artifact
        uses: montudor/[email protected]
        with:
          args: unzip -qq build/graphql-api.zip -d build/dist-plugin

      - id: previous_tag
        uses: "WyriHaximus/[email protected]"

      - name: Publish to DIST repo
        uses: symplify/[email protected]
        env:
          GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
        with:
          tag: ${{ steps.previous_tag.outputs.tag }}
          package-directory: 'build/dist-plugin'
          split-repository-organization: GraphQLAPI
          split-repository-name: graphql-api-for-wp-dist

How Phabel downgrades code

Phabel uses a different strategy, composed of two elements:

  1. A standalone executable under vendor/bin/phabel to tag the repo to be downgraded
  2. A Composer script that executes the downgrade

In the first stage, whenever tagging the code for the library, we must also execute vendor/bin/phabel publish to create and push two extra tags: tag.9999 and tag.9998 (for instance, after tagging the repo with 1.0.0, it will also produce 1.0.0.9999 and 1.0.0.9998).

tag.9999 is simply a duplicate of tag, hence it tells Composer it needs the same version of PHP used for development, most likely PHP 8.0.

tag.9998, instead, replaces the required PHP version in composer.json with *, thus telling Composer it can handle any PHP version. It moves all the dependencies from require to extra to avoid unsuccessful version constraints from taking effect, allowing Phabel to install and downgrade the dependencies, too.

In the second stage, the users must install the library in their projects via Composer. The library is required as usual, for example, with version constraint ^1.0. Then, when doing composer install or composer update, based on the environment’s PHP version, Composer will decide which version to use. If running PHP 8.0 it will use 1.0.0.9999; if running PHP 7.3, it will fallback to 1.0.0.9998.

Finally, if the dependency is resolved via tag 1.0.0.9998, then Phabel’s Composer script will be automatically triggered right after the dependency is installed, and downgrade its PHP code.

Transpiling code via Phabel (4x the speed)
Transpiling code via Phabel (4x the speed)

Contrasting the two approaches

An important implication arises from the two different approaches for downgrading code: while Rector will most likely be executed on the server, Phabel will run on the client!

At first, this may appear to be an advantage for Phabel, because it is simpler:

  • It doesn’t need be integrated within a CI process
  • There is no need to create an additional DIST repo
  • There is no need to upload the vendor/ folder (containing all dependencies from the library, which must also be downgraded) to a DIST repo

However, there are several drawbacks to this approach, which in my opinion make it less appealing than using Rector.

For one, in Rector, the maintainer of the library can fully control the downgrading experience, making sure everything goes well before releasing the downgraded library. In contrast, since Phabel’s downgrading process runs in the client, if the client encounters issues, then Phabel may fail.

Indeed, this happened to me: downgrading my code from PHP 8.0 to 7.1 would take several minutes and, time and again, the Composer script would get terminated before the code would be fully downgraded:

Killed Phabel process

I tried to overcome the issue; I thought it would be related to Composer’s process timeout, which is by default set to 300 seconds, so I increased the timeout in composer.json:

{
  "config": {
    "process-timeout": 600
  }
}

But it didn’t work. I spent a couple of hours trying to fix it to no avail. In the end, instead of PHP 7.1, I decided to downgrade my code to PHP 7.3, which needs to execute fewer rules and so would complete before the timeout was due. This is not a practical solution; it was good enough for my exploration of the tool, but not good enough if I needed to use it for production.

For another, when using Phabel, the same library will be downgraded time and again, further consuming processing power. The waiting time is also transferred from the maintainer of the library to every one of the users of the library, which is not optimal.

To put this in perspective, a popular library such as Symfony DependencyInjection has over 5000 projects depending on it. That means that 5000 projects will need to execute the process to downgrade the library; 5000 users will need to wait for the process to execute, and the consumption of energy will be 5000 times bigger than downgrading the library at the origin.

Now, this issue can be solved in Phabel. Indeed, the Phabel library is itself being downgraded using Phabel, and the downgraded versions are all published in the Phabel repo under different branches. But the simplicity of Phabel is all but lost, so it would no longer hold any advantage over using Rector.

How extensible is Phabel?

Downgrading PHP code is an all-or-nothing proposition: it either works, or it doesn’t. We cannot downgrade just 99% of the code, because the remaining 1% is enough to make the application fail.

Phabel has one project using it: MadelineProto, created by the same author as Phabel’s, so we can be confident that Phabel is good enough to downgrade the PHP features that MadelineProto uses. If your PHP project does not use any additional PHP features, then Phabel may be good to go.

In my case, though, after downgrading my plugin using Phabel, running the application would throw an error:

PHP Fatal error:  Class Symfony\\Component\\DependencyInjection\\Exception\\ExceptionInterface cannot implement previously implemented interface Throwable in /app/vendor/phabel.transpiler73:symfony/dependency-injection/Exception/ExceptionInterface.php on line 20

The failing code was this one (happening because ContainerExceptionInterface already extends from Throwable):

namespace Symfony\Component\DependencyInjection\Exception;

use Psr\Container\ContainerExceptionInterface;

interface ExceptionInterface extends ContainerExceptionInterface, \Throwable
{
}

After I fixed this piece of code, the error went away, and the application ran.

We can safely conclude that it is important for the library to be extensible, so we can provide the missing downgrading functionality. If the functionality is generic (as in the case above), we can attempt to code it and contribute to the repo (after all, open source is made by everyone).

But if the logic is specific to our application, then we should be able to extend Phabel using our own code.

A clear example is when using PHP 8.0 attributes, which must be downgraded to an equivalent functionality, possibly based on annotations. Attributes may be used for some generic goal, such as [#Deprecated] (to be downgraded as @deprecated), or may support a custom functionality from the application, for which the downgrade will also be specific to the application.

As of this writing, though, Phabel does not support downgrading attributes and, more importantly, it does not support custom downgrades. As a consequence, if your application uses attributes, or has some application-specific piece of code that needs be downgraded, then you can’t use Phabel.

How extensible is Rector?

Rector handles extensibility much better. It already provides a rule to downgrade attributes, which can be configured to handle the low-hanging fruits (such as [#Deprecated]). If this rule were not sufficient, Rector’s rule-based architecture means we can create and execute our own rules.

In addition, because Rector is usually executed in the CI process, we can also execute downgrade logic in it. In the GitHub Actions workflow demonstrated above, there’s this piece of code:

        run: |
          composer install
          vendor/bin/rector process
          sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php

That sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php is changing the PHP requirement for my WordPress plugin, from PHP 7.4. to 7.1. I could create a Rector rule for this, but there’s really no need, since this solution is much simpler.

How reliable is Phabel?

After executing the downgrade (and manually fixing the issues), the application would run. However, the downgrade logic unfortunately changed the behavior of the application in a way that would make it work improperly.

My plugin is a GraphQL server for WordPress. When executing a simple GraphQL query that should return a response, I got a validation error instead:

The validation error in our GraphQL query

Debugging the downgraded code, I found the following code to be causing the problem:

class IntScalarTypeResolver extends AbstractScalarTypeResolver
{
  public function coerceValue($inputValue)
  {
    if (!$inputValue instanceof stdClass) {
      if (!\is_bool($inputValue)) {
        if (!(\is_bool($inputValue) || \is_numeric($inputValue) || \is_string($inputValue))) {
          if (!\is_float($inputValue)) {
            if (!(\is_bool($inputValue) || \is_numeric($inputValue))) {
              if (!\is_int($inputValue)) {
                if (!(\is_bool($inputValue) || \is_numeric($inputValue))) {
                  if (!\is_string($inputValue)) {
                    if (!(\is_string($inputValue) || \is_object($inputValue) && \method_exists($inputValue, '__toString') || (\is_bool($inputValue) || \is_numeric($inputValue)))) {
                      throw new \TypeError(__METHOD__ . '(): Argument #1 ($inputValue) must be of type stdClass|string|int|float|bool, ' . \Phabel\Plugin\TypeHintReplacer::getDebugType($inputValue) . ' given, called in ' . \Phabel\Plugin\TypeHintReplacer::trace());
                    } else {
                      $inputValue = (string) $inputValue;
                    }
                  }
                } else {
                  $inputValue = (int) $inputValue;
                }
              }
            } else {
              $inputValue = (double) $inputValue;
            }
          }
        } else {
          $inputValue = (bool) $inputValue;
        }
      }
    }

    // ...
  }
}

What are all those type validations? They were added by Phabel to downgrade the union type in the original function argument to coerceValue:

function coerceValue(string|int|float|bool|stdClass $inputValue)
{
  // ...
}

Whether this logic is buggy or not, I do not know — I didn’t debug deep enough to see where the conflict happens — but, as it stands now, this logic is also unexpectedly casting the type of the variable from int to string, which then makes the validation of the GraphQL query fail, since it expects an Int and it receives a String.

After manually commenting all those extra lines, in that function and many similar ones throughout the downgraded code, then the application would work well:

Corrected GraphQL execution

Once again, I could perform this editing, manually, because I’m testing the tool. But if I had to use it for production, it would not be practical at all.

How reliable is Rector?

Phabel suffers from the issue above due to good intentions: it wants to recreate the same type validation behavior from PHP 8.0 into PHP 7.x. Unfortunately, something along the way did not come out right (hopefully it can be fixed).

Rector does not suffer from this issue because it does not bother to recreate the type validation. This is how Rector downgrades the same piece of code:

/**
 * @param string|int|float|bool|stdClass $inputValue
 */
function coerceValue($inputValue)
{
  // ...
}

The reason why Rector does not bother to recreate the same functionality is that it doesn’t need to. The downgrade is not expected to be perfect; it only needs to be good enough.

In this particular case, union types can help us prevent bugs in the application during development time, i.e., when we are using PHP 8.0. For production, we can expect bugs not to be there anymore. If they are there, an error will happen in the application nevertheless, whether we recreate the type validation or not; at most, the error message will be different (the original RuntimeException vs. Phabel’s TypeError).

As a consequence, Rector is not changing the behavior of the application, at least concerning type validation. Concerning my plugin, it has so far been reliable, and I’m still quite delighted with this tool.

(To be sure: new releases of Rector have introduced unannounced breaking changes every now and then. To counter eventualities and avoid surprises, I started committing my composer.lock into the repo and using only battle-tested versions of Rector in production. Since doing so I haven’t encountered any issues; no bug in my application has so far made it into production.)

Conclusion

As they stand now, Rector is clearly more reliable than Phabel. This is in part due to its community, which includes dozens of contributors, and a response time to new issues that they can boast about (bugs are normally fixed within days, if not hours).

Phabel still has some way to go before it can realistically become a competitor to Rector. However, the potential is there: even though this review may appear to a large extent negative, I want to stress that I am thoroughly impressed by it. The library is well designed and properly executed, and the code in its repo is very legible (on the down side, there’s not much documentation). And in spite of the drawbacks I mentioned earlier on, I believe that its integration with Composer is a great accomplishment.

Phabel is also promising because it actually does work! In the case where my GraphQL server was failing, and then after some manual fixes it started working, that working code was running PHP 7.3, being a downgrade from the original code in PHP 8.0. Success seems to be within reach!

Notwithstanding Phabel’s current issues, if work on it continues, it can perfectly become a great library. I’ll be watching out for its progress.

: Full visibility into your web and mobile 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 and mobile apps.

Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
.
Leonardo Losoviz Freelance developer and writer, with an ongoing quest to integrate innovative paradigms into existing PHP frameworks, and unify all of them into a single mental model.

Leave a Reply