From 46fe99ebdb68fb7b10629fcb57b8c08244329765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Thu, 27 Nov 2025 12:39:02 +0100 Subject: [PATCH 1/2] EnforceReadonlyPublicPropertyRule: support asymmetric visibility Do not report publicPropertyNotReadonly error when property uses private(set) or protected(set) modifiers, as these properties are effectively readonly from outside the class. Fixes #319 --- src/Rule/EnforceReadonlyPublicPropertyRule.php | 2 +- .../Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Rule/EnforceReadonlyPublicPropertyRule.php b/src/Rule/EnforceReadonlyPublicPropertyRule.php index a42ad50..e7f0776 100644 --- a/src/Rule/EnforceReadonlyPublicPropertyRule.php +++ b/src/Rule/EnforceReadonlyPublicPropertyRule.php @@ -41,7 +41,7 @@ public function processNode( return []; } - if (!$node->isPublic() || $node->isReadOnly() || $node->hasHooks()) { + if (!$node->isPublic() || $node->isReadOnly() || $node->hasHooks() || $node->isPrivateSet() || $node->isProtectedSet()) { return []; } diff --git a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php index 2323d44..2da599c 100644 --- a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php +++ b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php @@ -57,3 +57,9 @@ class ImplementingClass implements MyInterface { class ImplementingClass2 implements MyInterface { public readonly string $key; } + +class AsymmetricVisibility { + public private(set) string $privateSet; + public protected(set) string $protectedSet; + public public(set) string $publicSet; // error: Public property `publicSet` not marked as readonly. +} From 15f29df0961e29ab36c03d5a99e5041ec8b8df5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Thu, 27 Nov 2025 12:44:21 +0100 Subject: [PATCH 2/2] Bump minimum PHPStan version to 2.1.32 Required for asymmetric visibility support (isPrivateSet/isProtectedSet methods). --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index c3d1835..d7234a6 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.8" + "phpstan/phpstan": "^2.1.32" }, "require-dev": { "editorconfig-checker/editorconfig-checker": "^10.6.0", diff --git a/composer.lock b/composer.lock index a834ab6..bc4eadb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e2684e49804e46966e498e528dd23701", + "content-hash": "e45e2400e8fe0857f5bf825391d73768", "packages": [ { "name": "phpstan/phpstan",