Giter Club home page Giter Club logo

php-scoper's Introduction

PHP-Scoper

Package version Build Status Scrutinizer Code Quality Code Coverage Slack License

PHP-Scoper is a tool which essentially moves any body of code, including all dependencies such as vendor directories, to a new and distinct namespace.

Goal

PHP-Scoper's goal is to make sure that all code for a project lies in a distinct PHP namespace. This is necessary, for example, when building PHARs that:

  • Bundle their own vendor dependencies; and
  • Load/execute code from arbitrary PHP projects with similar dependencies

When a package (of possibly different versions) exists, and is found in both a PHAR and the executed code, the one from the PHAR will be used. This means these PHARs run the risk of raising conflicts between their bundled vendors and the vendors of the project they are interacting with, leading to issues that are potentially very difficult to debug due to dissimilar or unsupported package versions.

Table of Contents

Usage

php-scoper add-prefix

This will prefix all relevant namespaces in code found in the current working directory. The prefixed files will be accessible in a build folder. You can then use the prefixed code to build your PHAR.

Warning: After prefixing the files, if you are relying on Composer for the auto-loading, dumping the autoloader again is required.

For a more concrete example, you can take a look at PHP-Scoper's build step in Makefile, especially if you are using Composer as there are steps both before and after running PHP-Scoper to consider.

Refer to TBD for an in-depth look at scoping and building a PHAR taken from PHP-Scoper's makefile.

Building a Scoped PHAR

With Box

If you are using Box to build your PHAR, you can use the existing PHP-Scoper integration. Box will take care of most of the things for you so you should only have to adjust the PHP-Scoper configuration to your needs.

Without Box

Step 1: Configure build location and prep vendors

Assuming you do not need any development dependencies, run:

composer install --no-dev --prefer-dist

This will allow you to save time in the scoping process by not processing unnecessary files.

Step 2: Run PHP-Scoper

PHP-Scoper copies code to a new location during prefixing, leaving your original code untouched. The default location is ./build. You can change the default location using the --output-dir option. By default, it also generates a random prefix string. You can set a specific prefix string using the --prefix option. If automating builds, you can set the --force option to overwrite any code existing in the output directory without being asked to confirm.

Onto the basic command assuming default options from your project's root directory:

bin/php-scoper add-prefix

As there are no path arguments, the current working directory will be scoped to ./build in its entirety. Of course, actual prefixing is limited to PHP files, or PHP scripts. Other files are copied unchanged, though we also need to scope certain Composer related files.

Speaking of scoping Composer related files... The next step is to dump the Composer autoloader if we depend on it, so everything works as expected:

composer dump-autoload --working-dir build --classmap-authoritative

Recommendations

There is 3 things to manage when dealing with isolated PHARs:

  • The PHAR format: there is some incompatibilities such as realpath() which will no longer work for the files within the PHAR since the paths are not virtual.
  • Isolating the code: due to the dynamic nature of PHP, isolating your dependencies will never be a trivial task and as a result you should have some end-to-end test to ensure your isolated code is working properly. You will also likely need to configure the excluded and exposed symbols or patchers.
  • The dependencies: which dependencies are you shipping? Fine controlled ones managed with a composer.lock or you always ship your application with up-to-date dependencies? The latter, although more ideal, will by design result in more brittleness as any new release from a dependency may break something (although the changes may be SemVer compliant, we are dealing with PHARs and isolated code)

As a result, you should have end-to-end tests for your (at the minimum) your released PHAR.

Since dealing with the 3 issues mentioned above at once can be tedious, it is highly recommended having several tests for each step.

For example, you can have a test for both your non-isolated PHAR and your isolated PHAR, this way you will know which step is causing an issue. If the isolated PHAR is not working, you can try to test the isolated code directly outside the PHAR to make sure the scoping process is not the issue.

To check if the isolated code is working correctly, you have a number of solutions:

Debugging

Having a good breakdown like described in Recommendations will help to know where the issue is coming from. However, if you have a doubt or you are fiddling with patchers and want to check the result for a specific file without doing the whole scoping process, you can always check the result for that single individual file:

php-scoper inspect path/to/my-file.php

Contributing

Contribution Guide

Credits

Project originally created by: Bernhard Schussek (@webmozart) which has now been moved under the Humbug umbrella.

php-scoper's People

Contributors

