Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions _includes/manuals/1.0/en/contents.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
<li class="nav-item">
<a class="nav-link {% if page.permalink == '/manuals/1.0/en/production.html' %}active{% endif %}" href="/manuals/1.0/en/production.html">Production</a>
</li>
<li class="nav-item">
<a class="nav-link {% if page.permalink == '/manuals/1.0/en/phar.html' %}active{% endif %}" href="/manuals/1.0/en/phar.html">・Phar</a>
</li>
<li class="nav-item">
<a class="nav-link {% if page.permalink == '/manuals/1.0/en/import.html' %}active{% endif %}" href="/manuals/1.0/en/import.html">Import</a>
</li>
Expand Down
3 changes: 3 additions & 0 deletions _includes/manuals/1.0/ja/contents.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
<li class="nav-item">
<a class="nav-link {% if page.permalink == '/manuals/1.0/ja/production.html' %}active{% endif %}" href="/manuals/1.0/ja/production.html">プロダクション</a>
</li>
<li class="nav-item">
<a class="nav-link {% if page.permalink == '/manuals/1.0/ja/phar.html' %}active{% endif %}" href="/manuals/1.0/ja/phar.html">・Phar</a>
</li>
<li class="nav-item">
<a class="nav-link {% if page.permalink == '/manuals/1.0/ja/import.html' %}active{% endif %}" href="/manuals/1.0/ja/import.html">インポート</a>
</li>
Expand Down
109 changes: 109 additions & 0 deletions manuals/1.0/en/phar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
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
prod-app.phar the application, vendor/, compiled DI scripts
/tmp/MyVendor/MyProject/prod-app everything the runtime writes
```

Requires BEAR.Package 1.23+.

## Make your application a phar

The build script compiles, then packs. Both steps are on the compiler:

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

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

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

$context = 'prod-hal-app';
$writeDir = getenv('APP_WRITE_DIR') ?: null;

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

exit($code === 0 ? $compiler->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: `src/`, `public/`, `vendor/` and the DI scripts with their compile marker go in; logs, caches, `.env`, `autoload.php`, `preload.php` and `tests/` stay out. The marker is `.bear-compile.json`, and it is what `phar()` reads to decide: `app`, `context`, `tmpDir`, `writeDir`, `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/var/build/prod-hal-app.phar (7.5MB, 2100 files)
Writes: /tmp
```

`__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 for another write directory. It writes `{appDir}/var/build/{context}.phar`; another entry or another output are its two arguments. Compiling several contexts is a loop in your script; `preload.php` and `autoload.php` are written to fixed paths, so rename them between contexts as [Production](production.html#compilation-recommended) shows.

## Run

```bash
APP_WRITE_DIR=/tmp php var/build/prod-hal-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/prod-hal-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
<?php
// index.php, in the same directory as prod-hal-app.phar
require 'phar://' . __DIR__ . '/prod-hal-app.phar/vendor/autoload.php';

exit((new MyVendor\MyProject\Bootstrap())('prod-hal-app', $GLOBALS, $_SERVER, getenv('APP_WRITE_DIR') ?: null));
```

## Two rules

**One write directory, the same absolute path at the build and at the boot.** The compiled scripts hold the paths they were compiled with, so an archive is built for one write directory. `APP_WRITE_DIR` is that one place: the build reads it and the boot reads it. Imported applications receive it from the container, so nothing else names it.

**No binding that derives a runtime path from `$appMeta->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 |
| `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 |
| `PharWriteDirMismatchException` | An import writes somewhere other than under the host's write directory: it was compiled before the host was given this one |
| `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: nothing loose at the application root 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).
59 changes: 40 additions & 19 deletions manuals/1.0/en/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
<a id="compilation"></a>
### 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`.

Expand All @@ -178,34 +179,52 @@ 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.23+): [Phar](phar.html).

`Compiler::fromInjector($injector, $context, $writeDir)` is for a caller that already holds an injector - a command inside a running application, or an application whose own `Injector` class is customized, as BEAR.Skeleton's `bin/compile.php` is.

```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;

```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
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);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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`.
Expand All @@ -220,19 +239,19 @@ Serverless platforms and immutable containers restrict where an application may

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 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 compile whose write directory differs from the injector it was handed throws `WriteDirMismatchException`.

`$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))());

Expand Down Expand Up @@ -261,16 +280,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
Expand All @@ -288,7 +307,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).

Expand Down
Loading