Phar

Pharはアプリケーションを1ファイルにしたものです。コード、vendor/、コンパイル済みDIスクリプトが1つのアーカイブに収まります。アプリケーションはアーカイブに書き込みません。デプロイは1ファイルのコピーで済みます。BEAR.Package 1.24以降が必要です。

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

ReadOnlyAppModule

ReadOnlyAppModuleをインストールすると、実行時の書き込み(var/tmpvar/log)がアプリケーションディレクトリの外に移り、アプリケーションディレクトリは読み込みだけになります。

<?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())の下にvar/tmpvar/logが作られます。パスにはアプリケーションディレクトリのハッシュが含まれるため、同じアプリケーションを別の場所にチェックアウトしてもキャッシュは共有されません。

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

通常はsys_get_temp_dir()が書き込み可能な一時ディレクトリを返すので、設定は不要です。 変えたい場合は、php.iniのsys_temp_dirか環境変数TMPDIRで指定します。 プロジェクトで固定したい場合は、パスを渡すとその通りに使われます。

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

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

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

Pharにする

ビルドスクリプトでは、コンパイルの後にphar()メソッドを呼んで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); // コンパイルだけなら exit($code);

このスクリプトに必要なのは、アプリケーション名、起動する context(public/index.phpと同じもの)、アプリケーションのディレクトリです。残りは書く必要がありません。 この時何をアーカイブするか(pharファイルに収めるか)はフレームワークの仕事です: アーカイブに入るのは名前の決まったトップレベルのディレクトリだけで、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

phar()はディスク上にあるコンパイル結果からアーカイブを作ります。引数はエントリポイントのパス1つで、省略時はpublic/index.phpです。コンパイルされていない context や、ReadOnlyAppModuleなしでコンパイルしたビルドは、アーカイブにする前にエラーで止まります(ビルド時のエラー)。

app.pharは固定パスに書かれ、次のphar()で上書きされます。複数の context をPharにするときは、1つ作るごとにファイル名を変えます。

実行する

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

Pharのスタブがアーカイブ内の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を使う場合、autoload.phpは不要になるからです。コンパイルが書いたpreload.phpをアーカイブの隣に置いて使うことはできません。preload.phpのrequireは自身のディレクトリからの相対パスで書かれていて、アーカイブの外にはvendor/がないため、起動時にFailed opening required '…/vendor/autoload.php'で失敗します。またpreload.phpはコンパイルごとに{appDir}/preload.phpに上書きされるので、phar()は最後にコンパイルした context に対して実行します。別の context のpreload.phpが残っているとPharPreloadForAnotherBuildExceptionになります。

PHPを含んだワンバイナリ

static-php-cliはphpphp-fpmを作ります。ふつうにインストールするのと同じ実行ファイルで、違うのは共有ライブラリに依存しないことだけです。ホストにPHPをインストールする必要も、ディストリビューションのPHPバージョンに合わせる必要もありません。ビルド時にはphar拡張と、アプリケーションが使う拡張を含めます。

./spc download --for-extensions=phar,opcache -P
./spc build phar,opcache --build-cli --build-fpm

実行方法は同じです。ホストのphpの代わりにこのバイナリを使います。

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

php-fpmを使う場合は、ホストのphp-fpmで使っているphp-fpm.conf-yで渡して./buildroot/bin/php-fpmを起動します。エントリポイントの置き方も、opcacheとopcache.preloadの設定も、ホストのPHPと同じです。

1つの実行ファイル(CLI)

コマンドラインのアプリケーションなら、アーカイブとPHPを1ファイルにできます。--build-microはmicro SAPIをmicro.sfxとしてビルドします。後ろに付け足されたものを実行するPHPです。アーカイブを付け足します。

cat micro.sfx app.phar > myapp
chmod +x myapp
./myapp get '/index?name=BEAR'

できるのは、PHPとその拡張、アプリケーションとコンパイル済みDIスクリプトを持った1つの実行ファイルです。名前を変えても、同じプラットフォームの別のマシンにコピーしても、どのディレクトリから起動しても動き、書き込むのはReadOnlyAppModuleが決めた場所だけです。micro SAPIは1つのスクリプトを実行して終了するだけで、リクエストを処理するFPM相当のものはありません。Webのエントリポイントはphp-fpmと、その隣のアーカイブのままです。これはコマンドやBEAR.Cliのツール向けです。PHPバージョンとプラットフォームごとのビルド済みmicro.sfxdl.static-php.devにあります。

別の場所へのコピー

アーカイブは実行に必要なものをすべて含んでいるので、別のディレクトリや別のマシンにコピーしてそのまま起動できます。元のプロジェクトディレクトリを参照することはないので、ビルド後に削除しても問題ありません。

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

ファイル名を変えても、別のディレクトリから実行しても同じように動きます。

注意点

モジュールのconfigure()で実行時のファイルパスを決めることはできません。 コンパイル済みのアプリケーションでは、configure()はコンパイル時に一度だけ実行され、実行時には呼ばれません。configure()で計算した値はDIスクリプトに文字列として記録されます。出どころが$this->appMeta->appDirでも__DIR__でも同じです。テンプレートやSQLファイルなど実行時に読むファイルのパスは、それを読むクラスの中で__DIR__から組むか、Providerでリクエスト時に決めてください。tmpDirlogDirReadOnlyAppModuleが決める書き込み先なので、そのまま使えます。

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

インポートしたアプリケーションもアーカイブに含まれます。インポートしたアプリケーションは独立したアプリケーションとして、自身のMetaとコンパイル済みスクリプトを持ちます。インポートするためのコード変更は必要ありません。

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

ホストアプリケーションをコンパイルすると、その過程でインポートしたアプリケーションもそれぞれのディレクトリにコンパイルされます(ビルドログのCompiled DI scripts on demandがこれにあたります)。生成されたDIスクリプトは自動的にアーカイブに含まれます。

インポートしたアプリケーションの書き込み先はホストの設定を引き継がないので、それぞれのProdModuleReadOnlyAppModuleをインストールします。

<?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

インストールしていないと、インポートしたアプリケーションは自身のディレクトリ、つまりアーカイブの中に書き込む設定になります。この場合phar()は、該当するアプリケーション名を含むPharWritesInsideArchiveExceptionで停止します。

ビルド時のエラー

次のエラーはビルド時に検出されます。メッセージには該当するパスが含まれます。

エラー 意味
PharNotCompiledException その context がコンパイルされていない。先に$compiler()を実行します
PharPreloadForAnotherBuildException アプリケーションルートのpreload.phpが別の context のもの。最後にコンパイルした context に対してphar()を実行します
PharImportsUnreadableException コンパイル済みコンテナのimport宣言が、このバージョンのBEAR.Packageでは読めない形式。同じバージョンで再コンパイルします
PharWritesInsideArchiveException ホストまたはインポートしたアプリケーションが、アプリケーションディレクトリの中に書き込む設定でコンパイルされている。ReadOnlyAppModuleをインストールして再コンパイルします
PharImportOutsideTreeException インポートしたアプリケーションが、アプリケーションディレクトリの外にある
PharEntryNotFoundException public/index.phpがない。別のファイルをエントリにする場合はphar()の引数で指定します
PharEntryNotPackedException エントリに指定したファイルがアーカイブに含まれない。アプリケーションルート直下のファイルで含まれるのはpreload.phpだけです
PharStaleOutputException 出力先に前回のアーカイブが残っていて、削除できなかった
PharSymlinkedDirectoryException アプリケーションディレクトリ内にシンボリックリンクのディレクトリがあり、Pharに追加できない

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

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

背景: BEAR.Package#426