brianhenryie/strauss

Composes all dependencies as a package inside a WordPress plugin

0.19.1 2024-04-26 16:51 UTC

README

PHPUnit PHPStan

Strauss – PHP Namespace Renamer

A tool to prefix namespaces and classnames in PHP files to avoid autoloading collisions.

A fork of Mozart. For Composer for PHP.

The primary use case is WordPress plugins, where different plugins active in a single WordPress install could each include different versions of the same library. The version of the class loaded would be whichever plugin registered the autoloader first, and all subsequent instantiations of the class will use that version, with potentially unpredictable behaviour and missing functionality.

⚠️ Sponsorship: I don't want your money. Please write a unit test to help the project.

Breaking Changes

  • v0.16.0 – will no longer prefix PHP built-in classes seen in polyfill packages
  • v0.14.0 – psr/* packages no longer excluded by default
  • v0.12.0 – default output target_directory changes from strauss to vendor-prefixed

Please open issues to suggest possible breaking changes. I think we can probably move to 1.0.0 soon.

Use

Require as normal with Composer:

composer require --dev brianhenryie/strauss

and use vendor/bin/strauss to execute.

Or, download strauss.phar from releases,

curl -o strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/latest/download/strauss.phar

Then run it from the root of your project folder using php strauss.phar.

To update the files that call the prefixed classes, you can use --updateCallSites=true which uses your autoload key, or --updateCallSites=includes,templates to explicitly specify the files and directories.

Its use should be automated in Composer scripts.

"scripts": {
    "prefix-namespaces": [
        "strauss"
    ],
    "post-install-cmd": [
        "@prefix-namespaces"
    ],
    "post-update-cmd": [
        "@prefix-namespaces"
    ]
}

or

"scripts": {
    "prefix-namespaces": [
        "@php strauss.phar"
    ]
}

‼️ Add composer dump-autoload to your scripts/strauss if you set target_directory to vendor or delete_vendor_packages/delete_vendor_files to true, i.e. if you are using require __DIR__ . '/vendor/autoload.php' and Strauss modifies the files inside vendor, you must tell Composer to rebuild its autoload index.

Configuration

Strauss potentially requires zero configuration, but likely you'll want to customize a little, by adding in your composer.json an extra/strauss object. The following is the default config, where the namespace_prefix and classmap_prefix are determined from your composer.json's autoload or name key and packages is determined from the require key:

"extra": {
    "strauss": {
        "target_directory": "vendor-prefixed",
        "namespace_prefix": "BrianHenryIE\\My_Project\\",
        "classmap_prefix": "BrianHenryIE_My_Project_",
        "constant_prefix": "BHMP_",
        "packages": [
        ],
        "update_call_sites": false,
        "override_autoload": {
        },
        "exclude_from_copy": {
            "packages": [
            ],
            "namespaces": [
            ],
            "file_patterns": [
            ]
        },
        "exclude_from_prefix": {
            "packages": [
            ],
            "namespaces": [
            ],
            "file_patterns": [
            ]
        },
        "namespace_replacement_patterns" : {
        },
        "delete_vendor_packages": false,
        "delete_vendor_files": false
    }
},

The following configuration is inferred:

  • target_directory defines the directory the files will be copied to, default vendor-prefixed
  • namespace_prefix defines the default string to prefix each namespace with
  • classmap_prefix defines the default string to prefix class names in the global namespace
  • packages is the list of packages to process. If absent, all packages in the require key of your composer.json are included
  • classmap_output is a bool to decide if Strauss will create autoload-classmap.php and autoload.php. If it is not set, it is false if target_directory is in your project's autoload key, true otherwise.

The following configuration is default:

  • delete_vendor_packages: false a boolean flag to indicate if the packages' vendor directories should be deleted after being processed. It defaults to false, so any destructive change is opt-in.

  • delete_vendor_files: false a boolean flag to indicate if files copied from the packages' vendor directories should be deleted after being processed. It defaults to false, so any destructive change is opt-in. This is maybe deprecated! Is there any use to this that is more appropriate than delete_vendor_packages?

  • include_modified_date is a bool to decide if Strauss should include a date in the (phpdoc) header written to modified files. Defaults to true.

  • include_author is a bool to decide if Strauss should include the author name in the (phpdoc) header written to modified files. Defaults to true.

  • update_call_sites: false. This can be true, false or an array of directories/filepaths. When set to true it defaults to the directories and files in the project's autoload key. The PHP files and directories' PHP files will be updated where they call the prefixed classes.

The remainder is empty:

  • constant_prefix is for define( "A_CONSTANT", value ); -> define( "MY_PREFIX_A_CONSTANT", value );. If it is empty, constants are not prefixed (this may change to an inferred value).
  • override_autoload a dictionary, keyed with the package names, of autoload settings to replace those in the original packages' composer.json autoload property.
  • exclude_from_prefix / file_patterns
  • exclude_from_copy
    • packages array of package names to be skipped
    • namespaces array of namespaces to skip (exact match from the package autoload keys)
    • file_patterns array of regex patterns to check filenames against (including vendor relative path) where Strauss will skip that file if there is a match
  • exclude_from_prefix
    • packages array of package names to exclude from prefixing.
    • namespaces array of exact match namespaces to exclude (i.e. not substring/parent namespaces)
  • namespace_replacement_patterns a dictionary to use in preg_replace instead of prefixing with namespace_prefix.

Autoloading

Strauss uses Composer's own tools to generate a classmap file in the target_directory and creates an autoload.php alongside it, so in many projects autoloading is just a matter of:

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

If you prefer to use Composer's autoloader, add your target_directory (default vendor-prefixed) to your autoload classmap and Strauss will not create its own autoload.php when run. Then run composer dump-autoload to include the newly copied and prefixed files in Composer's own classmap.

"autoload": {
    "classmap": [
        "vendor-prefixed/"
    ]
},

Motivation & Comparison to Mozart

I was happy to make PRs to Mozart to fix bugs, but they weren't being reviewed and merged. At the time of writing, somewhere approaching 50% of Mozart's code was written by me with an additional nine open PRs and the majority of issues' solutions provided by me. This fork is a means to merge all outstanding bugfixes I've written and make some more drastic changes I see as a better approach to the problem.

Benefits over Mozart:

  • A single output directory whose structure matches source vendor directory structure (conceptually easier than Mozart's independent classmap_directory and dep_directory)
  • A generated autoload.php to include in your project (analogous to Composer's vendor/autoload.php)
  • Handles files autoloaders – and any autoloaders that Composer itself recognises, since Strauss uses Composer's own tooling to parse the packages
  • Zero configuration – Strauss infers sensible defaults from your composer.json
  • No destructive defaults – delete_vendor_files defaults to false, so any destruction is explicitly opt-in
  • Licence files are included and PHP file headers are edited to adhere to licence requirements around modifications. My understanding is that re-distributing code that Mozart has handled is non-compliant with most open source licences – illegal!
  • Extensively tested – PhpUnit tests have been written to validate that many of Mozart's bugs are not present in Strauss
  • More configuration options – allowing exclusions in copying and editing files, and allowing specific/multiple namespace renaming
  • Respects composer.json vendor-dir configuration
  • Prefixes constants (define)
  • Handles meta-packages and virtual-packages

Strauss will read the Mozart configuration from your composer.json to enable a seamless migration.

Alternatives

I don't have a strong opinion on these. I began using Mozart because it was easy, then I adapted it to what I felt was most natural. I've never used these.

Interesting

Changes before v1.0

  • Comprehensive attribution of code forked from Mozart – changes have been drastic and git blame is now useless, so I intend to add more attributions
  • More consistent naming. Are we prefixing or are we renaming?
  • Further unit tests, particularly file-system related
  • Regex patterns in config need to be validated
  • Change the name? "Renamespacer"?

Changes before v2.0

The correct approach to this problem is probably via PHP-Parser. At least all the tests will be useful.

Acknowledgements

Coen Jacobs and all the contributors to Mozart, particularly those who wrote nice issues.