====== PHP RFC: Scalar Extension Methods ====== * Version: 2.0 * Date: 2026-07-11 * Author: Holly Schilling, holly.a.schilling@outlook.com * Status: Draft * Depends on: [[rfc:extension_methods|PHP RFC: Extension Methods]] * Implementation: https://github.com/hollyschilling/php-src/tree/extension-methods-scalars * Discussion thread: tbd * Voting thread: tbd ===== Introduction ===== The [[rfc:extension_methods|Extension Methods RFC]] lets code declare additional methods for existing classes and interfaces, resolved only where PHP would otherwise raise an undefined-method error. This RFC extends the same construct to the built-in value types: extension string $s { public function length(): int { return strlen($s); } } var_dump("hello"->length()); // int(5) Every previous attempt at "scalar methods" for PHP has foundered on the same rock: designing the API. Which methods does ''string'' get, what are they named, how do the legacy argument orders map — a bikeshed with no exit. This RFC dissolves that problem rather than solving it: **PHP ships the mechanism and zero methods.** Userland defines, names, versions, and ships the scalar APIs as ordinary Composer packages, and with the [[rfc:extension_method_visibility|lexical visibility RFC]] competing scalar vocabularies cannot collide — each file imports the string API it wants. The language never has to bless anyone's ''length()''. **Why a separate RFC.** The base RFC's implementation is contained to the method-resolution miss path: if it is buggy, only code calling extension methods is affected. Value receivers still touch more of the engine than that — dispatch on the non-object call path and a brief handoff through the call frame — even though the base RFC's declared-receiver model (adopted //because// of this RFC; see the base RFC's Rejected Features) reduced the footprint to a fraction of earlier designs. It is co-proposed and fully implemented, but it deserves to be reviewed and voted on its own merits rather than riding on (or dragging down) the base proposal. ===== Proposal ===== ==== Targets ==== The five built-in value types become valid extension targets: ''string'', ''int'', ''float'', ''bool'' (covering ''true'' and ''false'' receivers), and ''array''. Scalar targets are matched on the **raw unqualified name**, never namespace-prefixed: ''extension string'' inside ''namespace App'' targets the built-in type, not ''App\string''. All other reserved type names (''iterable'', ''mixed'', ''object'', ''null'', ...) remain compile errors, as in the base RFC. ''null'' is deliberately not extendable — calls on ''null'' keep their existing error, and the nullsafe operator's short-circuit is unaffected. Named extensions and ''use extension'' compose unchanged: ''extension Str on string $s { }'' is lexically gated exactly like a class-targeted named extension — which is precisely what lets competing userland scalar APIs coexist. ==== Semantics ==== - **Dispatch.** A method call on a scalar is //unconditionally fatal// in PHP today, so this is the base RFC's error-path-only principle in its purest form: resolution sits exactly where "Call to a member function on string" is raised, keyed by the receiver's value type. No successful program changes behavior. - **The declared receiver holds the value.** ''extension string $s'' binds each method's receiver into ''$s'' — an ordinary by-value local, exactly like a parameter: mutation is local and does not propagate to the caller, which is PHP's value-type semantics stated in syntax. Chained calls — ''$s->shout()'' inside another string extension method — work naturally, and **generators work** (the receiver variable is populated before a generator's frame is captured). - **One restriction remains:** first-class callables of scalar extension methods throw an ''Error'' for now — a closure cannot yet carry a by-value receiver binding. Object-targeted extension methods support them; extending that to scalars is Future Scope. (''$this'', ''static::'', magic methods, and abstract methods are already rejected family-wide by the base RFC.) - **Direct calls only, by construction.** Callable arrays require objects, so ''call_user_func'' and friends can never reach a scalar extension method: the base RFC's open question about indirect invocation does not arise for scalars. ===== Backward Incompatible Changes ===== None. Every call this RFC makes resolvable is a fatal ''Error'' today, and ''extension string { }'' is a compile error under the base RFC. ===== Proposed PHP Version(s) ===== Same release as the base Extension Methods RFC. ===== RFC Impact ===== ==== To the Ecosystem ==== Tooling that learned the base RFC's blocks needs one addition: the five type names as targets, with methods surfaced in completion on receivers of the matching static type. The same static analysis that resolves extension calls on classes applies; there is no runtime discovery to model. ==== To Existing Extensions ==== The engine surface beyond the base RFC is small and narrow, and this section states it plainly: * **A brief frame handoff.** The scalar rides in the call frame's ''This'' slot (call-info bits preserved) only from call initialization until the body's first ''ZEND_RECV_RECEIVER'' instruction, which moves it into the declared variable, nulls the slot, and clears the release flag — the rest of the call is an ordinary scoped frame, and the receiver is freed by ordinary CV cleanup. Teardown paths discriminate for that narrow window by the receiver's type bits (the value types ''IS_FALSE''..''IS_ARRAY'', which no legacy frame stores there) — deliberately //not// by inspecting ''call->func'', which may already be a freed trampoline at teardown time. * **JIT.** Scalar-receiver bodies are excluded from JIT compilation (generated code must never observe the non-object handoff), matching the base RFC's existing rule that traces never follow into never-cached callees. Calls from JIT'd code dispatch correctly through the interpreter paths. JIT support for scalar bodies is Future Scope. * Extensions that inspect in-flight call frames directly and assume ''RELEASE_THIS'' implies an object ''This'' would need the same type-bits discrimination the engine now applies during the handoff window. ==== To SAPIs ==== None beyond the engine changes. ==== To Opcache ==== Nothing new beyond the base RFC: the flag is compile-time state persisted with the op_array; the registry lanes reuse the base registry. Verified under ''opcache.protect_memory=1'', ''file_cache'', and both JIT modes with aggressive hotness settings (the configuration that exposed — and now regression-tests — a JIT'd ''FETCH_THIS'' unsoundness during development). ===== Open Issues ===== - Should ''int'' methods apply to ''float'' receivers (numeric unification), or stay strictly type-keyed as proposed? ===== Unaffected PHP Functionality ===== All existing scalar behavior: casts, operators, string functions, array functions, ''null'' handling, and the nullsafe operator. Objects and the base RFC's semantics are untouched. Programs that never declare a scalar extension cannot observe this RFC. ===== Future Scope ===== * JIT compilation of scalar extension method bodies. * By-reference receiver variants (''function push($v): void'' mutating the caller's array) — deliberately excluded; value semantics are the safe default. ===== Voting Choices ===== Primary vote requiring a 2/3 majority (per the [[https://github.com/php/policies/blob/main/feature-proposals.rst#voting-phase|php/policies voting guidelines]]); void if the base Extension Methods RFC is declined: > Allow extension methods on the built-in value types as outlined in the RFC? Yes / No ===== Patches and Tests ===== Implemented on the [[https://github.com/hollyschilling/php-src/tree/extension-methods-scalars|extension-methods-scalars]] branch, stacked directly on the base RFC's ''extension-methods-phase1'' branch — [[https://github.com/hollyschilling/php-src/compare/extension-methods-phase1...extension-methods-scalars|the diff between the two branches]] is exactly this proposal. Tests cover all five targets, literal/temporary/dynamic-name receivers, chained calls, value semantics, generators, namespaced scalar targets, and the first-class-callable restriction; the lexical-gating interaction ships with the visibility RFC's branch. Verified: full Zend suite green plain, under ''opcache.protect_memory=1'', and under tracing and function JIT with ''jit_hot_*=1''. ===== Implementation ===== After acceptance: link to the merged commit(s), PHP version, and documentation. ===== References ===== * Base RFC: [[rfc:extension_methods|PHP RFC: Extension Methods]] * Companion: [[rfc:extension_method_visibility|PHP RFC: Extension Method Visibility]] * nikic's ''scalar_objects'' extension (proof-of-concept prior art for scalar receivers): https://github.com/nikic/scalar_objects * Kotlin extension functions on primitives; C# extension methods on value types ===== Rejected Features ===== * **Extending ''null''** — a method call on ''null'' is far more often a bug than a dispatch opportunity; nullsafe ''?->'' already expresses the intentional case. * **By-reference receivers** — would reintroduce every aliasing question value semantics avoids; see Future Scope. ===== Changelog ===== * 2.0 (2026-07-10): redesigned around the base RFC's declared receiver variable — the by-value-''$this'' binding model is rejected (see the base RFC's Rejected Features for the implementation experience). The engine footprint collapsed to a brief frame handoff: generators now work, ''get_called_class()'' is fully defined, and the ''static::''/generator bans disappeared. First-class callables of scalar methods remain deferred. * 1.0 (2026-07-09): initial draft, split from the base RFC after implementation showed a distinctly larger engine footprint; fully implemented and verified.