Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Zend/tests/extension_methods/basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Extension methods: basic resolution, receiver binding, real-method precedence
--FILE--
<?php
class Money {
public function __construct(public int $cents) {}
public function label(): string { return "real"; }
}

extension Money $m {
public function dollars(): float { return $m->cents / 100; }
public function label(): string { return "extension"; } // must lose to real method
}

extension \DateTimeImmutable $d {
public function isWeekend(): bool {
return in_array((int)$d->format('N'), [6, 7], true);
}
}

$m = new Money(1250);
var_dump($m->dollars());
var_dump($m->label());
var_dump((new DateTimeImmutable('2026-07-11'))->isWeekend());
try { $m->missing(); } catch (Error $e) { echo $e->getMessage(), "\n"; }
?>
--EXPECT--
float(12.5)
string(4) "real"
bool(true)
Call to undefined method Money::missing()
10 changes: 10 additions & 0 deletions Zend/tests/extension_methods/builtin_string_target.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Built-in value type names cannot be extension targets (see the scalar extension methods RFC)
--FILE--
<?php
extension string $s {
public function length(): int { return strlen($s); }
}
?>
--EXPECTF--
Fatal error: Cannot extend reserved type string in %s on line %d
28 changes: 28 additions & 0 deletions Zend/tests/extension_methods/first_class_callable.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
First-class callables of extension methods bind the receiver
--FILE--
<?php
class Money {
public function __construct(public int $cents) {}
}

extension Money $m {
public function dollars(): float { return $m->cents / 100; }
public function plus(int $more): float { return ($m->cents + $more) / 100; }
}

$a = new Money(1250);
$f = $a->dollars(...);
var_dump($f());

$g = $a->plus(...);
var_dump($g(50));

/* The callable captured $a's binding, not a live variable. */
$a = new Money(9900);
var_dump($f());
?>
--EXPECT--
float(12.5)
float(13)
float(12.5)
12 changes: 12 additions & 0 deletions Zend/tests/extension_methods/magic_methods.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Extension blocks may not declare magic methods
--FILE--
<?php
class C {}

extension C $c {
public function __construct() {}
}
?>
--EXPECTF--
Fatal error: Extension blocks may not declare magic method __construct() in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/extension_methods/receiver_name_this.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
The receiver variable cannot be named $this
--FILE--
<?php
class C {}

extension C $this {
public function m(): int { return 1; }
}
?>
--EXPECTF--
Fatal error: Cannot use $this as the extension receiver variable in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/extension_methods/receiver_param_conflict.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
A parameter cannot shadow the extension receiver variable
--FILE--
<?php
class C {}

extension C $c {
public function m(int $c): int { return $c; }
}
?>
--EXPECTF--
Fatal error: Cannot redeclare extension receiver $c as a parameter in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/extension_methods/receiver_rules.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
$this is banned in extension methods
--FILE--
<?php
class C { public int $v = 7; }

extension C $c {
public function m(): int { return $this->v; }
}
?>
--EXPECTF--
Fatal error: Cannot use $this in an extension method (use the declared receiver $c) in %s on line %d
10 changes: 10 additions & 0 deletions Zend/tests/extension_methods/reserved_targets.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Reserved type names cannot be extension targets
--FILE--
<?php
extension iterable $it {
public function m(): int { return 1; }
}
?>
--EXPECTF--
Fatal error: Cannot extend reserved type iterable in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/extension_methods/static_ban.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
static:: is banned in extension methods
--FILE--
<?php
class C {}

extension C $c {
public function m(): string { return static::class; }
}
?>
--EXPECTF--
Fatal error: Cannot use "static" in an extension method in %s on line %d
3 changes: 3 additions & 0 deletions Zend/zend.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "zend_virtual_cwd.h"
#include "zend_smart_str.h"
#include "zend_smart_string.h"
#include "zend_extension_methods.h"
#include "zend_cpuinfo.h"
#include "zend_attributes.h"
#include "zend_observer.h"
Expand Down Expand Up @@ -1060,6 +1061,7 @@ void zend_startup(zend_utility_functions *utility_functions) /* {{{ */
zend_interned_strings_init();
zend_object_handlers_startup();
zend_startup_builtin_functions();
zend_extension_methods_startup();
zend_register_standard_constants();
zend_register_auto_global(zend_string_init_interned("GLOBALS", sizeof("GLOBALS") - 1, 1), 1, php_auto_globals_create_globals);

Expand Down Expand Up @@ -1192,6 +1194,7 @@ void zend_shutdown(void) /* {{{ */
zend_hash_destroy(GLOBAL_AUTO_GLOBALS_TABLE);
free(GLOBAL_AUTO_GLOBALS_TABLE);

zend_extension_methods_shutdown();
zend_shutdown_extensions();
free(zend_version_info);

