diff --git a/_config.yml b/_config.yml index b6c65cc1..b48cc52f 100644 --- a/_config.yml +++ b/_config.yml @@ -15,6 +15,11 @@ repository: bearsunday/bearsunday.github.io include: - "manuals" +# Exclude sample apps and their vendor dirs (composer install can create +# broken symlinks, e.g. bear/devtools' xhprof_html, that crash Jekyll's reader) +exclude: + - "MyVendor.Ticket" + # Exclude from processing but include as static files keep_files: - "manuals" diff --git a/_includes/manuals/1.0/en/contents.html b/_includes/manuals/1.0/en/contents.html index 1adfc76f..7e8907c6 100644 --- a/_includes/manuals/1.0/en/contents.html +++ b/_includes/manuals/1.0/en/contents.html @@ -73,6 +73,9 @@ + diff --git a/_includes/manuals/1.0/ja/contents.html b/_includes/manuals/1.0/ja/contents.html index 2c4e498c..22e783e7 100644 --- a/_includes/manuals/1.0/ja/contents.html +++ b/_includes/manuals/1.0/ja/contents.html @@ -73,6 +73,9 @@ + diff --git a/manuals/1.0/en/phar.md b/manuals/1.0/en/phar.md new file mode 100644 index 00000000..70da8178 --- /dev/null +++ b/manuals/1.0/en/phar.md @@ -0,0 +1,157 @@ +--- +layout: docs-en +title: Phar +category: Manual +permalink: /manuals/1.0/en/phar.html +--- + +# Phar + +A [phar](https://www.php.net/manual/en/intro.phar.php) is the application as one file: the code, `vendor/`, and the compiled DI scripts in a single archive. The boot reads the archive and writes nothing into it — a deploy is a copy of one file, a rollback is the file before it. + +```text +app.phar the application, vendor/, compiled DI scripts +/tmp/MyVendor/MyProject/prod-hal-app everything the runtime writes +``` + +Requires BEAR.Package 1.24+. + +## Make your application a phar + +The build script compiles, then packs. Both steps are on the compiler: + +```php +phar() : $code); +``` + +The script names the application, the context it boots — the same one `public/index.php` uses — and reads the write directory from the environment. The rest it does not have to say, because packing is the framework's business: the archive carries named top-level directories only — `src`, `public`, `bin`, `vendor`, `var`, and wherever an imported application sits — and of `var/` only this build: `var/build/{context}`, which holds the DI scripts with their compile marker and whatever [compile steps](production.html#compile-steps) wrote. `var/log` and `var/tmp` stay out, as do `.env`, `autoload.php` and `tests/`; of the files at the root only `preload.php` ships; the directories left behind are printed as `Not packed:`. The marker is `.bear-compile.json`, and it is what `phar()` reads to decide: `app`, `context`, `tmpDir`, `time`. The `.env` file stays out, but the values it held are compiled into the DI scripts, and those ship: treat the archive as a secret. `phar.readonly` is handled in a child process, so there is no ini flag to remember. + +```bash +APP_WRITE_DIR=/tmp php bin/compile.php +``` + +```text +Compiled: 16 resource classes +Phar: /app/app.phar (7.5MB, 2100 files) +Not packed: tests +``` + +`__invoke()` and `phar()` are separate steps, so a build pipeline can compile in one job and pack in another. `phar()` packs what is on disk and refuses a context that was never compiled, or one compiled to write inside the tree. It writes `{appDir}/app.phar`, beside the `autoload.php` and `preload.php` the compile wrote; another entry is its one argument. + +All three are fixed paths, so several contexts are a loop that packs each one before it compiles the next, and moves the archive aside: + +```php +// bin/compile.php +$appDir = dirname(__DIR__); +$writeDir = getenv('APP_WRITE_DIR') ?: null; + +foreach (['prod-hal-api-app', 'prod-html-app'] as $context) { + $compiler = new Compiler('MyVendor\MyProject', $context, $appDir, $writeDir); + $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); +``` + +Not the `preload.php` rename [Production](production.html#compilation-recommended) shows: that one is for a deployment without an archive, where the preloads have to sit side by side on disk. Each archive carries its own at `phar://…/{context}.phar/preload.php`, and one renamed before the pack leaves the archive with none — silently, because an application that uses no preload is a legitimate build. + +## Run + +```bash +APP_WRITE_DIR=/tmp php app.phar get '/index?name=BEAR' +``` + +The stub runs `public/index.php` from inside the archive, so `dirname(__DIR__)` in `src/Injector.php` is `phar:///path/app.phar`. The entry points are the ones [read-only deployments](production.html#writable-paths) shows; nothing else changes. + +php-fpm runs a file, not an archive, so the entry point sits next to it and loads the autoloader from inside: + +```php +appDir`.** The compiled scripts carry the `Meta` of the build, so the injected `appDir` is the build directory, not `phar://…`; `tmpDir` and `logDir` are the write directory and are correct. Anything that reads a file at runtime — a template directory, a data file — takes its path from `__DIR__`, which resolves inside the archive. + +## Imported applications + +An [imported application](import.html) in the archive is a second application: its own `Meta`, its own compiled scripts, its own write directory. It needs no change at all - the container hands it the write directory the host was given: + +```php +$this->install(new ImportAppModule([ + new ImportApp('greeting', 'ImportVendor\Greeting', 'prod-app') +])); +``` + +The compile boots the application, that boot compiles each imported application into its own tree (`Compiled DI scripts on demand` in the build log is that), and the pack ships their DI scripts automatically. An imported application resolves its own directory at boot, so it follows the archive. + +## When the build stops + +Everything that used to fail at the deploy fails at the build, with the path in the message: + +| Error | Meaning | +|---|---| +| `PharNotCompiledException` | The context was never compiled: `phar()` packs what is on disk | +| `PharPreloadForAnotherBuildException` | The `preload.php` at the application root was written by another context: pack the context you compiled last | +| `PharImportsUnreadableException` | The compiled container declares its imports in a form this version cannot read: recompile with the version that packs | +| `PharWritesInsideArchiveException` | An application — the host or an import — was compiled to write into the tree. Compile with `APP_WRITE_DIR` set | +| `PharImportOutsideTreeException` | An imported application lies outside the tree being packed and cannot ship in it | +| `PharEntryNotFoundException` | No `public/index.php`; pass another entry to `Compiler::phar()` | +| `PharEntryNotPackedException` | The entry exists but does not ship: of the files loose at the application root only `preload.php` does | +| `PharStaleOutputException` | An archive of a previous build survived at the output path and could not be removed | +| `PharSymlinkedDirectoryException` | A directory in the tree is a symlink, which `Phar` cannot pack | + +At boot, an archive started without `APP_WRITE_DIR` stops with `WriteDirRequiredException`, and one started with a different `APP_WRITE_DIR` than the build stops with `CompiledForAnotherWriteDirException`, naming both directories. + +Background: [BEAR.Package#426](https://github.com/bearsunday/BEAR.Package/issues/426). diff --git a/manuals/1.0/en/production.md b/manuals/1.0/en/production.md index 81b20b12..c6c894ac 100644 --- a/manuals/1.0/en/production.md +++ b/manuals/1.0/en/production.md @@ -161,7 +161,8 @@ Refer to the [existing implementation ProdLogger](https://github.com/bearsunday/ * It is recommended to incorporate compilation into CI as the compiler outputs exit code 1 when it finds dependency issues and 0 when compilation succeeds. -### Compilation Recommended {: #compilation } + +### Compilation Recommended When setting up, you can **warm up** the project: create static cache files for DI/AOP and annotations in advance, and write optimized `autoload.php` and `preload.php`. @@ -178,61 +179,93 @@ use BEAR\Package\Compiler; require dirname(__DIR__) . '/vendor/autoload.php'; +ini_set('memory_limit', '-1'); + // Load build-time-only stubs (null objects / fake env) if present. $dotCompile = dirname(__DIR__) . '/.compile.php'; is_file($dotCompile) && require $dotCompile; $context = $argv[1] ?? 'prod-app'; -$writeDir = $argv[2] ?? null; +$writeDir = getenv('APP_WRITE_DIR') ?: null; exit((new Compiler('MyVendor\MyProject', $context, dirname(__DIR__), $writeDir))()); ``` -`Compiler::fromInjector($injector, $context, $writeDir)` is for a caller that already holds an injector - a command inside a running application. A build script does not use it. +The script names the application, the context and the write directory, and it does not boot the application. `.compile.php` build stubs are loaded by the Compiler itself. `Compiler::phar()` packs the compiled result into one archive (BEAR.Package 1.24+): [Phar](phar.html). ```json "scripts": { - "compile": "php bin/compile.php prod-app" + "compile": "php bin/compile.php" } ``` * If you compile, the possibility of DI errors at runtime is extremely low because injection is performed in all classes. * The contents included in `.env` are incorporated into the PHP file, so `.env` can be deleted after compilation. -When compiling multiple contexts (e.g. api-app and html-app for content negotiation), call `bin/compile.php` per context and evacuate project-root `autoload.php` / `preload.php` so a later compile does not overwrite them. +Compiling multiple contexts (e.g. api-app and html-app for content negotiation) is a loop in the script. `autoload.php` and `preload.php` are written to fixed paths and the next compile removes them, so rename them as you go: + +```php +// bin/compile.php +$appDir = dirname(__DIR__); +$writeDir = getenv('APP_WRITE_DIR') ?: null; + +foreach (['prod-hal-api-app', 'prod-html-app'] as $context) { + $code = (new Compiler('MyVendor\MyProject', $context, $appDir, $writeDir))(); + if ($code !== 0) { + exit($code); + } + + foreach (['preload.php', 'autoload.php'] as $written) { + if (! rename($appDir . '/' . $written, $appDir . '/' . $context . '.' . $written)) { + exit(1); + } + } +} -```bash -php bin/compile.php prod-hal-api-app -mv autoload.php api.autoload.php -mv preload.php api.preload.php -php bin/compile.php prod-html-app +exit(0); ``` [`opcache.preload`](https://www.php.net/manual/en/opcache.preloading.php) is a per-process setting, so preloading multiple contexts means **separate PHP processes** (e.g. php-fpm pools), each pointing at its evacuated preload (e.g. the api pool: `opcache.preload=/path/to/api.preload.php`). In the example the html side keeps the default name because its process points at the default `preload.php`. -DI scripts are written under `{appDir}/var/tmp/{context}/di`. They are a build output: when the artifact carries them, runtime reads them instead of compiling. +Packing each context into an archive is a loop of its own, and it does not rename the preload: [Phar](phar.html). + +DI scripts are written under `{appDir}/var/build/{context}/di`. The build directory holds what a compile produced and nothing a request writes, so it can ship read-only: when the artifact carries it, runtime reads the scripts instead of compiling. `vendor/bin/bear.compile` is deprecated. Migration: [BEAR.Package#482](https://github.com/bearsunday/BEAR.Package/issues/482). +#### Compile steps {#compile-steps} + +A module can bind a compile step — `BEAR\Sunday\Compile\CompileStepInterface` — and the compile runs it. Each step is handed an empty directory of its own under the build directory, named after its binding key, and what it writes ships with the artifact: + +```text +{appDir}/var/build/{context}/di compiled DI scripts +{appDir}/var/build/{context}/qiq the templates Qiq compiled +{appDir}/var/build/{context}/twig Twig's cache +``` + +Template engines use this: the first request has nothing left to compile, and nothing under the application root has to be writable for them. A step that fails leaves no compile marker, so the next boot compiles again rather than serve a build whose templates never arrived. + +Requires bear/sunday 1.9+. Background: [BEAR.Package#501](https://github.com/bearsunday/BEAR.Package/pull/501). + #### Read-only deployments (serverless, immutable containers) {#writable-paths} Serverless platforms and immutable containers restrict where an application may write. On Vercel or AWS Lambda, or in a container started with `docker run --read-only` / `readOnlyRootFilesystem: true`, the project directory is read-only and one directory - `/tmp`, typically - is the only writable location. Ordinary VPS and shared hosting are unaffected. Tell the application which directory it may write to. Pass the same directory to both the build and the boot, and keep to two rules: -* Pass an absolute path. A relative path throws `InvalidWriteDirException`. -* Pass the same path to the build and the boot. The paths are compiled into the DI scripts, so a compile whose write directory differs from the injector it was handed throws `WriteDirMismatchException`. +* Pass an absolute path. A relative path throws `WriteDirNotAbsoluteException` where the `Meta` is built. +* Pass the same path to the build and the boot. The paths are compiled into the DI scripts, so a boot given another one compiles again where it can write, and stops with `CompiledForAnotherWriteDirException` where it cannot. -`$writeDir` is the optional last argument on `Bootstrap::__invoke()`, `Injector::getInstance()` and `new Compiler()`. Change the entry points like this: +`$writeDir` is the optional last argument on `Bootstrap::__invoke()`, `Injector::getInstance()`, `Injector::getOverrideInstance()` and `new Compiler()`. Change the entry points like this: ```diff // public/index.php -exit((new Bootstrap())('prod-app', $GLOBALS, $_SERVER)); +exit((new Bootstrap())('prod-app', $GLOBALS, $_SERVER, getenv('APP_WRITE_DIR') ?: null)); - // bin/compile.php php bin/compile.php prod-app /tmp + // bin/compile.php APP_WRITE_DIR=/tmp php bin/compile.php -exit((new Compiler('MyVendor\MyProject', $context, dirname(__DIR__)))()); -+$writeDir = $argv[2] ?? null; ++$writeDir = getenv('APP_WRITE_DIR') ?: null; + +exit((new Compiler('MyVendor\MyProject', $context, dirname(__DIR__), $writeDir))()); @@ -261,16 +294,16 @@ Tell the application which directory it may write to. Pass the same directory to - } + public static function getInstance(string $context, string|null $writeDir = null): InjectorInterface + { -+ return PackageInjector::getInstance(__NAMESPACE__, $context, dirname(__DIR__), null, $writeDir); ++ return PackageInjector::getInstance(__NAMESPACE__, $context, dirname(__DIR__), writeDir: $writeDir); + } ``` -`BEAR\Package\Injector` builds the `Meta` and the injector cache pool from the write directory, so the skeleton's own `Meta` / `LocalCacheProvider` lines go away. Development entry points pass nothing and keep the default paths. Reading environment variables is the entry point's business, not the framework's. +`BEAR\Package\Injector` builds the `Meta` and the injector cache pool from the write directory, so the skeleton's own `Meta` / `LocalCacheProvider` lines go away. Development entry points pass nothing and keep the default paths. -The build takes the directory as an argument, the runtime as an environment variable: +`APP_WRITE_DIR` is the one source, for the build and for the runtime — `AppModule` runs during the compile, so what it reads must be what the build uses: ```text -build php bin/compile.php prod-app /tmp +build APP_WRITE_DIR=/tmp php bin/compile.php runtime APP_WRITE_DIR=/tmp php-fpm env[APP_WRITE_DIR] = /tmp docker --env APP_WRITE_DIR=/tmp @@ -279,7 +312,7 @@ runtime APP_WRITE_DIR=/tmp With `/tmp` as the write directory, the layout is: ```text -{appDir}/var/tmp/{context}/di compiled DI scripts, in the artifact +{appDir}/var/build/{context}/di compiled DI scripts, in the artifact /tmp/MyVendor/MyProject/{context}/tmp query repository cache, serialized injector /tmp/MyVendor/MyProject/{context}/log ``` @@ -288,7 +321,9 @@ The application and the context are in the path because local cache keys are res Compiled DI scripts stay under `appDir` and ship inside the artifact. A new instance starts with an empty `/tmp`, so following the write directory would compile again on every cold start - 0.38s against 0.018s on a five-resource application. -If the boot is given a different write directory than the build used, the DI scripts are compiled again instead of read from the old paths: the compile fails if the artifact is read-only, and emits a `Compiled DI scripts on demand` notice if it is writable. +If the boot is given a different write directory than the build used, the DI scripts are compiled again instead of read from the old paths: a read-only artifact stops with `CompiledForAnotherWriteDirException`, naming both directories, and a writable one emits a `Compiled DI scripts on demand` notice. + +A single-file artifact that never writes into itself is [Phar](phar.html). Requires BEAR.Package 1.22+. Background: [BEAR.Package#491](https://github.com/bearsunday/BEAR.Package/pull/491). @@ -317,7 +352,7 @@ Note: Please refer to the [benchmark](https://github.com/bearsunday/BEAR.Hellowo When there are classes that cannot be generated in a non-production environment (for example, a ResourceObject that requires successful authentication to complete injection), you can compile them by describing dummy class loading in the root `.compile.php` file, which is only loaded during compilation. **Its purpose is to let construction succeed at compile time, so its contents should be null objects (do-nothing implementations).** This applies not only to ahead-of-time builds (real services unreachable) but also to resources that need per-request state such as authentication, which is absent during compilation even when you compile on the deploy target. Keep value fakes (`$_SERVER['X'] = 'fake'`, etc.) to the minimum needed to pass construction, and never use them for values that must be real at runtime (they get baked in). -**Note (BEAR.Package 1.21+):** `Compiler::fromInjector()` does not load `.compile.php` automatically (the deprecated `bear.compile` did). Load it from your `bin/compile.php` as shown above. +**Note:** the compiler loads `.compile.php` itself, before it builds the container. The deprecated `vendor/bin/bear.compile` did too; nothing else has to. .compile.php diff --git a/manuals/1.0/en/tutorial.md b/manuals/1.0/en/tutorial.md index 7b428675..219ca8a6 100644 --- a/manuals/1.0/en/tutorial.md +++ b/manuals/1.0/en/tutorial.md @@ -841,7 +841,7 @@ require dirname(__DIR__) . '/autoload.php'; exit((new Bootstrap())('prod-hal-app', $GLOBALS, $_SERVER)); ``` -PHP code that generates instances according to the context is created. Check the `var/tmp/{context}/di` folder of the application. +PHP code that generates instances according to the context is created. Check the `var/build/{context}/di` folder of the application. You don't usually need to see these files, but you can check how the objects are created. ## REST API diff --git a/manuals/1.0/ja/phar.md b/manuals/1.0/ja/phar.md new file mode 100644 index 00000000..2dc2d6ca --- /dev/null +++ b/manuals/1.0/ja/phar.md @@ -0,0 +1,157 @@ +--- +layout: docs-ja +title: Phar +category: Manual +permalink: /manuals/1.0/ja/phar.html +--- + +# Phar + +[Phar](https://www.php.net/manual/ja/intro.phar.php)はアプリケーションを1ファイルにしたものです。コード、`vendor/`、コンパイル済みDIスクリプトが1つのアーカイブに収まります。起動はアーカイブを読むだけで、アーカイブには何も書き込みません。デプロイは1ファイルのコピーで、ロールバックは1つ前のファイルです。 + +```text +app.phar アプリケーション、vendor/、コンパイル済みDIスクリプト +/tmp/MyVendor/MyProject/prod-hal-app 実行時に書き込むものすべて +``` + +BEAR.Package 1.24以降が必要です。 + +## Pharにする + +ビルドスクリプトはコンパイルし、続けてアーカイブ化します。どちらもコンパイラのメソッドです。 + +```php +phar() : $code); +``` + +このスクリプトが名乗るのは、アプリケーション名、起動する context(`public/index.php`と同じもの)、そして環境変数から読む書き込み先です。残りは書く必要がありません。何を収めるかはフレームワークの仕事です: アーカイブに入るのは名前の決まったトップレベルのディレクトリだけで、`src`、`public`、`bin`、`vendor`、`var`、そしてインポートしたアプリケーションの置かれた場所です。`var/`のうち入るのはこのビルドの`var/build/{context}`だけで、そこにはコンパイルマーカーを含むDIスクリプトと、[compile step](production.html#compile-steps)が書いたものが入ります。`var/log`と`var/tmp`は入りません。`.env`、`autoload.php`、`tests/`も同じです。ルート直下のファイルで入るのは`preload.php`だけです。残ったディレクトリは`Not packed:`として表示されます。マーカーは`.bear-compile.json`で、`phar()`はこれを見て判断します(`app`、`context`、`tmpDir`、`time`)。`.env`ファイル自体は入りませんが、その値はDIスクリプトに焼き込まれ、そのスクリプトは同梱されます。アーカイブは秘密情報として扱ってください。`phar.readonly`は子プロセスで処理されるので、iniフラグを覚える必要もありません。 + +```bash +APP_WRITE_DIR=/tmp php bin/compile.php +``` + +```text +Compiled: 16 resource classes +Phar: /app/app.phar (7.5MB, 2100 files) +Not packed: tests +``` + +`__invoke()`と`phar()`は別の段階なので、CIでコンパイルとアーカイブ化を別ジョブに分けられます。`phar()`はディスク上のものを詰めるだけで、コンパイルされていない context や、ツリーの中に書くようコンパイルされたものは拒否します。出力先は`{appDir}/app.phar`で、コンパイルが書いた`autoload.php`と`preload.php`の隣です。引数は別のエントリを渡す1つだけです。 + +3つの出力はどれも固定パスです。複数 context のときは、次をコンパイルする前にパックしてアーカイブを退避するループにします。 + +```php +// bin/compile.php +$appDir = dirname(__DIR__); +$writeDir = getenv('APP_WRITE_DIR') ?: null; + +foreach (['prod-hal-api-app', 'prod-html-app'] as $context) { + $compiler = new Compiler('MyVendor\MyProject', $context, $appDir, $writeDir); + $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); +``` + +[プロダクション](production.html#compilation-recommended)の`preload.php`のrenameはここではしません。あれはアーカイブにしないデプロイのためのもので、preloadがディスク上に並んでいる必要があるからです。アーカイブはそれぞれ自分のpreloadを`phar://…/{context}.phar/preload.php`に持ちます。パックの前にrenameすると、アーカイブはpreloadなしになります。しかも黙ってそうなります。preloadを使わないビルドも正当なので、何も止めません。 + +## 動かす + +```bash +APP_WRITE_DIR=/tmp php app.phar get '/index?name=BEAR' +``` + +スタブがアーカイブの中の`public/index.php`を実行するので、`src/Injector.php`の`dirname(__DIR__)`は`phar:///path/app.phar`になります。エントリポイントは[読み取り専用デプロイ](production.html#writable-paths)のままで、他に変更はありません。 + +php-fpmが実行するのはアーカイブではなくファイルなので、エントリポイントはアーカイブの隣に置き、オートローダーを中から読みます。 + +```php +appDir`から実行時のパスを組むバインディングは書きません。** コンパイル済みスクリプトはビルド時の`Meta`を持つため、注入される`appDir`は`phar://…`ではなくビルド時のディレクトリです(`tmpDir`と`logDir`は書き込み先なので正しい値です)。実行時にファイルを読むもの(テンプレートのディレクトリ、データファイルなど)は`__DIR__`を基点にします。`__DIR__`はアーカイブの中を指します。 + +## インポートしたアプリケーション + +アーカイブの中の[インポートしたアプリケーション](import.html)は別のアプリケーションです。`Meta`もコンパイル済みスクリプトも書き込み先も、それぞれのものを持ちます。変更は要りません。ホストに渡した書き込み先はコンテナが渡します。 + +```php +$this->install(new ImportAppModule([ + new ImportApp('greeting', 'ImportVendor\Greeting', 'prod-app') +])); +``` + +コンパイルはアプリケーションを起動し、その起動がインポートしたアプリケーションをそれぞれのツリーにコンパイルします(ビルドのログに出る`Compiled DI scripts on demand`がそれです)。DIスクリプトは自動でアーカイブに入ります。インポートしたアプリケーションのディレクトリは起動時に解決されるので、アーカイブの移動に追従します。 + +## ビルドが止まるとき {#when-the-build-stops} + +以前はデプロイ先で起きていた失敗が、パス入りのメッセージでビルド時に止まります。 + +| エラー | 意味 | +|---|---| +| `PharNotCompiledException` | その context がコンパイルされていない。`phar()`はディスク上のものを詰めます | +| `PharPreloadForAnotherBuildException` | アプリケーションルートの`preload.php`が別の context のもの。最後にコンパイルした context をパックします | +| `PharImportsUnreadableException` | コンパイル済みコンテナのimport宣言がこのバージョンでは読めない形式。アーカイブ化するバージョンで再コンパイルします | +| `PharWritesInsideArchiveException` | ホストまたはインポートしたアプリケーションが、ツリーの中に書く設定でコンパイルされている。`APP_WRITE_DIR`を設定してコンパイルします | +| `PharImportOutsideTreeException` | インポートしたアプリケーションが、アーカイブにするツリーの外にある | +| `PharEntryNotFoundException` | `public/index.php`がない。別のエントリは`Compiler::phar()`に渡します | +| `PharEntryNotPackedException` | エントリは存在するが同梱されない。アプリケーションルートの直置きファイルで入るのは`preload.php`だけです | +| `PharStaleOutputException` | 出力先に前回のアーカイブが残っていて、削除できなかった | +| `PharSymlinkedDirectoryException` | ツリー内のディレクトリが symlink で、`Phar`が詰められない | + +起動時に`APP_WRITE_DIR`なしでアーカイブを開始すると`WriteDirRequiredException`で、ビルドと違う`APP_WRITE_DIR`で開始すると両方のパスを名指しする`CompiledForAnotherWriteDirException`で止まります。 + +背景: [BEAR.Package#426](https://github.com/bearsunday/BEAR.Package/issues/426) diff --git a/manuals/1.0/ja/production.md b/manuals/1.0/ja/production.md index 10ba2c6d..fbce8b62 100644 --- a/manuals/1.0/ja/production.md +++ b/manuals/1.0/ja/production.md @@ -156,7 +156,8 @@ final class MyProdLoggerModule extends AbstractModule #### クラウドにディプロイする時には * コンパイルが成功すると0、依存関係の問題を見つけるとコンパイラはexitコード1を出力します。それを利用してCIにコンパイルを組み込むことを推奨します。 -### コンパイル {: #compilation } + +### コンパイル {#compilation-recommended} セットアップ時にプロジェクトを**ウォームアップ**できます。DI/AOP 用の動的ファイルやアノテーションなどの静的キャッシュを事前に作成し、最適化された `autoload.php` と `preload.php` を出力します。 @@ -173,59 +174,93 @@ use BEAR\Package\Compiler; require dirname(__DIR__) . '/vendor/autoload.php'; +ini_set('memory_limit', '-1'); + // Load build-time-only stubs (null objects / fake env) if present. $dotCompile = dirname(__DIR__) . '/.compile.php'; is_file($dotCompile) && require $dotCompile; $context = $argv[1] ?? 'prod-app'; -$writeDir = $argv[2] ?? null; +$writeDir = getenv('APP_WRITE_DIR') ?: null; exit((new Compiler('MyVendor\MyProject', $context, dirname(__DIR__), $writeDir))()); ``` -`Compiler::fromInjector($injector, $context, $writeDir)`は、すでにinjectorを持っている呼び出し元(動作中のアプリケーション内のコマンドなど)のためのものです。ビルドスクリプトでは使いません。 +スクリプトが名乗るのはアプリケーション名、context、書き込み先で、アプリケーションは起動しません。`.compile.php`のビルド用スタブはCompiler自身が読み込みます。`Compiler::phar()`はコンパイル結果を1つのアーカイブにします(BEAR.Package 1.24以降)→ [Phar](phar.html) ```json "scripts": { - "compile": "php bin/compile.php prod-app" + "compile": "php bin/compile.php" } ``` * コンパイルをすれば全てのクラスでインジェクションを行うのでランタイムでDIのエラーが出る可能性が極めて低くなります。 -* `.env`に含まれた内容はPHPファイルに取り込まれるのでコンパイル後に`.env`を消去可能です。コンテントネゴシエーションを行う場合など(例:api-app, html-app)1つのアプリケーションで複数コンテキストのコンパイルを行うときには、コンテキストごとに `bin/compile.php` を呼び、プロジェクト直下に出る `autoload.php` / `preload.php` を退避します(後続コンパイルで上書きされないようにします)。 +* `.env`に含まれた内容はPHPファイルに取り込まれるのでコンパイル後に`.env`を消去可能です。 -```bash -php bin/compile.php prod-hal-api-app -mv autoload.php api.autoload.php -mv preload.php api.preload.php -php bin/compile.php prod-html-app +コンテントネゴシエーションを行う場合など(例:api-app, html-app)1つのアプリケーションで複数コンテキストをコンパイルするときは、スクリプト内のループにします。`autoload.php`と`preload.php`は固定パスに書かれ、次のコンパイルで消えるので、その都度 rename します。 + +```php +// bin/compile.php +$appDir = dirname(__DIR__); +$writeDir = getenv('APP_WRITE_DIR') ?: null; + +foreach (['prod-hal-api-app', 'prod-html-app'] as $context) { + $code = (new Compiler('MyVendor\MyProject', $context, $appDir, $writeDir))(); + if ($code !== 0) { + exit($code); + } + + foreach (['preload.php', 'autoload.php'] as $written) { + if (! rename($appDir . '/' . $written, $appDir . '/' . $context . '.' . $written)) { + exit(1); + } + } +} + +exit(0); ``` [`opcache.preload`](https://www.php.net/manual/ja/opcache.preloading.php) は PHP プロセス単位の設定です。複数コンテキストを preload する場合は**それぞれ別プロセス(php-fpm プール等)**になり、プロセスごとに退避した preload を指します(例:api 用プールは `opcache.preload=/path/to/api.preload.php`)。上の例で html 側を既定名のままにしているのは、そのプロセスが既定の `preload.php` を指すからです。 -DIスクリプトの出力先は`{appDir}/var/tmp/{context}/di`です。これはビルド成果物で、成果物に同梱されていれば実行時はコンパイルせず読むだけです。 +context ごとにアーカイブにする場合はループが別で、preloadのrenameもしません → [Phar](phar.html) + +DIスクリプトの出力先は`{appDir}/var/build/{context}/di`です。ビルドディレクトリにはコンパイルが作ったものだけが入り、リクエストが書くものは入りません。だから読み取り専用で配れます。成果物に同梱されていれば実行時はコンパイルせず読むだけです。 `vendor/bin/bear.compile` は非推奨です。移行手順は [BEAR.Package#482](https://github.com/bearsunday/BEAR.Package/issues/482) を参照してください。 +#### compile step {#compile-steps} + +モジュールは compile step(`BEAR\Sunday\Compile\CompileStepInterface`)をバインドでき、コンパイルがそれを実行します。step にはビルドディレクトリの下に自分専用の空のディレクトリが渡されます。名前はバインディングのキーで、書いたものは成果物に同梱されます。 + +```text +{appDir}/var/build/{context}/di コンパイル済みDIスクリプト +{appDir}/var/build/{context}/qiq Qiqがコンパイルしたテンプレート +{appDir}/var/build/{context}/twig Twigのキャッシュ +``` + +テンプレートエンジンがこれを使います。最初のリクエストでコンパイルするものは残っておらず、そのためにアプリケーションルート配下を書き込み可能にする必要もありません。step が失敗するとコンパイルマーカーが残らないので、テンプレートの無いビルドを配信するのではなく、次の起動が再びコンパイルします。 + +bear/sunday 1.9以降が必要です。背景: [BEAR.Package#501](https://github.com/bearsunday/BEAR.Package/pull/501) + #### 読み取り専用デプロイ(サーバーレス、イミュータブルコンテナ) {#writable-paths} サーバーレスやイミュータブルコンテナでは書き込めるディレクトリが制限されることがあります。VercelやAWS Lambda、`docker run --read-only`や`readOnlyRootFilesystem: true`で起動したコンテナでは、プロジェクトのディレクトリは読み取り専用で、書き込めるのは`/tmp`など1つのディレクトリだけです。通常のVPSや共有ホストでは不要です。 この場合は書き込めるディレクトリをアプリケーションに渡します。ビルド時と起動時の両方に渡し、次の2つを守ります。 -* 絶対パスを渡します。相対パスを渡すと`InvalidWriteDirException`が投げられます。 -* ビルドと起動で同じパスを渡します。パスはDIスクリプトに焼き込まれるため、渡されたinjectorとコンパイルの書き込み先が違う場合は`WriteDirMismatchException`が投げられます。 +* 絶対パスを渡します。相対パスを渡すと`Meta`を組む時点で`WriteDirNotAbsoluteException`が投げられます。 +* ビルドと起動で同じパスを渡します。パスはDIスクリプトに焼き込まれるため、違うパスで起動すると、書き込めるなら再コンパイルになり、書き込めないなら`CompiledForAnotherWriteDirException`で止まります。 -`$writeDir`は`Bootstrap::__invoke()`、`Injector::getInstance()`、`new Compiler()`の末尾の省略可能な引数です。エントリポイントを次のように変更します。 +`$writeDir`は`Bootstrap::__invoke()`、`Injector::getInstance()`、`Injector::getOverrideInstance()`、`new Compiler()`の末尾の省略可能な引数です。エントリポイントを次のように変更します。 ```diff // public/index.php -exit((new Bootstrap())('prod-app', $GLOBALS, $_SERVER)); +exit((new Bootstrap())('prod-app', $GLOBALS, $_SERVER, getenv('APP_WRITE_DIR') ?: null)); - // bin/compile.php php bin/compile.php prod-app /tmp + // bin/compile.php APP_WRITE_DIR=/tmp php bin/compile.php -exit((new Compiler('MyVendor\MyProject', $context, dirname(__DIR__)))()); -+$writeDir = $argv[2] ?? null; ++$writeDir = getenv('APP_WRITE_DIR') ?: null; + +exit((new Compiler('MyVendor\MyProject', $context, dirname(__DIR__), $writeDir))()); @@ -254,16 +289,16 @@ DIスクリプトの出力先は`{appDir}/var/tmp/{context}/di`です。これ - } + public static function getInstance(string $context, string|null $writeDir = null): InjectorInterface + { -+ return PackageInjector::getInstance(__NAMESPACE__, $context, dirname(__DIR__), null, $writeDir); ++ return PackageInjector::getInstance(__NAMESPACE__, $context, dirname(__DIR__), writeDir: $writeDir); + } ``` -`Meta`とinjectorのキャッシュプールは書き込み先から`BEAR\Package\Injector`が組むので、スケルトン側の`Meta`/`LocalCacheProvider`の行はなくなります。開発用のエントリは何も渡さず既定のパスを使います。環境変数を読むのはエントリの仕事で、フレームワークの仕事ではありません。 +`Meta`とinjectorのキャッシュプールは書き込み先から`BEAR\Package\Injector`が組むので、スケルトン側の`Meta`/`LocalCacheProvider`の行はなくなります。開発用のエントリは何も渡さず既定のパスを使います。 -書き込み先はビルドには引数で、実行時には環境変数で渡します。 +書き込み先の源は`APP_WRITE_DIR`の1つです。ビルドも実行時も同じ変数を読みます。`AppModule`はコンパイル中に動くので、`AppModule`が読む値とビルドが使う値は同じでなければなりません。 ```text -build php bin/compile.php prod-app /tmp +build APP_WRITE_DIR=/tmp php bin/compile.php runtime APP_WRITE_DIR=/tmp php-fpm env[APP_WRITE_DIR] = /tmp docker --env APP_WRITE_DIR=/tmp @@ -272,7 +307,7 @@ runtime APP_WRITE_DIR=/tmp 書き込み先を`/tmp`にした場合の配置は次のとおりです。 ```text -{appDir}/var/tmp/{context}/di コンパイル済みDIスクリプト(成果物内) +{appDir}/var/build/{context}/di コンパイル済みDIスクリプト(成果物内) /tmp/MyVendor/MyProject/{context}/tmp クエリリポジトリのキャッシュ、serializeしたinjector /tmp/MyVendor/MyProject/{context}/log ``` @@ -281,7 +316,9 @@ runtime APP_WRITE_DIR=/tmp コンパイル済みDIスクリプトは`appDir`配下に残り、デプロイ成果物に同梱されます。新しいインスタンスの`/tmp`は空なので、DIスクリプトまで書き込み先に移すとコールドスタートのたびに再コンパイルになります(リソース5個のアプリケーションで、再コンパイルが0.38秒、成果物からの読み込みが0.018秒)。 -ビルドと違う書き込み先で起動した場合は、古いパスを使わずに再コンパイルされます。成果物が読み取り専用なら例外で止まり、書き込み可能なら`Compiled DI scripts on demand`のnoticeが出ます。 +ビルドと違う書き込み先で起動した場合は、古いパスを使わずに再コンパイルされます。成果物が読み取り専用なら両方のパスを名指しする`CompiledForAnotherWriteDirException`で止まり、書き込み可能なら`Compiled DI scripts on demand`のnoticeが出ます。 + +自身に書き込まない1ファイルの成果物にするには[Phar](phar.html)を参照してください。 BEAR.Package 1.22以降が必要です。背景: [BEAR.Package#491](https://github.com/bearsunday/BEAR.Package/pull/491) @@ -309,7 +346,7 @@ Note: パフォーマンスベンチマークは[benchmark](https://github.com/b 実環境ではないと生成ができないクラス(例えば認証が成功しないとインジェクトが完了しないResourceObject)がある場合には、コンパイル時にのみ読み込まれるダミークラス読み込みをルートの`.compile.php`に記述することによってコンパイルをすることができます。**目的は「コンパイル時に構築を通す」ことなので、中身は Null オブジェクト(何もしない実装)が基本**です。これは事前コンパイル(実サービスに触れない)だけでなく、**認証などリクエスト時の状態が要るために、デプロイ先でコンパイルしても構築できない**リソースにも当てはまります。値の偽装(`$_SERVER['X'] = 'fake'` など)は最小限にとどめ、ランタイムで本物が要る値には使わないでください(焼き込まれます)。 -**注意(BEAR.Package 1.21+)**: `Compiler::fromInjector()` はルートの `.compile.php` を自動では読み込みません(非推奨の `bear.compile` は自動でした)。上の `bin/compile.php` のように、アプリ側で明示的に `require` してください。 +**注意**: `.compile.php` はCompiler自身が、コンテナを組む前に読み込みます。非推奨の `vendor/bin/bear.compile` も読み込んでいました。アプリ側で何かする必要はありません。 .compile.php diff --git a/manuals/1.0/ja/tutorial.md b/manuals/1.0/ja/tutorial.md index 81757c7e..f63abb7e 100644 --- a/manuals/1.0/ja/tutorial.md +++ b/manuals/1.0/ja/tutorial.md @@ -842,7 +842,7 @@ exit((require dirname(__DIR__) . '/bootstrap.php')('prod-hal-app')); ``` コンテキストに応じたインスタンス生成用のPHPコードが自動的に生成されます。 -これらのコードは`var/tmp/{context}/di`フォルダに保存されます。通常は確認する必要はありませんが、 +これらのコードは`var/build/{context}/di`フォルダに保存されます。通常は確認する必要はありませんが、 インスタンスがどのように生成されているか知りたい場合に参照できます。 ## REST API