Phar: Packaging a PHP Application into a Single Archive
Build and understand Phar archives -- how they package a whole PHP application into a single executable file, how the phar:// stream wrapper and stub work, how signatures verify archive integrity, and what security considerations apply to running a Phar (especially one from an untrusted source).
Distributing a PHP tool traditionally means distributing a directory tree -- dozens or hundreds of source files, a vendor/ directory full of dependencies, and instructions for wiring it all together with the right include paths. Phar (PHP Archive) solves this the way a .jar file solves it for Java: it bundles an entire application -- source files, resources, dependencies, even a bootstrap script -- into one single file that PHP can execute directly. You've almost certainly already run a Phar without necessarily thinking of it that way: Composer itself, the tool most PHP developers install first, ships as a single composer.phar file. This lesson covers how Phar archives are built and run, the phar:// stream wrapper, stub files, signatures, and the security considerations of running a Phar you didn't build yourself.
What a Phar Archive Is
A Phar file is, structurally, an archive (similar in spirit to a .zip or .tar) containing a full set of PHP source files and any other resources (config files, templates, binary assets) an application needs, plus some Phar-specific metadata: a manifest describing the archive's contents, an optional signature, and a stub -- a small bootstrap script that runs first when the Phar is executed and decides what to load and run. Internally you can build a Phar using Phar::TAR or Phar::ZIP-compatible formats, or Phar's own native format; ext-phar (usually bundled with PHP) provides the Phar and PharData classes used to build, read, and manipulate these archives from PHP itself.
The defining property of a Phar is that it can be run exactly like a standalone script: php app.phar (or, with the right stub and a set executable bit plus a shebang line, even ./app.phar directly on Unix-like systems) executes the application inside it without ever extracting the files to disk first. PHP reads the archive's contents in place and serves require/include calls for files inside it transparently through the phar:// stream wrapper, described next.
This makes Phar the natural distribution format for command-line PHP tools especially: instead of telling a user 'clone this repo, run composer install, then run bin/tool', you hand them one file (composer-installed dependencies and all) and they run it directly -- exactly how PHPUnit, Composer, Box, and many other PHP CLI tools are commonly distributed.
• Phar = a single file bundling PHP source, resources, a manifest, an optional signature, and a stub. • ext-phar's Phar/PharData classes build and inspect archives programmatically from PHP. • A Phar runs in place -- PHP reads directly from the archive, no extraction-to-disk step required. • Common use: distributing a CLI tool (Composer, PHPUnit, Box) as one self-contained executable file.
The phar:// Stream Wrapper and Stub Files
Once ext-phar registers the phar:// stream wrapper, files inside an archive become addressable with ordinary PHP file functions using a phar:// URL: phar:///path/to/app.phar/src/Foo.php is readable with file_get_contents(), requirable with require, and so on, exactly as if it were a real path on disk. This is what lets require/include statements inside the packaged application's own code keep working unmodified once bundled -- the application's internal file references don't need to know they're running from inside an archive rather than a plain directory.
The stub is a small, mandatory PHP snippet embedded at the start of the archive that runs first; a minimal stub typically sets up the phar:// alias, requires the application's actual entry-point file from inside the archive, and ends with the required __HALT_COMPILER(); statement, which marks the exact byte offset where PHP code ends and the binary archive data begins -- PHP's parser stops executing at that point, and everything after it is the packed archive content, not code.
A common stub pattern uses Phar::mapPhar() to register a convenient internal alias, then require 'phar://' . Phar::running() . '/bin/console.php'; or similar to hand off to the real application entry point, so the stub itself stays tiny and all real logic lives in ordinary application files bundled inside the archive.
• phar:// stream wrapper makes archive contents addressable/requirable via ordinary file functions, no unpacking needed. • The stub is the bootstrap code that runs first, typically requiring the app's real entry point from inside the archive. • __HALT_COMPILER(); marks where PHP code ends and binary archive data begins -- it's mandatory in a valid Phar. • Phar::running() returns the currently executing Phar's path, useful inside the stub for building phar:// paths.
PHAR Signatures and Archive Validation
A Phar can optionally be signed -- Phar::setSignatureAlgorithm() (or, for OpenSSL-based signing, providing a private key) embeds a cryptographic signature (SHA-1, SHA-256, or OpenSSL-based) covering the archive's contents at build time. When PHP loads a signed Phar, it verifies the signature automatically and refuses to run the archive if the contents don't match what was signed -- meaning any tampering with the file after it was built and signed (corruption, or a malicious modification) is detected before any of the packaged code ever executes.
A signature verifies integrity (the file hasn't changed since it was signed) but by itself doesn't automatically verify *who* signed it unless you also independently trust and check the specific public key involved -- an OpenSSL-signed Phar's public key is typically distributed alongside the archive (often as a .pubkey file), and verifying that the public key itself actually belongs to the expected publisher (not just that the signature matches some key) is a separate trust decision the consumer has to make, the same way verifying a downloaded binary's checksum only matters if you obtained the expected checksum through a trustworthy channel.
Beyond signatures, basic archive validation includes checking the Phar's manifest is well-formed (Phar's own loading logic will reject a corrupted manifest outright) and, for anything beyond a trivial internal tool, treating an unsigned Phar from an unknown source with real suspicion -- exactly the caution you'd apply to any other unsigned, unverified executable downloaded from the internet.
• Phar signing embeds a cryptographic signature covering the archive's contents at build time. • PHP verifies a signed Phar's integrity automatically on load and refuses to run a tampered archive. • A signature proves integrity (unchanged since signing); trusting *who* signed it requires separately verifying the public key. • Treat an unsigned Phar from an unknown source with the same caution as any other unverified downloaded executable.
How Composer Ships as a Phar, and Deployment/Security Considerations
Composer itself is a compelling real-world case study: composer.phar bundles Composer's entire PHP source tree plus its own bundled dependencies into one file, with a stub that boots the application, so a user only ever needs to download one file and run php composer.phar install -- no separate install step for Composer's own code, no risk of a partially-copied source tree, and a single file whose integrity can be checked against a published hash before running it (which the official Composer installer script actually does automatically).
Deployment-wise, a Phar simplifies shipping a CLI tool (one file to copy, one file to version, one file to put on a PATH) but doesn't fundamentally change how you'd deploy a full web application -- Phars are used far more commonly for command-line tools and libraries-as-binaries than for entire web app deployments, partly because things like file uploads, large temp files, and some extensions interact awkwardly with a read-only, in-archive filesystem model.
Security-wise, running any Phar means running arbitrary PHP code with whatever privileges the invoking process has, exactly like running any other script or executable -- a malicious or compromised Phar downloaded from an untrusted source is no safer than a malicious script would be, and is arguably more dangerous precisely because bundling everything into one opaque file makes casual code review before running it less likely than it would be for a visible multi-file source tree. The practical defenses are the same as for any downloaded executable: verify a published signature or checksum against a trusted source before running it, avoid running Phars from unknown or unverified publishers, and never run an unfamiliar Phar with elevated privileges.
• Composer.phar bundles Composer's own source and dependencies into one file with a booting stub -- the canonical real-world Phar example. • Phars simplify CLI tool distribution more than full web-app deployment, due to filesystem-model friction for the latter. • Running a Phar means running arbitrary PHP code with the invoking process's privileges -- treat it like any other executable. • Verify signatures/checksums from a trusted source before running an unfamiliar Phar; never run one with elevated privileges casually.
Building a Minimal Executable Phar
buildFromDirectory() packs every file under a directory into the archive, preserving relative paths. setStub() must end with __HALT_COMPILER(); -- it marks the boundary between PHP code and binary archive data. php.ini's phar.readonly setting must be off to build/write Phars (it's commonly on by default for safety).
$phar = new Phar('app.phar', 0, 'app.phar');
$phar->buildFromDirectory(__DIR__ . '/src');
$phar->setStub(
"<?php\n" .
"Phar::mapPhar('app.phar');\n" .
"require 'phar://app.phar/entry.php';\n" .
"__HALT_COMPILER();"
);
$phar->setSignatureAlgorithm(Phar::SHA256);Code Examples
buildFromDirectory() packs entry.php into the archive; the stub's shebang line lets the archive be executed directly on Unix once chmod'd, while require 'phar://greet.phar/entry.php' demonstrates the phar:// wrapper pulling a file straight out of the archive at runtime, with no extraction step. Expected result: Built /path/to/greet.phar
$phar->setStub("#!/usr/bin/env php\n<?php\n...__HALT_COMPILER();");
Iterating a Phar object lists its contents like a directory listing, and prefixing a real filesystem path with phar:// makes any file inside the archive readable through ordinary file functions -- both without ever unpacking the archive to a temp directory first. getSignature() surfaces the embedded integrity signature if one was set at build time. This is exactly the mechanism that lets an application's own internal require statements keep working unmodified once it's packaged -- from inside the running Phar, its own files are just phar:// paths.
file_get_contents('phar://' . __DIR__ . '/greet.phar/entry.php');
The unsafe version downloads and immediately executes arbitrary code with zero verification -- if the download URL, a mirror, or the network path is compromised, malicious code runs with full local privileges. The safe version treats the .phar exactly like any other downloaded executable: it compares against a checksum obtained separately (from the publisher's official, trusted documentation) using a constant-time comparison, and refuses to proceed on mismatch.
Common mistake: Piping a downloaded .phar straight into php (or curl ... | php) without ever checking it against a checksum or signature obtained through an independent, trusted channel.
<?php
declare(strict_types=1);
// build.php -- run once (php.ini phar.readonly must be Off) to produce greet.phar
$srcDir = __DIR__ . '/src';
@mkdir($srcDir);
file_put_contents($srcDir . '/entry.php', <<<'PHP'
<?php
$name = $argv[1] ?? 'world';
echo "Hello, {$name}!" . PHP_EOL;
PHP);
$pharFile = __DIR__ . '/greet.phar';
if (file_exists($pharFile)) {
unlink($pharFile);
}
$phar = new Phar($pharFile, 0, 'greet.phar');
$phar->buildFromDirectory($srcDir);
$phar->setStub(
"#!/usr/bin/env php\n" .
"<?php\n" .
"Phar::mapPhar('greet.phar');\n" .
"require 'phar://greet.phar/entry.php';\n" .
"__HALT_COMPILER();"
);
$phar->setSignatureAlgorithm(Phar::SHA256);
chmod($pharFile, 0755);
echo "Built {$pharFile}\n";
// Run it: php greet.phar Alice --> "Hello, Alice!"
// Or, on Unix: ./greet.phar Alice (executable bit + shebang line let it run directly)<?php
declare(strict_types=1);
// Given greet.phar built in the previous example, inspect it without extracting anything.
$phar = new Phar(__DIR__ . '/greet.phar');
foreach ($phar as $file) {
/** @var SplFileInfo $file */
echo $file->getFilename() . ': ' . $file->getSize() . " bytes\n";
}
// Files inside the archive are addressable like ordinary paths through phar://
$contents = file_get_contents('phar://' . __DIR__ . '/greet.phar/entry.php');
echo $contents;
// Signature info, when the Phar was signed:
$sig = $phar->getSignature();
if ($sig !== false) {
echo "Signed with {$sig['hash_type']}: {$sig['hash']}\n";
}<?php
declare(strict_types=1);
// WRONG: download and immediately execute a .phar from the internet with no verification --
// this is functionally identical to running an unknown, unsigned executable.
function fetchAndRunUnsafe(string $url): void
{
file_put_contents('tool.phar', file_get_contents($url));
system('php tool.phar'); // no integrity check of any kind before executing
}
// RIGHT: verify a published checksum/signature obtained through a trusted, separate channel
// before ever executing the downloaded file.
function fetchAndVerify(string $url, string $expectedSha256): bool
{
$bytes = file_get_contents($url);
if ($bytes === false) {
throw new RuntimeException('Download failed');
}
$actualHash = hash('sha256', $bytes);
if (!hash_equals($expectedSha256, $actualHash)) {
throw new RuntimeException('Checksum mismatch -- refusing to run this Phar');
}
file_put_contents('tool.phar', $bytes);
chmod('tool.phar', 0755);
return true; // now safe to run, matching the publisher's known-good checksum
}Phar Archive Anatomy
PHP's parser stops at __HALT_COMPILER(); everything below it is binary archive data, read on demand through the phar:// wrapper.
+--------------------------------------------------+
| Stub (<?php ... require app code ... __HALT_COMPILER();) |
+--------------------------------------------------+
| Manifest (file list, permissions, metadata) |
+--------------------------------------------------+
| Archived files (src/*.php, vendor/, templates/) |
+--------------------------------------------------+
| Signature (optional -- SHA-256 / OpenSSL) |
+--------------------------------------------------+
accessed at runtime via: phar://app.phar/src/Foo.phpImportant Rules
Every Phar's stub must end with __HALT_COMPILER(); -- it marks where executable PHP code ends and archive data begins.
Sign Phars you distribute (setSignatureAlgorithm) so PHP can detect tampering automatically on load.
Never trust a signature's validity without separately verifying the public key actually belongs to the expected publisher.
Verify a checksum or signature obtained from a trusted, independent channel before running any downloaded Phar.
Treat running a Phar exactly like running any other executable -- never run an unfamiliar one with elevated privileges.
Prefer Phar for CLI tool distribution; full web applications usually deploy better as a plain, extracted source tree.
Why This Matters in Production
Composer's own composer.phar is the strongest real-world argument for Phar: millions of developers run it daily as a single downloaded file, and the official installer explicitly verifies a published SHA-384 hash before ever executing it, specifically because running an unverified downloaded executable is a real supply-chain risk, not a theoretical one. Teams building internal CLI tools reach for Phar (often via the Box project, a popular Phar-building tool that adds compression, stub templating, and signing on top of ext-phar) for the same one-file convenience, and teams accepting Phars built by others -- vendor tools, CI plugins -- apply the same discipline Composer's own installer does: verify before you execute.
system('curl -sL https://example.com/tool.phar -o tool.phar && php tool.phar'); // no verification at all
Download the file, compute hash('sha256', $bytes), compare with hash_equals() against a checksum published on the vendor's official site/docs, and only then execute it.
Running a downloaded Phar with zero integrity or provenance check means trusting the network path and hosting completely -- a compromised CDN, DNS hijack, or man-in-the-middle can substitute malicious code that then runs with full local privileges.
$phar->setStub("<?php\nrequire 'phar://app.phar/entry.php';"); // missing __HALT_COMPILER();
$phar->setStub("<?php\nrequire 'phar://app.phar/entry.php';\n__HALT_COMPILER();");
Without __HALT_COMPILER();, PHP has no defined boundary between the stub's PHP source and the following binary archive data, and the Phar is invalid/unbuildable -- this statement is mandatory in every Phar stub, not optional boilerplate.
[multiple-choice] What is the purpose of __HALT_COMPILER(); in a Phar's stub? A. It stops the archive from ever being executed B. It marks the exact boundary where PHP source code ends and the archive's binary data begins -- PHP's parser stops there C. It compresses the rest of the archive D. It is optional boilerplate with no functional effect
B. It marks the exact boundary where PHP source code ends and the archive's binary data begins -- PHP's parser stops there — __HALT_COMPILER(); is mandatory in a Phar stub: it tells PHP's parser exactly where to stop treating the file as PHP source code, with everything after that offset being the packed archive's binary content.
[multiple-choice] What does a Phar signature guarantee, and what does it NOT automatically guarantee? A. It guarantees the code is bug-free but not that it's unmodified B. It guarantees the archive's contents haven't been tampered with since signing, but it does not by itself guarantee that the public key used to verify it actually belongs to the publisher you trust C. It guarantees both integrity and publisher identity automatically with no further checks needed D. It only applies to Phars built with Phar::TAR format
B. It guarantees the archive's contents haven't been tampered with since signing, but it does not by itself guarantee that the public key used to verify it actually belongs to the publisher you trust — A signature proves the file matches exactly what was signed (integrity). Trusting that the signer is who you think they are requires separately and independently verifying the public key itself, the same way a checksum only means something if you obtained the expected value through a trustworthy channel.
[multiple-choice] Why is running an unverified, downloaded Phar from an unknown source risky? A. Phars cannot contain malicious code because PHP sandboxes them automatically B. Running a Phar means running arbitrary PHP code with the invoking process's full privileges, exactly like running any other unverified downloaded executable C. Phars can only read files, never write or execute system commands D. This is not a real risk -- Phar's format itself prevents dangerous operations
B. Running a Phar means running arbitrary PHP code with the invoking process's full privileges, exactly like running any other unverified downloaded executable — A Phar is just packaged PHP code; PHP applies no special sandboxing to code because it happens to be bundled in a Phar. Running one means trusting its contents exactly as much as you'd trust any other executable script from the same source.
Write a build script that packages a directory of PHP source files into a signed, executable Phar with a Unix shebang stub, and a separate verification script that checks a downloaded Phar's SHA-256 checksum against a known-good value before running it.
Build: $phar = new Phar('tool.phar', 0, 'tool.phar'); $phar->buildFromDirectory(__DIR__.'/src'); $phar->setStub("#!/usr/bin/env php\n<?php\nPhar::mapPhar('tool.phar');\nrequire 'phar://tool.phar/entry.php';\n__HALT_COMPILER();"); $phar->setSignatureAlgorithm(Phar::SHA256); chmod('tool.phar', 0755); -- Verify: $bytes = file_get_contents('tool.phar'); if (!hash_equals($knownGoodSha256, hash('sha256', $bytes))) { throw new RuntimeException('checksum mismatch'); } else { system('php tool.phar'); }
Reuse the Phar::buildFromDirectory() + setStub() + setSignatureAlgorithm() pattern for building, and hash()/hash_equals() for verification -- don't call system('php ...') until the checksum check has passed.