Expand Down
1 change: 1 addition & 0 deletions Zend/zend_ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ enum _zend_ast_kind {
/* 3 child nodes */
ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT,
ZEND_AST_NULLSAFE_METHOD_CALL,
ZEND_AST_EXTENSION_DECL,
ZEND_AST_STATIC_CALL,
ZEND_AST_CONDITIONAL,

Expand Down
139 changes: 134 additions & 5 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ void zend_init_compiler_data_structures(void) /* {{{ */
CG(active_class_entry) = NULL;
CG(in_compilation) = 0;
CG(skip_shebang) = 0;
CG(extension_receiver) = NULL;

CG(encoding_declared) = 0;
CG(memoized_exprs) = NULL;
Expand Down Expand Up @@ -1787,6 +1788,13 @@ static zend_string *zend_resolve_const_class_name_reference(zend_ast *ast, const

static void zend_ensure_valid_class_fetch_type(uint32_t fetch_type) /* {{{ */
{
if (fetch_type == ZEND_FETCH_CLASS_STATIC && UNEXPECTED(CG(extension_receiver) != NULL)) {
/* Late static binding has no meaningful referent in extension
* methods: the scope is the synthetic extension class, and the
* receiver is an ordinary variable, not $this. */
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot use \"static\" in an extension method");
}
if (fetch_type != ZEND_FETCH_CLASS_DEFAULT && zend_is_scope_known()) {
zend_class_entry *ce = CG(active_class_entry);
if (!ce) {
Expand Down Expand Up @@ -2994,7 +3002,16 @@ static bool is_this_fetch(const zend_ast *ast) /* {{{ */
{
if (ast->kind == ZEND_AST_VAR && ast->child[0]->kind == ZEND_AST_ZVAL) {
const zval *name = zend_ast_get_zval(ast->child[0]);
return Z_TYPE_P(name) == IS_STRING && zend_string_equals(Z_STR_P(name), ZSTR_KNOWN(ZEND_STR_THIS));
if (Z_TYPE_P(name) == IS_STRING && zend_string_equals(Z_STR_P(name), ZSTR_KNOWN(ZEND_STR_THIS))) {
if (UNEXPECTED(CG(extension_receiver) != NULL)) {
/* Extension bodies have no $this: the receiver is the
* declared variable, an ordinary by-value local. */
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot use $this in an extension method (use the declared receiver $%s)",
ZSTR_VAL(CG(extension_receiver)));
}
return true;
}
}

return false;
Expand Down Expand Up @@ -5648,7 +5665,8 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type
}
/* }}} */

static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool toplevel);
static zend_class_entry *zend_compile_class_decl(znode *result, const zend_ast *ast, bool toplevel);
static void zend_compile_extension_decl(zend_ast *ast);

static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */
{
Expand Down Expand Up @@ -8841,6 +8859,29 @@ static zend_op_array *zend_compile_func_decl_ex(

zend_compile_params(params_ast, return_type_ast,
is_method && zend_string_equals_literal(lcname, ZEND_TOSTRING_FUNC_LCNAME) ? IS_STRING : 0);

if (UNEXPECTED(CG(extension_receiver) != NULL) && is_method) {
/* Extension method: bind the receiver into its declared variable.
* Emitted after the parameter RECVs so the engine's RECV-skip fast
* path lands exactly on this opcode; before GENERATOR_CREATE so the
* receiver CV is populated in the frame a generator would copy. */
znode recv_node;
zend_op *recv_op;

for (uint32_t i = 0; i < op_array->num_args; i++) {
if (zend_string_equals(op_array->arg_info[i].name, CG(extension_receiver))) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot redeclare extension receiver $%s as a parameter",
ZSTR_VAL(CG(extension_receiver)));
}
}

recv_node.op_type = IS_CV;
recv_node.u.op.var = lookup_cv(CG(extension_receiver));
recv_op = zend_emit_op(NULL, ZEND_RECV_RECEIVER, NULL, NULL);
SET_NODE(recv_op->result, &recv_node);
}

if (CG(active_op_array)->fn_flags & ZEND_ACC_GENERATOR) {
if (CG(active_class_entry) != NULL) {
if (zend_is_constructor(CG(active_op_array)->function_name)) {
Expand Down Expand Up @@ -9546,7 +9587,91 @@ static void zend_compile_enum_backing_type(zend_class_entry *ce, zend_ast *enum_
zend_type_release(type, 0);
}

static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool toplevel) /* {{{ */
static void zend_compile_extension_decl(zend_ast *ast) /* {{{ */
{
zend_ast *target_ast = ast->child[0];
zend_ast *receiver_ast = ast->child[1];
zend_ast *class_ast = ast->child[2];
zend_ast_decl *decl = (zend_ast_decl *) class_ast;
zend_string *target_name, *target_lc;
znode class_node;
zend_op *opline;

zend_string *receiver_name = zend_ast_get_str(receiver_ast);
if (zend_string_equals(receiver_name, ZSTR_KNOWN(ZEND_STR_THIS))) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot use $this as the extension receiver variable");
}

/* v1 restriction: methods only (no properties, consts, or promoted state;
* object layout is fixed at link time and shared under opcache). */
zend_ast_list *stmts = zend_ast_get_list(decl->child[2]);
for (uint32_t i = 0; i < stmts->children; i++) {
if (stmts->child[i] && stmts->child[i]->kind != ZEND_AST_METHOD) {
zend_error_noreturn(E_COMPILE_ERROR,
"Extension blocks may only declare methods");
}
if (stmts->child[i]) {
const zend_ast_decl *method = (const zend_ast_decl *) stmts->child[i];
/* Magic methods dispatch through dedicated class-entry slots and
* handlers, never through the get_method miss path where
* extension methods live, so none of them could ever work here.
* The __ prefix is reserved for them; reject it wholesale. */
if (ZSTR_LEN(method->name) >= 2
&& ZSTR_VAL(method->name)[0] == '_' && ZSTR_VAL(method->name)[1] == '_') {
zend_error_noreturn(E_COMPILE_ERROR,
"Extension blocks may not declare magic method %s()",
ZSTR_VAL(method->name));
}
if (method->flags & ZEND_ACC_ABSTRACT) {
zend_error_noreturn(E_COMPILE_ERROR,
"Extension methods cannot be abstract");
}
}
}

/* Built-in type names are not classes and cannot be extended: reject
* them rather than registering entries no object can ever match.
* (Extension methods on the scalar value types are proposed separately.) */
if (target_ast->kind == ZEND_AST_ZVAL
&& target_ast->attr == ZEND_NAME_NOT_FQ
&& zend_is_reserved_class_name(zend_ast_get_str(target_ast))) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot extend reserved type %s", ZSTR_VAL(zend_ast_get_str(target_ast)));
}

target_name = zend_resolve_class_name_ast(target_ast);
target_lc = zend_string_tolower(target_name);

/* Compile the body as an anonymous, final, uninstantiable class. Its
* methods have no $this: each receives the receiver in the declared
* variable via ZEND_RECV_RECEIVER (see zend_compile_func_decl), so
* extension bodies hold the receiver like any outside code would — and
* consequently see only the target's public API. */
zend_string *orig_extension_receiver = CG(extension_receiver);
CG(extension_receiver) = receiver_name;
zend_class_entry *ext_ce = zend_compile_class_decl(&class_node, class_ast, false);
CG(extension_receiver) = orig_extension_receiver;

/* Keep extension methods out of the polymorphic inline cache for the
* prototype (correctness over speed; the cached path + JIT support is
* future work). Must happen at compile time: under opcache the CE is
* persisted to (protected) shared memory and is immutable at runtime. */
zend_function *ext_fn;
ZEND_HASH_MAP_FOREACH_PTR(&ext_ce->function_table, ext_fn) {
ext_fn->common.fn_flags |= ZEND_ACC_NEVER_CACHE;
} ZEND_HASH_FOREACH_END();

/* Runtime registration once the synthetic CE is declared. */
opline = zend_emit_op(NULL, ZEND_BIND_EXTENSION, &class_node, NULL);
opline->op2_type = IS_CONST;
opline->op2.constant = zend_add_literal_string(&target_lc);

zend_string_release(target_name);
}
/* }}} */

static zend_class_entry *zend_compile_class_decl(znode *result, const zend_ast *ast, bool toplevel) /* {{{ */
{
const zend_ast_decl *decl = (const zend_ast_decl *) ast;
zend_ast *extends_ast = decl->child[0];
Expand Down Expand Up @@ -9683,7 +9808,7 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top
&& !zend_compile_ignore_class(parent_ce, ce->info.user.filename)) {
if (zend_try_early_bind(ce, parent_ce, lcname, NULL)) {
zend_string_release(lcname);
return;
return ce;
}
}
} else if (EXPECTED(zend_hash_add_ptr(CG(class_table), lcname, ce) != NULL)) {
Expand All @@ -9692,7 +9817,7 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top
zend_inheritance_check_override(ce);
ce->ce_flags |= ZEND_ACC_LINKED;
zend_observer_class_linked_notify(ce, lcname);
return;
return ce;
} else {
goto link_unbound;
}
Expand Down Expand Up @@ -9762,6 +9887,7 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top
opline->result.opline_num = -1;
}
}
return ce;
}
/* }}} */

Expand Down Expand Up @@ -12085,6 +12211,9 @@ static void zend_compile_stmt(zend_ast *ast) /* {{{ */
case ZEND_AST_USE_TRAIT:
zend_compile_use_trait(ast);
break;
case ZEND_AST_EXTENSION_DECL:
zend_compile_extension_decl(ast);
break;
case ZEND_AST_CLASS:
zend_compile_class_decl(NULL, ast, false);
break;
Expand Down
Loading
Loading