class B extends A {
public function method(...$everything) {}
}
+ . "static" (as in "late static binding") can now be used as a return type:
+
+ class Test {
+ public function create(): static {
+ return new static();
+ }
+ }
+
+ RFC: https://wiki.php.net/rfc/static_return_type
. It is now possible to fetch the class name of an object using
`$object::class`. The result is the same as `get_class($object)`.
RFC: https://wiki.php.net/rfc/class_name_literal_on_object
--- /dev/null
+--TEST--
+While it may not be very useful, static is also permitted in final classes
+--FILE--
+<?php
+
+final class Test {
+ public static function create(): static {
+ return new static;
+ }
+}
+
+var_dump(Test::create());
+
+?>
+--EXPECT--
+object(Test)#1 (0) {
+}
--- /dev/null
+--TEST--
+Static type outside class generates compile error
+--FILE--
+<?php
+
+function test(): static {}
+
+?>
+--EXPECTF--
+Fatal error: Cannot use "static" when no class scope is active in %s on line %d
--- /dev/null
+--TEST--
+Static type is not allowed in parameters
+--FILE--
+<?php
+
+// TODO: We could prohibit this case in the compiler instead.
+
+class Test {
+ public function test(static $param) {
+ }
+}
+
+?>
+--EXPECTF--
+Parse error: syntax error, unexpected 'static' (T_STATIC), expecting variable (T_VARIABLE) in %s on line %d
--- /dev/null
+--TEST--
+Static type is not allowed in properties
+--FILE--
+<?php
+
+// Testing ?static here, to avoid ambiguity with static properties.
+class Test {
+ public ?static $foo;
+}
+
+?>
+--EXPECTF--
+Parse error: syntax error, unexpected 'static' (T_STATIC) in %s on line %d
--- /dev/null
+--TEST--
+Static return type
+--FILE--
+<?php
+
+class A {
+ public function test1(): static {
+ return new static;
+ }
+ public function test2(): static {
+ return new self;
+ }
+ public function test3(): static {
+ return new static;
+ }
+ public function test4(): static|array {
+ return new self;
+ }
+}
+
+class B extends A {
+ public function test3(): static {
+ return new C;
+ }
+}
+
+class C extends B {}
+
+$a = new A;
+$b = new B;
+
+var_dump($a->test1());
+var_dump($b->test1());
+
+echo "\n";
+var_dump($a->test2());
+try {
+ var_dump($b->test2());
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "\n";
+var_dump($a->test3());
+var_dump($b->test3());
+
+echo "\n";
+var_dump($a->test4());
+try {
+ var_dump($b->test4());
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+echo "\n";
+$test = function($x): static {
+ return $x;
+};
+
+try {
+ var_dump($test(new stdClass));
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+$test = $test->bindTo($a);
+var_dump($test($a));
+
+?>
+--EXPECT--
+object(A)#3 (0) {
+}
+object(B)#3 (0) {
+}
+
+object(A)#3 (0) {
+}
+Return value of A::test2() must be an instance of B, instance of A returned
+
+object(A)#3 (0) {
+}
+object(C)#3 (0) {
+}
+
+object(A)#3 (0) {
+}
+Return value of A::test4() must be of type B|array, instance of A returned
+
+Return value of {closure}() must be an instance of static, instance of stdClass returned
+object(A)#1 (0) {
+}
--- /dev/null
+--TEST--
+static type in trait
+--FILE--
+<?php
+
+trait T {
+ public function test($arg): static {
+ return $arg;
+ }
+}
+
+class C {
+ use T;
+}
+class P extends C {
+}
+
+$c = new C;
+$p = new P;
+var_dump($c->test($c));
+var_dump($c->test($p));
+var_dump($p->test($p));
+var_dump($p->test($c));
+
+?>
+--EXPECTF--
+object(C)#1 (0) {
+}
+object(P)#2 (0) {
+}
+object(P)#2 (0) {
+}
+
+Fatal error: Uncaught TypeError: Return value of C::test() must be an instance of P, instance of C returned in %s:%d
+Stack trace:
+#0 %s(%d): C->test(Object(C))
+#1 {main}
+ thrown in %s on line %d
--- /dev/null
+--TEST--
+object and static are redundant
+--FILE--
+<?php
+
+class Test {
+ public function foo(): static|object {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Type static|object contains both object and a class type, which is redundant in %s on line %d
--- /dev/null
+--TEST--
+Failure case for static variance: Replace static with self
+--FILE--
+<?php
+
+class A {
+ public function test(): static {}
+}
+class B extends A {
+ public function test(): self {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Declaration of B::test(): B must be compatible with A::test(): static in %s on line %d
--- /dev/null
+--TEST--
+Success cases for static variance
+--FILE--
+<?php
+
+class A {
+ public function test1(): self {}
+ public function test2(): B {}
+ public function test3(): object {}
+ public function test4(): X|Y|self {}
+ public function test5(): ?static {}
+}
+
+class B extends A {
+ public function test1(): static {}
+ public function test2(): static {}
+ public function test3(): static {}
+ public function test4(): X|Y|static {}
+ public function test5(): static {}
+}
+
+?>
+===DONE===
+--EXPECT--
+===DONE===
}
uint32_t type_mask = ZEND_TYPE_FULL_MASK(type);
+ if (type_mask & MAY_BE_STATIC) {
+ zend_string *name = ZSTR_KNOWN(ZEND_STR_STATIC);
+ if (scope) {
+ zend_class_entry *called_scope = zend_get_called_scope(EG(current_execute_data));
+ if (called_scope) {
+ name = called_scope->name;
+ }
+ }
+ str = add_type_string(str, name);
+ }
if (type_mask & MAY_BE_CALLABLE) {
str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_CALLABLE));
}
{
ZEND_ASSERT(!(ast->attr & ZEND_TYPE_NULLABLE));
if (ast->kind == ZEND_AST_TYPE) {
+ if (ast->attr == IS_STATIC && !CG(active_class_entry) && zend_is_scope_known()) {
+ zend_error_noreturn(E_COMPILE_ERROR,
+ "Cannot use \"static\" when no class scope is active");
+ }
return (zend_type) ZEND_TYPE_INIT_CODE(ast->attr, 0, 0);
} else {
zend_string *class_name = zend_ast_get_str(ast);
ZSTR_VAL(type_str));
}
- if ((type_mask & MAY_BE_OBJECT) && ZEND_TYPE_HAS_CLASS(type)) {
+ if ((type_mask & MAY_BE_OBJECT) && (ZEND_TYPE_HAS_CLASS(type) || (type_mask & MAY_BE_STATIC))) {
zend_string *type_str = zend_type_to_string(type);
zend_error_noreturn(E_COMPILE_ERROR,
"Type %s contains both object and a class type, which is redundant",
}
if (is_union_type(arg_info->type)) {
- zend_string *type_str = zend_type_to_string(arg_info->type);
+ zend_string *type_str = zend_type_to_string_resolved(arg_info->type, zf->common.scope);
smart_str_appends(&str, "be of type ");
smart_str_append(&str, type_str);
zend_string_release(type_str);
case MAY_BE_ITERABLE:
smart_str_appends(&str, "be iterable");
break;
+ case MAY_BE_STATIC: {
+ zend_class_entry *called_scope = zend_get_called_scope(EG(current_execute_data));
+ smart_str_appends(&str, "be an instance of ");
+ if (called_scope) {
+ smart_str_append(&str, called_scope->name);
+ } else {
+ smart_str_appends(&str, "static");
+ }
+ break;
+ }
default:
{
/* Hack to print the type without null */
*need_msg = smart_str_extract(&str);
if (value) {
- if (ZEND_TYPE_HAS_CLASS(arg_info->type) && Z_TYPE_P(value) == IS_OBJECT) {
+ zend_bool has_class = ZEND_TYPE_HAS_CLASS(arg_info->type)
+ || (ZEND_TYPE_FULL_MASK(arg_info->type) & MAY_BE_STATIC);
+ if (has_class && Z_TYPE_P(value) == IS_OBJECT) {
*given_msg = "instance of ";
*given_kind = ZSTR_VAL(Z_OBJCE_P(value)->name);
} else {
return 1;
}
- ZEND_ASSERT(!(ZEND_TYPE_FULL_MASK(info->type) & MAY_BE_CALLABLE));
- if ((ZEND_TYPE_FULL_MASK(info->type) & MAY_BE_ITERABLE) && zend_is_iterable(property)) {
+ uint32_t type_mask = ZEND_TYPE_FULL_MASK(info->type);
+ ZEND_ASSERT(!(type_mask & (MAY_BE_CALLABLE|MAY_BE_STATIC)));
+ if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(property)) {
return 1;
}
- return zend_verify_scalar_type_hint(ZEND_TYPE_FULL_MASK(info->type), property, strict, 0);
+ return zend_verify_scalar_type_hint(type_mask, property, strict, 0);
}
static zend_always_inline zend_bool i_zend_verify_property_type(zend_property_info *info, zval *property, zend_bool strict)
return zend_assign_to_variable(property_val, &tmp, IS_TMP_VAR, EX_USES_STRICT_TYPES());
}
+ZEND_API zend_bool zend_value_instanceof_static(zval *zv) {
+ if (Z_TYPE_P(zv) != IS_OBJECT) {
+ return 0;
+ }
+
+ zend_class_entry *called_scope = zend_get_called_scope(EG(current_execute_data));
+ if (!called_scope) {
+ return 0;
+ }
+ return instanceof_function(Z_OBJCE_P(zv), called_scope);
+}
+
+
static zend_always_inline zend_bool zend_check_type_slow(
zend_type type, zval *arg, zend_reference *ref, void **cache_slot, zend_class_entry *scope,
zend_bool is_return_type, zend_bool is_internal)
if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(arg)) {
return 1;
}
+ if ((type_mask & MAY_BE_STATIC) && zend_value_instanceof_static(arg)) {
+ return 1;
+ }
if (ref && ZEND_REF_HAS_TYPE_SOURCES(ref)) {
/* We cannot have conversions for typed refs. */
return 0;
}
type_mask = ZEND_TYPE_FULL_MASK(type);
- ZEND_ASSERT(!(type_mask & MAY_BE_CALLABLE));
+ ZEND_ASSERT(!(type_mask & (MAY_BE_CALLABLE|MAY_BE_STATIC)));
if (type_mask & MAY_BE_ITERABLE) {
return zend_is_iterable(zv);
}
ZEND_API ZEND_COLD void zend_verify_return_error(
const zend_function *zf, void **cache_slot, zval *value);
ZEND_API zend_bool zend_verify_ref_array_assignable(zend_reference *ref);
+ZEND_API zend_bool zend_value_instanceof_static(zval *zv);
+
#define ZEND_REF_TYPE_SOURCES(ref) \
(ref)->sources
return 0;
}
+static zend_bool zend_type_permits_self(
+ zend_type type, zend_class_entry *scope, zend_class_entry *self) {
+ if (ZEND_TYPE_FULL_MASK(type) & MAY_BE_OBJECT) {
+ return 1;
+ }
+
+ /* Any types that may satisfy self must have already been loaded at this point
+ * (as a parent or interface), so we never need to register delayed variance obligations
+ * for this case. */
+ zend_type *single_type;
+ ZEND_TYPE_FOREACH(type, single_type) {
+ if (ZEND_TYPE_HAS_NAME(*single_type)) {
+ zend_string *name = resolve_class_name(scope, ZEND_TYPE_NAME(*single_type));
+ zend_class_entry *ce = lookup_class(self, name, /* register_unresolved */ 0);
+ if (ce && unlinked_instanceof(self, ce)) {
+ return 1;
+ }
+ }
+ } ZEND_TYPE_FOREACH_END();
+ return 0;
+}
+
/* Unresolved means that class declarations that are currently not available are needed to
* determine whether the inheritance is valid or not. At runtime UNRESOLVED should be treated
* as an ERROR. */
if (added_types) {
// TODO: Make "iterable" an alias of "array|Traversable" instead,
// so these special cases will be handled automatically.
- if (added_types == MAY_BE_ITERABLE
+ if ((added_types & MAY_BE_ITERABLE)
&& (proto_type_mask & MAY_BE_ARRAY)
&& zend_type_contains_traversable(proto_type)) {
/* Replacing array|Traversable with iterable is okay */
- } else if (added_types == MAY_BE_ARRAY && (proto_type_mask & MAY_BE_ITERABLE)) {
+ added_types &= ~MAY_BE_ITERABLE;
+ }
+ if ((added_types & MAY_BE_ARRAY) && (proto_type_mask & MAY_BE_ITERABLE)) {
/* Replacing iterable with array is okay */
- } else {
+ added_types &= ~MAY_BE_ARRAY;
+ }
+ if ((added_types & MAY_BE_STATIC)
+ && zend_type_permits_self(proto_type, proto_scope, fe_scope)) {
+ /* Replacing type that accepts self with static is okay */
+ added_types &= ~MAY_BE_STATIC;
+ }
+ if (added_types) {
/* Otherwise adding new types is illegal */
return INHERITANCE_ERROR;
}
%type <ast> unprefixed_use_declarations const_decl inner_statement
%type <ast> expr optional_expr while_statement for_statement foreach_variable
%type <ast> foreach_statement declare_statement finally_statement unset_variable variable
-%type <ast> extends_from parameter optional_type argument global_var
+%type <ast> extends_from parameter optional_type_without_static argument global_var
%type <ast> static_var class_statement trait_adaptation trait_precedence trait_alias
%type <ast> absolute_trait_method_reference trait_method_reference property echo_expr
%type <ast> new_expr anonymous_class class_name class_name_reference simple_variable
%type <ast> ctor_arguments alt_if_stmt_without_else trait_adaptation_list lexical_vars
%type <ast> lexical_var_list encaps_list
%type <ast> array_pair non_empty_array_pair_list array_pair_list possible_array_pair
-%type <ast> isset_variable type return_type type_expr
-%type <ast> identifier
+%type <ast> isset_variable type return_type type_expr type_without_static
+%type <ast> identifier type_expr_without_static union_type_without_static
%type <ast> inline_function union_type
%type <num> returns_ref function fn is_reference is_variadic variable_modifiers
;
parameter:
- optional_type is_reference is_variadic T_VARIABLE
+ optional_type_without_static is_reference is_variadic T_VARIABLE
{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $2 | $3, $1, $4, NULL); }
- | optional_type is_reference is_variadic T_VARIABLE '=' expr
+ | optional_type_without_static is_reference is_variadic T_VARIABLE '=' expr
{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $2 | $3, $1, $4, $6); }
;
-optional_type:
+optional_type_without_static:
%empty { $$ = NULL; }
- | type_expr { $$ = $1; }
+ | type_expr_without_static { $$ = $1; }
;
type_expr:
- type { $$ = $1; }
- | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; }
- | union_type { $$ = $1; }
+ type { $$ = $1; }
+ | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; }
+ | union_type { $$ = $1; }
;
type:
- T_ARRAY { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY); }
- | T_CALLABLE { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE); }
- | name { $$ = $1; }
+ type_without_static { $$ = $1; }
+ | T_STATIC { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_STATIC); }
;
union_type:
| union_type '|' type { $$ = zend_ast_list_add($1, $3); }
;
+/* Duplicate the type rules without "static",
+ * to avoid conflicts with "static" modifier for properties. */
+
+type_expr_without_static:
+ type_without_static { $$ = $1; }
+ | '?' type_without_static { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; }
+ | union_type_without_static { $$ = $1; }
+;
+
+type_without_static:
+ T_ARRAY { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY); }
+ | T_CALLABLE { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE); }
+ | name { $$ = $1; }
+;
+
+union_type_without_static:
+ type_without_static '|' type_without_static
+ { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $1, $3); }
+ | union_type_without_static '|' type_without_static
+ { $$ = zend_ast_list_add($1, $3); }
+;
+
return_type:
%empty { $$ = NULL; }
| ':' type_expr { $$ = $2; }
class_statement:
- variable_modifiers optional_type property_list ';'
+ variable_modifiers optional_type_without_static property_list ';'
{ $$ = zend_ast_create(ZEND_AST_PROP_GROUP, $2, $3);
$$->attr = $1; }
| method_modifiers T_CONST class_const_list ';'
#define MAY_BE_CALLABLE (1 << IS_CALLABLE)
#define MAY_BE_ITERABLE (1 << IS_ITERABLE)
#define MAY_BE_VOID (1 << IS_VOID)
+#define MAY_BE_STATIC (1 << IS_STATIC)
#define MAY_BE_ARRAY_SHIFT (IS_REFERENCE)
#define IS_CALLABLE 12
#define IS_ITERABLE 13
#define IS_VOID 14
+#define IS_STATIC 15
/* internal types */
#define IS_INDIRECT 12
}
/* These types are not represented exactly */
- if (ZEND_TYPE_FULL_MASK(info->type) & (MAY_BE_CALLABLE|MAY_BE_ITERABLE)) {
+ if (ZEND_TYPE_FULL_MASK(info->type) & (MAY_BE_CALLABLE|MAY_BE_ITERABLE|MAY_BE_STATIC)) {
return 0;
}
if (type_mask & MAY_BE_ITERABLE) {
result_mask |= MAY_BE_OBJECT|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF;
}
+ if (type_mask & MAY_BE_STATIC) {
+ result_mask |= MAY_BE_OBJECT;
+ }
if (type_mask & MAY_BE_ARRAY) {
result_mask |= MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF;
}
if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(arg)) {
return 1;
}
+ if ((type_mask & MAY_BE_STATIC) && zend_value_instanceof_static(arg)) {
+ return 1;
+ }
if (zend_verify_scalar_type_hint(type_mask, arg, ZEND_ARG_USES_STRICT_TYPES(), /* is_internal */ 0)) {
return 1;
}
}
GET_REFLECTION_OBJECT_PTR(param);
- RETVAL_BOOL(ZEND_TYPE_IS_ONLY_MASK(param->type));
+ /* Treat "static" as a class type for the purposes of reflection. */
+ RETVAL_BOOL(ZEND_TYPE_IS_ONLY_MASK(param->type)
+ && !(ZEND_TYPE_FULL_MASK(param->type) & MAY_BE_STATIC));
}
/* }}} */
--- /dev/null
+--TEST--
+ReflectionType for static types
+--FILE--
+<?php
+
+class A {
+ public function test(): static {
+ return new static;
+ }
+}
+
+$rm = new ReflectionMethod(A::class, 'test');
+$rt = $rm->getReturnType();
+var_dump($rt->isBuiltin());
+var_dump($rt->getName());
+var_dump((string) $rt);
+
+?>
+--EXPECT--
+bool(false)
+string(6) "static"
+string(6) "static"