rfc:extension_methods_scalars

PHP RFC: Scalar Extension Methods

Introduction

The 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 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

  1. 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.
  2. 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).
  3. 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.)
  4. 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

  1. 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 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 extension-methods-scalars branch, stacked directly on the base RFC's extension-methods-phase1 branch — 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

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.
rfc/extension_methods_scalars.txt · Last modified: by thecodelorax