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
- Parse the entry point with
nikic/php-parserand extract all class references (extends, implements, trait use, type hints,new, etc.). - Resolve class names to files using Composer autoload mappings and
vendor/composermetadata. - Build a dependency graph and topologically sort files so dependencies come before dependents.
- Emit a single output file: strip
declare(strict_types=1), inline vendorfilesentries, inlinerequire/includecalls whose path is built from__DIR__, wrap top-levelreturnguards in a closure so they do not abort the whole file, and annotate each file with a// file:comment. - 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/includeis 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!