Skip to content

chore: raise PHPStan to level 9 - #134

Merged
dakorpar merged 1 commit into
masterfrom
chore/phpstan-level-9
Aug 14, 2026
Merged

chore: raise PHPStan to level 9#134
dakorpar merged 1 commit into
masterfrom
chore/phpstan-level-9

Conversation

@dakorpar

Copy link
Copy Markdown
Member

Follow-up to #133. Raises the PHPStan level in make phpstan from 8 to 9.

Level 9 stops accepting mixed where a concrete type is required. Most of the 35 errors traced back to one place: the renderer's config arrays and Nette's getOption() / translate() / getValue() all return mixed, and that value was passed straight into typed APIs.

Renderer config plumbing

  • getConfig() / getConfigOverride() now declare their real shape (array<string, mixed[]> and array<int, array<string, mixed[]>>), so fetchConfig() returns an array rather than mixed. It reads them through the getters instead of the SmartObject magic properties — the same call, without the untyped detour.
  • configElem() narrows what it reads out of the config instead of trusting it. A non-string element name is ignored, class config is normalized once up front, and an attributes entry that is not an array is skipped rather than fed to foreach.
  • Class handling moved into a fetchClasses() helper. CLASS_REMOVE now uses a strict array_filter instead of array_diff, which insisted every class be castable to string — Nette also accepts ['class-name' => bool] class attributes, and the filter handles those without a cast while preserving keys exactly as array_diff did.

Drawing values that came from options

Group labels and validation messages are drawn only when they are actually drawable — HtmlStringable / string / Stringable — instead of handing whatever the option happened to hold to addHtml() or addText(). Previously a group label option holding, say, an int went to addHtml() unchecked.

Inputs

  • ChoiceInputTrait only indexes the disabled-values array with something usable as an array key, and compares choice values through a stringifyChoiceValue() helper instead of casting mixed to string.
  • CheckboxInput, CheckboxListInput and RadioInput narrow the result of translate() before using it as a caption. CheckboxInput::makeCheckbox()'s $caption doc widened from string|Html|null to string|Stringable|null, which is what it always accepted — setText() takes Stringable, and Html is one.

One real bug: DateInput::validateFormat()

This one was not just a typing gap. The rule passed $input->getValue() into a string parameter, but getValue() returns a DateTime once the input has been validated — so calling the rule in that state was a guaranteed TypeError:

TypeError: DateTimeFormat::validate(): Argument #2 ($timeString) must be of type string, DateTime given

It survived in practice only because BaseControl::validate() calls cleanErrors() first, which DateInput overrides to reset its isValidated flag — so during normal form validation getValue() still returns the raw text. Any other route to the rule crashed.

The rule now checks the format only when there is text to check, and returns true otherwise (an already-parsed value has no textual format to reject; emptiness is the required rule's job). getValue() gained the same guard before handing its value to DateTime::createFromFormat().

Verification

  • make phpstan (now -l 9) — no errors
  • make cs — clean
  • make tests — 173 tests, 266 assertions, all passing

4 new tests: three pin the DateInput rule behaviour (parsed value, unparseable text, null value on a nullable input), one covers class config given as a plain string rather than an array.

Rendered HTML is unchanged — no fixture updates. The existing RendererConfigTest cases for CLASS_SET / CLASS_ADD / CLASS_REMOVE / ATTRIBUTES / CONTAINER all still pass against the rewritten configElem().

🤖 Generated with Claude Code

Level 9 stops accepting `mixed` where a concrete type is required. Most of
the 35 errors came from one place: the renderer's config arrays and Nette's
`getOption()`/`translate()`/`getValue()` all hand back `mixed`, which was
then passed straight into typed APIs.

- `getConfig()`/`getConfigOverride()` now declare their real shape
  (`array<string, mixed[]>`), so `fetchConfig()` returns an array instead of
  `mixed`. It reads them through the getters rather than the SmartObject
  magic properties, which is the same call without the untyped detour.
- `configElem()` narrows what it reads out of the config instead of trusting
  it: a non-string element name is ignored, class config is normalized once,
  and a non-array `attributes` entry is skipped. Class handling moved into
  `fetchClasses()`, and `CLASS_REMOVE` uses a strict `array_filter` rather
  than `array_diff`, which required every class to be castable to string.
- Group labels and validation messages are drawn only when they are
  something drawable (`HtmlStringable`, string, `Stringable`), instead of
  handing whatever the option happened to hold to `addHtml()`/`addText()`.
- `ChoiceInputTrait` only indexes the disabled-values array with a usable
  array key, and compares choice values through `stringifyChoiceValue()`.
- `CheckboxInput`, `CheckboxListInput` and `RadioInput` narrow the result of
  `translate()` before using it as a caption.

`DateInput::validateFormat()` was a real bug, not just a typing gap: it
passed `getValue()` to a `string` parameter, and `getValue()` returns a
`DateTime` once the input has been validated, so calling the rule in that
state was a guaranteed TypeError. The rule now checks the format only when
there is text to check. `getValue()` itself gained the same guard before
handing its value to `DateTime::createFromFormat()`.

Rendered HTML is unchanged — no fixture updates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.73684% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.51%. Comparing base (af238e5) to head (6308fab).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/Inputs/RadioInput.php 80.00% 2 Missing ⚠️
src/Traits/ChoiceInputTrait.php 83.33% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #134      +/-   ##
============================================
+ Coverage     97.48%   97.51%   +0.02%     
- Complexity      313      333      +20     
============================================
  Files            25       25              
  Lines           996     1007      +11     
============================================
+ Hits            971      982      +11     
  Misses           25       25              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dakorpar
dakorpar merged commit faea76b into master Aug 14, 2026
10 of 11 checks passed
@dakorpar
dakorpar deleted the chore/phpstan-level-9 branch August 14, 2026 20:13
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.

1 participant