Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add #[NonpublicConstructor] attribute #17846

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
13 changes: 13 additions & 0 deletions Zend/tests/attributes/NonpublicConstructor/Generator.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
#[\NonpublicConstructor]: works as a replacement for get_constructor
--FILE--
<?php

new Generator();

?>
--EXPECTF--
Fatal error: Uncaught Error: Call to private Generator::__construct() from global scope, the "Generator" class is reserved for internal use and cannot be manually instantiated, in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
#[\NonpublicConstructor]: affects message from ReflectionClass::newInstance()
--FILE--
<?php

class Demo {
#[\NonpublicConstructor("use ::getInstance() instead")]
private function __construct( $unused ) {}
}

$reflection = new ReflectionClass( Demo::class );
$reflection->newInstance( true );

?>
--EXPECTF--
Fatal error: Uncaught ReflectionException: Access to non-public constructor of class Demo, use ::getInstance() instead, in %s:%d
Stack trace:
#0 %s(%d): ReflectionClass->newInstance(true)
#1 {main}
thrown in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
#[\NonpublicConstructor]: affects message from ReflectionClass::newInstanceArgs()
--FILE--
<?php

class Demo {
#[\NonpublicConstructor("use ::getInstance() instead")]
private function __construct( $unused ) {}
}

$reflection = new ReflectionClass( Demo::class );
$reflection->newInstanceArgs( [ true ] );

?>
--EXPECTF--
Fatal error: Uncaught ReflectionException: Access to non-public constructor of class Demo, use ::getInstance() instead, in %s:%d
Stack trace:
#0 %s(%d): ReflectionClass->newInstanceArgs(Array)
#1 {main}
thrown in %s on line %d
14 changes: 14 additions & 0 deletions Zend/tests/attributes/NonpublicConstructor/as_object.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
#[\NonpublicConstructor]: using the attribute itself as an object
--FILE--
<?php

$attrib = new NonpublicConstructor('example');
var_dump( $attrib );

?>
--EXPECTF--
object(NonpublicConstructor)#%d (1) {
["message"]=>
string(7) "example"
}
18 changes: 18 additions & 0 deletions Zend/tests/attributes/NonpublicConstructor/global_private.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
#[\NonpublicConstructor]: affects error message (private constructor, global scope)
--FILE--
<?php

class Demo {
#[\NonpublicConstructor("use ::getInstance() instead")]
private function __construct() {}
}

new Demo();

?>
--EXPECTF--
Fatal error: Uncaught Error: Call to private Demo::__construct() from global scope, use ::getInstance() instead, in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %s
18 changes: 18 additions & 0 deletions Zend/tests/attributes/NonpublicConstructor/global_protected.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
#[\NonpublicConstructor]: affects error message (protected constructor, global scope)
--FILE--
<?php

class Demo {
#[\NonpublicConstructor("use ::getInstance() instead")]
protected function __construct() {}
}

new Demo();

?>
--EXPECTF--
Fatal error: Uncaught Error: Call to protected Demo::__construct() from global scope, use ::getInstance() instead, in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %s
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
#[\NonpublicConstructor]: On a private non-constructor
--FILE--
<?php

class Demo {
#[\NonpublicConstructor]
private function test() {}
}

?>
--EXPECTF--
Fatal error: #[NonpublicConstructor] can only be applied to constructors in %s on line %d
13 changes: 13 additions & 0 deletions Zend/tests/attributes/NonpublicConstructor/must_be_nonpublic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
#[\NonpublicConstructor]: On a public constructor
--FILE--
<?php

class Demo {
#[\NonpublicConstructor]
public function __construct() {}
}

?>
--EXPECTF--
Fatal error: #[NonpublicConstructor] can only be applied to protected or private constructors in %s on line %d
13 changes: 13 additions & 0 deletions Zend/tests/attributes/NonpublicConstructor/non_string.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
#[\NonpublicConstructor]: Message argument must be a string
--FILE--
<?php

class Demo {
#[\NonpublicConstructor(true)]
private function __construct() {}
}

?>
--EXPECTF--
Fatal error: NonpublicConstructor::__construct(): Argument #1 ($message) must be of type string, true given in %s on line %d
25 changes: 25 additions & 0 deletions Zend/tests/attributes/NonpublicConstructor/subclass_private.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
#[\NonpublicConstructor]: affects error message (private constructor, protected scope)
--FILE--
<?php

class Demo {
#[\NonpublicConstructor("use ::getInstance() instead")]
private function __construct() {}
}

class Subclass extends Demo {
public static function create() {
return new Demo();
}
}

Subclass::create();

?>
--EXPECTF--
Fatal error: Uncaught Error: Call to private Demo::__construct() from scope Subclass, use ::getInstance() instead, in %s:%d
Stack trace:
#0 %s(%d): Subclass::create()
#1 {main}
thrown in %s on line %s
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
#[\NonpublicConstructor]: affects error message (private constructor, protected scope, called via `parent::__construct()`)
--FILE--
<?php

class Demo {
#[\NonpublicConstructor("use ::getInstance() instead")]
private function __construct() {}
}

class Subclass extends Demo {
public function __construct() {
parent::__construct();
}
}

new Subclass();

?>
--EXPECTF--
Fatal error: Uncaught Error: Cannot call private Demo::__construct(), use ::getInstance() instead, in %s:%d
Stack trace:
#0 %s(%d): Subclass->__construct()
#1 {main}
thrown in %s on line %s
13 changes: 13 additions & 0 deletions Zend/tests/attributes/NonpublicConstructor/without_message.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
#[\NonpublicConstructor]: Message argument is required
--FILE--
<?php

