I believe this was introduced with the wrapper changes (unified syntax style) in v0.10.3 (#74)
final class RIPromotedReadonly
{
/** @param iterable<RIAdapter> $adapters */
public function __construct(private readonly iterable $adapters) {}
}
final class RIPromotedReadonlyClosure
{
/** @param \Closure(int): int $factory */
public function __construct(private readonly \Closure $factory) {}
}
new RIPromotedReadonly([new RIAdapterA()]); // Error: Cannot modify readonly property …::$adapters
new RIPromotedReadonlyClosure(static fn (int $i): int => $i + 1); // Error: Cannot modify readonly property …::$factory
The same class without readonly, a promoted readonly array<int,T> and a promoted readonly iterable without a docblock all pass. Cause, from the shipped source: src/Internal/Ast/FunctionContractInjector.php → buildParamWrappers() injects $adapters = \TypePHP\Internal\RuntimeTypeChecker::wrapIterable(__METHOD__, 'adapters', $adapters, $this); for an iterable-typed parameter (isIterableCandidate(), and wrapCallable for a callable-typed one) and, when $param->isPromoted(), appends $this->adapters = $adapters; — a second write to a property the engine already initialised from the promotion, which PHP rejects for readonly.
Expected: a promoted readonly parameter is validated without a second assignment (validate the wrapped value and leave the promoted property as initialised, or initialise the property once from the wrapped value instead of relying on the promotion), or the wrapper is skipped for readonly promotions with a documented note.
I believe this was introduced with the wrapper changes (unified syntax style) in v0.10.3 (#74)
The same class without
readonly, a promoted readonlyarray<int,T>and a promoted readonlyiterablewithout a docblock all pass. Cause, from the shipped source:src/Internal/Ast/FunctionContractInjector.php→buildParamWrappers()injects$adapters = \TypePHP\Internal\RuntimeTypeChecker::wrapIterable(__METHOD__, 'adapters', $adapters, $this);for an iterable-typed parameter (isIterableCandidate(), andwrapCallablefor a callable-typed one) and, when$param->isPromoted(), appends$this->adapters = $adapters;— a second write to a property the engine already initialised from the promotion, which PHP rejects forreadonly.Expected: a promoted readonly parameter is validated without a second assignment (validate the wrapped value and leave the promoted property as initialised, or initialise the property once from the wrapped value instead of relying on the promotion), or the wrapper is skipped for
readonlypromotions with a documented note.