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--
Swift-style extension methods: basic resolution, $this binding, real-method precedence
--FILE--
<?php
class Money {
public function __construct(public int $cents) {}
public function label(): string { return "real"; }
}

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

extension \DateTimeImmutable {
public function isWeekend(): bool {
return in_array((int)$this->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()
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 {
public function __construct() {}
}
?>
--EXPECTF--
Fatal error: Extension blocks may not declare magic method __construct() 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 @@ -153,6 +153,7 @@ enum _zend_ast_kind {
ZEND_AST_MATCH_ARM,
ZEND_AST_NAMED_ARG,
ZEND_AST_PIPE,
ZEND_AST_EXTENSION_DECL,

/* 3 child nodes */
ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT,
Expand Down
71 changes: 67 additions & 4 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -5648,7 +5648,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 @@ -9546,7 +9547,65 @@ 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 *class_ast = ast->child[1];
zend_ast_decl *decl = (zend_ast_decl *) class_ast;
zend_string *target_name, *target_lc;
znode class_node;
zend_op *opline;

/* 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]) {
/* 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. */
const zend_ast_decl *method = (const zend_ast_decl *) stmts->child[i];
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));
}
}
}

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. The
* methods bind $this normally; their scope is the synthetic CE, so only
* the target's public API is visible from extension bodies. */
zend_class_entry *ext_ce = zend_compile_class_decl(&class_node, class_ast, false);

/* 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 +9742,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 +9751,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 +9821,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 +12145,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
1 change: 1 addition & 0 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#include "zend_system_id.h"
#include "zend_call_stack.h"
#include "zend_attributes.h"
#include "zend_extension_methods.h"
#include "Optimizer/zend_func_info.h"

/* Virtual current working directory support */
Expand Down
76 changes: 76 additions & 0 deletions Zend/zend_extension_methods.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#include "zend.h"
#include "zend_API.h"
#include "zend_extension_methods.h"

/* PROTOTYPE NOTE: stored in a true-global for NTS simplicity.
* TODO(land): move into zend_executor_globals for ZTS; opcache SHM persistence. */
static HashTable *ext_registry = NULL; /* lc target name -> HashTable* of methods */

void zend_extension_methods_startup(void)
{
ext_registry = pemalloc(sizeof(HashTable), 1);
zend_hash_init(ext_registry, 8, NULL, NULL, 1);
}

void zend_extension_methods_shutdown(void)
{
if (ext_registry) {
HashTable *methods;
ZEND_HASH_FOREACH_PTR(ext_registry, methods) {
zend_hash_destroy(methods);
pefree(methods, 1);
} ZEND_HASH_FOREACH_END();
zend_hash_destroy(ext_registry);
pefree(ext_registry, 1);
ext_registry = NULL;
}
}

ZEND_API void zend_extension_methods_register(zend_string *target_lc, zend_class_entry *ext_ce)
{
HashTable *methods = zend_hash_find_ptr(ext_registry, target_lc);
zend_string *name;
zval *zv;

if (!methods) {
methods = pemalloc(sizeof(HashTable), 1);
zend_hash_init(methods, 8, NULL, NULL, 1);
zend_hash_add_ptr(ext_registry, target_lc, methods);
}

ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(&ext_ce->function_table, name, zv) {
zend_function *fn = Z_PTR_P(zv);
/* Real methods must always win; conflicts between extensions: first wins.
* TODO(land): E_WARNING or fatal on duplicate registration.
* NOTE: fn may live in opcache SHM and must not be written to here;
* ZEND_ACC_NEVER_CACHE is applied at compile time (zend_compile.c). */
zend_hash_add_ptr(methods, name, fn);
} ZEND_HASH_FOREACH_END();
}

ZEND_API zend_function *zend_extension_methods_get(const zend_class_entry *ce, zend_string *lc_method_name)
{
if (!ext_registry || zend_hash_num_elements(ext_registry) == 0) {
return NULL;
}
/* Most-derived match first: walk the inheritance chain, then interfaces. */
for (const zend_class_entry *c = ce; c; c = c->parent) {
HashTable *methods = zend_hash_find_ptr_lc(ext_registry, c->name);
if (methods) {
zend_function *fn = zend_hash_find_ptr(methods, lc_method_name);
if (fn) {
return fn;
}
}
}
for (uint32_t i = 0; i < ce->num_interfaces; i++) {
HashTable *methods = zend_hash_find_ptr_lc(ext_registry, ce->interfaces[i]->name);
if (methods) {
zend_function *fn = zend_hash_find_ptr(methods, lc_method_name);
if (fn) {
return fn;
}
}
}
return NULL;
}
22 changes: 22 additions & 0 deletions Zend/zend_extension_methods.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* Prototype: Swift-style extension methods registry (RFC draft).
* Registry maps lc(target class name) -> HashTable of lc(method) -> zend_function*.
*/
#ifndef ZEND_EXTENSION_METHODS_H
#define ZEND_EXTENSION_METHODS_H

