Phar

Pharはアプリケーションを1ファイルにしたものです。コード、vendor/、コンパイル済みDIスクリプトが1つのアーカイブに収まります。起動はアーカイブを読むだけで、アーカイブには何も書き込みません。デプロイは1ファイルのコピーで、ロールバックは1つ前のファイルです。

app.phar                                            アプリケーション、vendor/、コンパイル済みDIスクリプト
{一時ディレクトリ}/MyVendor/MyProject/{appDirハッシュ}/var   実行時に書き込むもの(tmpとlog)

BEAR.Package 1.24以降が必要です。

書き込み先

アプリケーションは自身のProdModuleで宣言します。

<?php
// src/Module/ProdModule.php
namespace MyVendor\MyProject\Module;

use BEAR\Package\Context\ProdModule as PackageProdModule;
use BEAR\Package\Module\ReadOnlyAppModule;
use Ray\Di\AbstractModule;

class ProdModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new ReadOnlyAppModule());
        $this->install(new PackageProdModule());
    }
}

省略したときは、起動したマシンの一時ディレクトリ(sys_get_temp_dir())の下になります。ツリーの中と同じ形が、そのまま移ります。パスにはアプリケーションディレクトリのハッシュが入るので、同じアプリケーションの別チェックアウトがキャッシュを共有することはありません。

{一時ディレクトリ}/MyVendor/MyProject/{appDirハッシュ}/var/tmp/prod-hal-app
{一時ディレクトリ}/MyVendor/MyProject/{appDirハッシュ}/var/log/prod-hal-app

アーカイブは書き込み先を持たないので、どのマシンでもそのマシンの答えで起動します。ビルドマシンと合わせるものはありません。

パスを渡すと、その通りに使われます。

$this->install(new ReadOnlyAppModule('/var/tmp/myapp', '/var/log/myapp'));

渡した値はコンパイル時に、そのままコンテナに入ります。このビルドを起動するマシンで使える絶対パスを渡してください。書けるかどうかは、何かが書く時点でファイルシステムが答えます。

片方だけ渡すこともできます。渡さなかった方は起動時に決まります。

$this->install(new ReadOnlyAppModule(logDir: '/var/log/myapp'));

Pharにする

ビルドスクリプトはコンパイルし、続けてアーカイブ化します。どちらもコンパイラのメソッドです。

<?php
// bin/compile.php
use BEAR\Package\Compiler;

require dirname(__DIR__) . '/vendor/autoload.php';

ini_set('memory_limit', '-1');

$context = 'prod-hal-app';

$compiler = new Compiler('MyVendor\MyProject', $context, dirname(__DIR__));
$code = $compiler();

exit($code === 0 ? $compiler->phar() : $code);

このスクリプトが名乗るのは、アプリケーション名、起動する context(public/index.phpと同じもの)、アプリケーションのディレクトリです。残りは書く必要がありません。何を収めるかはフレームワークの仕事です: アーカイブに入るのは名前の決まったトップレベルのディレクトリだけで、srcpublicbinvendorvar、そしてインポートしたアプリケーションの置かれた場所です。var/のうち入るのはこのビルドのvar/build/{context}だけで、そこにはコンパイルマーカーを含むDIスクリプトと、compile stepが書いたものが入ります。var/logvar/tmpは入りません。.envautoload.phptests/も同じです。ルート直下のファイルで入るのはpreload.phpだけです。残ったディレクトリはNot packed:として表示されます。マーカーは.bear-compile.jsonappcontexttime)で、phar()はこれを見てコンパイル済みのビルドかどうかを判断します。.envファイル自体は入りませんが、その値はDIスクリプトに焼き込まれ、そのスクリプトは同梱されます。アーカイブは秘密情報として扱ってください。phar.readonlyは子プロセスで処理されるので、iniフラグを覚える必要もありません。

php bin/compile.php
Compiled: 16 resource classes
Phar: /app/app.phar (7.5MB, 2100 files)
Not packed: tests

__invoke()phar()は別の段階なので、CIでコンパイルとアーカイブ化を別ジョブに分けられます。phar()はディスク上のものを詰めるだけで、コンパイルされていない context や、ツリーの中に書くようコンパイルされたものは拒否します。出力先は{appDir}/app.pharで、コンパイルが書いたautoload.phppreload.phpの隣です。引数は別のエントリを渡す1つだけです。

3つの出力はどれも固定パスです。複数 context のときは、次をコンパイルする前にパックしてアーカイブを退避するループにします。

// bin/compile.php
$appDir = dirname(__DIR__);

foreach (['prod-hal-api-app', 'prod-html-app'] as $context) {
    $compiler = new Compiler('MyVendor\MyProject', $context, $appDir);
    $code = $compiler();
    if ($code !== 0) {
        exit($code);
    }

    $code = $compiler->phar();
    if ($code !== 0) {
        exit($code);
    }

    if (! rename($appDir . '/app.phar', $appDir . '/' . $context . '.phar')) {
        exit(1);
    }
}

exit(0);

