tm/tooly-composer-script
Tooly Composer Script integrates Tooly with Composer to simplify running common Laravel/PHP development tasks. Define reusable scripts in composer.json for testing, linting, static analysis, and other tooling, then run them consistently in local workflows and CI.
Tooly is a Composer script (not a Composer plugin) that compiles your project into a phar based on JSON you add to composer.json. It's a lighter, Composer-native alternative to Box.
Minimum setup:
composer require --dev tommy-muehle/tooly-composer-script:^1.4.1
Make sure the PHP binary that runs Composer has phar.readonly disabled:
php -d phar.readonly=0 $(which composer) build
# or permanently: php.ini -> phar.readonly = 0
Add config and a script alias in composer.json:
{
"require-dev": {
"tommy-muehle/tooly-composer-script": "^1.4.1"
},
"extra": {
"tooly": {
"phar": {
"output": "build/app.phar",
"stub": "bin/app.php",
"files": ["src", "vendor", "composer.json"],
"compression": "GZ",
"signature": "SHA256"
}
}
},
"scripts": {
"build:phar": "Tooly\\Composer\\ScriptHandler::createPhar"
}
}
Then:
composer build:phar
php build/app.phar # smoke-test the artifact
Where to look first: the extra.tooly.phar block is the whole mental model — output, stub, files, compression, signature. Everything else is composition.
First real use case in a Laravel project: wrap an artisan/symfony-console entrypoint (bin/app.php) as a distributable CLI, e.g. an internal ops tool or a customer-support "log inspector" binary. Keep it out of public/; phars are executables, not web assets.
Single-binary layout. Keep three things explicit:
output — relative to composer.json, always under a build/dist directory you clean.stub — a thin #!/usr/bin/env php header + require 'phar://app.phar/vendor/autoload.php'; style bootstrap. Don't inline the whole app in the stub.files — a whitelist, not "everything" (vendor, src, composer.json, plus any config/locale dirs your app needs at runtime).Multiple phars in one project. The tooly block is keyed by phar name, so you can emit several artifacts from a single composer build:phar call:
"tooly": {
"cli": { "output": "build/cli.phar", "stub": "bin/cli.php", "files": ["src", "vendor"] },
"cron": { "output": "build/cron.phar", "stub": "bin/cron.php", "files": ["src", "vendor"] }
}
Useful when a Laravel app ships a general CLI plus a smaller "worker" phar with a trimmed dependency set.
Finder-style inclusion for vendored trees. For bigger projects prefer glob/finder semantics over hand-listing every directory — e.g. files: ["src/**/*.php", "vendor/**/*.php", "resources/lang"]. Verify with phar -t or by browsing phar://build/app.phar/ that nothing critical (migrations, translations, config defaults) got silently dropped.
Release workflow. Put the script in a chain, not alone:
"scripts": {
"build:phar": [
"composer install --no-dev --optimize-autoloader",
"Tooly\\Composer\\ScriptHandler::createPhar",
"composer install"
]
}
That gives you a lean vendor tree without leaving the dev environment broken.
CI integration (GitHub Actions). A release job that runs composer build:phar and uploads build/*.phar as a release asset is the highest-value pattern. Two things matter in CI: phar.readonly=0 on the runner (php-actions/composer or an explicit php -d), and generating the phar before --dev dependencies are installed.
Signature as a distribution step. SHA256/SHA512 with a .pubkey allows consumers to phar.require_hash=1 and verify updates; for internal tools SHA1 is fine. This is the one feature where Tooly has no equivalent in a plain phar call and is worth the dependency. Pin ^1.4.1 or later — the checksum generation path had a bug fixed in 1.4.1 (issue #18), so a phar built with 1.4.0 may fail verification even though the build itself succeeded.
Verify the artifact, not the config. Bake a check into the build chain: php build/app.phar --version plus, for signed builds, php -r "echo (new Phar('build/app.phar'))->getSignature()['hash_type'];". A phar that builds but doesn't verify is worse than no phar, because the failure surfaces on a customer machine.
phar.readonly=1 is the #1 failure. The error ("creating archive ... disabled by ini setting") won't point at Composer; it will look like the script is broken. Fix in php.ini for the CLI SAPI, or prefix every invocation with php -d phar.readonly=0.^1.4.1; a bad hash doesn't fail the build, only phar.require_hash=1 at consume time. Add a verification step to CI and never trust "the build succeeded" as proof of a valid signature.strlen(null), preg_match null coercion in the script handler). If you're on PHP 8.3+, smoke-test on a throwaway branch before wiring it into a release.allow-plugins isn't required. But composer install --no-dev then building means your dev-only Tooly package disappears. Use --no-dev only if Tooly is installed globally or the build script itself doesn't need re-resolving after.files paths are resolved relative to composer.json, not the script's cwd. Globs use PHP's glob/Finder semantics — * doesn't cross directory boundaries the way some expect; prefer explicit directory names over ambitious globs until you can inspect the output.phar://build/app.phar/ or tar-style tools and check vendor/composer/autoload_*.php. Almost always a missing vendor subdir or a PSR-4 root that got excluded.__DIR__. Inside a phar, __DIR__ points into phar://, so require __DIR__.'/../vendor/autoload.php' fails. Use Phar::running(false) to resolve the on-disk location when you need to read external files (e.g., a .env next to the phar).GZ gives the smallest artifact but decompresses the whole entry on require; BZ2 is smaller and slower; NONE is best when the phar is invoked in a hot loop (cron, queue worker). Benchmark once with hyperfine 'php build/app.phar list' and pick.signature is set, tests that unpack and patch the phar will invalidate it; rebuild instead. This bites hardest during incident debugging — keep an unsigned build/debug.phar target in extra.tooly for poking at internals.~/.local/bin", "write a .sha256 sidecar", or "verify the signature we just generated" (worth doing on 1.4.1), chain it as another Composer script in the same array. That's the intended extension point.tests/, .git, storage/, or node_modules. The excluded list (or the inverse files whitelist) is the only thing standing between you and a 200 MB phar. Check ls -lh build/app.phar before and after every config change.composer.lock inside the phar if you ever need to debug a customer's installed version — it's the cheapest forensic breadcrumb.How can I help you explore Laravel packages today?