How to Box Symfony App to PHAR without Killing Yourself

Do you have a Symfony Application like Composer and you want to ship it as a PHAR? Composer is actually pretty simple - just see the Compiler class.

But what if you use Symfony Dependency Injection with PSR-4 autodiscovery like Rector does? Well, better be ready for nasty traps.

Note: all these tips take 5 minutes to apply (in total), but took us ~6 hours to discover. I'd like to thank Jan Linhart from Mautic, Kerrial Becket Newham and Ivan Kvasnica for cooperation that made this happen.

Rector needs prefixed PHAR for the same reasons PHPStan does.

Let's say your composer.json looks like this:

    "require": {
        "symfony/console": "2.8"

If you want to install Rector:

composer require rector/rector --dev

You'll end up with an error:

rector/rector cannot be installed, because it requires symfony/* ^3.4|^4.4... but you have 2.8

This leads to many issues reported on Github, mostly grouped around this one.

PHP Version Conflicts

If you have PHP 5.6, you'll get a different error:

rector/rector needs at least PHP 7.2, you have PHP 5.6

That's where Docker becomes useful. Yet, it still doesn't solve the Symfony 2.8 in your project vs Symfony 4.4 in Rector project issue.

That's why prefixed rector.phar is needed. With such a file, you don't care about Rector's dependencies, you just use it.

How Does "Scoping" Work?

Basically any Symfony\Component\Console\Command becomes UniquePrefix\Symfony\Component\Console\Command. That way there will never be conflicts between your code without prefix and unique Rector code.

Box + Scope Industry Standard

To make it happen, we don't need to re-invent the wheel. There are 2 amazing tools maintained and developed by Théo Fidry (thank you!):

It takes around 10 seconds to scope + wraps 5 000 files of Rector to rector.phar. This speed is amazing.

Nobody Ever used Symfony in PHAR Before

These 2 tools work very well for PHP-based manual containers like PHPStan has. But fails for Symfony autodiscovery that uses globs. It's not the fault of these tools, but rather Symfony, because nobody ever tested it to compiled PHAR :).

Where and how to overcome it? There are 4 steps you need to watch out for:

1. From excluded files to Globs

If you have following config:

        resource: '../src'
            - '../src/ValueObject/SpecificFile.php'

You'll end up with an error:

Directory "../src/ValueObject/SpecificFile.php" was not found.

Where does it come from? It's an error from Symfony/Finder.

But how did Symfony got there? Well, the Symfony takes missing files from "excluded" as directory and the rest is history.

My super random guess is for missing local phar:// prefix.

How to Fix it?

Just change the relative path to each file to glob (*) and move your files there:

         resource: '../src'
-          - '../src/ValueObject/SpecificFile.php'
+          - '../src/ValueObject/*'

Positive Side-Effect

In the end, it was architecture improvement, as we had to move files to a generic directory, that clearly states it's not a service - here ValueObject.

2. Symfony Autodiscovery Slash Fail

This one give me an headache, but is simple to fix:

-        resource: '../src/'
+        resource: '../src'

3. SHA1 cannot be Verified...

This one is not strictly related to Symfony, but it happened while we shipped box.phar:

Error: Fatal error: Uncaught PharException: phar "compiler/build/box.phar" SHA1
the signature could not be verified: broken signature in ...

What the box.phar worked locally but doesn't work on Travis?

I re-downloaded files many times and it worked in other CI. WTF?

Is that corrupted version of box.phar? I tried version before/after, still the same error.

2 hours later...

Damn. Spaces? Line-ending? Yes!

The solution is to remove this from .gitattributes:

-# Set default behavior, in case of users, don't have core.autocrlf set.
-* text=auto
-* text eol=lf

Because it changed line-endings in the box.phar on commit and thus made it valid locally, but broken remotely on Travis CI.

4. Don't do Multiple bin Files

Rector had multiple bin files, just to split the complexity:

The Box takes only the file in compser.json > bin section, so the latter 2 were missed. I tried to change configuration many times, but it mostly failed on malformed paths.

How to solve it?

Now we have just single file:

With use strict typed classes written on the bottom of the file and use them in the same file. Also, nice side effects as we moved from many-functions to few classes.

Do you want to know more about Box + Scoper automated Travis CI deploy in practice?

Check this PR on Rector

Happy coding!

Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!