I wrote phpfilemerger, a small CLI tool that takes a PHP entry point and all of its class dependencies and produces a single self-contained PHP file. It uses an AST parser to resolve dependencies properly, so the output is ordered and ready to run.

Use case: PHP-CRUD-API

I maintain a PHP project that is distributes as a single file (PHP-CRUD-API) and phpfilemerger automates the process of building. It produces that one-file distribution, keeping class order and namespaces intact so the merged file works just like the original project. It statically analyzes your codebase using PSR-4/PSR-0 mappings and inlines all dependencies in the correct order.

Why not PHAR?

PHAR archives can bundle code, autoloaders and metadata and are generally a more robust single-file format. They do what phpfilemerger does, but better. However, PHAR execution is frequently disabled or restricted on web hosting. The phar stream wrapper may be turned off and/or execution of PHAR archives is blocked for security reasons. Using phpfilemerger allows you to work around these restrictions. It provides you the convenience of single file deployment whenever PHAR is not an option.

Note: A less elegant way to work around the PHAR execution limitation is pharscript that creates a PHP wrapper script that bootstraps an embedded PHAR archive. See: https://github.com/mevdschee/pharscript

How it works

  1. Parse the entry point with nikic/php-parser and extract all class references (extends, implements, trait use, type hints, new, etc.).
  2. Resolve class names to files using Composer autoload mappings and vendor/composer metadata.
  3. Build a dependency graph and topologically sort files so dependencies come before dependents.
  4. Emit a single output file: strip declare(strict_types=1), inline vendor files entries, inline require/include calls whose path is built from __DIR__, wrap top-level return guards in a closure so they do not abort the whole file, and annotate each file with a // file: comment.
  5. Validate the generated file with php -l.

Examples

Merge src/index.php and all its dependencies into one file:

php phpfilemerger.php merge src/index.php

Produce an includeable library (no entry logic):

php phpfilemerger.php merge src/index.php --exclude-entry --output dist/lib.php

This can be used to include complex dependencies.

Limitations

Where PHAR archives can contain more or less any PHP project, phpfilemerger works best when the project sticks to PSR-style autoloading. It now inlines require/include calls that resolve from __DIR__ and contains top-level return guards in a closure, so many vendor packages that used to be rejected now merge cleanly. A few things still cannot be expressed in a single flat file:

  • Dynamic class loading: new $className() and similar dynamic patterns cannot be statically analyzed and will not be automatically included.
  • Multiple namespaces: files using more than one namespace block may not be handled correctly, and a dynamically-required file that mixes namespaces is left in place rather than inlined.
  • Dynamic file inclusions: require/include is inlined when its path resolves statically from __DIR__. A path that is only known at runtime (a variable, a glob) is skipped with a warning, since it would break in the merged file.
  • Runtime __DIR__ resource access: code that uses __DIR__ or __FILE__ at runtime to read non-PHP resource files (templates, data directories) cannot be bundled, because those files are not code. Such a path resolves relative to the merged file and fails at runtime.

Self compilation

Versions before 1.2 could not self-compile: the merge would fail or produce a broken file. As of 1.2 phpfilemerger can merge its own source into a single file, and that file is a fully working copy of the tool. The merge completes cleanly with no warnings:

git clone https://github.com/mevdschee/phpfilemerger.git
cd phpfilemerger
composer install
php src/index.php merge src/index.php

The result, index.merged.php, runs on its own with every dependency inlined. You can even use it to merge the tool again, and the second-generation file is identical to the first apart from its dated header comment, so the build reaches a fixed point:

php index.merged.php merge src/index.php --output index.merged2.php

Getting there took three fixes. First, the nikic/php-parser package uses a lot of include/require and even contains a top-level return; those are now inlined and wrapped so the merge produces clean output. Second, Symfony Console registers a completion command whose constructor reads its Resources/ directory through __DIR__, which cannot exist in a flat file, so that command is left out of merged builds. Third, the dependency analyzer was discarding any class whose short name matched a built-in, which silently dropped PhpParser\Node\Expr\Error and a few others; it now keeps namespaced classes and warns at merge time if a class it should have bundled goes missing.

See: https://github.com/mevdschee/phpfilemerger

Enjoy!