プロダクションpreload.phpのrenameはここではしません。あれはアーカイブにしないデプロイのためのもので、preloadがディスク上に並んでいる必要があるからです。アーカイブはそれぞれ自分のpreloadをphar://…/{context}.phar/preload.phpに持ちます。パックの前にrenameすると、アーカイブはpreloadなしになります。しかも黙ってそうなります。preloadを使わないビルドも正当なので、何も止めません。

動かす

php app.phar get '/index?name=BEAR'

スタブがアーカイブの中のpublic/index.phpを実行するので、src/Injector.phpdirname(__DIR__)phar:///path/app.pharになります。エントリポイントはRead-only deploymentのままで、他に変更はありません。

php-fpmが実行するのはアーカイブではなくファイルなので、エントリポイントはアーカイブの隣に置き、オートローダーを中から読みます。

<?php
// index.php (app.pharと同じディレクトリ)
require 'phar://' . __DIR__ . '/app.phar/vendor/autoload.php';

exit((new MyVendor\MyProject\Bootstrap())('prod-hal-app', $GLOBALS, $_SERVER));

preload.phpはアーカイブに入るので、opcache.preloadはアーカイブの中を指します。

opcache.preload=phar:///path/to/app.phar/preload.php

autoload.phpは入りません。preloadを使うならすることが残らないからです。同じpreload.phpをアーカイブの隣に置くと、起動時にFailed opening required '…/vendor/autoload.php'で止まります。requireは置かれたディレクトリからの相対で書かれていて、アーカイブの外にそのディレクトリのvendor/はありません。preloadはコンパイルごとに1つ、固定パスに書かれます。最後にコンパイルした context をパックしてください。別の context が残したものはパックが拒否します。

移動できる

アーカイブがビルドそのものです。別のディレクトリでも別のマシンでも、コピーしてそこで起動できます。外のものは何も読まないので、パックした元のツリーは消して構いません。

cp app.phar /srv/releases/2026-08-23.phar
php /srv/releases/2026-08-23.phar get '/index?name=BEAR'

改名しても、別のディレクトリから実行しても同じです。

守ること

$appMeta->appDirから実行時のパスを組むバインディングは書きません。 コンパイル済みスクリプトはビルド時のMetaを持つため、注入されるappDirphar://…ではなくビルド時のディレクトリです(tmpDirlogDirは書き込み先なので正しい値です)。実行時にファイルを読むもの(テンプレートのディレクトリ、データファイルなど)は__DIR__を基点にします。__DIR__はアーカイブの中を指します。

インポートしたアプリケーション

アーカイブの中のインポートしたアプリケーションは別のアプリケーションです。Metaもコンパイル済みスクリプトも、それぞれのものを持ちます。変更は要りません。

$this->install(new ImportAppModule([
    new ImportApp('greeting', 'ImportVendor\Greeting', 'prod-app')
]));

コンパイルはアプリケーションを起動し、その起動がインポートしたアプリケーションをそれぞれのツリーにコンパイルします(ビルドのログに出るCompiled DI scripts on demandがそれです)。DIスクリプトは自動でアーカイブに入ります。

書き込み先は自分で宣言します。ホストのものを継ぎません。

<?php
// imports/greeting/src/Module/ProdModule.php
$this->install(new ReadOnlyAppModule());
{一時ディレクトリ}/ImportVendor/Greeting/{appDirハッシュ}/var/tmp/prod-app
{一時ディレクトリ}/ImportVendor/Greeting/{appDirハッシュ}/var/log/prod-app

宣言がないと自分のツリーに書くことになり、そのツリーはアーカイブの内側なので、パックがPharWritesInsideArchiveExceptionでそのアプリケーションを名指して止まります。

ビルドが止まるとき

以前はデプロイ先で起きていた失敗が、パス入りのメッセージでビルド時に止まります。

エラー 意味
PharNotCompiledException その context がコンパイルされていない。phar()はディスク上のものを詰めます
PharPreloadForAnotherBuildException アプリケーションルートのpreload.phpが別の context のもの。最後にコンパイルした context をパックします
PharImportsUnreadableException コンパイル済みコンテナのimport宣言がこのバージョンでは読めない形式。アーカイブ化するバージョンで再コンパイルします
PharWritesInsideArchiveException ホストまたはインポートしたアプリケーションが、ツリーの中に書く設定でコンパイルされている。ReadOnlyAppModuleをinstallしてコンパイルします
PharImportOutsideTreeException インポートしたアプリケーションが、アーカイブにするツリーの外にある
PharEntryNotFoundException public/index.phpがない。別のエントリはCompiler::phar()に渡します
PharEntryNotPackedException エントリは存在するが同梱されない。アプリケーションルートの直置きファイルで入るのはpreload.phpだけです
PharStaleOutputException 出力先に前回のアーカイブが残っていて、削除できなかった
PharSymlinkedDirectoryException ツリー内のディレクトリが symlink で、Pharが詰められない

コンパイルされていないアーカイブを起動するとNotCompiledExceptionで止まります。アーカイブは書き込めないので、そこでコンパイルすることはできません。

このアーカイブをブラウザの中で動かすにはWasmを参照してください。動くデモはkoriym/wasm-todo公開ページ)です。

背景: BEAR.Package#426