PHP-Scoper

repository·main·Indexed 21 days ago

https://github.com/humbug/php-scoper

A tool designed to prefix PHP namespaces to allow developers to bundle dependencies into a PHAR without causing version conflicts with the host environment. It moves code and vendor directories into a distinct namespace, providing a CLI for prefixing, an inspection tool for debugging, and a configuration system to exclude or expose specific symbols and files.

Tokens
7.7K
Snippets
26
Records
36
Agent score
73%

What's inside PHP-Scoper

  1. What is PHP-Scoper and why use it?

    main

    PHP-Scoper is a tool that moves a body of code (including all dependencies like vendor directories) into a new, distinct PHP namespace.

    This is primarily used when building PHAR files that bundle their own dependencies. By prefixing namespaces, you prevent version conflicts between the dependencies bundled inside your PHAR and the dependencies of the project executing the PHAR. This ensures that if a package exists in both places, the version inside the PHAR is used, avoiding difficult-to-debug issues caused by incompatible package versions.

  2. Handle dynamic symbols and string values

    main

    PHP-Scoper attempts to prefix class and function names found within strings, but it cannot identify symbols in certain dynamic contexts. If your code relies on these, you must use [patchers] to fix them.

    Unscopable patterns include:

    • Strings within regular expressions (e.g., /^Acme\\Foo/).
    • Concatenated strings (e.g., $class = 'Symfony\Component\'.$name;).
    • Callables represented as arrays ['Acme\Foo', 'bar'] or strings 'Acme\Foo::bar'.
    • Heredoc values: Content inside <<<EOF blocks is not prefixed.
    • Plain strings that look like class names but are not (e.g., 'Acme\Foo' used as a label).
    • Global scope strings like 'Foo' or 'Acme_Foo'.
    // Unscopable: Concatenation
    $class = 'Symfony\Component\'.$name;
    
    // Unscopable: Callables
    ['Acme\Foo', 'bar'];
    'Acme\Foo::bar';
    
    // Unscopable: Heredoc
    <<<EOF
    use Acme\Foo;
    EOF;
  3. How to deal with unknown third-party symbols

    main

    When scoping code that uses third-party symbols (like WordPress functions) that are not declared within your codebase or vendor directory, PHP-Scoper will attempt to prefix them, which causes errors because the prefixed version does not exist.

    There are two ways to handle this:

    1. Excluding the symbol (Recommended): Mark the symbol as "internal" using the exclude-functions, exclude-classes, or exclude-constants configuration keys. This treats the symbol as if it were part of the PHP core or a PHP extension, preventing PHP-Scoper from prefixing it.
    2. Exposing the symbol (Fragile): Use exposed-functions or exposed-classes. This registers an alias, but requires that the original symbol is declared somewhere in your codebase at some point.
  4. How autoload aliases work

    main

    When you use the exposed-symbols configuration, PHP-Scoper registers aliases to allow the scoped code to access un-prefixed symbols.

    • Class aliases: Registered when [exposing a class].
    • Function aliases: Registered when [exposing a function] or when a globally declared excluded-function declaration is found.
  5. Best practices for isolated PHARs

    main

    When managing isolated PHARs, keep these three areas in mind:

    1. PHAR Format Limitations: Be aware of incompatibilities like realpath(), which may not work correctly for files inside a PHAR because paths are virtual.
    2. Code Isolation: Isolating dependencies is complex. You will likely need to configure excluded and exposed symbols or use patchers. Always implement end-to-end tests to ensure the isolated code works.
    3. Dependency Management: Decide whether to ship specific versions via composer.lock or always ship up-to-date dependencies. Shipping up-to-date dependencies is more ideal but increases brittleness if new releases break compatibility.

    Testing Strategy: To debug issues, compare your non-isolated PHAR against your isolated PHAR. If the isolated version fails, try testing the scoped code directly in a directory (outside the PHAR) to determine if the issue lies in the scoping process or the PHAR packaging itself.

  6. Understand PSR-0 and Trait/Enum limitations

    main

    PHP-Scoper has the following structural limitations:

    • PSR-0 Support: PHP-Scoper attempts to transform PSR-0 autoloading into PSR-4. This can cause issues where classes in the root of a PSR-0 directory (e.g., src/JsonMapper.php) fail to load if the subdirectories (e.g., src/JsonMapper/Exception.php) are successfully transformed.
    • Traits and Enums: There is currently no way to expose or exclude Traits or Enums because there is no mechanism to alias them.
  7. Handle date format string collisions

    main

    PHP-Scoper may mistake date format strings for class names. For example, a constant like const ISO8601_BASIC = 'Ymd\THis\Z'; might be incorrectly identified as a symbol.

    Solution: If PHP-Scoper incorrectly prefixes date format strings, you must use [patchers] to correct them.

    // Potential collision
    const ISO8601_BASIC = 'Ymd\THis\Z';
  8. Install PHP-Scoper via Phive

    main

    You can use Phive to install or update PHP-Scoper. Note that you may need to use the --force-accept-unsigned flag.

    # Install
    phive install humbug/php-scoper --force-accept-unsigned
    
    # Upgrade
    phive update humbug/php-scoper --force-accept-unsigned
  9. Install PHP-Scoper via Composer

    main

    You can install PHP-Scoper globally using Composer. If you encounter dependency conflicts or prefer a project-specific installation, it is recommended to use the bamarni/composer-bin-plugin to isolate the installation in a separate bin directory.

    # Global installation
    composer global require humbug/php-scoper
    
    # Project-specific installation using composer-bin-plugin
    composer require --dev bamarni/composer-bin-plugin
    composer bin php-scoper require --dev humbug/php-scoper
    
    # Run the installed binary
    vendor/bin/php-scoper
  10. Use Patchers to fix scoping issues in strings

    main

    PHP-Scoper may not automatically prefix class names or namespaces found inside strings or string manipulations. You can use patchers—a list of callables in scoper.inc.php—to manually replace these occurrences.

    A patcher signature is: callable(string $filePath, string $prefix, string $contents): string.

    Example: Patching a class name in a string

    If your code contains $class = 'Humbug\Format\Type\' . $type;, you can use a patcher to inject the prefix:

    <?php declare(strict_types=1);
    
    return [
        'patchers' => [
            static function (string $filePath, string $prefix, string $content): string {
                if ($filePath === '/path/to/offending/file') {
                    return preg_replace(
                        "%\$class = 'Humbug\\Format\\Type\\' . \$type;%",
                        '$class = \'' . $prefix . '\\Humbug\\Format\\Type\\\' . $type;',
                        $content
                    );
                }
                return $content;
            },
        ],
    ];

    To test a patcher on a specific file, use the inspect command:

    php-scoper inspect /path/to/offending/file
  11. Build a scoped PHAR without Box

    main

    To build a scoped PHAR manually, follow these two steps:

    Step 1: Prepare vendors

    Install only production dependencies to speed up the scoping process by avoiding unnecessary files:

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

    Step 2: Run PHP-Scoper and update autoloader

    Run PHP-Scoper from your project root. By default, it scopes the current directory into the ./build folder. After scoping, you must regenerate the Composer autoloader inside the build directory to ensure the prefixed namespaces work:

    # Run the scoper
    bin/php-scoper add-prefix
    
    # Regenerate the autoloader in the build directory
    composer dump-autoload --working-dir build --classmap-authoritative

    CLI Options for add-prefix:

    • --output-dir <dir>: Change the default output location (default is ./build).
    • --prefix <string>: Set a specific prefix string instead of a random one.
    • --force: Overwrite any existing code in the output directory without confirmation.
    composer install --no-dev --prefer-dist
    bin/php-scoper add-prefix
    composer dump-autoload --working-dir build --classmap-authoritative
  12. Configure Composer autoloading and plugins

    main

    PHP-Scoper has specific limitations regarding Composer integration:

    • Autoloader Dumping: PHP-Scoper does not prefix the dumped Composer autoloader. You must manually dump the autoloader again after prefixing your application. (Note: [Box] can automate this).
    • Static File Autoloaders: Composer's static file autoloaders (based on hashes) are not supported. Use [patchers] as a workaround.
    • Composer Plugins: Plugins are not supported because they do not use the vendor/scoper-autoload.php file required for class aliasing.

    Workaround for Plugins: If using an isolated version of Composer, use the --no-plugins flag.

    composer install --no-plugins