Skip to main content

php-type-errors

Use when PHPStan or Psalm reports a type error on PHP code: resolving it with a less-strict type that satisfies both tools, declaring template parameters, the MixedAssignment and invariant-generic patterns, and tool-specific docblocks as the last resort.

Jump to install

Source facts

Repository
ScientaNL/DoctrineJsonFunctions
Last source activity
September 16, 2026 at 07:17
Detected SKILL.md language
English
Stars
591
Forks
51

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
php-type-errors
description
Use when PHPStan or Psalm reports a type error on PHP code: resolving it with a less-strict type that satisfies both tools, declaring template parameters, the MixedAssignment and invariant-generic patterns, and tool-specific docblocks as the last resort.
# PHPStan and Psalm type errors Both tools gate a change and must pass simultaneously (`composer phpstan`, `composer psalm`). Proactive PHP conventions (docblocks, promotion, array types): [rules/php-quality.md](../../rules/php-quality.md). Whether to suppress at all: [rules/code-quality.md § Suppression Policy](../../rules/code-quality.md#suppression-policy). Unfamiliar error identifier → read `https://phpstan.org/error-identifiers/<identifier>` before guessing. ## Prefer a less-strict type that satisfies both tools When an annotation satisfies one tool but not the other, loosen it rather than writing parallel `@phpstan-`/`@psalm-` docblocks: one annotation, easier to maintain, usually matches real call-site usage. - Check actual call-site usage before tightening; if call sites vary, loosen the annotation rather than changing dozens of callers. - `array<array-key, mixed>` not `array<int, mixed>`: survives inheritance, both tools accept. - `array<mixed>` when call sites mix enums, associative, and list shapes. - Flexible property types on base classes children extend: `@var array<array-key, string>`, not `array{0: string, 1: string}|array{}`. Applies to: abstract nodes with per-platform implementations that disagree, constructors with varied params, properties inherited by many children, any case where PHPStan passes but Psalm reports an inheritance or usage conflict. ## `MissingTemplateParam`: declare the parameter explicitly Declare the parameter on every link in the chain: the interface, the implementation, and each concrete child of a generic base. ```php /** @template T of \Doctrine\ORM\Query\AST\Node */ abstract class NodeCollection { /** @param list<T> $nodes */ public function __construct(protected array $nodes) {} } /** @template-extends NodeCollection<\Doctrine\ORM\Query\AST\PathExpression> */ final class PathExpressionCollection extends NodeCollection {} ``` ## Psalm `MixedAssignment`: pass the mixed value, don't assign it The check fires on assigning a `mixed` expression to a variable, never on passing one to a parameter declared `mixed`. Narrow an untyped source (a decoded JSON payload, a raw driver result row) by handing it straight to a helper: ```php // ✅ no suppression needed return self::asNonEmptyString($row['data']); private static function asNonEmptyString(mixed $value): string | null { return is_string($value) && $value !== '' ? $value : null; } // ❌ reaching for the suppression first /** @psalm-suppress MixedAssignment */ $data = $row['data']; ``` Go to the helper first for an array offset: `is_string($arr['k'] ?? null)` does not narrow the offset, so keeping the guard inline just moves the error to `MixedArgument` on the next call. ## `Assert::allPositiveInteger` narrows for PHPStan, not Psalm Satisfying a `list<positive-int>` parameter from a `list<int>` via inline `Assert::allPositiveInteger($list)` passes PHPStan (phpstan-webmozart-assert) but leaves Psalm reporting `ArgumentTypeCoercion`. Narrow at the source (`@return positive-int` on the producing method and `@param list<positive-int>` on the intermediate), not at the inline `all*` assert at the call. ## Invariant generic looks like a false positive PHPStan reports `argument.type` with identical types on both sides and `💡 Template type T is not covariant`. Not a false positive: the hint is the diagnosis: an invariant `@template T` can't prove exact type identity when a variable annotated `Foo<X>` is passed to another `Foo<X>` parameter. | Approach | Verdict | |---|---| | Remove the generic annotation (leave the raw PHP type) | ✅ preferred first attempt: accepts any instantiation | | Declare `@template-covariant T` on the class or interface you own | ✅ best long-term fix | | `@param Foo<covariant T>` (generic tag) | ❌ Psalm parses `covariant` as a namespace | | `@phpstan-param Foo<covariant T>` + `@psalm-param Foo<T>` | ✅ last resort only | **Never** `@param Foo<covariant T>` in `src/`: Psalm misparses `covariant` regardless of PHPStan. ## Tool-specific docblock tags (last resort) Only when one tool needs syntax that crashes the other (PHPStan `covariant`, star projection, call-site variance). Write parallel tags: Psalm reads `@psalm-*` and ignores `@phpstan-*`; PHPStan the reverse. Drop the plain `@param`/`@return` to avoid ambiguity. ```php /** * @phpstan-param Collection<covariant Node> $nodes * @psalm-param Collection<Node> $nodes */ ``` ## Registered suppressions already in this repo `psalm.xml` holds the registered `<issueHandlers>`; read it before concluding an issue is unsilenced. A new entry there is a suppression like any other and goes through the [suppression policy](../../rules/code-quality.md#suppression-policy). `findUnusedBaselineEntry` is on, so a stale entry fails the run.
View on GitHub