class Demo {
#[\NonpublicConstructor]
private function __construct() {}
}

?>
--EXPECTF--
Fatal error: #[NonpublicConstructor] takes 1 parameter, 0 given in %s on line %d
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ new Generator;

?>
--EXPECTF--
Fatal error: Uncaught Error: The "Generator" class is reserved for internal use and cannot be manually instantiated in %s:%d
Fatal error: Uncaught Error: Call to private Generator::__construct() from global scope, the "Generator" class is reserved for internal use and cannot be manually instantiated, in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d
133 changes: 133 additions & 0 deletions Zend/zend_attributes.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ ZEND_API zend_class_entry *zend_ce_sensitive_parameter;
ZEND_API zend_class_entry *zend_ce_sensitive_parameter_value;
ZEND_API zend_class_entry *zend_ce_override;
ZEND_API zend_class_entry *zend_ce_deprecated;
ZEND_API zend_class_entry *zend_ce_nonpublic_constructor;

static zend_object_handlers attributes_object_handlers_sensitive_parameter_value;

Expand Down Expand Up @@ -94,6 +95,110 @@ static void validate_allow_dynamic_properties(
scope->ce_flags |= ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES;
}

static void validate_nonpublic_constructor(
zend_attribute *attr,
uint32_t target,
zend_class_entry *scope
) {
zend_op_array *op_array = CG(active_op_array);
ZEND_ASSERT(op_array);

if (!(op_array->fn_flags & ZEND_ACC_CTOR)) {
zend_error_noreturn(
E_ERROR,
"#[NonpublicConstructor] can only be applied to constructors"
);
}
if (!(op_array->fn_flags & (ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE))) {
zend_error_noreturn(
E_ERROR,
"#[NonpublicConstructor] can only be applied to protected or private constructors"
);
}
if (attr->argc != 1) {
zend_error_noreturn(
E_ERROR,
"#[NonpublicConstructor] takes 1 parameter, %d given",
attr->argc
);
}
zval message;
if (FAILURE == zend_get_attribute_value(&message, attr, 0, NULL)) {
ZEND_ASSERT(EG(exception));
return;
}

if (Z_TYPE(message) != IS_STRING) {
zend_error_noreturn(
E_ERROR,
"NonpublicConstructor::__construct(): Argument #1 ($message) must be of type string, %s given",
zend_zval_value_name(&message)
);
}
zval_ptr_dtor(&message);
}

ZEND_API ZEND_COLD zend_result ZEND_FASTCALL zend_attribute_get_nonpublic_suffix(HashTable *attributes, zend_string **message_suffix)
{
*message_suffix = ZSTR_EMPTY_ALLOC();

if (!attributes) {
return SUCCESS;
}

zend_attribute *nonpublicConstructor = zend_get_attribute_str(
attributes,
"nonpublicconstructor",
sizeof("nonpublicconstructor")-1
);

if (!nonpublicConstructor) {
return SUCCESS;
}

ZEND_ASSERT(nonpublicConstructor->argc == 1);

zend_result result = FAILURE;
zend_string *message_prop = ZSTR_EMPTY_ALLOC();

zval obj;
ZVAL_UNDEF(&obj);
zval *z;

/* Construct the NonpublicConstructor object to correctly handle parameter processing. */
if (FAILURE == zend_get_attribute_object(
&obj,
zend_ce_nonpublic_constructor,
nonpublicConstructor,
NULL,
NULL
)) {
goto out;
}

/* Extract the $message property. */
z = zend_read_property_ex(zend_ce_nonpublic_constructor, Z_OBJ_P(&obj), ZSTR_KNOWN(ZEND_STR_MESSAGE), false, NULL);
ZEND_ASSERT(z != &EG(uninitialized_zval));
ZEND_ASSERT(Z_TYPE_P(z) == IS_STRING);
message_prop = zend_string_copy(Z_STR_P(z));

/* Construct the suffix. */
*message_suffix = zend_strpprintf_unchecked(
0,
", %S,",
message_prop
);

result = SUCCESS;

out:

zend_string_release(message_prop);
zval_ptr_dtor(&obj);

return result;
}

ZEND_METHOD(Attribute, __construct)
{
zend_long flags = ZEND_ATTRIBUTE_TARGET_ALL;
Expand Down Expand Up @@ -193,6 +298,30 @@ ZEND_METHOD(Deprecated, __construct)
}
}

ZEND_METHOD(NonpublicConstructor, __construct)
{
zend_string *message = NULL;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(message)
ZEND_PARSE_PARAMETERS_END();

zval messageValue;
ZVAL_STR(&messageValue, message);

zend_update_property_ex(
zend_ce_nonpublic_constructor,
Z_OBJ_P(ZEND_THIS),
ZSTR_KNOWN(ZEND_STR_MESSAGE),
&messageValue
);

/* The assignment might fail due to 'readonly'. */
if (UNEXPECTED(EG(exception))) {
RETURN_THROWS();
}
}

static zend_attribute *get_attribute(HashTable *attributes, zend_string *lcname, uint32_t offset)
{
if (attributes) {
Expand Down Expand Up @@ -520,6 +649,10 @@ void zend_register_attribute_ce(void)

zend_ce_deprecated = register_class_Deprecated();
attr = zend_mark_internal_attribute(zend_ce_deprecated);

zend_ce_nonpublic_constructor = register_class_NonpublicConstructor();
attr = zend_mark_internal_attribute(zend_ce_nonpublic_constructor);
attr->validator = validate_nonpublic_constructor;
}

void zend_attributes_shutdown(void)
Expand Down
Loading
Loading