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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
.gitattributes export-ignore
.github/ export-ignore
.gitignore export-ignore
AGENTS.md export-ignore
ncs.* export-ignore
phpstan*.neon export-ignore
src/**/*.latte export-ignore
docs/ export-ignore
tests/ export-ignore

*.php* diff=php
Expand Down
79 changes: 79 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# To My Agents!

It is my fervent wish that this file guide every AI coding agent working with code in this repository.

## Documentation

Any distilled, agent-facing documentation for this package - how it works
internally and the rationale behind key design decisions - lives in `docs/`.
Consult it before non-trivial changes; it is the source of truth from which the
public manual is distilled.

The compiler is the richest emergent model in the framework: an A/B/C phase system
where **what is safe to do depends entirely on which phase you are in**. Read
`docs/internals/` before changing compilation, resolution, or code generation - it
will save you from the classic phase-ordering bugs.

## Project Overview

**Nette DI** is a *compiled* Dependency Injection container: the service graph is
resolved once and emitted as an optimized PHP class (cached on disk), so runtime
requests just `include` it. Full autowiring, NEON configuration, and a
`CompilerExtension` plug-in system. Library component, not an application.

- **PHP Version**: 8.1 - 8.5
- **Package**: `nette/di`

## Essential Commands

```bash
# Run all tests
vendor/bin/tester tests -s -C # or: composer tester

# Run one directory / file
vendor/bin/tester tests/DI/ -s -C
vendor/bin/tester tests/DI/Compiler.configurator.phpt -s -C

# Static analysis (PHPStan level 5)
composer phpstan
```

`-C` uses the system-wide php.ini.

## Test Infrastructure

- Tests are Nette Tester `.phpt` files; `tests/bootstrap.php` provides
`createContainer($source, $config, $params = [])` (compiles a `Compiler`/
`ContainerBuilder` + NEON into a live container), `getTempDir()`, and
`Notes::add()`/`fetch()` for in-test tracing.
- **The compiled code is written to `tests/tmp/{pid}/code.php`** - read it when a
generation test fails. Expected output fixtures live in `tests/DI/expected/`.

## Conventions

- Every file starts with `declare(strict_types=1);`; everything typed; two blank
lines between methods; Nette Coding Standard.
- Exceptions are grouped in `exceptions.php`; messages are natural language
("The file does not exist.").

## Working in this repo

The container has **two worlds**: at compile time only *definitions* (recipes)
exist in `ContainerBuilder`; at runtime only *instances* exist in `Container`.
`%param%` and `@service` are text markers translated during compilation, already
baked into code at runtime. The compile timeline is three phases (see
`docs/internals/compilation.md`), and most bugs come from ignoring it:

- **Don't look services up by type in `loadConfiguration()`.** The graph is
incomplete (user `services:` register last) and `getByType()` forces a premature
resolve. Move type introspection to `beforeCompile()`. `findByTag()` is fine any time.
- **`getByType()` during `resolve()` throws `NotAllowedDuringResolvingException`.**
- **`@Type` is a *type* reference, not a service name** - autowiring translates it
to a name in the `complete` phase, so it stays unresolved during `resolve()`.
- **`initialization->addBody()` runs on *every request***, not at compile time (it
is PHP emitted into `initialize()`) - keep it tiny.
- **A `getenv()`/env-derived parameter is baked at compile time** unless it is a
*dynamic* parameter.
- User-facing how-to (NEON syntax, autowiring rules, service-definition patterns,
decorator/search/di sections, extension-development lifecycle, generated
factories) is manual material and lives in the public web docs, not here.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"nette/php-generator": "^4.1.6",
"nette/robot-loader": "^4.0",
"nette/schema": "^1.2.5",
"nette/utils": "^4.0"
"nette/utils": "^4.0.6"
},
"require-dev": {
"nette/tester": "^2.6",
Expand Down
64 changes: 64 additions & 0 deletions docs/internals/code-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Code generation & runtime

## Phase C: `generateCode()`

`Compiler::generateCode()` builds a `PhpGenerator`, generates the class, then loops
extensions to let each `afterCompile($class)` edit the code and contribute boot code to
`initialize()`.

`PhpGenerator::generate()` creates a class extending `Container` (with
`parent::__construct($params)`), fills the `aliases`/`tags`/`wiring` properties from
`builder->exportMeta()`, and for **every** definition emits a `createServiceXxx()`
method by delegating to that definition's `generateMethod` (see resolution.md) — the
method name is `createService` + the ucfirst'd service name with dots turned into
`__` (`mail.mailer` → `createServiceMail__mailer`).
`formatStatement` translates a `Statement` into PHP (`new Foo(...)`, method calls,
property get/set, static calls, functions), and `convertArguments` turns a `Reference`
into `$this->getService(...)`, self into `$service`, and the container into `$this`.

## `afterCompile` and `initialize()`

Extensions use `afterCompile` to mutate the finished `ClassType` — e.g.
`ParametersExtension` emits `getStaticParameters()`/`getDynamicParameter()`, and
`DIExtension` sets the parent class, restricts exported metadata, and injects the Tracy
panel in debug.

**The critical distinction:** `$this->initialization` (a per-extension closure,
emitted into the container's `initialize()` method and invoked there when non-empty)
runs **after the container is constructed, on every request** — not at compile time.
So it must hold only small runtime actions:
starting a session, sending HTTP headers, `define()`, `ini_set()`, validating dynamic
parameters. Putting heavy work in `initialization->addBody()` runs it every request.

## Runtime: the generated `Container`

At runtime none of the above runs; the generated `Container` just reads precomputed
metadata:

- **`getService($name)`** lazily creates the instance via `createServiceXxx()` and
**caches** it (aliases redirect); `createService` is deadlock-guarded.
- **`getByType($type)`** reads **only the high-priority bucket** of the precomputed
`wiring` index — exactly one service there returns it, more than one throws
"Multiple services" (even if lower-priority candidates exist), zero gives a
specific error (does the type exist / is it not autowired / missing from the
export).
- **`findByType`/`findByTag`** read the `wiring`/`tags` metadata (`findByType` merges
all buckets, `findAutowired` only high + low); `getParameter` lazily computes
dynamic parameters. `wiring` carries three buckets `[high, low, no]`: the high/low
split comes from `Autowiring::rebuild`, the third — type-matching but
**non-autowired** services — is added by `ContainerBuilder::exportMeta`.

Two runtime facts invisible from the compile-time side:

- **Service existence is the method map.** The constructor snapshots
`get_class_methods($this)`; `hasService`/`getService` are driven by that map plus
the `instances`/`factories` added via `addService()`. At runtime "the service
exists" means "a `createServiceXxx()` method exists" — definitions are gone.
- **Runtime autowiring reuses the compile-time engine.** `createInstance()`,
`callMethod()` and inject processing call the static
`Resolver::autowireArguments()` — the same argument matching that fills arguments
in the `complete` phase. Changing it in `Resolver` changes both compile-time and
runtime behaviour.

`di: export:` restricts what metadata is emitted (parameters/tags/types), which shrinks
the generated container when the full autowiring index isn't needed at runtime.
91 changes: 91 additions & 0 deletions docs/internals/compilation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Compilation phases & introspection safety

`Compiler::compile()` is three steps, and the whole model hinges on what each
guarantees:

```php
$this->processExtensions(); // PHASE A: schemas + loadConfiguration
$this->processBeforeCompile(); // PHASE B: resolve + beforeCompile + complete
return $this->generateCode(); // PHASE C: generate + afterCompile
```

- **Phase A** fills the definition graph. Service **types are not yet reliably known.**
- **Phase B** resolves all types (`resolve`), lets extensions edit the graph
(`beforeCompile`), then **autowires arguments** (`complete`).
- **Phase C** generates PHP and lets extensions touch the code.

## Extension order is deliberate — and load-bearing

`processExtensions()` runs `getConfigSchema` → `setConfig` → `loadConfiguration` on
each extension, but in a carefully controlled order:

1. **`ParametersExtension` + `ExtensionsExtension` first.** Parameters must expand
`%param%` across the *whole* config before any other extension sees its section;
Extensions registers further extensions from `extensions:`, so it must exist before
the rest run.
2. **`SearchExtension` just before `DecoratorExtension`** — Search registers
discovered classes (in its `beforeCompile`) so Decorator can then apply setup/tags
to them.
3. Everyone else: all `setConfig`, then all `loadConfiguration`.
4. **`InjectExtension` moved to the very end** — its `beforeCompile` must see the
setups added by everyone else.
5. **`ServicesExtension` last** — the user `services:` section always gets the last
word and can override anything an extension set.

Two errors are guarded at the end: an extension registered later than it should have
been, and an orphan config section with no matching extension (with a "did you mean"
suggestion).

At the end of phase A the graph is **complete in count** (every extension and the
user registered what they meant to), but service types from factory return values
are unresolved, arguments are not autowired, and `@service` references are still
partly strings — which is exactly why type introspection here is unreliable.

## When `ContainerBuilder` introspection is safe

This is the question every extension author hits. Two flags and one exception decide
it. `ContainerBuilder` tracks `needsResolve` (set `true` after **any** definition
change) and `resolving` (`true` while `resolve()` runs). The type-lookup methods
(`getByType`, `getDefinitionByType`, `findByType`, `findAutowired`) all route through
a private guard:

- if `resolving` → **throw `NotAllowedDuringResolvingException`** (you are inside
`resolve()`);
- else if `needsResolve` → **lazily run `resolve()`** first.

**`findByTag()` does not go through this guard** — tags don't depend on types, so
tag lookup works in **every** phase.

Two related traps: the guard is absolute — during `resolve()` even
`getByType($type, throw: false)` throws. And the queries differ: `getByType`/
`findAutowired` answer from the autowiring index (honouring `autowired: false` and
excluded classes), while `findByType` scans all definitions by declared type and
**ignores autowiring settings** — they can return different sets.

Phase by phase:

- **`loadConfiguration()` (phase A) — type introspection is unreliable.** The graph is
incomplete (later extensions and the user `services:` are not registered yet). A
`getByType()` "works" but answers from a partial graph *and* forces a premature
resolve. Rule: **only register definitions here; do not look up by type.**
`findByTag()` is fine.
- **`beforeCompile()` (phase B) — the right place.** `processBeforeCompile` runs
`builder->resolve()` **first** (types resolved, autowiring index built), *then* the
`beforeCompile()` loop, and `builder->complete()` **only after** all of them. So in
`beforeCompile` every definition exists, types are resolved, and
`getByType`/`findByType`/`findByTag` are **reliable** — but **arguments are not yet
autowired**. Editing a definition here sets `needsResolve` (via a notifier hook
installed by `addDefinition`), and the next `getByType()` transparently
re-resolves, so you can freely interleave edits and queries. This holds only
until `complete()`, which detaches all notifiers — the graph is frozen from
there on.
- **`afterCompile()` (phase C)** operates on the generated `ClassType`, not the
builder.

| I want to… | phase |
|---|---|
| register a service | `loadConfiguration()` |
| look up by **tag** and edit definitions | `loadConfiguration()` or `beforeCompile()` |
| look up by **type** | **`beforeCompile()`** |
| touch generated code | `afterCompile()` |
| run code after container start | `$this->initialization` (see code-generation.md) |
87 changes: 87 additions & 0 deletions docs/internals/config-loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Config loading, merging & parameters

## Cache & recompilation

`ContainerLoader::load()` derives the class name from a hash of the cache key
(`Container_<hash>`). If the class isn't loaded it `@include`s the compiled file when
not expired (no compilation), else takes an **exclusive `flock` lock** on a sibling
`.lock` file (against concurrent compilation), re-checks expiry under the lock,
generates the code, and writes both the `.php` and its `.meta` **atomically** (`.tmp`
+ `rename`, with `opcache_invalidate` before *and* after the rename; on Windows the
rename retries briefly against transient file locks). `isExpired()` returns **`false`
always when `autoRebuild === false`** (production never recompiles); in debug it
defers to `DependencyChecker`, whose meta records the mtime of every config/PHP file
**and a structural hash of every touched class** (parents, interfaces, traits, `use`
statements, public members with signatures *and docComments*, `#[Inject]` on
properties). The rebuild rule is asymmetric: a **config-file mtime change always
rebuilds** (content-independent), but a PHP-file mtime change rebuilds **only when
the structural hash changed too** — otherwise the cached container is kept and just
the mtimes in `.meta` are refreshed. Dependency files a custom extension reads must
be registered via `$builder->addDependency($file)` or the cache won't know about
them.

## Loading & merging

`Config\Loader` picks an adapter by extension: `.php` is trivial (`return require`);
`.neon` runs `NeonAdapter` (below). With the default `merge: true`, `includes` are
resolved first (a later include wins) and the main file is merged over them. But
`Compiler::loadConfig()` loads with `merge: false`: the loader returns a **flat list**
of sources with the main file **appended last**, stored as `$configs[section][] =
$data` — **the cross-source merge per section happens later**, in phase A via Schema
(`processSchema` → `Schema\Processor::processMultiple`, where the **last** dataset
wins — which is what makes the main file override its includes).
Merge semantics (`Schema\Helpers::merge`): the **left/new operand wins**; numeric keys
append (lists concatenate), string keys merge recursively, `null` does not overwrite an
array, a scalar does; a `PREVENT_MERGING` marker returns the left operand only.

## NEON adapter

`NeonAdapter` parses NEON to an AST, runs a chain of visitors, then `process()`. The
facts that matter downstream:

- **`@` escaping.** A **quoted** string starting with `@` is doubled to `@@` (in
quotes `@` means literal text, not a reference); an **unquoted** `@foo` passes
through and becomes a reference later. So `@foo` is a reference, `'@foo'` is the
literal text `@foo`.
- **Entities become `Statement`s.** `Foo(a, b)` → `new Statement('Foo', ['a', 'b'])`;
a chain `Foo()::bar()::baz()` nests them.
- **`!` suffix = prevent-merge.** A key ending `!` means "replace, don't merge" and
injects the `PREVENT_MERGING` marker.

Crucially, the adapter **does not** turn `@service` argument strings into `Reference`
objects — they stay strings until an extension calls `Helpers::filterArguments`
(see resolution.md).

## `%param%` expansion — once, at the start of phase A

Because `ParametersExtension` runs **first**, `%param%` is expanded across the entire
config before any other extension (including `ServicesExtension`) sees its section. In
`loadConfiguration` it:

1. replaces each **dynamic** parameter with a `DynamicParameter('$this->getParameter(...)')`
object;
2. expands `%param%` **inside the parameters themselves** (recursively — a parameter
may reference another);
3. expands `%param%` **in the whole rest of the config** (its `compilerConfig` is a
**reference** to `Compiler::$configs`).

`Helpers::expand()`: a `%%` is a literal `%`; a placeholder that is the **entire
string** (`%foo%` alone) returns the value **as-is** (so `%mailer%` can return a whole
array), otherwise it concatenates into a string (a non-scalar embedded in a larger
string throws; a *dynamic* value embedded in one becomes a `::implode` `Statement`);
dotted notation `%foo.bar%` reaches into nested arrays; a cyclic reference or a
missing parameter throws.

## Static vs dynamic parameters

A value that varies by environment (an env var, `baseUrl` from the request) must stay
**dynamic**. Names are declared via `Compiler::setDynamicParameterNames()` (Bootstrap
passes the dynamic-parameter names plus `baseUrl`, which is always dynamic).
`ParametersExtension::afterCompile` splits them: **static** parameters are baked into
`getStaticParameters()`; **dynamic** ones (carrying a `DynamicParameter` or `Statement`)
generate a `getDynamicParameter($key)` computed at runtime; and dynamic-value
**validation** is emitted into `initialize()` as `Validators::assert()`. At runtime
`Container::getParameter()` lazily computes a dynamic parameter on first access
(deadlock-guarded). `Helpers::escape()` (`%`→`%%`, leading `@`→`@@`) is the inverse,
used by Bootstrap on programmatically-injected values so they aren't misread as
placeholders.
Loading