Table of Contents

PHP RFC: Direction-Aware Property Resolution

Introduction

PHP 8.4's asymmetric visibility (private(set) / protected(set)) is enforced by re-checking set access on every write: each assignment to an asymmetric property calls zend_asymmetric_property_has_set_access(), which resolves the currently executing scope and compares it against the property's declaring class. Ordinary visibility (public / protected / private) does not work this way — it is checked once per call site, when the engine's per-opline runtime cache is populated, and every subsequent execution of that opline is a plain cached property access.

The reason for the difference is structural, not intentional: the engine's shared property resolver, zend_get_property_offset(), serves reads and writes alike and does not know which it is resolving for. Get-visibility can therefore be checked at population, but set-visibility cannot — so it was bolted onto every mutation site instead.

The measurable consequence: a write to a private(set) property costs about 3× as much as a write to a plain public typed property, and protected(set) about (the compatible-scope walk is longer). This is a permanent tax on adopting asymmetric visibility, paid on the hottest write path shape in the engine.

This RFC makes property resolution direction-aware and moves the asymmetric set check to cache-population time. After the first resolution at a call site, writes to asymmetric properties cost exactly the same as writes to public typed properties. The change is behavior-neutral: no observable semantics change, no error message changes, and the entire test suite passes unmodified.

Proposal

Mechanics