PHPackages                             naoki-tsuchiya/ray-di-context - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. naoki-tsuchiya/ray-di-context

ActiveLibrary

naoki-tsuchiya/ray-di-context
=============================

Context, meta, and compile management for Ray.Di applications with separated read-only compile dir and writable tmp dir

0.1.0(today)01↑2900%[9 issues](https://github.com/NaokiTsuchiya/RayDiContext/issues)MITPHPPHP ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0CI passing

Since Jul 29Pushed todayCompare

[ Source](https://github.com/NaokiTsuchiya/RayDiContext)[ Packagist](https://packagist.org/packages/naoki-tsuchiya/ray-di-context)[ RSS](/packages/naoki-tsuchiya-ray-di-context/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (4)Versions (4)Used By (0)

Ray.Di Context
==============

[](#raydi-context)

[![CI](https://github.com/NaokiTsuchiya/RayDiContext/actions/workflows/ci.yml/badge.svg)](https://github.com/NaokiTsuchiya/RayDiContext/actions/workflows/ci.yml)[![codecov](https://camo.githubusercontent.com/c7ecf384dc3d645391921fef31eafa82c7adc7838f5c924c22bc273a1e17834b/68747470733a2f2f636f6465636f762e696f2f67682f4e616f6b6954737563686979612f5261794469436f6e746578742f67726170682f62616467652e737667)](https://codecov.io/gh/NaokiTsuchiya/RayDiContext)[![PHP Version](https://camo.githubusercontent.com/692916ed5942a1510194ac1c45e70518e7bcb2cd3f29905b09f164bbd8803020/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d382e322532302d2d253230382e352d373737424234)](composer.json)[![License](https://camo.githubusercontent.com/df75c7851c0525b7905dd1d92c00165c1e39a5afdfba5b4ac28396a18b1a5de3/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f4e616f6b6954737563686979612f5261794469436f6e74657874)](LICENSE)[![Packagist Version](https://camo.githubusercontent.com/318778bb8249f7aeec57d04f99d2776da00857b222f42273eb5a7fac384f6b13/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e616f6b692d74737563686979612f7261792d64692d636f6e74657874)](https://packagist.org/packages/naoki-tsuchiya/ray-di-context)

Context, meta, and compile management for [Ray.Di](https://github.com/ray-di/Ray.Di) applications.

Why
---

[](#why)

Ray.Di's plain `Injector` compiles each binding into a PHP script written to `tmpDir` the first time it's resolved — at request time. A container running with a read-only root filesystem (Docker `--read-only`, Kubernetes `securityContext.readOnlyRootFilesystem: true`) has nowhere writable for that first write to land, so the first request fails.

`Ray\Compiler` solves the *write*: compile ahead of time, ship the compiled scripts, run `CompiledInjector` against them at runtime — no writes needed. It doesn't solve the *path*: the compiled scripts and the app that reads them have to resolve `compileDir`/`tmpDir` to the exact same strings, and nothing stops a runtime-only path from getting frozen into a script by accident. `AppMeta`, `ContextInterface`, and `BakedPathGuard` exist to make that separation safe — `compileDir` stays read-only and gets baked into the image, `tmpDir` stays writable and never does, and CI can verify the split before the image is even built.

If you're on [BEAR.Sunday](https://bear.sunday.dev/), you already have this — `BEAR\AppMeta\Meta` and `AbstractAppContext` solve the same problem, and this package's vocabulary (`AppMeta`, context, compile) deliberately echoes theirs. You don't need both.

DirectoryRoleLifecycle`compileDir`Pre-compiled DI scriptsBaked into the image, **read-only** at runtime`tmpDir`Runtime scratch area**Writable** at runtime, never baked`AppMeta` keeps the two independent, so `compileDir` can be baked into a `readOnlyRootFilesystem` container while `tmpDir` stays a writable volume.

- `compileDir`/`tmpDir` default to `{appDir}/var/di/{context}` / `{appDir}/var/tmp/{context}`
- `appDir` must be an absolute path — `AppMeta::fromAppDir()` rejects a relative one outright rather than resolving it, so the spelling baked into compiled scripts is always the same spelling the running app binds. `BakedPathGuard` compares those strings verbatim, so resolving symlinks here would make the guard fail open
- Neither `AppMeta::fromAppDir()` nor the bundled CLI reads the environment — pass overrides in explicitly (e.g. as CLI arguments, sourced from env vars by your shell or Dockerfile). Compile-time and runtime code must agree on the same values, or the compiled scripts and the running app will look in different places
- This package creates `compileDir` for you but never creates `tmpDir` — `mkdir` it yourself before runtime (e.g. in your Dockerfile or bootstrap script). Ray.Di's `Injector` silently falls back to `sys_get_temp_dir()` when `tmpDir` doesn't exist, so a missing directory doesn't throw — writes just land somewhere you didn't expect. Add `var/di/` and `var/tmp/` (or wherever your `compileDir`/`tmpDir` point) to your application's `.gitignore`

**Never bind a runtime-determined value or secret with `toInstance()`** — Ray.Compiler freezes whatever you pass into the compiled scripts, and `compileDir`ships inside your image. Binding `AppMeta` this way leaks `appDir`/`tmpDir`, which `BakedPathGuard` catches and fails the compile on — but the guard only scans for those two path strings. It won't catch anything else: `$this->bind()->annotatedWith('db_password')->toInstance('s3cr3t-P@ssw0rd')` writes the password in plaintext into a compiled script under `compileDir`, and nothing stops it *by default*. If you know what your own secrets look like, hand them to the guard and it will fail the compile the same way it does for a baked path:

```
use NaokiTsuchiya\RayDiContext\BakedPathGuard;

$dbPassword = getenv('DB_PASSWORD');
$needles = $dbPassword === false || $dbPassword === '' ? [] : [$dbPassword];

(new CompileRunner($provider, bakedPathGuard: new BakedPathGuard($needles)))->run($meta);
```

A rejection names the script but never repeats the value — these are supplied precisely because they must not ship, and quoting one would move it out of the image and into your CI log. For anything the bundled scanner can't express, implement `BakedPathGuardInterface`yourself and pass that instead.

Better still, bind secrets and other runtime-determined values through a provider — a provider's `get()` runs each time the compiled injector resolves the binding, not once at compile time, so nothing gets frozen into the script:

```
use Ray\Di\ProviderInterface;

/** @implements ProviderInterface */
final class TmpDirProvider implements ProviderInterface
{
    public function get(): string
    {
        return getenv('APP_TMP_DIR') ?: sys_get_temp_dir();
    }
}
```

```
$this->bind()->annotatedWith('tmp_dir')->toProvider(TmpDirProvider::class);
```

`TmpDirProvider` reads the environment itself, at the moment its value is asked for — the same rule this package follows in `AppMeta::fromAppDir()` and the CLI: resolve runtime-determined values at runtime, never freeze them into a compiled script.

Install
-------

[](#install)

```
composer require naoki-tsuchiya/ray-di-context

```

Usage
-----

[](#usage)

```
use NaokiTsuchiya\RayDiContext\AbstractContext;
use Ray\Compiler\CompiledInjector;
use Ray\Compiler\DiCompileModule;
use Ray\Di\AbstractModule;
use Ray\Di\Injector;
use Ray\Di\InjectorInterface;

final class ProdContext extends AbstractContext
{
    public function __invoke(): AbstractModule
    {
        return new DiCompileModule(true, new AppModule());
    }

    public function getInjectorInstance(): InjectorInterface
    {
        return new CompiledInjector($this->meta->compileDir);
    }
}

final class DevContext extends AbstractContext
{
    public function __invoke(): AbstractModule
    {
        return new AppModule();
    }

    public function getInjectorInstance(): InjectorInterface
    {
        return new Injector($this(), $this->meta->tmpDir);
    }
}
```

Compile ahead of time with the bundled `bin/ray-di-compile` CLI. It takes a *bootstrap* file that returns your `ContextProviderInterface`, the app dir, the context, and optionally `compileDir`/`tmpDir` overrides:

```
// bootstrap.php — see examples/bootstrap.php
use NaokiTsuchiya\RayDiContext\MapContextProvider;

return new MapContextProvider(['prod' => ProdContext::class, 'dev' => DevContext::class]);
```

```
php vendor/bin/ray-di-compile bootstrap.php "$(pwd)" prod

```

The CLI itself never reads the environment; if your deployment sets `APP_COMPILE_DIR`/`APP_TMP_DIR`, pass them through explicitly (e.g. in a Dockerfile `RUN` step):

```
php vendor/bin/ray-di-compile bootstrap.php "$(pwd)" prod "$APP_COMPILE_DIR" "$APP_TMP_DIR"

```

Ray.Compiler writes every script `0600`, so a compile dir built as `root` would be unreadable to a non-root runtime user; the compiled scripts are normalized to `0644`(their directories to `0755`) so the image stays readable after a `USER` switch.

The CLI cleans the compile dir, compiles the context, guards the result against baked paths, and normalizes the permissions of what it wrote. **A compile that fails the guard leaves `compileDir` empty**, so scripts it refused can never be `COPY`-ed into an image by a later build step. Under the hood it is:

```
$provider = require 'bootstrap.php';
$meta = AppMeta::fromAppDir(getcwd(), 'prod', $compileDir, $tmpDir); // args 4/5, or null

(new CompileRunner($provider))->run($meta); // returns void, throws on failure
```

### Exit status

[](#exit-status)

The exit status is a public contract — gate your CI on it.

CodeMeaning`0`The context compiled successfully`1`The compile failed. **Anything** thrown while loading the bootstrap or compiling is caught and its message written to STDERR as a single line — no stack trace, so the CI log stays readable. That covers this package's own exceptions (`UnknownContext`, `BakedPathFound`, `CompileDirNotWritable`, `InvalidAppMeta` — e.g. a relative `appDir` — …) and equally the failures that come from your module: a missing binding surfaces as `Ray\Di\Exception\Unbound`, and anything foreign is prefixed with its class name so you can tell where it came from. The autoloader also being unfindable reports here`2`Usage error: wrong number of arguments, `appDir` does not exist, bootstrap file not found, or a bootstrap that does not return a `ContextProviderInterface`Bootstrap at runtime. Resolve `compileDir`/`tmpDir` to the **same** values you passed to the CLI above — a mismatch means the running app looks for compiled scripts in a different place than they were baked into:

```
$provider = require 'bootstrap.php';
$meta = AppMeta::fromAppDir(
    dirname(__DIR__),
    getenv('APP_ENV') ?: 'prod',
    getenv('APP_COMPILE_DIR') ?: null,
    getenv('APP_TMP_DIR') ?: null,
);
$context = $provider->get($meta);
$injector = $context->getInjectorInstance();

foreach ($context->getSavedSingleton() as $class) {
    $injector->getInstance($class);
}
```

`getInjectorInstance()` is called once and the result reused above — see the docblock on `ContextInterface::getInjectorInstance()` for why that matters. `getSavedSingleton()` names classes to instantiate once, right after boot: a compiled injector never unserializes instances, so anything holding a runtime resource (a database connection, for example) needs this explicit warmup, or it won't exist until something happens to request it first.

Deploying to Docker / Kubernetes
--------------------------------

[](#deploying-to-docker--kubernetes)

Build the compiled scripts in a build stage, `COPY` only the result into the runtime image, and run as a non-root user with a read-only root filesystem:

```
# syntax=docker/dockerfile:1
FROM php:8.3-cli AS build
WORKDIR /build
COPY . .
RUN composer install --no-dev --optimize-autoloader \
 && php bin/ray-di-compile bootstrap.php /build prod /app/var/di/prod

FROM php:8.3-cli
WORKDIR /app
COPY --from=build /build /app
RUN useradd --system appuser
USER appuser
CMD ["php", "bin/console"]
```

The build stage compiles at `/build`; the runtime image runs at `/app`. Left to its defaults, `AppMeta::fromAppDir()` would derive `compileDir` from `appDir`, so the scripts above would be compiled for `/build/var/di/prod` while the running app looks for them at `/app/var/di/prod` — a mismatch that `CompiledInjector`reports as `ScriptDirNotReadable` the moment the first class is resolved. That's why the `RUN` step above passes `APP_COMPILE_DIR` explicitly, resolved to the *runtime* path, not the build path — and the app's own bootstrap must resolve `APP_COMPILE_DIR` to that same runtime path:

```
$meta = AppMeta::fromAppDir(dirname(__DIR__), 'prod', '/app/var/di/prod');
```

`APP_TMP_DIR` doesn't fail this loudly at compile time — a `tmpDir` that doesn't exist yet only surfaces once something tries to write to it, silently, in `sys_get_temp_dir()` (see above) — but resolve it to the runtime path for the same reason.

`APP_COMPILE_DIR`/`APP_TMP_DIR` override the whole directory, not just the `appDir` they're derived from, so they ignore `context` entirely. One override can only ever serve one context: baking `prod-cli` and `prod-html` into the same image means compiling both to their conventional, context-suffixed paths (`{appDir}/var/di/prod-cli`, `{appDir}/var/di/prod-html`) and `COPY`-ing the whole `appDir`, rather than pointing `APP_COMPILE_DIR` at one shared path.

In Kubernetes, mount `tmpDir` as an `emptyDir` (`medium: Memory` for tmpfs) and leave everything else read-only:

```
securityContext:
  readOnlyRootFilesystem: true
  runAsNonRoot: true
volumeMounts:
  - name: tmp
    mountPath: /app/var/tmp
volumes:
  - name: tmp
    emptyDir: {}
```

Ray.Compiler writes two housekeeping files into `compileDir` alongside the compiled scripts: `compile.lock` (held only during the compile) and `_bindings.log` (a human-readable dump of the binding graph, including the compile-time `compileDir` path). Both get baked into the image along with the rest of `compileDir` — neither is meant to hold secrets, but they're not scripts `BakedPathGuard` scans, either.

Requirements
------------

[](#requirements)

PHP 8.2 – 8.5, ray/di ^2.19, ray/compiler ^1.14

Development
-----------

[](#development)

CI also runs weekly on a schedule, and `workflow_dispatch` runs it on demand. The `test` job installs with `composer update`, so a scheduled run tests the declared ranges against whatever upstream has released since — a red one means the newest release broke something, not that anyone changed this repository. `composer show --direct` in the job log records which versions it resolved.

`phpunit.xml.dist` sets `failOnDeprecation="true"`. Combined with the PHP 8.5 job in the CI matrix, a deprecation raised by `ray/di` or `ray/aop` under a newer PHP version can turn CI red with zero changes in this repository — if a Renovate PR or a new PHP minor suddenly fails for no apparent reason, check here first. If it happens, either add `#[WithoutErrorHandler]` to the affected test or relax `failOnDeprecation` for that PHP version only.

### Releasing

[](#releasing)

Before tagging, move the entries from `CHANGELOG.md`'s `## [Unreleased]`section into a new `## [x.y.z] - ` section. Keep an empty `## [Unreleased]` section at the top. The tag is never re-pointed, so the changelog entry has to be right the first time.

Tags carry no `v` prefix — `0.1.0`, not `v0.1.0`. Composer accepts either, so the only thing that matters is not mixing the two.

```
git tag -a 0.1.0 -m 'Initial release'
git push origin 0.1.0
gh release create 0.1.0 --generate-notes
```

Packagist builds the version from the git tag, not from the GitHub release — the release is for humans. A published tag is never re-pointed: the tag ruleset restricts updates and deletions, and anything wrong in a tag is fixed by the next patch version.

Versioning
----------

[](#versioning)

While on 0.x, minor releases may include backwards-incompatible changes. v1.0.0 will be tagged once the package has run in a real production application. From v1.0.0 on, semantic versioning applies strictly.

License
-------

[](#license)

MIT

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 64.2% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/17171732?v=4)[Naoki Tsuchiya](/maintainers/NaokiTsuchiya)[@NaokiTsuchiya](https://github.com/NaokiTsuchiya)

---

Top Contributors

[![NaokiTsuchiya](https://avatars.githubusercontent.com/u/17171732?v=4)](https://github.com/NaokiTsuchiya "NaokiTsuchiya (136 commits)")[![claude](https://avatars.githubusercontent.com/u/81847?v=4)](https://github.com/claude "claude (74 commits)")[![renovate[bot]](https://avatars.githubusercontent.com/in/2740?v=4)](https://github.com/renovate[bot] "renovate[bot] (2 commits)")

---

Tags

dependency-injectiondi-compilerphpray-compilerray-didependency-injectiondiContextcompilerRay.Di

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/naoki-tsuchiya-ray-di-context/health.svg)

```
[![Health](https://phpackages.com/badges/naoki-tsuchiya-ray-di-context/health.svg)](https://phpackages.com/packages/naoki-tsuchiya-ray-di-context)
```

###  Alternatives

[php-di/php-di

The dependency injection container for humans

2.8k58.2M1.3k](/packages/php-di-php-di)[bear/package

BEAR.Sunday application framework package

31578.6k28](/packages/bear-package)[laminas/laminas-servicemanager

Factory-Driven Dependency Injection Container

15960.1M829](/packages/laminas-laminas-servicemanager)[level-2/dice

A minimalist Dependency injection container (DIC) for PHP. Please note: 3.0+ is only compatible with PHP 7.0. The 2.0 branch is compatbile with PHP 5.6.

439818.0k21](/packages/level-2-dice)[aura/di

A serializable dependency injection container with constructor and setter injection, interface and trait awareness, configuration inheritance, and much more.

353992.9k60](/packages/aura-di)[yiisoft/injector

PSR-11 compatible injector. Executes a callable and makes an instances by injecting dependencies from a given DI container.

943.6M52](/packages/yiisoft-injector)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