alexander-schranz avatar backendtea avatar carusogabriel avatar dependabot[bot] avatar huluti avatar joel-james avatar kmgalanakis avatar kocal avatar leoloso avatar liqueurdetoile avatar marekdedic avatar matthieuauger avatar mattstauffer avatar matzeeable avatar moorscode avatar ngyuki avatar nyholm avatar padraic avatar peter279k avatar poisa avatar sebastianbergmann avatar smoench avatar szepeviktor avatar theofidry avatar tomasvotruba avatar tw-360vier avatar veewee avatar villfa avatar webmozart avatar wirone avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

php-scoper's Issues

Some namespaces are not prefixed

Example:

<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new \Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
            new AppBundle\AppBundle(),
        ];

        return $bundles;
    }

Will be:

<?php

use PhpScoper595ca8c756b76\Symfony\Component\HttpKernel\Kernel;
use PhpScoper595ca8c756b76\Symfony\Component\Config\Loader\LoaderInterface;
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), 
            new \PhpScoper595ca8c756b76\Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), 
            new AppBundle\AppBundle()
        ];
        return $bundles;
    }

Add --diff option

A --diff option should be added which displays all changes as diff, but does not actually change any file.

Classes from global namespace incorrectly prefixed

Given the following class:

<?php

class AppKernel
{
}

which is whitelisted (hence should be prefixed), the prefixed code won't change i.e. the namespace with the prefix added is missing:

<?php

namespace PhpScoperPefix; // <- this is missing

class AppKernel
{
}

PSR-0 dependencies are not correctly prefixed

Question Answer
PHP-Scoper version master 1ca326d
PHP version 7.1.4
Composer version 1.5.2 2017-09-11 16:59:25
Github Repo https://github.com/borNfreee/php-scoper-bug

I'm trying to prefix Infection application but faced an issue with PSR-0 dependency which is not correctly resolved.

I've created a github repo with reproducible issue - just a very small application with only 1 dependency - Pimple container which uses PSR-0 in its composer.json.

How to reproduce:

git clone https://github.com/borNfreee/php-scoper-bug
cd php-scoper-bug

composer install

# check it works
php src/main.php // prints "Hello"

# run PHP-Scoper
php-scoper add-prefix --output-dir=build-prefixed --force --prefix=Mutant

composer dump-autoload --working-dir=build-prefixed/ --classmap-authoritative --no-dev

# Then try to run prefixed application:
cd build-prefixed

php src/main.php

This throws an error:

PHP Fatal error:  Uncaught Error: Class 'Mutant\Pimple\Container' not found in php-scoper-bug/build-prefixed/src/Application.php:10
Stack trace: ...

Please note that composer create autoload_static.php with the following content for PSR-0 dependencies:

public static $prefixesPsr0 = array (
        'P' => 
        array (
            'Pimple' =>  // <------- this is not prefixed
            array (
                0 => __DIR__ . '/..' . '/pimple/pimple/src',
            ),
        ),
    );

while PSR-4 dependencies are prefixed:

public static $prefixDirsPsr4 = array (
        'Mutant\\ScoperBug\\' => 
        array (
            0 => __DIR__ . '/../..' . '/src',
        ),
        'Mutant\\Psr\\Container\\' => 
        array (
            0 => __DIR__ . '/..' . '/psr/container/src',
        ),
    );

Did I miss something?

Hope this helps!

Apply PhpScoper to... itself

We will generate a PHAR for this library which is build via make build. I think it would be good to apply PhpScoper to the generated PHAR like shown in #38

Also tests run for #43 could be applied to the generated PHAR as well.

Performance

