Skip to content

A promoted readonly parameter with an iterable<T> or Closure(…) docblock fails with "Cannot modify readonly property" #79

Closed

Description

@klunejko

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.

Activity

  1. self-assigned this
    on Sep 18, 2026
  2. rcalicdan commented on Sep 18, 2026

    @rcalicdan
    Member

    @klunejko Hello, I released a patch (v0.10.5) and also fix the other issue earlier.. Enjoy.. Many Thanks for submitting critical bug reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions