Skip to content

Fix phpstan/phpstan#12964: Support for covariant templates in interface properties in 8.4#5415

Open
phpstan-bot wants to merge 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ylaxurz
Open

Fix phpstan/phpstan#12964: Support for covariant templates in interface properties in 8.4#5415
phpstan-bot wants to merge 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ylaxurz

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHP 8.4 introduced property hooks and asymmetric visibility. Properties with only a get hook (no set hook) and properties with private(set) or protected(set) are effectively read-only from the public API perspective. PHPStan was incorrectly treating these properties as invariant for template variance checking, producing false positive errors like "Template type T is declared as covariant, but occurs in invariant position in property".

Changes

  • Modified src/Rules/Generics/PropertyVarianceRule.php to detect effectively read-only properties:
    • Properties with private(set) or protected(set) asymmetric visibility
    • Properties with only a get hook and no set hook
  • These properties are now treated as covariant position (same as readonly properties)

Root cause

The PropertyVarianceRule only checked isReadOnly() and isReadOnlyByPhpDoc() when deciding whether a property is in covariant position. It did not account for PHP 8.4's property hooks (get-only) and asymmetric visibility (private(set), protected(set)), which also make properties effectively read-only from the external API.

Test

Added tests/PHPStan/Rules/Generics/data/bug-12964.php with comprehensive test cases covering:

  • Interface properties with only { get; } hook (covariant and contravariant templates)
  • Properties with private(set) (covariant and contravariant templates)
  • Properties with protected(set) (covariant templates)
  • Properties with both { get; set; } hooks (should remain invariant)
  • The original issue examples: interface with @template-covariant and { get; }, class with private(set)

Fixes phpstan/phpstan#12964

…oks and asymmetric visibility

- Treat properties with only a get hook (no set hook) as covariant position
- Treat properties with private(set) or protected(set) as covariant position
- New regression test in tests/PHPStan/Rules/Generics/data/bug-12964.php
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

isEffectivelyReadOnly is an unclear name

Address review feedback: the previous name was unclear about what it checked.
The new name better communicates that the method detects properties with
restricted write access (asymmetric visibility or get-only hooks).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. I renamed isEffectivelyReadOnly to hasRestrictedWriteAccess to address the reviewer's feedback. The new name more clearly communicates that the method checks whether a property has restricted write access (via asymmetric visibility like private(set)/protected(set), or by having only a get hook with no set hook). Tests and static analysis both pass.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

/**
 * @template-covariant T
 */
class A
{
    /**
     * @param T $data
     */
    public function __construct(
        protected mixed $data,
    ) {}
}

is currently reported by PHPStan so I don't see a reason to ignore protectedSet

protected(set) allows subclasses to write to the property, just like
regular protected properties. Only private(set) truly restricts writes
to the declaring class, so only private(set) should be treated as
covariant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

All 11,736 tests pass. Here's a summary of the change:

Problem: The reviewer correctly pointed out that protected(set) still allows subclasses to write to the property, just like a regular protected property. Since regular protected properties are treated as invariant for template variance, protected(set) should be too.

Fix: Removed $node->isProtectedSet() from hasRestrictedWriteAccess() in PropertyVarianceRule.php. Now only private(set) (which truly restricts writes to the declaring class) is treated as covariant. Updated test expectations for class E accordingly — all four protected(set) properties now correctly report "invariant position" errors.

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