Scoping PHAR-files

PHAR-files are a great way to bundle code that can then be used like a binary. Therefore PHAR-files are often used for tools that can be included in a CI/CD setup. As they are self-contained archives they use their own autoloading mechanism and therefore don’t depend on your code. Which is great if you want to use them as build-tools because the tools dependencies don’t interfere with your project dependencies.

Imagine you’d want to use a build tool that requires PHP 7.2 with your legacy code that still needs to run with PHP 5.6… Most probably your composer require --dev awesome/build-tool would not work because of a dependency mismatch. Even though you might be on PHP 7.2 at the moment. Using the PHAR-file removes that dependency as all the required files are contained within the archive. And the PHARs autoloader takes care of getting the right files.


But that has one major drawback as I learned from @Ocramius: By using their own internal files the PHAR file will also resolve every use-statement or every call to a new class with its internal autoloader first. So when the PHAR-file actually executes your file (like f.e. phpunit should do) all dependencies in your file will be resolved to the classes contained in the PHAR-file. And only if they aren’t available they might be resolved by your autoloader. So if you f.e. depend on one of the dependencies of PHPUnit those dependencies will be resolved to the PHAR-internal ones and not the ones within your vendor-folder.

“Is this really a problem?” you might ask yourself. And the only valid answer is: it depends. But it will be an issue sooner or later for one or the other developer.

So how can one resolve this?

Enter scoping.

Sco…what?

All our code runs within a certain scope. Either the global scope or the scope of a certain namespace. So when you require a class foo\bar it’s the class bar in the namespace foo. That namespace is the scope of the class. And we can use this to circumvent the issue with resolving classes to PHAR-internal ones by changing the scope; the Namespace of the class.

That can either be done with all the classes you are using in your project (your own ones as well as the ones in the vendor-folder) or (much easier) all the files that are within the PHAR-archive.

So to make it as easy as possible for the user of a PHAR-file the creator of that PHAR-file would scope their PHAR, meaning they’d move all their files to a different namespace.

Sounds complicated and error prone? Well, it is if you do it by hand. But thankfully there’s this open-source project called humbug/php-scoper which takes care of the heavy lifting.

And it’s not more than one line:

./vendor/bin/php-scoper add-prefix --output-dir=/tmp --working-dir=. --config=./scoper.inc.php

It will copy all the code from your current working-directory to the output-directory and prefix all fully qualified class names with an individual prefix. So then all the classes in the output directory belong to a different namespace and will not interfere with your dependencies.

My config-file is a copy of the default config-file and looks like this:

<?php

declare(strict_types=1);

use Isolated\Symfony\Component\Finder\Finder;

return [
    // By default when running php-scoper add-prefix, it will prefix all relevant code found in the current working
    // directory. You can however define which files should be scoped by defining a collection of Finders in the
    // following configuration key.
    //
    // For more see: https://github.com/humbug/php-scoper#finders-and-paths
    'finders' => [
        Finder::create()->files()->in('src'),
        Finder::create()
            ->files()
            ->ignoreVCS(true)
            ->notName('/LICENSE|.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock/')
            ->exclude([
                'doc',
                'test',
                'test_old',
                'tests',
                'Tests',
                'vendor-bin',
            ])
            ->in('vendor')
            ->in('bin'),
        Finder::create()->append([
            'composer.json',
            'composer.lock',
        ]),
    ],

    // When scoping PHP files, there will be scenarios where some of the code being scoped indirectly references the
    // original namespace. These will include, for example, strings or string manipulations. PHP-Scoper has limited
    // support for prefixing such strings. To circumvent that, you can define patchers to manipulate the file to your
    // heart contents.
    //
    // For more see: https://github.com/humbug/php-scoper#patchers
    'patchers' => [
        function (string $filePath, string $prefix, string $contents): string {
            // Change the contents here.

            return $contents;
        },
    ],

    // PHP-Scoper's goal is to make sure that all code for a project lies in a distinct PHP namespace. However, you
    // may want to share a common API between the bundled code of your PHAR and the consumer code. For example if
    // you have a PHPUnit PHAR with isolated code, you still want the PHAR to be able to understand the
    // PHPUnit\Framework\TestCase class.
    //
    // A way to achieve this is by specifying a list of classes to not prefix with the following configuration key. Note
    // that this does not work with functions or constants neither with classes belonging to the global namespace.
    //
    // Fore more see https://github.com/humbug/php-scoper#whitelist
    'whitelist' => [
        'PHPUnit\Framework\TestCase',
    ],
];

Integrating that one-liner into the build-process I described in my blog-post about creating PHAR-files was then a short story. Instead of creating the PHAR-file directly from my files I create it from the files from the output-directory. For a working example have a look at github.com/tonymanero/manero/blob/master/createPhar.sh

Update 1 (10.04.2018)

Théo Fidry mentioned on twitter that scoping a PHAR is already possible out of the box when you are using the box-project to build your PHAR. More on that can be found in the box-documentation