Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d594bc8
added AGENTS.md & DOCS
dg Dec 27, 2025
a52c716
ControlNode: deprecate missing comma before arguments
dg Jun 29, 2026
4adfc1f
UI\Form: compatible with both nette/forms 3.x and the upcoming 4.0
dg Jul 2, 2026
541bb67
UI\Form: adapted to the SubmissionSource model of nette/forms 4.0
dg Jul 2, 2026
71e64f1
typo
dg Jul 4, 2026
382e0c1
added support for backed enums in parameters [Closes nette/nette#1558]
dg Aug 5, 2026
75b1f52
opened 4.0-dev
dg Dec 14, 2023
764fb3e
composer: increased dependencies versions
dg Jan 21, 2026
1394113
added type hints (BC break)
dg Feb 8, 2024
1141151
Presenter::handleInvalidLink() -> processInvalidLink() (BC break)
dg May 12, 2024
fe246c3
Revert "UI\PresenterComponent: removed references created by loadStat…
dg Oct 24, 2022
14b7b8c
Component: method checkRequirements() is called for createComponent<N…
dg Apr 20, 2024
0329c7b
Component: only UI components can be added to presenter/component (BC…
dg Apr 7, 2024
b71f318
Presenter: removed constructor (BC break!)
dg Dec 11, 2023
f3a4ca1
removed support for @annotations (BC break)
dg Nov 29, 2025
c3e0cd2
RoutingPanel: added support for AI agents
dg Apr 22, 2026
dfac8f5
uses nette/routing 4.0
dg Jan 21, 2024
0098f56
removed {templatePrint}
dg Apr 19, 2024
e6c304f
Presenter::argsToParams() now distinguish between NULL and ''
dg May 13, 2024
e1f6e1b
fix: Template::render() accepts object for template parameters (#351)
jakduch Mar 5, 2026
e0111be
fixed object template parameter tests
jakduch Sep 11, 2026
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
61 changes: 61 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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 presenter lifecycle, link generation, persistent state, routing, and template
lookup carry subtle non-local invariants - read `docs/internals` before touching
them.

## Project Overview

Nette Application is a full-stack, component-based MVC framework library for PHP -
the core application layer of the Nette Framework: presenters, components, routing,
templating integration, and the request/response cycle. It is a library package,
not a runnable application, and integrates with the rest of the framework through
`src/Bridges` (DI, Latte, Tracy).

- **PHP Version**: 8.3 - 8.5
- **Package**: `nette/application`

## Essential Commands

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

# Run one test directory / file
vendor/bin/tester tests/UI -s
php tests/Application/Presenter.twoDomains.phpt

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

## Conventions

- Every file starts with `declare(strict_types=1);`; tabs for indentation; Nette
Coding Standard (PSR-12 based) with the opening brace on its own line.
- Tests are Nette Tester `.phpt` files mirroring `src/` under `tests/`; use
`test()` / `testException()` and `Assert::*`. Temp files go to `tests/tmp/{pid}/`,
and `tests/types/` holds PHPStan type-inference fixtures.

## Working in this repo

- **The presenter lifecycle is exact and load-bearing.** The precise phase order
(action, canonicalization, signal dispatch, `beforeRender`, `render`, the
`onRender` hook, `afterRender`), `AbortException`-based termination, and the
4xx-not-500 invalid-link contract are documented in `docs/internals/lifecycle.md`.
Don't reconstruct them from memory.
- **Links and persistent state span the whole component tree.** Link-generation
modes and whole-tree global-state aggregation are the trickiest code here; see
`docs/internals/link-generation.md` and `persistent-state.md`.
- User-facing how-to (routing DSL, signals, snippets, component factories, flash
messages, DI/NEON configuration) is manual material and lives in the public web
docs, not here.
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
],
"require": {
"php": "8.3 - 8.5",
"nette/component-model": "^3.2 || ^4.0",
"nette/component-model": "^4.0",
"nette/http": "^3.4",
"nette/routing": "^3.1.1",
"nette/utils": "^4.1"
Expand All @@ -29,7 +29,7 @@
"nette/tester": "^2.6",
"nette/caching": "^3.2",
"nette/di": "^3.2",
"nette/forms": "^3.2",
"nette/forms": "^4.0",
"nette/robot-loader": "^4.0",
"nette/security": "^3.2",
"latte/latte": "^3.1.4",
Expand All @@ -43,9 +43,9 @@
"conflict": {
"nette/caching": "<3.2",
"nette/di": "<3.2",
"nette/forms": "<3.2",
"nette/forms": "<4.0",
"nette/schema": "<1.3",
"latte/latte": "<3.1.4 || >=3.2",
"latte/latte": "<3.1.5 || >=3.2",
"tracy/tracy": "<2.12"
},
"autoload": {
Expand All @@ -61,7 +61,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "3.3-dev"
"dev-master": "4.0-dev"
}
},
"config": {
Expand Down
81 changes: 81 additions & 0 deletions docs/decisions/2025-02-27 linkbase-tag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# {linkBase} tag - compile-time base for relative links in a template

Status: accepted · 2025-02-27 · branch `v3.2` (released in 3.2.7)

## Context

A flexible directory structure is one of the most valuable features of Nette
Application, but it creates a problem with shared layouts: relative link targets
(`Dashboard:`, `Products:Overview:`) are resolved against the *current*
presenter. When the same layout is used by both `Admin:Dashboard` and
`Admin:Products:Detail`, the target `Dashboard:` resolves correctly to
`Admin:Dashboard:default` in one case and to the non-existent
`Admin:Products:Dashboard:default` in the other.

The previous workaround was to write every target in layouts absolutely
(`:Admin:Dashboard:`). It works, but the module prefix is duplicated in every
link, so moving or renaming a module means editing templates en masse.

## Considered options

- **Status quo (absolute links everywhere)** - no new concept, but prefix
duplication and fragility when refactoring modules.
- **Runtime solution** (setting the base on the presenter/component, or a
template variable threaded into `LinkGenerator`) - collides with the shared
template cache: the same compiled layout is used by presenters from different
modules, so the base would have to become part of the cache key, or a runtime
cost would be paid on every link. It would also make the template no longer
self-describing - a link's target would depend on state set somewhere in PHP.
- **Compile-time tag in the template** (chosen) - the base is declared directly
in the template it concerns, and expansion happens at compile time.

## Decision

A new tag `{linkBase module}` plus a compile-time pass `applyLinkBasePass`
registered in `UIExtension::getPasses()`:

- The tag must appear in the template head (`Tag::isInHead()`, otherwise
`CompileException`) and applies to the **whole template file** - not to
included files, which are compiled separately and may carry their own
`{linkBase}`. It emits no code itself (`print()` returns `''`); all the work
is done by the pass. Only the first tag found is used
(`NodeHelpers::findFirst`).
- The pass traverses the AST and rewrites **all** `LinkNode`s - i.e. `{link}`,
`{plink}` and `n:href` uniformly.
- When both the target and the base are static strings, the destination is
rewritten directly at compile time (`LinkGenerator::applyBase()` called from
the pass) - **zero runtime overhead**. When either is an expression, the
destination is wrapped in a runtime call `LinkGenerator::applyBase(%node,
%node)` via `AuxiliaryNode`.
- Semantics of `LinkGenerator::applyBase($link, $base)`: the prefix `:$base:` is
added only when the target **contains a colon and does not start with one**.
Thus the base applies exclusively to relative targets that name a presenter
(`Users:list` → `:Admin:Users:list`); untouched are absolute targets
(`:Foo:`), targets within the current presenter (`show`, `this`), signals
(`click!`) and aliases - none of which have a colon in the middle.

## Consequences

- (+) Layouts and templates shared by presenters at different module depths link
consistently, regardless of who renders them.
- (+) Renaming or moving a module is a single change in `{linkBase}` instead of
editing every link.
- (+) For static links, expansion happens at compile time; nothing is paid at
runtime.
- (−) Another tag and concept to learn; the per-template-file scope may surprise
(includes do not inherit the base).
- (−) The base cannot be set per presenter/module from PHP - that remains open.

## Code

- `src/Bridges/ApplicationLatte/Nodes/LinkBaseNode.php` - the tag and the
`applyLinkBasePass` pass
- `src/Application/LinkGenerator.php` - `applyBase()` (`@internal`)
- tag and pass registration in `src/Bridges/ApplicationLatte/UIExtension.php`
(`getTags()`, `getPasses()`)
- test: `tests/Bridges.Latte/{linkBase}.phpt` (static and dynamic targets)
- commit `e3beab847`, released in `v3.2.7`

The `|absoluteUrl` filter shipped in the same release for forcing an absolute URL
on an arbitrary value (image path, database value), complementing this tag for
cases outside presenter link targets.
58 changes: 58 additions & 0 deletions docs/internals/access-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Access control & CSRF

`AccessPolicy` (internal) evaluates `#[Requires]` attributes. The attribute is
repeatable, class- or method-level, and matched with `IS_INSTANCEOF`, so subclasses
(like the deprecated `#[CrossOrigin]`) count as `Requires` instances.

## Enforcement runs at three sites

No single file shows this; the policy is applied wherever user code is about to run:

1. **Class-level** — `Presenter::run()`, in the checkRequirements phase, on the
presenter's own reflection (before `startup`).
2. **Method-level** — `Component::tryCall()`, i.e. on every `action*`, `render*`
and `handle*` invocation, right before argument conversion.
3. **Factory methods** — `Component::createComponent()` checks the policy on the
`createComponent<Name>()` method, so a component can be guarded at creation.

At the first two sites the (older, unrelated) `checkRequirements()` user hook is
also called with the same reflection — the two mechanisms coexist. The factory
site runs only the attribute policy.

## `#[Requires]` semantics

- `methods:` — allowed HTTP methods; `['*']` is a wildcard. **Trap:** a
*class-level* `methods:` empties `$presenter->allowedMethods` to bypass the
deprecated legacy `Presenter::checkHttpMethod()`, so the attribute fully replaces
it. Violation → 405 with an `Allow` header.
- `actions:` — method-level use is allowed **only in presenters**
(`LogicException` otherwise). Violation → 4xx via `error()`.
- `forward: true` — the element is reachable only via forward; it also makes the
element **non-linkable**: `AccessPolicy::isLinkable()` is what
`LinkGenerator::validateLinkTarget` consults for every mode except `forward`
(see link-generation.md). Violation at runtime → 4xx.
- `ajax: true` — non-AJAX request → 403.
- `sameOrigin: true` — see below.

## The implicit signal CSRF rule

**Every `handle*` method implicitly gets `Requires(sameOrigin: true)`** — added by
`AccessPolicy::applyInternalRules` — unless the method opts out via
`#[Requires(sameOrigin: false)]`, the deprecated `#[CrossOrigin]`, or a legacy
`@crossOrigin` annotation. Same-origin is verified from the **`Sec-Fetch-Site`
header** (`$httpRequest->isFrom(FetchSite::SameOrigin)`), not from a SameSite
cookie.

A violation does **not** produce an error: it calls `Presenter::detectedCsrf()`,
whose default is `redirect('this')` — **the signal is silently dropped** and the
page reloads without it. `detectedCsrf()` is public and overridable.

## `UI\Form` has its own, parallel check

`Form::signalReceived('submit')` does **not** go through `AccessPolicy` for CSRF:
it re-checks `Sec-Fetch-Site` itself, gated by the `$crossOrigin` flag from
nette/forms (`allowCrossOrigin()`), and calls the same `detectedCsrf()`. Two more
suppressions live here: a request with the `RESTORED` flag (set by
`Presenter::restoreRequest` for re-sent POSTs) skips `fireEvents()` entirely, and
`receiveHttpData()` returns null for forwarded requests — a form never submits
across a forward.
75 changes: 75 additions & 0 deletions docs/internals/lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Presenter lifecycle

`Presenter::run(Request): Response` is a fixed sequence; the exact ordering is the
non-local knowledge (some steps sit where a reader would not expect).

## Phase order (`run`)

Before the `try`: `initGlobalParameters()` parses the action, the signal, and the
signal receiver, and calls `loadState()`. Then, inside the `try`:

1. **checkRequirements** — access policy, `checkRequirements`, HTTP method check.
2. **startup** — `onStartup`, then `startup()`, then a guard that the child called
`parent::startup()` (else `InvalidStateException`).
3. **`action<Action>()`** — via `tryCall`. A `SwitchException` here re-enters with a
changed action **and disables `autoCanonicalize`**.
4. **component autoload** — every id in `globalParams` is `getComponent(…, throw:
false)`d.
5. **canonicalize** — only if `autoCanonicalize` (see below).
6. **HEAD** — terminates early on a HEAD request.
7. **`handle<Signal>()`** — `processSignal()`. **This runs after the action and
before rendering**, which is the ordering that surprises people.
8. **beforeRender** → **`onRender`** (a hook that is easy to forget) → **`render<View>()`**
(again `SwitchException` → `setView` + retry) → **afterRender**.
9. **sendTemplate** — completes and sends the template response.

So the true order is **startup → action → (autoload + canonicalize) → signal →
beforeRender → onRender → render → afterRender → sendTemplate**.

A `SwitchException` escaping anywhere other than the two retry sites above is
turned by `run()` into a `LogicException` ("Switch is only allowed inside
action*() or render*() method").

## Termination is by exception; there is no explicit "frozen" flag

Every terminating method funnels through `sendResponse()` → `terminate()`, which
**throws `AbortException`**: `sendTemplate`, `sendPayload`/`sendJson`,
`redirectUrl`, `forward`. `run()`'s **empty** `catch (AbortException)` then lets
control fall through to the post-lifecycle steps. So there is no separate "state is
frozen" mechanism — the response method simply throws, and nothing after it in the
lifecycle runs. (`error()` is different: it throws `BadRequestException`, which is
**not** caught in `run()` and bubbles up to `Application`.)

**Post-lifecycle, always runs** (even after redirect/JSON, because the abort lands
here): `saveGlobalState()`, then the AJAX payload block (`payload->state =
getGlobalState()`, snippet mode for an invalidated `TextResponse`), flash-session
30-second expiration, a `VoidResponse` fallback, `onShutdown`, `shutdown()`, and
`return $this->response`.

## The template file is resolved lazily, at `sendTemplate`

`completeTemplate()` copies the presenter's `#[TemplateVariable]` properties into
the template (see templates.md) and, **only if the template has no file set**,
calls `findTemplateFile()` **then** — so template lookup happens at send time, not
when the template object is created. Changing the view is possible right
up to `render<View>`; after `sendTemplate` throws `AbortException` nothing more can
affect it.

## Canonicalization uses the `redirectX` mode

`canonicalize()` (auto-called at step 5 unless `autoCanonicalize` was turned off by
an action switch) returns immediately for AJAX or non-GET/HEAD. Otherwise it builds
the canonical URL with `link(..., 'redirectX')` (see link-generation.md — an
absolute URL with no flash key, used purely for comparison), swallows any
`InvalidLinkException`, and if the current URL differs, redirects — **302 when the
`VARYING` request flag is set, else 301**. That flag is set by the router (external
`nette/routing`); this package only reads it.

## Signals

`initGlobalParameters` reads the signal from `_do` (POST) / `do` (the `SignalKey`),
splitting `{receiver}-{signal}` at the **last** `-` (so a receiver id may itself
contain dashes). `processSignal()` resolves the receiver (`''` = the presenter
itself), raising `BadSignalException` if it is missing or not a `SignalReceiver`,
then calls `signalReceived()` → `handle<Signal>()`. A signal always targets the
current presenter/action; the URL carries it as `?do=<receiver>-<signal>`.
Loading