Extend DenormalizerInterface.stub gate to cover Symfony 7.4+ (LSP errors on per-type denormalizer implementations)

Open Beginner friendly
#494 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
75/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
php, symfony
Domain
devtools, tooling

Research direction

Start in src/Stubs/Symfony/StubFilesExtensionLoader.php:125 and inspect stubs/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub. Adjust the Symfony Serializer version condition so the existing stub is loaded for the affected versions, then run the relevant extension tests to verify the stub is applied and the reported implementation errors are resolved.

Written by the indexing model from the issue text.

Description

Summary

Implementing DenormalizerInterface with a single concrete return type now triggers method.childReturnType (and, if narrowing @param to compensate, method.childParameterType) errors that have no clean PHPDoc resolution. The errors are caused by the conditional return type that has been part of Symfony's DenormalizerInterface since 7.4. phpstan-symfony already ships a stub that resolves this — it just isn't loaded for Symfony ≥ 7.4.

Environment

  • phpstan/phpstan: 2.1.56
  • phpstan/phpstan-symfony: 2.0.18
  • symfony/serializer: 8.0.10
  • PHP: 8.5

Reproduction

A typical per-type denormalizer:

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

class DateRangeNormalizer implements DenormalizerInterface
{
    public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): DateRange
    {
        return new DateRange(/* ... */);
    }

    public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
    {
        return $type === DateRange::class;
    }

    public function getSupportedTypes(?string $format): array
    {
        return [DateRange::class => true];
    }
}

PHPStan (level 8) reports:

Return type (DateRange) of method ...DateRangeNormalizer::denormalize() should be covariant
with return type (($type is class-string<object> ? object : mixed)) of method
Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize()
  identifier: method.childReturnType

The natural attempt to fix it — narrowing the parameter to bind TObject:

/** @param class-string<DateRange> $type */
public function denormalize(mixed $data, string $type, ...): DateRange

produces a second error:

Parameter #2 $type (class-string<DateRange>) of method ...::denormalize() should be
contravariant with parameter $type (string) of method DenormalizerInterface::denormalize()
  identifier: method.childParameterType

The two requirements (bind TObject to satisfy covariance, keep param as wide as string for contravariance) are mutually exclusive in PHPStan's LSP model. There is no PHPDoc that satisfies both.

Root cause

Since Symfony 7.4, DenormalizerInterface::denormalize() is annotated:

/**
 * @template TObject of object
 * @param class-string<TObject>|string $type
 * @return ($type is class-string<TObject> ? TObject : mixed)
 */

The conditional return is great for callers of Serializer::denormalize($data, Foo::class) (return resolves to Foo), but for implementations of the interface — which are per-type and rely on supportsDenormalization() for runtime dispatch — the conditional cannot be honored in general by a single concrete return type.

phpstan-symfony already has the right workaround: a stub at stubs/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub that flattens @return to mixed. That stub is loaded at src/Stubs/Symfony/StubFilesExtensionLoader.php:125:

if ($this->isInstalledVersionBelow('symfony/serializer', '7.4.0.0')) {
    $files[] = $stubsDir . '/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub';
}

The conditional return type has been present in the source since 7.4, so the gate excludes exactly the versions where the stub is needed.

Proposed fix

Either remove the version gate entirely, or bump it to cover currently-supported Symfony versions (e.g. '9.0.0.0'):

- if ($this->isInstalledVersionBelow('symfony/serializer', '7.4.0.0')) {
+ if ($this->isInstalledVersionBelow('symfony/serializer', '9.0.0.0')) {
      $files[] = $stubsDir . '/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.stub';
  }

The existing stub content already does the right thing; it just isn't being applied.

Workaround in the meantime

Users can suppress the diagnostic per-method:

/** @phpstan-ignore method.childReturnType */
public function denormalize(...): DateRange

but that scales poorly across codebases with many normalizers.

Dominant language
PHP
Stars
800
Forks
104
Avg merge
4m
Merged PRs (30d)
1

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from phpstan/phpstan-symfony

All issues in phpstan/phpstan-symfony

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.