Several paths could be taken to enhance performances:

  • Using the same trick as Composer to disable xdebug when running the analysis
  • See if that work can be parallelised (my only worry is concurrent creating of a same directory, otherwise should be easy)
  • The biggest and quickest perf boost is to scope the least number of file as possible, for that a box optimization would be optimal (see #49)

Other perf boost could be in:

  • PhpParser (hard to tell and not sure I want to dig into this one)
  • Box (which is super slow as well)

In any case I think for now waiting a couple of minutes to build the PHAR is acceptable so this task is not a hight priority one

Add --fix-strings and --fix-global-strings options

The options --fix-strings and --fix-global-strings (suggestions for better names welcome) should be added which do the following:

In a first pass, all class names in the project are collected. Class names that already contain the prefix should be collected as well, but without the prefix (see #6). Then:

  • --fix-strings adds the namespace prefix to strings that contain non-global class names, e.g. 'Composer\Autoload\ClassLoader'. This operation should be pretty safe to do.
  • --fix-global-strings adds the namespace prefix to all strings that contain global class names, e.g. 'ComposerAutoloaderInitFilesAutoloadOrder'. This operation is risky since strings could look like class names even though they are none (e.g. 'Profile'), so this needs manual review. The option will typically be used combined with --diff to create a patch that can be reviewed and adjusted before being applied.

Simplify configuration

Maybe the configuration could be simplified by requiring to pass a simple function for the global namespace whitelist and patchers instead of an array of things... tbd

Namespace/classes whitelist

In the context of tools like Humbug or PhpUnit, the code shipped in the PHAR need to consume external code for which the namespaces must not be prefixed. For example PhpUnit\Framework\TestCase is a class that must be shared between the isolated PHAR and the consumer code.

Fix class_exists

This is not rewritten:

if (!class_exists('Symfony\\Component\\Yaml\\Parser')) {
     throw new RuntimeException('Unable to load YAML config files as the Symfony Yaml Component is not installed.');
}

Check namespace aliases for single level renames?

While pointless single level namespaces will raise a warning in use, there is an exception for setting an alias. As equally pointless as that may be...

<?php
use Symfony; // raise warning
use Symfony as Laravel; // no warning
new Laravel\Component\Finder\Finder; // Symfony Finder instance

With the most recent change in scoping, any use XXX as YYY would escape scoping for now.

Scope files in the global namespace

I had a simple file:

<?php

require_once __DIR__.'/vendor/autoload.php';

echo (new \Foo\Foo())().PHP_EOL;

Where Foo was an invokable. Surprisingly the scoping did not prefix \Foo\Foo() to PhpScoperPrefix\Foo\Foo().

PHAR versioning

AFAIK, the package for setting the version string ignores local release tags, so this will set the version for all phars to the commit hash. As we need a valid version string for updating down the line, an alternative mode when building phars (or using the phar) is needed.

Pass custom finders

I think we should provide a file discovery experience similar to box to ease the usage outside of box (and we don't know when the box integration will be done) which is easily doable by adding 2 entries in the config.

@padraic thoughts?

`new static` not respected

Hey guys,

Ran into an issue today where new static(...) is translated to new Prefix\static(...), where it shouldn't be prefixed at all.

Here's an example using an example from Guzzle: php-scoper-issue.zip

Comparing the src and build files you can see this

@@ -47,21 +41,12 @@
      */
     public static function factory($config = array())
     {
-        return new static(isset($config['base_url']) ? $config['base_url'] : null, $config);
-    }
-
+        return new \Prefix\Guzzle\Service\static(isset($config['base_url']) ? $config['base_url'] : null, $config);
+    }

The command I ran here was

php-scoper add-prefix --prefix="Prefix\\" src/

php-scoper@227a4fd

Anything I can do to help out here?

Fix autoloading for whitelisted classes

The only piece missing in the puzzle for the next part is to fix the autoloading for the whitelisted class. We can take for example PHP-Scoper itself which needs to whitelist Symfony\Component\Finder\Finder which can be used in the configuration file.

The Finder class itself is ok, the namespace is left untouched and the scoped code still uses references to the correct Finder class. The issue is for the autoloading, where before you had a PSR-4 loader with Symfony\Component\Finder => vendor/symfony/finder, you now have PhpScoperPrefix\Symfony\Component\Finder => vendor/symfony/finder. This is correct for the prefixed classes, but will fail for the autoloading of the finder class...

Parallel processing

As the scoping is done file by file in a complete isolated way, it should be easy to parallelize that work.

Add check about conflicting whitelists

Add a check to prevent the user to whitelist a class from the global namespace, e.g.Foo. Indeed they are already whitelisted and the global namespace whitelister is about scoping them when necessary. This validation can be done in the configuration.

Create prototype

The first task here is to create a prototype of the "add-prefix" command. The stub of this command and its test is already there. The implementation should look for all

  • namespace declarations
  • use statements
  • fully qualified usages of class/interface/trait names

in a directory/file and add the passed prefix. Read the README to learn more. Probably we can use https://github.com/nikic/PHP-Parser to do the parsing, although I don't know whether it's possible to access the position of the parsed token in the file from the parser to do the actual string replacement (cc @nikic).

This should be fun :) This is available for grabs if anyone wants to give it a try.

Handle custom string manipulations

@theofidry I checked briefly into the PHAR problems, and it seems to boil down to PHP Parser referring to specific classes and functions using strings (which we aren't scoping). Two examples:

PhpParser\Lexer::createTokenMap():

if (defined($name = 'PhpParser\\Parser\\Tokens::' . $name)) {
    // Other tokens can be mapped directly
    $tokenMap[$i] = constant($name);
}

Straightforward class name in a string which is not scoped. This is the first problem encountered. When fixed, you find another when Pretty Printing. I assume at that point we've gone through most of the parsing, so there may just be a couple of these.

PhpParser\PrettyPrinterAbstract::p():

/**
 * Pretty prints a node.
 *
 * @param Node $node Node to be pretty printed
 *
 * @return string Pretty printed node
 */
protected function p(Node $node)
{
    return $this->{'p' . $node->getType()}($node);
}

The $node->getType() returns the class ref which is prefixed, so the function name is corrupted and causes a fatal error.

The outcome of the problem is that scoped files are empty apart from an opening PHP tag. So essentially, we need a facility to apply simple patches to files. I'm not sure if they should target before or after scoping. I suspect doing it beforehand is simpler, but debugging takes place after (and pretty printing meddles with line numbers in between). Might be useful to have a shortcut to extract the phar from php-scoper itself for ease of debugging. I'm using the following the extract the phar at the moment:

php -r '$phar = new Phar("bin/php-scoper.phar"); $phar->extractTo("./build/extract");'

Optimise function calls

Some functions such as is_array should be transformed into \is_array to help opcache. Maybe it could be possible to do the same trick as #117 and prefix all internal functions

Add console output during process

Currently, the scoper outputs "..." during the process.

For a first version, we could to the same thing than php-cs-fixer: list all the paths of the processed files

/home/matthieu/www/puli/file1.php
/home/matthieu/www/puli/file2.php
/home/matthieu/www/puli/file3.php
...

Prevent duplicate prefixes

PHP-Scoper should not add the namespace prefix to classes that already contain the prefix. In this way, the scoper can be run multiple times on the same code.

Add --fix-composer option

The option --fix-composer should be added which adds the namespace prefix to the "autoload" section of all composer.json files found in the searched paths.

version shows hash

please make version show actual version. i downloaded 0.5.1, but it shows git hash:

[~/.local/bin/phars/phars (master)โ˜…] โž” php71 ./php-scoper.phar --version
PHP Scoper version 600abecf9bd8ed8c02807c64529b9be4129b8b1e

Running make build fails on latest

First of all, thanks for creating this! It solves a real problem for shipping code with bundled dependencies ๐Ÿ‘

I saw on the readme it says that the phar is coming soon, but by the looks of things it seems as though it may be possible to do yourself? I tried building the phar with make build but it's looking for box which is missing in the vendor-bin directory.

make build
# everything goes fine until this part...
composer dump-autoload -d build  --classmap-authoritative
Generating optimized autoload files
php -d zend.enable_gc=0 vendor-bin/box/vendor/bin/box build
Could not open input file: vendor-bin/box/vendor/bin/box
make: *** [build] Error 1

Am I missing something?

Keep coding style during prefixing process

Currently, the coding style is not kept when files are processed by the scoper. For exemple

<?php

namespace MyNamespace;

use MyOtherNamespace\MyClass;

class MyClass
{
}

is modified like this

<?php

namespace MyNamespace;

use MyOtherNamespace\MyClass;
class MyClass
{
}

Sign PHAR

I saw a box option regarding the PHAR version as well, not sure how you configure/handle it @padraic.

--no-ansi option has no effect

I am working on integrating php-scoper into the build process for phpunit.phar. Here is the current state of the relevant section from PHPUnit's build.xml:

<exec executable="${basedir}/build/tools/php-scoper" taskname="php-scoper">
    <arg value="add-prefix" />
    <arg value="--no-ansi" />
    <arg value="--no-config" />
    <arg value="--no-interaction" />
    <arg value="--stop-on-failure" />
    <arg value="--output-dir" />
    <arg path="${basedir}/build/phar-prefixed" />
    <arg value="--prefix" />
    <arg value="PHPUnit" />
    <arg path="${basedir}/build/phar" />
</exec>

The progress bar is printed although the --no-ansi option is passed:
screenshot from 2017-12-23 10-21-24
I am reluctant to pass --quiet as I am not sure whether that would also suppress error messages.

Box integration

As people use box to build their PHAR, it would be nice if the prefixing could be done directly via box by exposing a config.

This is very likely to require to switch from a JSON config file to a PHP one for Box.

Add windows support

When I try to run php-scoper add-prefix in my simple project I get an Exception:

[Symfony\Component\Filesystem\Exception\IOException]                          
Failed to create "C:\www\test\public\wp-content\plugins\asdasdasdsad\buildC:\www\test\public\wp-content\plugins\asdasdasdsad": mkdir(): No such file or directory.   

As you can see the path is duplicated. Using output dir / working dir parameters doesn't help.

I'm on Windows 10 using the MINGW64 command line.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.