Skip to content

Know the static Html::text() and Html::html() factories - #235

Open
spaze wants to merge 1 commit into
phpstan:2.0.xfrom
spaze:spaze/html-methods
Open

spaze wants to merge 1 commit into
phpstan:2.0.xfrom
spaze:spaze/html-methods

Conversation

@spaze

@spaze spaze commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

nette/utils 4.1.5 added Html::text() and Html::html() as @method static on the class and deprecated Html::fromText() and Html::fromHtml() in their favor. PHPStan never sees those declarations: the stub for the class replaces its docblock, and HtmlClassReflectionExtension claims every method name as an attribute setter, an instance method, so a call to either factory is reported as a static call to an instance method.

The stub now declares the two methods the way the class does, and the extension leaves their names alone, because PHPStan asks the extension before it reads the annotations and the extension would otherwise keep winning. Either change alone is not enough: with only the stub the extension still answers first, with only the extension the calls are undefined, since the stub also hides __callStatic().

Copilot AI balanced review requested due to automatic review settings November 6, 2026 00:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unconditional factory annotations allow static calls that fail at runtime on supported nette/utils versions and override existing variadic instance setters, requiring version-gated annotations and matching reflection handling.

2 open issues
What changed in this PR

This PR teaches the Nette extension to recognize Html::text() and Html::html() as static factories rather than instance attribute setters.

Changes:

  • Adds factory annotations with late-static return types to the Html stub.
  • Excludes both factory names from the dynamic attribute-method handler.
  • Extends unit tests to verify those exclusions.
File Description
tests/​Reflection/​Nette/​HtmlClassReflectionExtensionTest.php Tests method-name-specific handling for factories and attribute setters.
stubs/​Utils/​Html.stub Declares the two static factories.
src/​Reflection/​Nette/​HtmlClassReflectionExtension.php Defers factory-name resolution to the annotations.

Review effort: Balanced

Comment thread stubs/Utils/Html.stub
Comment on lines +8 to +9
* @method static static text(mixed $text)
* @method static static html(mixed $html)
{
$classReflection = $this->reflectionProvider->getClass($className);
self::assertSame($result, $this->extension->hasMethod($classReflection, 'href'));
self::assertSame($result, $this->extension->hasMethod($classReflection, $methodName));
nette/utils 4.1.5 added `Html::text()` and `Html::html()` as `@method static` on the class and deprecated `Html::fromText()` and `Html::fromHtml()` in their favor. PHPStan never sees those declarations: the stub for the class replaces its docblock, and `HtmlClassReflectionExtension` claims every method name as an attribute setter, an instance method, so a call to either factory is reported as a static call to an instance method.

The stub now declares the two methods the way the class does, and the extension leaves their names alone, because PHPStan asks the extension before it reads the annotations and the extension would otherwise keep winning. Either change alone is not enough: with only the stub the extension still answers first, with only the extension the calls are undefined, since the stub also hides `__callStatic()`.
@spaze
spaze force-pushed the spaze/html-methods branch from 3b028de to 7c6653b Compare November 6, 2026 00:29
spaze added a commit to spaze/michalspacek.cz that referenced this pull request Nov 6, 2026
The app's packages move to
- nette/application 3.3.1
- nette/forms 3.3.1
- nette/utils 4.1.6
- nette/component-model 4.0
- Latte 3.1.7
- ... among others

The Lambda and dev-tools get their updates too. The Nette releases deprecate things this code used, with notices at runtime that Tester turns into failures, and tighten types the analysers now hold the code to, so the rest of the branch follows them:
- a running presenter's request is never null
- `#[Persistent]` replaces the `@persistent` annotation
- `Request::Forward` replaces `Request::FORWARD`
- captions and selected items go through their methods
- `Html::text()` and `Html::html()` replace `fromText()` and `fromHtml()`
- `{form}` and `{control}` take a comma between the name and the rest

Our `LinkGenerator` existed to turn the `string|null` of Nette's into a string. 3.3 types `link()` as a string unless a mode is given, so the wrapper is gone and its consumers inject Nette's directly.

The forms lose their CSRF token. nette/forms 3.3 deprecates `addProtection()` because a form submit that is not same-origin is refused before any handler runs, going by `Sec-Fetch-Site`, with the `_nss` cookie for browsers that don't send it; the check was same-site before and is same-origin now.

On top of that, `ResourceIsolationPolicy` already forwards a cross-site request that is not a plain top-level GET navigation to the Forbidden page, so the token was a third layer.

`FormFactory` and `UnprotectedFormFactory` existed only to add it or skip it, so they are deleted, every form factory builds its form itself, and the Forms section of CLAUDE.md describes what protects the forms now. One visible effect of the tightening: a form submit or a `#[Requires(sameOrigin: true)]` action reached from another subdomain is refused like a cross-site one, which a new test pins.

nette/forms types validators as `callable(never, never): bool` so that a callable with any parameter types fits. PHPStan infers an inline closure's parameter types from that and gets `never`, which turned the type checks in the validator closures into dead code, so those closures are methods now. Psalm reads the same type as a mismatch with the `callable(Control)` we pass, hence the handler in `psalm.xml`.

PHPStan doesn't know `Html::text()` and `Html::html()` yet: phpstan-nette's stub of the class hides their declaration and its reflection extension takes any method name for an attribute setter. `phpstan.neon` ignores exactly those two calls until phpstan/phpstan-nette#235 is released.

webauthn-lib 5.3.12 validates the submitted credential in its denormalizers and throws its own exception for valid JSON of the wrong shape. The guards around `deserialize()` caught only the serializer's exceptions, so such a credential ended as a 500 instead of the sign-in or registration error; they catch both now.

The vendor PHPStan config allows the `mt_rand()` and `uniqid()` calls the updated nette/caching and nette/utils make; neither picks anything secret.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants