Why Don't PHP Attributes Allow Functions

Why don't PHP attributes allow functions?

The compiler code suggests that this is by design, though I don't know what the official reasoning behind that is. I'm also not sure how much effort it would take to reliably implement this functionality, but there are definitely some limitations in the way that things are currently done.

Though my knowledge of the PHP compiler isn't extensive, I'm going try and illustrate what I believe goes on so that you can see where there is an issue. Your code sample makes a good candidate for this process, so we'll be using that:

class Foo {
public $path = array(
realpath(".")
);
}

As you're well aware, this causes a syntax error. This is a result of the PHP grammar, which makes the following relevant definition:

class_variable_declaration: 
//...
| T_VARIABLE '=' static_scalar //...
;

So, when defining the values of variables such as $path, the expected value must match the definition of a static scalar. Unsurprisingly, this is somewhat of a misnomer given that the definition of a static scalar also includes array types whose values are also static scalars:

static_scalar: /* compile-time evaluated scalars */
//...
| T_ARRAY '(' static_array_pair_list ')' // ...
//...
;

Let's assume for a second that the grammar was different, and the noted line in the class variable delcaration rule looked something more like the following which would match your code sample (despite breaking otherwise valid assignments):

class_variable_declaration: 
//...
| T_VARIABLE '=' T_ARRAY '(' array_pair_list ')' // ...
;

After recompiling PHP, the sample script would no longer fail with that syntax error. Instead, it would fail with the compile time error "Invalid binding type". Since the code is now valid based on the grammar, this indicates that there actually is something specific in the design of the compiler that's causing trouble. To figure out what that is, let's revert to the original grammar for a moment and imagine that the code sample had a valid assignment of $path = array( 2 );.

Using the grammar as a guide, it's possible to walk through the actions invoked in the compiler code when parsing this code sample. I've left some less important parts out, but the process looks something like this:

// ...
// Begins the class declaration
zend_do_begin_class_declaration(znode, "Foo", znode);
// Set some modifiers on the current znode...
// ...
// Create the array
array_init(znode);
// Add the value we specified
zend_do_add_static_array_element(znode, NULL, 2);
// Declare the property as a member of the class
zend_do_declare_property('$path', znode);
// End the class declaration
zend_do_end_class_declaration(znode, "Foo");
// ...
zend_do_early_binding();
// ...
zend_do_end_compilation();

While the compiler does a lot in these various methods, it's important to note a few things.

  1. A call to zend_do_begin_class_declaration() results in a call to get_next_op(). This means that it adds a new opcode to the current opcode array.
  2. array_init() and zend_do_add_static_array_element() do not generate new opcodes. Instead, the array is immediately created and added to the current class' properties table. Method declarations work in a similar way, via a special case in zend_do_begin_function_declaration().
  3. zend_do_early_binding() consumes the last opcode on the current opcode array, checking for one of the following types before setting it to a NOP:
    • ZEND_DECLARE_FUNCTION
    • ZEND_DECLARE_CLASS
    • ZEND_DECLARE_INHERITED_CLASS
    • ZEND_VERIFY_ABSTRACT_CLASS
    • ZEND_ADD_INTERFACE

Note that in the last case, if the opcode type is not one of the expected types, an error is thrown – The "Invalid binding type" error. From this, we can tell that allowing the non-static values to be assigned somehow causes the last opcode to be something other than expected. So, what happens when we use a non-static array with the modified grammar?

Instead of calling array_init(), the compiler prepares the arguments and calls zend_do_init_array(). This in turn calls get_next_op() and adds a new INIT_ARRAY opcode, producing something like the following:

DECLARE_CLASS   'Foo'
SEND_VAL '.'
DO_FCALL 'realpath'
INIT_ARRAY

