kherge/box
Box is a PHP tool for building PHAR archives, letting you package PHP applications and libraries into a single executable file. It provides configuration-driven builds, file compaction, and reproducible output for easy distribution.
Install box globally (it's a build-time tool, not a runtime dependency):
composer global require kherge/box
export PATH="$HOME/.composer/vendor/bin:$PATH"
box --version
This package is archived (last release 2016). For anything on PHP 7.4+/8.x, use its successor
box-project/box(composer global require humbug/box), which keeps the samebox.jsonformat andbox buildcommand.
Confirm the Phar extension is writable:
php -d phar.readonly=0 -r 'var_dump(ini_get("phar.readonly"));' # => string(1) "0"
phar.readonly can only be turned off in php.ini or on the CLI (-d), never at runtime.
Look here first: the repo's README.md, box.json.dist/schema docs, and the examples/ directory. box reads a single box.json in your project root and produces one PHAR.
Minimal box.json:
{
"main": "bin/console",
"output": "build/my-tool.phar",
"directories": ["src"],
"files": ["composer.json", "LICENSE"],
"finder": [
{ "name": "*.php", "exclude": ["Tests", "tests"], "in": "vendor" }
],
"stub": true,
"compression": "GZ",
"compactors": ["Herrera\\Box\\Compactor\\Php", "Herrera\\Box\\Compactor\\Json"]
}
First use case for a Laravel dev: packaging an internal CLI — a deployment/backup/scaffolding command, or a Laravel Zero app — into a single distributable file so teammates run ./artisan-deploy without cloning the repo or running composer install. (Laravel Zero itself is built on the successor package.)
First build:
composer install --no-dev --optimize-autoloader
php -d phar.readonly=0 $(which box) build -v
php build/my-tool.phar --help
Bootstrap everything through the stub. Box generates a stub from "main" unless you supply "stub": "stub.php". A Laravel-Zero-style stub:
#!/usr/bin/env php
<?php
Phar::mapPhar('my-tool.phar');
require 'phar://my-tool.phar/vendor/autoload.php';
$app = require 'phar://my-tool.phar/bootstrap/app.php';
$app->run();
__HALT_COMPILER();
Ship a custom stub whenever you need a shebang, chmod +x, or a different working directory (chdir(__DIR__) before mapPhar if your app relies on relative paths).
Treat the PHAR as read-only. The reliable pattern is to detect the archive root and redirect all mutable paths outside it:
$base = Phar::running(false) ? dirname(Phar::running(false)) : base_path();
$app->useStoragePath(getenv('APP_STORAGE') ?: $base.'/storage');
In config/filesystems.php and config/logging.php, resolve paths from Phar::running() rather than storage_path(). Never let bootstrap/cache or logs be written inside the archive.
Bundle a lean dependency set. Run composer install --no-dev --classmap-authoritative before box build, and let box.json's directories/finder/files pick up vendor. Exclude tests, .git, node_modules, phpunit.xml, and any .env via "blacklist" or the finder.exclude list. The PHAR should be able to run with zero composer install at the destination.
Sign releases in CI. Build with an OpenSSL key so the archive can't be tampered with:
php -d phar.readonly=0 box build --key=/path/to/private.key --key-pass=env:BOX_KEY_PASS
php -d phar.readonly=0 box verify build/my-tool.phar
Store the key + passphrase as CI secrets; publish the .phar as a release asset. Consumers can check Phar::getSignature() or run box verify.
Wire it into a deploy pipeline:
- run: composer install --no-dev --classmap-authoritative
- run: php -d phar.readonly=0 vendor/bin/box build -v
- run: php -d phar.readonly=0 vendor/bin/box info build/my-tool.phar
- uses: actions/upload-artifact@v4
box info dumps the file manifest and signature — cheap smoke test before publishing.
Versioning pattern: keep the version in your app (e.g. config/app.php or a VERSION file listed in files), and stamp the PHAR name with it: "output": "build/my-tool-${VERSION}.phar" via a small build script, or read from Git in CI.
Debug builds: add "compactors": [] and "compression": "NONE" for a readable archive you can box extract and diff; switch to GZ + PHP/JSON compactors only for release builds.
phar.readonly=0 is non-negotiable. You cannot set it with ini_set(). Every box build invocation in scripts/CI must go through php -d phar.readonly=0 (or a php.ini used only for builds). This is the #1 source of "creating archive ... failed" errors.box-project/box 4.x; the box.json schema, commands, and stub conventions are compatible, so the migration is usually a composer require swap.__DIR__/__FILE__ become phar:// URLs inside the archive. realpath(), is_file(), file_exists(), and getcwd() behave subtly differently. Use Phar::running(false) as the canonical "am I in a PHAR, and where am I" check, and prefer __DIR__-relative paths only if your code tolerates phar:// scheme.storage/, bootstrap/cache/, session/cache files, putenv-based dotenv loading) must be redirected. Dotenv reading .env from inside the archive also won't work — ship defaults and override via real environment variables.composer.json. Box picks the entry via "main"; if you omit it, the archive has no executable stub. __HALT_COMPILER(); must be the last statement in a hand-written stub.dirname(__DIR__), which resolves inside the PHAR correctly only if you require 'phar://name.phar/vendor/autoload.php' (or let the stub do it). If you instead rely on a globally installed Composer autoloader, the archive will boot but be missing classes.chmod +x the output. Box does not always set the executable bit on the resulting .phar. Add "chmod": "0755" to box.json or chmod +x build/*.phar in your release script when using a shebang stub.box verify fails, but the error appears at runtime as phar error: ... failed to verify. Always box verify in CI.php -d display_errors=1 -d phar.readonly=0 build/tool.phar, then box extract build/tool.phar -d /tmp/extracted to inspect exactly what got bundled. Use box info to see the manifest before running.blacklist vs finder.exclude. Both filter content; blacklist is a top-level glob list applied to all sources, finder.exclude only applies within the Finder in paths. Prefer blacklist for repo-wide exclusions (.git, tests, phpunit.xml, .env) so nothing slips in via directories.~/.box; a corrupt cache produces stale archives — delete ~/.box and rebuild. Use --no-parallel (newer box) if you hit flaky builds on CI.pdo_mysql, imagick, etc. must exist on the target machine; box doesn't bundle C extensions. If you're shipping Laravel-dependent code, expect the target to need the same ext-* set.How can I help you explore Laravel packages today?