Почему атрибуты PHP не позволяют выполнять функции?

Я новичок в PHP, но я много лет программирую на подобных языках. Меня смутило следующее:

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

Появилась синтаксическая ошибка: Parse error: syntax error, unexpected '(', expecting ')' in test.php on line 5, которая является вызовом realpath.

Но это прекрасно работает:

$path = array(
    realpath(".")
);

После некоторой удары головой об этом, мне сказали, что вы не можете вызывать функции в атрибуте default; вы должны сделать это в __construct. Мой вопрос: почему?! Является ли это "особенностью" или небрежной реализацией? Какое обоснование?

Ответ 1

Код компилятора предполагает, что это по дизайну, хотя я не знаю, что такое официальная аргументация. Я также не уверен, сколько усилий потребуется для надежного внедрения этой функции, но есть определенные ограничения в том, как это делается в настоящее время.

Хотя мои знания о компиляторе PHP не являются исчерпывающими, я попытаюсь проиллюстрировать, на что я верю, чтобы вы могли видеть, где есть проблема. Ваш образец кода является хорошим кандидатом для этого процесса, поэтому мы будем использовать это:

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

Как вам хорошо известно, это вызывает синтаксическую ошибку. Это результат грамматики PHP, что делает следующее релевантное определение:

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

Таким образом, при определении значений переменных, таких как $path, ожидаемое значение должно соответствовать определению статического скаляра. Неудивительно, что это несколько неправильно, учитывая, что определение статического скаляра также включает типы массивов, значения которых также являются статическими скалярами:

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

Предположим на секунду, что грамматика различна, а отмеченная строка в правиле delcaration переменной класса выглядит примерно как следующее, которое будет соответствовать вашему образцу кода (несмотря на нарушение других действительных назначений):

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

После перекомпиляции PHP образец script больше не будет терпеть неудачу с этой синтаксической ошибкой. Вместо этого с ошибкой компиляции "Недействительный тип привязки" будет сбой. Поскольку код теперь действителен на основе грамматики, это указывает на то, что в дизайне компилятора действительно есть что-то конкретное, что вызывает проблемы. Чтобы понять, что это такое, пусть на мгновение вернется к исходной грамматике и представьте, что образец кода имел правильное назначение $path = array( 2 );.

Используя грамматику в качестве руководства, при анализе этого примера кода можно просмотреть действия, вызванные кодом компилятора. Я оставил несколько менее важных частей, но процесс выглядит примерно так:

// ...
// 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();

В то время как компилятор делает много в этих различных методах, важно отметить несколько вещей.

  • Вызов zend_do_begin_class_declaration() вызывает вызов get_next_op(). Это означает, что он добавляет новый код операции в текущий массив операций opcode.
  • array_init() и zend_do_add_static_array_element() не генерируют новые коды операций. Вместо этого массив сразу создается и добавляется в таблицу свойств текущего класса. Аналогично, объявления метода работают через специальный случай в zend_do_begin_function_declaration().
  • zend_do_early_binding() использует последний код операции в текущем массиве опкодов, проверяя один из следующих типов, прежде чем устанавливать его в NOP:
    • ZEND_DECLARE_FUNCTION
    • ZEND_DECLARE_CLASS
    • ZEND_DECLARE_INHERITED_CLASS
    • ZEND_VERIFY_ABSTRACT_CLASS
    • ZEND_ADD_INTERFACE

Обратите внимание, что в последнем случае, если тип кода операции не является одним из ожидаемых типов, возникает ошибка: ndash; Ошибка "Недопустимый тип привязки". Из этого можно сказать, что разрешение назначения нестатических значений каким-то образом приводит к тому, что последний код операции является чем-то другим, чем ожидалось. Итак, что происходит, когда мы используем нестатический массив с измененной грамматикой?

Вместо вызова array_init() компилятор готовит аргументы и вызовы zend_do_init_array(). Это, в свою очередь, вызывает get_next_op() и добавляет новый код операции INIT_ARRAY, создавая что-то вроде следующего:

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

В этом корень проблемы. Добавляя эти коды операций, zend_do_early_binding() получает неожиданный ввод и генерирует исключение. Поскольку процесс раннего определения класса привязки и функций кажется довольно неотъемлемой частью процесса компиляции PHP, его нельзя просто игнорировать (хотя производство/потребление DECLARE_CLASS является довольно грязным). Точно так же нецелесообразно пытаться оценить эти дополнительные коды операций inline (вы не можете быть уверены, что данная функция или класс еще разрешены), поэтому нет способа избежать генерации кодов операций.

Потенциальным решением могло бы стать создание нового массива opcode, который был бы привязан к объявлению переменной класса, подобно тому, как обрабатываются определения методов. Проблема с этим заключается в том, чтобы решить, когда следует оценивать такую ​​последовательность очередей. Будет ли это сделано, когда файл, содержащий класс, будет загружен, когда свойство будет впервые доступно, или когда объект такого типа будет сконструирован?

Как вы уже указали, другие динамические языки нашли способ справиться с этим сценарием, поэтому не просто принять это решение и заставить его работать. Из того, что я могу сказать, сделав это в случае PHP, это не будет однострочное исправление, и разработчики языка, похоже, решили, что в этот момент это не было чем-то стоящим.

Ответ 2

Мой вопрос: почему?! Является ли это "особенностью" или небрежной реализацией?

Я бы сказал, что это определенно особенность. Определение класса - это схема кода и не должна выполнять код во время определения. Это нарушит абстракцию объекта и инкапсуляцию.

Однако это только мое мнение. Я не могу точно сказать, какую идею разработчики имели при определении этого.

Ответ 3

Возможно, вы можете достичь чего-то подобного:

class Foo
{
    public $path = __DIR__;
}

IIRC __DIR__ нуждается в php 5.3+, __FILE__ был дольше

Ответ 4

Это небрежная реализация парсера. У меня нет правильной терминологии для ее описания (я думаю, что термин "бета-редукция" как-то вписывается...), но парсер PHP-языка более сложный и сложный, чем он должен быть, и поэтому всевозможные специальная обложка требуется для разных языковых конструкций.

Ответ 5

Я предполагаю, что вы не сможете получить правильную трассировку стека, если ошибка не возникает в исполняемой строке... Поскольку не может быть никакой ошибки при инициализации значений с константами, нет проблем с этим, но функция может генерировать исключения/ошибки и должна вызываться внутри исполняемой строки, а не декларативной.