#include "zend.h"

BEGIN_EXTERN_C()

void zend_extension_methods_startup(void);
void zend_extension_methods_shutdown(void);

/* Called when an `extension Target { ... }` block's synthetic CE is linked. */
ZEND_API void zend_extension_methods_register(zend_string *target_lc, zend_class_entry *ext_ce);

/* Fallback lookup: walks ce and its ancestry/interfaces for a registered method. */
ZEND_API zend_function *zend_extension_methods_get(const zend_class_entry *ce, zend_string *lc_method_name);

END_EXTERN_C()

#endif
13 changes: 12 additions & 1 deletion Zend/zend_language_parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%token <ident> T_CLASS "'class'"
%token <ident> T_TRAIT "'trait'"
%token <ident> T_INTERFACE "'interface'"
%token <ident> T_EXTENSION "'extension'"
%token <ident> T_ENUM "'enum'"
%token <ident> T_EXTENDS "'extends'"
%token <ident> T_IMPLEMENTS "'implements'"
Expand Down Expand Up @@ -284,6 +285,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
%type <ast> attribute_decl attribute attributes attribute_group namespace_declaration_name
%type <ast> match match_arm_list non_empty_match_arm_list match_arm match_arm_cond_list
%type <ast> enum_declaration_statement enum_backing_type enum_case enum_case_expr
%type <ast> extension_declaration_statement
%type <ast> function_name non_empty_member_modifiers
%type <ast> property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body
%type <ast> optional_parameter_list clone_argument_list non_empty_clone_argument_list
Expand All @@ -310,7 +312,7 @@ reserved_non_modifiers:
| T_THROW | T_USE | T_INSTEADOF | T_GLOBAL | T_VAR | T_UNSET | T_ISSET | T_EMPTY | T_CONTINUE | T_GOTO
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT | T_BREAK
| T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_FN | T_MATCH | T_ENUM
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_FN | T_MATCH | T_ENUM | T_EXTENSION
| T_PROPERTY_C
;

Expand Down Expand Up @@ -392,6 +394,7 @@ attributed_statement:
| trait_declaration_statement { $$ = $1; }
| interface_declaration_statement { $$ = $1; }
| enum_declaration_statement { $$ = $1; }
| extension_declaration_statement { $$ = $1; }
;

attributed_top_statement:
Expand Down Expand Up @@ -651,6 +654,14 @@ enum_declaration_statement:
{ $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_ENUM|ZEND_ACC_FINAL, $<num>2, $6, zend_ast_get_str($3), NULL, $5, $8, NULL, $4); }
;

extension_declaration_statement:
T_EXTENSION { $<num>$ = CG(zend_lineno); }
class_name backup_doc_comment '{' class_statement_list '}'
{ $$ = zend_ast_create(ZEND_AST_EXTENSION_DECL, $3,
zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_ANON_CLASS|ZEND_ACC_FINAL, $<num>2, $4,
NULL, NULL, NULL, $6, NULL, NULL)); }
;

enum_backing_type:
%empty { $$ = NULL; }
| ':' type_expr { $$ = $2; }
Expand Down
13 changes: 11 additions & 2 deletions Zend/zend_language_scanner.l
Original file line number Diff line number Diff line change
Expand Up @@ -1563,9 +1563,18 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_
}

/*
* The enum keyword must be followed by whitespace and another identifier.
* This avoids the BC break of using enum in classes, namespaces, functions and constants.
* The enum and extension keywords must be followed by whitespace and another identifier.
* This avoids the BC break of using them in classes, namespaces, functions and constants.
*/
<ST_IN_SCRIPTING>"extension"{WHITESPACE_OR_COMMENTS}("extends"|"implements") {
yyless(9);
RETURN_TOKEN_WITH_STR(T_STRING, 0);
}
<ST_IN_SCRIPTING>"extension"{WHITESPACE_OR_COMMENTS}("\\"|[a-zA-Z_\x80-\xff]) {
yyless(9);
RETURN_TOKEN_WITH_IDENT(T_EXTENSION);
}

<ST_IN_SCRIPTING>"enum"{WHITESPACE_OR_COMMENTS}("extends"|"implements") {
yyless(4);
RETURN_TOKEN_WITH_STR(T_STRING, 0);
Expand Down
Loading
Loading