Herein lies the root of the problem. By adding these opcodes, zend_do_early_binding() gets an unexpected input and throws an exception. As the process of early binding class and function definitions seems fairly integral to the PHP compilation process, it can't just be ignored (though the DECLARE_CLASS production/consumption is kind of messy). Likewise, it's not practical to try and evaluate these additional opcodes inline (you can't be sure that a given function or class has been resolved yet), so there's no way to avoid generating the opcodes.

A potential solution would be to build a new opcode array that was scoped to the class variable declaration, similar to how method definitions are handled. The problem with doing that is deciding when to evaluate such a run-once sequence. Would it be done when the file containing the class is loaded, when the property is first accessed, or when an object of that type is constructed?

As you've pointed out, other dynamic languages have found a way to handle this scenario, so it's not impossible to make that decision and get it to work. From what I can tell though, doing so in the case of PHP wouldn't be a one-line fix, and the language designers seem to have decided that it wasn't something worth including at this point.

Why can't you have require* statements in a class definition?

It was possible to require and include files both within function scope and at global scope, before Classes were added to PHP.

This is only a guess — I'm not sure what else we could do other than for the language designers to come and tell us their story — but I imagine it was believed that no benefit would be gained from adding this functionality to the "new scope" invented by the addition of Classes, especially considering the complexity added to the back-end in order to support it.

It's also not entirely clear what the scoping rules would be for any declarations made inside the required file.

In conclusion, I think you're asking the wrong question. Instead of "why isn't this supported?" it's more a case of "why should it be supported?".

I hope that this helps in some small way.

Use constant as Function attributes

I don't really understand your issue

constants like PASSWORD_DEFAULT, but just it (no function name with double :).

but my guess is, you are looking for define().

I suggest you to utilize OOP as much as possible and so like others said, use class constants. But if you want global constants for whatever reason, it goes like this:

define('DIRECTION_FORWARD', 0);
define('DIRECTION_LEFT', 1);
define('DIRECTION_RIGHT', 2);

function goInDirection($direction = DIRECTION_FORWARD)
{
// ....
}

Instead of just sequential numbers as values, you can use bitmasks, which work with bitwise operators and the power of 2.

https://www.php.net/manual/en/language.operators.bitwise.php

Why can't we use the new keyword in a class property?

You have to put it into the constructor:

class Foo() {
private $bar;
function __construct() {
$this->bar = new Bar();
}
}

PHP 8 Attribute constructor calling

Attributes can be accessed only through reflection, here is example:

// you have to specify that your custom class can serve as attribute
// by adding the build-in attribute Attribute:

#[Attribute]
class MyAttribute {
public function __construct($message) {
// I want to show this message when using MyAttribute
echo $message;
}
}

#[MyAttribute('hello')]
class SomeClass {
// ...
}

// first you need to create Reflection* in order to access attributes, in this case its class
$reflection = new ReflectionClass(SomeClass::class);
// then you can access the attributes
$attributes = $reflection->getAttributes();
// notice that its an array, as you can have multiple attributes
// now you can create the instance
$myAttributeInstance = $attributes[0]->newInstance();

Some documentation: ReflectionClass ReflectionAttribute

There are other Reflection* classes like: method, function, parameter, property, class constant.

Declaring closure to class attribute in PHP

PHP Currently doesn't allow directly calling a function stored as an object property.

It allows properties and methods of an object to have the same name actually.

One suggested solution to this issue is from another, almost identical question

class Totalizer
{
public $count;

public function __construct()
{
$this->count = function ($product) {
return $product;
};
}

public function __call($method, $args)
{
if (is_callable(array($this, $method))) {
return call_user_func_array($this->$method, $args);
} else {
// else throw exception
}
}
}

math symbols don't work within PHP class definitions?

Expression isn't allowed as field default value. You may try this

class Foo {
public static $test;

public function __construct(){
$this->test = 1+1;
}
}

echo Foo::$test;

PHP 8 Attributes: Is it possible to validate metadata passed to attributes or force attribute instantiation?

Everything is possible - as of today I have implemented a working solution for this problem which can very well be used in libraries (just in case somebody needs that too):

function cis_shutdown_validate_attributes()
{
if(!defined('CIS_DISABLE_ATTRIBUTE_VALIDATION')) {
foreach(get_declared_classes() as $class) {
try {
$reflection = new ReflectionClass($class);
} catch (\Throwable $throwable) {
continue;
}
$attributes = $reflection->getAttributes();

foreach ($attributes as $attribute) {
$attribute->newInstance();
}
}
}
}

register_shutdown_function('cis_shutdown_validate_attributes');

(You may also autoload it: Composer/PSR - How to autoload functions?)

Access and return private attribute in a function

Welcome to StackOverflow.

Where did you get the $rate variable? It is basically used in nowhere. If $rating comes in, and $this->rating is the global variable, then there is no $rate variable.

There are also no spaces between $this->title and etc.

Code:

<?php

// Use this function to make sure your error handling is tightest:
error_reporting(E_ALL);

// Start a new class
class Book {

// We are setting the rating to be private:
private $rating;

// And we are setting the title to be public: You could also use 'var' here instead:
var $title;

// This is the function behind new Book () .. it is a construction function.
function __construct ($title, $rating) {

// You have $title coming and you are setting the classes global variable to it as well:
$this->title = $title;

// Same as above, but this is private, so outside of this class you cant access it:
$this->rating = $rating;
}

// This the function to get the rating:
function getRating () {

// This is the variable from the 5th line now. It is in fact private, but since the
// function is inside the class, then this function getRating is allowed to access the variable
// there for it will print it out without problems:
return $this->rating;
}
}

// Init the class and insert some basic information:
$book1 = new Book('Harry Potter', 'PG-13');

// Will print out 'PG-13'
echo $book1->getRating() . '<br>';

// Title will show up, as it is public:
echo $book1->title . '<br>';

// But accessing the rating directly, will not show anything:
echo $book1->rating . '<br>';

// Since the rating is private, then it will ultimate throw an error,
// so this will kill the script or show the error, depending on your hosting settings:
echo 'This probably wount show up';
// yup, it gives you:
// Fatal error: Uncaught Error: Cannot access private property Book::$rating in [.........]
?>

Output:

Sample Image

Hope this helps you move forward learning about more PHP.



Related Topics



Leave a reply



Submit