mirror of
https://github.com/php/php-src.git
synced 2025-08-16 05:58:45 +02:00
Adds support for DNF types in internal functions and properties (#11969)
Note that this does not add support for items generated by gen_stubs, only for items registered dynamically via the Zend API. Closes GH-10120
This commit is contained in:
parent
4ff93f779c
commit
7f1c3bf09b
8 changed files with 205 additions and 24 deletions
|
@ -75,6 +75,8 @@ object(_ZendTestClass)#1 (3) {
|
|||
uninitialized(Traversable&Countable)
|
||||
["readonlyProp"]=>
|
||||
uninitialized(int)
|
||||
["dnfProperty"]=>
|
||||
uninitialized(Iterator|(Traversable&Countable))
|
||||
}
|
||||
int(123)
|
||||
Cannot assign string to property _ZendTestClass::$intProp of type int
|
||||
|
@ -91,6 +93,8 @@ object(Test)#4 (3) {
|
|||
uninitialized(Traversable&Countable)
|
||||
["readonlyProp"]=>
|
||||
uninitialized(int)
|
||||
["dnfProperty"]=>
|
||||
uninitialized(Iterator|(Traversable&Countable))
|
||||
}
|
||||
int(123)
|
||||
Cannot assign string to property _ZendTestClass::$staticIntProp of type int
|
||||
|
|
|
@ -2756,6 +2756,28 @@ ZEND_API void zend_add_magic_method(zend_class_entry *ce, zend_function *fptr, z
|
|||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arg_info_toString, 0, 0, IS_STRING, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
static zend_always_inline void zend_normalize_internal_type(zend_type *type) {
|
||||
ZEND_ASSERT(!ZEND_TYPE_HAS_LITERAL_NAME(*type));
|
||||
zend_type *current;
|
||||
ZEND_TYPE_FOREACH(*type, current) {
|
||||
if (ZEND_TYPE_HAS_NAME(*current)) {
|
||||
zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*current));
|
||||
zend_alloc_ce_cache(name);
|
||||
ZEND_TYPE_SET_PTR(*current, name);
|
||||
} else if (ZEND_TYPE_HAS_LIST(*current)) {
|
||||
zend_type *inner;
|
||||
ZEND_TYPE_FOREACH(*current, inner) {
|
||||
ZEND_ASSERT(!ZEND_TYPE_HAS_LITERAL_NAME(*inner) && !ZEND_TYPE_HAS_LIST(*inner));
|
||||
if (ZEND_TYPE_HAS_NAME(*inner)) {
|
||||
zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*inner));
|
||||
zend_alloc_ce_cache(name);
|
||||
ZEND_TYPE_SET_PTR(*inner, name);
|
||||
}
|
||||
} ZEND_TYPE_FOREACH_END();
|
||||
}
|
||||
} ZEND_TYPE_FOREACH_END();
|
||||
}
|
||||
|
||||
/* registers all functions in *library_functions in the function hash */
|
||||
ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend_function_entry *functions, HashTable *function_table, int type) /* {{{ */
|
||||
{
|
||||
|
@ -2934,10 +2956,12 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
|
|||
memcpy(new_arg_info, arg_info, sizeof(zend_internal_arg_info) * num_args);
|
||||
reg_function->arg_info = new_arg_info + 1;
|
||||
for (i = 0; i < num_args; i++) {
|
||||
if (ZEND_TYPE_IS_COMPLEX(new_arg_info[i].type)) {
|
||||
ZEND_ASSERT(ZEND_TYPE_HAS_NAME(new_arg_info[i].type)
|
||||
&& "Should be stored as simple name");
|
||||
if (ZEND_TYPE_HAS_LITERAL_NAME(new_arg_info[i].type)) {
|
||||
// gen_stubs.php does not support codegen for DNF types in arg infos.
|
||||
// As a temporary workaround, we split the type name on `|` characters,
|
||||
// converting it to an union type if necessary.
|
||||
const char *class_name = ZEND_TYPE_LITERAL_NAME(new_arg_info[i].type);
|
||||
new_arg_info[i].type.type_mask &= ~_ZEND_TYPE_LITERAL_NAME_BIT;
|
||||
|
||||
size_t num_types = 1;
|
||||
const char *p = class_name;
|
||||
|
@ -2948,8 +2972,10 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
|
|||
|
||||
if (num_types == 1) {
|
||||
/* Simple class type */
|
||||
ZEND_TYPE_SET_PTR(new_arg_info[i].type,
|
||||
zend_string_init_interned(class_name, strlen(class_name), 1));
|
||||
zend_string *str = zend_string_init_interned(class_name, strlen(class_name), 1);
|
||||
zend_alloc_ce_cache(str);
|
||||
ZEND_TYPE_SET_PTR(new_arg_info[i].type, str);
|
||||
new_arg_info[i].type.type_mask |= _ZEND_TYPE_NAME_BIT;
|
||||
} else {
|
||||
/* Union type */
|
||||
zend_type_list *list = malloc(ZEND_TYPE_LIST_SIZE(num_types));
|
||||
|
@ -2961,8 +2987,8 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
|
|||
uint32_t j = 0;
|
||||
while (true) {
|
||||
const char *end = strchr(start, '|');
|
||||
zend_string *str = zend_string_init_interned(
|
||||
start, end ? end - start : strlen(start), 1);
|
||||
zend_string *str = zend_string_init_interned(start, end ? end - start : strlen(start), 1);
|
||||
zend_alloc_ce_cache(str);
|
||||
list->types[j] = (zend_type) ZEND_TYPE_INIT_CLASS(str, 0, 0);
|
||||
if (!end) {
|
||||
break;
|
||||
|
@ -2977,10 +3003,14 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
|
|||
zend_error(E_CORE_WARNING, "iterable type is now a compile time alias for array|Traversable,"
|
||||
" regenerate the argument info via the php-src gen_stub build script");
|
||||
*/
|
||||
zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
|
||||
(new_arg_info[i].type.type_mask|MAY_BE_ARRAY));
|
||||
zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_MASK(
|
||||
ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
|
||||
(new_arg_info[i].type.type_mask | MAY_BE_ARRAY)
|
||||
);
|
||||
new_arg_info[i].type = legacy_iterable;
|
||||
}
|
||||
|
||||
zend_normalize_internal_type(&new_arg_info[i].type);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4367,16 +4397,7 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z
|
|||
property_info->type = type;
|
||||
|
||||
if (is_persistent_class(ce)) {
|
||||
zend_type *single_type;
|
||||
ZEND_TYPE_FOREACH(property_info->type, single_type) {
|
||||
// TODO Add support and test cases when gen_stub support added
|
||||
ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*single_type));
|
||||
if (ZEND_TYPE_HAS_NAME(*single_type)) {
|
||||
zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*single_type));
|
||||
ZEND_TYPE_SET_PTR(*single_type, name);
|
||||
zend_alloc_ce_cache(name);
|
||||
}
|
||||
} ZEND_TYPE_FOREACH_END();
|
||||
zend_normalize_internal_type(&property_info->type);
|
||||
}
|
||||
|
||||
zend_hash_update_ptr(&ce->properties_info, name, property_info);
|
||||
|
|
|
@ -6438,7 +6438,7 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
|
|||
/* Transform iterable into a type union alias */
|
||||
if (type_code == IS_ITERABLE) {
|
||||
/* Set iterable bit for BC compat during Reflection and string representation of type */
|
||||
zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
|
||||
zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
|
||||
(MAY_BE_ARRAY|_ZEND_TYPE_ITERABLE_BIT));
|
||||
return iterable;
|
||||
}
|
||||
|
|
|
@ -115,6 +115,7 @@ typedef void (*copy_ctor_func_t)(zval *pElement);
|
|||
* ZEND_TYPE_IS_ONLY_MASK() - checks if type-hint refer to standard type only
|
||||
* ZEND_TYPE_IS_COMPLEX() - checks if type is a type_list, or contains a class either as a CE or as a name
|
||||
* ZEND_TYPE_HAS_NAME() - checks if type-hint contains some class as zend_string *
|
||||
* ZEND_TYPE_HAS_LITERAL_NAME() - checks if type-hint contains some class as const char *
|
||||
* ZEND_TYPE_IS_INTERSECTION() - checks if the type_list represents an intersection type list
|
||||
* ZEND_TYPE_IS_UNION() - checks if the type_list represents a union type list
|
||||
*
|
||||
|
@ -145,8 +146,10 @@ typedef struct {
|
|||
#define _ZEND_TYPE_MASK ((1u << 25) - 1)
|
||||
/* Only one of these bits may be set. */
|
||||
#define _ZEND_TYPE_NAME_BIT (1u << 24)
|
||||
// Used to signify that type.ptr is not a `zend_string*` but a `const char*`,
|
||||
#define _ZEND_TYPE_LITERAL_NAME_BIT (1u << 23)
|
||||
#define _ZEND_TYPE_LIST_BIT (1u << 22)
|
||||
#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT)
|
||||
#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT)
|
||||
/* For BC behaviour with iterable type */
|
||||
#define _ZEND_TYPE_ITERABLE_BIT (1u << 21)
|
||||
/* Whether the type list is arena allocated */
|
||||
|
@ -171,6 +174,9 @@ typedef struct {
|
|||
#define ZEND_TYPE_HAS_NAME(t) \
|
||||
((((t).type_mask) & _ZEND_TYPE_NAME_BIT) != 0)
|
||||
|
||||
#define ZEND_TYPE_HAS_LITERAL_NAME(t) \
|
||||
((((t).type_mask) & _ZEND_TYPE_LITERAL_NAME_BIT) != 0)
|
||||
|
||||
#define ZEND_TYPE_HAS_LIST(t) \
|
||||
((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0)
|
||||
|
||||
|
@ -289,11 +295,14 @@ typedef struct {
|
|||
#define ZEND_TYPE_INIT_CLASS(class_name, allow_null, extra_flags) \
|
||||
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags)
|
||||
|
||||
#define ZEND_TYPE_INIT_CLASS_MASK(class_name, type_mask) \
|
||||
ZEND_TYPE_INIT_PTR_MASK(class_name, _ZEND_TYPE_NAME_BIT | (type_mask))
|
||||
|
||||
#define ZEND_TYPE_INIT_CLASS_CONST(class_name, allow_null, extra_flags) \
|
||||
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags)
|
||||
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_LITERAL_NAME_BIT, allow_null, extra_flags)
|
||||
|
||||
#define ZEND_TYPE_INIT_CLASS_CONST_MASK(class_name, type_mask) \
|
||||
ZEND_TYPE_INIT_PTR_MASK(class_name, _ZEND_TYPE_NAME_BIT | (type_mask))
|
||||
ZEND_TYPE_INIT_PTR_MASK(class_name, (_ZEND_TYPE_LITERAL_NAME_BIT | (type_mask)))
|
||||
|
||||
typedef union _zend_value {
|
||||
zend_long lval; /* long value */
|
||||
|
|
|
@ -884,11 +884,116 @@ static void le_throwing_resource_dtor(zend_resource *rsrc)
|
|||
zend_throw_exception(NULL, "Throwing resource destructor called", 0);
|
||||
}
|
||||
|
||||
static ZEND_METHOD(_ZendTestClass, takesUnionType)
|
||||
{
|
||||
zend_object *obj;
|
||||
ZEND_PARSE_PARAMETERS_START(1, 1);
|
||||
Z_PARAM_OBJ(obj)
|
||||
ZEND_PARSE_PARAMETERS_END();
|
||||
// we have to perform type-checking to avoid arginfo/zpp mismatch error
|
||||
bool type_matches = (
|
||||
instanceof_function(obj->ce, zend_standard_class_def)
|
||||
||
|
||||
instanceof_function(obj->ce, zend_ce_iterator)
|
||||
);
|
||||
if (!type_matches) {
|
||||
zend_string *ty = zend_type_to_string(execute_data->func->internal_function.arg_info->type);
|
||||
zend_argument_type_error(1, "must be of type %s, %s given", ty->val, obj->ce->name->val);
|
||||
zend_string_release(ty);
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
RETURN_NULL();
|
||||
}
|
||||
|
||||
// Returns a newly allocated DNF type `Iterator|(Traversable&Countable)`.
|
||||
//
|
||||
// We need to generate it "manually" because gen_stubs.php does not support codegen for DNF types ATM.
|
||||
static zend_type create_test_dnf_type(void) {
|
||||
zend_string *class_Iterator = zend_string_init_interned("Iterator", sizeof("Iterator") - 1, true);
|
||||
zend_alloc_ce_cache(class_Iterator);
|
||||
zend_string *class_Traversable = ZSTR_KNOWN(ZEND_STR_TRAVERSABLE);
|
||||
zend_string *class_Countable = zend_string_init_interned("Countable", sizeof("Countable") - 1, true);
|
||||
zend_alloc_ce_cache(class_Countable);
|
||||
//
|
||||
zend_type_list *intersection_list = malloc(ZEND_TYPE_LIST_SIZE(2));
|
||||
intersection_list->num_types = 2;
|
||||
intersection_list->types[0] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Traversable, 0, 0);
|
||||
intersection_list->types[1] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Countable, 0, 0);
|
||||
zend_type_list *union_list = malloc(ZEND_TYPE_LIST_SIZE(2));
|
||||
union_list->num_types = 2;
|
||||
union_list->types[0] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Iterator, 0, 0);
|
||||
union_list->types[1] = (zend_type) ZEND_TYPE_INIT_INTERSECTION(intersection_list, 0);
|
||||
return (zend_type) ZEND_TYPE_INIT_UNION(union_list, 0);
|
||||
}
|
||||
|
||||
static void register_ZendTestClass_dnf_property(zend_class_entry *ce) {
|
||||
zend_string *prop_name = zend_string_init_interned("dnfProperty", sizeof("dnfProperty") - 1, true);
|
||||
zval default_value;
|
||||
ZVAL_UNDEF(&default_value);
|
||||
zend_type type = create_test_dnf_type();
|
||||
zend_declare_typed_property(ce, prop_name, &default_value, ZEND_ACC_PUBLIC, NULL, type);
|
||||
}
|
||||
|
||||
// arg_info for `zend_test_internal_dnf_arguments`
|
||||
// The types are upgraded to DNF types in `register_dynamic_function_entries()`
|
||||
static zend_internal_arg_info arginfo_zend_test_internal_dnf_arguments[] = {
|
||||
// first entry is a zend_internal_function_info (see zend_compile.h): {argument_count, return_type, unused}
|
||||
{(const char*)(uintptr_t)(1), {0}, NULL},
|
||||
{"arg", {0}, NULL}
|
||||
};
|
||||
|
||||
static ZEND_NAMED_FUNCTION(zend_test_internal_dnf_arguments)
|
||||
{
|
||||
zend_object *obj;
|
||||
ZEND_PARSE_PARAMETERS_START(1, 1);
|
||||
Z_PARAM_OBJ(obj)
|
||||
ZEND_PARSE_PARAMETERS_END();
|
||||
// we have to perform type-checking to avoid arginfo/zpp mismatch error
|
||||
bool type_matches = (
|
||||
instanceof_function(obj->ce, zend_ce_iterator)
|
||||
|| (
|
||||
instanceof_function(obj->ce, zend_ce_traversable)
|
||||
&& instanceof_function(obj->ce, zend_ce_countable)
|
||||
)
|
||||
);
|
||||
if (!type_matches) {
|
||||
zend_string *ty = zend_type_to_string(arginfo_zend_test_internal_dnf_arguments[1].type);
|
||||
zend_argument_type_error(1, "must be of type %s, %s given", ty->val, obj->ce->name->val);
|
||||
zend_string_release(ty);
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
RETURN_OBJ_COPY(obj);
|
||||
}
|
||||
|
||||
static const zend_function_entry dynamic_function_entries[] = {
|
||||
{
|
||||
.fname = "zend_test_internal_dnf_arguments",
|
||||
.handler = zend_test_internal_dnf_arguments,
|
||||
.arg_info = arginfo_zend_test_internal_dnf_arguments,
|
||||
.num_args = 1,
|
||||
.flags = 0,
|
||||
},
|
||||
ZEND_FE_END,
|
||||
};
|
||||
|
||||
static void register_dynamic_function_entries(int module_type) {
|
||||
// return-type is at index 0
|
||||
arginfo_zend_test_internal_dnf_arguments[0].type = create_test_dnf_type();
|
||||
arginfo_zend_test_internal_dnf_arguments[1].type = create_test_dnf_type();
|
||||
//
|
||||
zend_register_functions(NULL, dynamic_function_entries, NULL, module_type);
|
||||
}
|
||||
|
||||
PHP_MINIT_FUNCTION(zend_test)
|
||||
{
|
||||
register_dynamic_function_entries(type);
|
||||
|
||||
zend_test_interface = register_class__ZendTestInterface();
|
||||
|
||||
zend_test_class = register_class__ZendTestClass(zend_test_interface);
|
||||
register_ZendTestClass_dnf_property(zend_test_class);
|
||||
zend_test_class->create_object = zend_test_class_new;
|
||||
zend_test_class->get_static_method = zend_test_class_static_method_get;
|
||||
|
||||
|
|
|
@ -53,6 +53,8 @@ namespace {
|
|||
public function returnsThrowable(): Throwable {}
|
||||
|
||||
static public function variadicTest(string|Iterator ...$elements) : static {}
|
||||
|
||||
public function takesUnionType(stdclass|Iterator $arg): void {}
|
||||
}
|
||||
|
||||
class _ZendTestChildClass extends _ZendTestClass
|
||||
|
|
8
ext/zend_test/test_arginfo.h
generated
8
ext/zend_test/test_arginfo.h
generated
|
@ -1,5 +1,5 @@
|
|||
/* This is a generated file, edit the .stub.php file instead.
|
||||
* Stub hash: 87c580bffe8794d7597572c0d8571c7459420df8 */
|
||||
* Stub hash: b458993ee586284b1e33848313d9ddf61273604e */
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
@ -172,6 +172,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestClass_variadicTes
|
|||
ZEND_ARG_VARIADIC_OBJ_TYPE_MASK(0, elements, Iterator, MAY_BE_STRING)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestClass_takesUnionType, 0, 1, IS_VOID, 0)
|
||||
ZEND_ARG_OBJ_TYPE_MASK(0, arg, stdclass|Iterator, 0, NULL)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class__ZendTestChildClass_returnsThrowable, 0, 0, Exception, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
|
@ -258,6 +262,7 @@ static ZEND_METHOD(_ZendTestClass, __toString);
|
|||
static ZEND_METHOD(_ZendTestClass, returnsStatic);
|
||||
static ZEND_METHOD(_ZendTestClass, returnsThrowable);
|
||||
static ZEND_METHOD(_ZendTestClass, variadicTest);
|
||||
static ZEND_METHOD(_ZendTestClass, takesUnionType);
|
||||
static ZEND_METHOD(_ZendTestChildClass, returnsThrowable);
|
||||
static ZEND_METHOD(_ZendTestTrait, testMethod);
|
||||
static ZEND_METHOD(ZendTestParameterAttribute, __construct);
|
||||
|
@ -340,6 +345,7 @@ static const zend_function_entry class__ZendTestClass_methods[] = {
|
|||
ZEND_ME(_ZendTestClass, returnsStatic, arginfo_class__ZendTestClass_returnsStatic, ZEND_ACC_PUBLIC)
|
||||
ZEND_ME(_ZendTestClass, returnsThrowable, arginfo_class__ZendTestClass_returnsThrowable, ZEND_ACC_PUBLIC)
|
||||
ZEND_ME(_ZendTestClass, variadicTest, arginfo_class__ZendTestClass_variadicTest, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
|
||||
ZEND_ME(_ZendTestClass, takesUnionType, arginfo_class__ZendTestClass_takesUnionType, ZEND_ACC_PUBLIC)
|
||||
ZEND_FE_END
|
||||
};
|
||||
|
||||
|
|
34
ext/zend_test/tests/internal_dnf_arguments.phpt
Normal file
34
ext/zend_test/tests/internal_dnf_arguments.phpt
Normal file
|
@ -0,0 +1,34 @@
|
|||
--TEST--
|
||||
DNF types for internal functions
|
||||
--EXTENSIONS--
|
||||
zend_test
|
||||
spl
|
||||
reflection
|
||||
--FILE--
|
||||
<?php
|
||||
|
||||
$rf = new \ReflectionFunction('zend_test_internal_dnf_arguments');
|
||||
var_dump((string)$rf->getReturnType());
|
||||
$paramType = $rf->getParameters()[0]->getType();
|
||||
var_dump((string)$paramType);
|
||||
|
||||
try {
|
||||
zend_test_internal_dnf_arguments(new stdClass);
|
||||
} catch (\Throwable $err) {
|
||||
echo $err->getMessage(), "\n";
|
||||
}
|
||||
|
||||
$obj = new \ArrayIterator([]);
|
||||
$result = zend_test_internal_dnf_arguments($obj);
|
||||
var_dump($result);
|
||||
|
||||
?>
|
||||
--EXPECT--
|
||||
string(32) "Iterator|(Traversable&Countable)"
|
||||
string(32) "Iterator|(Traversable&Countable)"
|
||||
zend_test_internal_dnf_arguments(): Argument #1 ($arg) must be of type Iterator|(Traversable&Countable), stdClass given
|
||||
object(ArrayIterator)#5 (1) {
|
||||
["storage":"ArrayIterator":private]=>
|
||||
array(0) {
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue