Давайте повеселимся. Некоторое время назад я экспериментировал с макросами PHP , добавляя синтаксис диапазона Python. Затем талантливый SaraMG упомянул RFC , и LordKabelo предложил вместо этого добавить в PHP методы получения и установки в стиле C #.
Зная о том, насколько посторонним может быть крайне медленно предлагать и реализовывать новую языковую функцию, я обратился к своему редактору…
Код для этого урока можно найти на Github . Он был протестирован с PHP
^7.1
, и сгенерированный код должен работать на PHP^5.6|^7.0
.
Как макросы снова работают?
Прошло много времени (и, возможно, вы никогда о них не слышали), так как я говорил о макросах. Чтобы освежить вашу память, они берут код, который выглядит следующим образом:
macro { →(···expression) } >> { ··stringify(···expression) } macro { T_VARIABLE·A[ ···range ] } >> { eval( '$list = ' . →(T_VARIABLE·A) . ';' . '$lower = ' . explode('..', →(···range))[0] . ';' . '$upper = ' . explode('..', →(···range))[1] . ';' . 'return array_slice($list, $lower, $upper - $lower);' ) }
… И включите пользовательский синтаксис PHP следующим образом:
$few = many[1..3];
… В действительный синтаксис PHP, например так:
$few = eval( '$list = ' . '$many' . ';'. '$lower = ' . explode('..', '1..3')[0] . ';' . '$upper = ' . explode('..', '1..3')[1] . ';' . 'return array_slice($list, $lower, $upper - $lower);' );
Если вы хотите посмотреть, как это работает, зайдите на пост, о котором я писал .
Хитрость заключается в том, чтобы понять, как синтаксический анализатор маркирует строку кода, построить шаблон макроса, а затем рекурсивно применить этот шаблон к новому синтаксису.
Однако библиотека макросов не очень хорошо документирована. Трудно точно знать, как должен выглядеть шаблон или какой правильный синтаксис сгенерировать в конце. Каждое новое приложение требует, чтобы этот учебник был написан, прежде чем другие смогут понять, что на самом деле происходит.
Строительство базы
Итак, давайте посмотрим на приложение под рукой. Мы бы хотели добавить синтаксис getter и setter, похожий на синтаксис C #, к PHP. Прежде чем мы сможем это сделать, нам нужно иметь хорошую базу кода для работы. Возможно, что-то в форме черты, которую мы можем добавить к классам, нуждающимся в этой новой функциональности.
Нам нужно реализовать код, который будет проверять определение класса и создавать эти динамические методы получения и установки для каждого специального свойства или комментария, которые он видит.
Возможно, мы можем начать с определения специального формата имени метода и магических методов __get
и __set
:
namespace App; trait AccessorTrait { /** * @inheritdoc * * @param string $property * @param mixed $value */ public function __get($property) { if (method_exists($this, "__get_{$property}")) { return $this->{"__get_{$property}"}(); } } /** * @inheritdoc * * @param string $property * @param mixed $value */ public function __set($property, $value) { if (method_exists($this, "__set_{$property}")) { return $this->{"__set_{$property}"}($value); } } }
Каждый метод, начинающийся с имен __get_
и __set_
должен быть связан с пока неопределенным свойством. Мы можем представить этот синтаксис:
namespace App; class Sprocket { private $type { get { return $this->type; } set { $this->type = strtoupper($value); } }; }
… превращается во что-то очень похожее на:
namespace App; class Sprocket { use AccessorTrait; private $type; private function __get_type() { return $this->type; } private function __set_type($value) { $this->type = strtoupper($value); } }
Определение макросов
Определение необходимых макросов является самой сложной частью всего этого. Учитывая отсутствие документации (и широкое использование), и только с небольшим количеством полезных сообщений об исключениях, это в основном много проб и ошибок.
Я провел несколько часов, придумывая следующие схемы:
macro ·unsafe { ·ns()·class { ···body } } >> { ·class { use AccessorTrait; ···body } } macro ·unsafe { private T_VARIABLE·var { get { ···getter } set { ···setter } }; } >> { private T_VARIABLE·var; private function ··concat(__get_ ··unvar(T_VARIABLE·var))() { ···getter } private function ··concat(__set_ ··unvar(T_VARIABLE·var))($value) { ···setter } }
Хорошо, давайте посмотрим, что делают эти два макроса:
- Мы начнем с сопоставления
class MyClass { ... }
и вставки построенного ранееAccessorTrait
. Это обеспечивает реализацию__get
и__set
, которая связывает__get_bar
дляprint $class->bar
и т. Д. - Мы сопоставляем синтаксис блока доступа и заменяем его обычным определением свойства, за которым следует пара отдельных определений метода. Мы можем заключить точное содержимое блоков
get { ... }
иset { ... }
в эти функции.
Сначала, когда вы запустите этот код, вы получите ошибку. Это потому, что функция ··unvar
не является стандартной частью макропроцессора. Это то, что я должен был добавить, чтобы конвертировать из $type
в type
:
namespace Yay\DSL\Expanders; use Yay\Token; use Yay\TokenStream; function unvar(TokenStream $ts) : TokenStream { $str = str_replace('$', '', (string) $ts); return TokenStream::fromSequence( new Token( T_CONSTANT_ENCAPSED_STRING, $str ) ) ; }
Мне удалось скопировать (почти точно) расширитель ··stringify
, который включен в синтаксический анализатор макросов. Вам не нужно много понимать о внутренностях Yay, чтобы увидеть, что это делает. TokenStream
к строке (в этом контексте) означает, что вы получаете строковое значение любого токена, на который в данный момент ссылаются — в данном случае это ··unvar(T_VARIABLE·var)
— и выполняете строковые манипуляции с ним.
(string) $ts
становится"$type"
, в отличие от"T_VARIABLE·var"
.
Обычно эти макросы применяются, когда они находятся внутри скрипта, к которому они предназначены. Другими словами, мы могли бы создать скрипт, похожий на:
<?php macro ·unsafe { ... } >> { ... } macro ·unsafe { ... } >> { ... } namespace App; trait AccessorTrait { ... } class Sprocket { private $type { get { return $this->type; } set { $this->type = strtoupper($value); } }; }
… тогда мы могли бы запустить его с помощью команды вроде:
vendor/bin/yay src/Sprocket.pre >> src/Sprocket.php
Наконец, мы могли бы использовать этот код (с некоторыми автозагрузками Composer PSR-4), используя:
require __DIR__ . "/vendor/autoload.php"; $sprocket = new App\Sprocket(); $sprocket->type = "acme sprocket"; print $sprocket->type; // Acme Sprocket
Автоматизация конвертации
Как ручной процесс, это отстой. Кто хочет запускать эту команду bash каждый раз, когда они меняют src/Sprocket.pre
? К счастью, мы можем автоматизировать это!
Первый шаг — определить пользовательский автозагрузчик:
spl_autoload_register(function($class) { $definitions = require __DIR__ . "/vendor/composer/autoload_psr4.php"; foreach ($definitions as $prefix => $paths) { $prefixLength = strlen($prefix); if (strncmp($prefix, $class, $prefixLength) !== 0) { continue; } $relativeClass = substr($class, $prefixLength); foreach ($paths as $path) { $php = $path . "/" . str_replace("\\", "/", $relativeClass) . ".php"; $pre = $path . "/" . str_replace("\\", "/", $relativeClass) . ".pre"; $relative = ltrim(str_replace(__DIR__, "", $pre), DIRECTORY_SEPARATOR); $macros = __DIR__ . "/macros.pre"; if (file_exists($pre)) { // ... convert and load file } } } }, false, true);
Вы можете сохранить этот файл как
autoload.php
и использовать автозагрузкуfiles
чтобы включить его через автозагрузчик Composer, как описано в документации .
Первая часть этого определения вытекает непосредственно из примера реализации спецификации PSR-4 . Мы выбираем файл определений Composer’s PSR-4 и для каждого префикса проверяем, соответствует ли он классу, загружаемому в данный момент.
Если он совпадает, мы проверяем каждый потенциальный путь, пока не найдем file.pre
, в котором определен наш собственный синтаксис. Затем мы получаем содержимое файла macros.pre
(в базовом каталоге проекта) и создаем промежуточный файл — используя содержимое macros.pre
+ содержимое соответствующего файла. Это означает, что макросы доступны для файла, который мы передаем Yay. Как только Yay скомпилирует file.pre.interim
→ file.php
, мы удаляем file.pre.interim
.
Код для этого процесса:
if (file_exists($php)) { unlink($php); } file_put_contents( "{$pre}.interim", str_replace( "<?php", file_get_contents($macros), file_get_contents($pre) ) ); exec("vendor/bin/yay {$pre}.interim >> {$php}"); $comment = " # This file is generated, changes you make will be lost. # Make your changes in {$relative} instead. "; file_put_contents( $php, str_replace( "<?php", "<?php\n{$comment}", file_get_contents($php) ) ); unlink("{$pre}.interim"); require_once $php;
Обратите внимание на эти два логических значения в конце вызова spl_autoload_register
. Во-первых, должен ли этот автозагрузчик генерировать исключения для ошибок загрузки. Второй вопрос — должен ли этот автозагрузчик быть добавлен в стек. Это ставит его перед автозагрузчиками Composer, что означает, что мы можем конвертировать file.pre
до того, как Composer попытается загрузить file.php
!
Создание инфраструктуры плагинов
Эта автоматизация великолепна, но она бесполезна, если нужно повторять ее для каждого проекта. Что, если бы мы могли просто composer require
зависимость (для новой языковой функции), и это просто сработало бы? Давайте сделаем это…
Для начала нам нужно создать новый репо, содержащий следующие файлы:
-
composer.json
→ автозагрузка следующих файлов -
functions.php
→ создавать функции пути макроса (в другие библиотеки можно динамически добавлять свои собственные файлы макроса) -
expanders.php
→ создавать функции экспандера, такие как··unvar
-
autoload.php
→ дополнитьautoload.php
Composer, загружая.pre
каждой библиотеки в каждый скомпилированный файл.pre
{ "name": "pre/plugin", "require": { "php": "^7.0", "yay/yay": "dev-master" }, "autoload": { "files": [ "functions.php", "expanders.php", "autoload.php" ] }, "minimum-stability": "dev", "prefer-stable": true }
Это из
composer.json
<?php namespace Pre; define("GLOBAL_KEY", "PRE_MACRO_PATHS"); /** * Creates the list of macros, if it is undefined. */ function initMacroPaths() { if (!isset($GLOBALS[GLOBAL_KEY])) { $GLOBALS[GLOBAL_KEY] = []; } } /** * Adds a path to the list of macro files. * * @param string $path */ function addMacroPath($path) { initMacroPaths(); array_push($GLOBALS[GLOBAL_KEY], $path); } /** * Removes a path to the list of macro files. * * @param string $path */ function removeMacroPath($path) { initMacroPaths(); $GLOBALS[GLOBAL_KEY] = array_filter( $GLOBALS[GLOBAL_KEY], function($next) use ($path) { return $next !== $path; } ); } /** * Gets all macro file paths. * * @return array */ function getMacroPaths() { initMacroPaths(); return $GLOBALS[GLOBAL_KEY]; }
Это из
functions.php
Возможно, вы недовольны мыслью об использовании $GLOBALS
в качестве хранилища путей к файлам макросов. Это неважно, так как мы можем хранить эти пути любым другим способом. Это просто самый простой подход для демонстрации шаблона.
<?php namespace Yay\DSL\Expanders; use Yay\Token; use Yay\TokenStream; function unvar(TokenStream $ts) : TokenStream { $str = str_replace('$', '', (string) $ts); return TokenStream::fromSequence( new Token( T_CONSTANT_ENCAPSED_STRING, $str ) ) ; }
Это из
expanders.php
<?php namespace Pre; if (file_exists(__DIR__ . "/../../autoload.php")) { define("BASE_DIR", realpath(__DIR__ . "/../../../")); } spl_autoload_register(function($class) { $definitions = require BASE_DIR . "/vendor/composer/autoload_psr4.php"; foreach ($definitions as $prefix => $paths) { // ...check $prefixLength foreach ($paths as $path) { // ...create $php and $pre $relative = ltrim(str_replace(BASE_DIR, "", $pre), DIRECTORY_SEPARATOR); $macros = BASE_DIR . "/macros.pre"; if (file_exists($pre)) { // ...remove existing PHP file foreach (getMacroPaths() as $macroPath) { file_put_contents( "{$pre}.interim", str_replace( "<?php", file_get_contents($macroPath), file_get_contents($pre) ) ); } // ...write and include the PHP file } } } }, false, true);
Это из
autoload.php
Теперь дополнительные макро-плагины могут использовать эти функции для подключения собственного кода в систему…
Создание новой языковой функции
С помощью встроенного кода плагина мы можем изменить наши средства доступа к классам, чтобы они стали автономными автоматически применяемыми функциями. Нам нужно создать еще несколько файлов, чтобы это произошло:
-
composer.json
→ требуется репозиторий базовых плагинов и автозагрузка следующих файлов -
macros.pre
→ код макроса для этого плагина -
functions.php
→ место, чтобы подключить макросы доступа к базовой системе плагинов -
src/AccessorsTrait.php
→ в основном не изменился
{ "name": "pre/class-accessors", "require": { "php": "^7.0", "pre/plugin": "dev-master" }, "autoload": { "files": [ "functions.php" ], "psr-4": { "Pre\\": "src" } }, "minimum-stability": "dev", "prefer-stable": true }
Это из
composer.json
namespace Pre; addMacroPath(__DIR__ . "/macros.pre");
Это из
functions.php
macro ·unsafe { ·ns()·class { ···body } } >> { ·class { use \Pre\AccessorsTrait; ···body } } macro ·unsafe { private T_VARIABLE·variable { get { ···getter } set { ···setter } }; } >> { // ... } macro ·unsafe { private T_VARIABLE·variable { set { ···setter } get { ···getter } }; } >> { // ... } macro ·unsafe { private T_VARIABLE·variable { set { ···setter } }; } >> { // ... } macro ·unsafe { private T_VARIABLE·variable { get { ···getter } }; } >> { // ... }
Это из
macros.pre
Этот файл макроса немного более подробный по сравнению с предыдущей версией. Вероятно, есть более элегантный способ обработки всех устройств, в которых могут быть определены методы доступа, но я пока не нашел его.
Собираем все вместе
Теперь, когда все так хорошо упаковано, использовать новую языковую функцию довольно просто. Взгляните на эту быструю демонстрацию!
Вы можете найти эти репозитории плагинов на Github:
Вывод
Как и во всем, этим можно злоупотреблять. Макросы не являются исключением. Этот код определенно не готов к работе, хотя концептуально это здорово.
Пожалуйста, не будьте тем человеком, который комментирует, насколько плохим, по вашему мнению, будет использование этого кода . На самом деле я не рекомендую вам использовать этот код в этой форме.
Сказав это, возможно, вы думаете, что это крутая идея. Можете ли вы вспомнить другие языковые возможности, которые вы хотели бы получить в PHP? Возможно, вы можете использовать репозиторий доступа к классам в качестве примера, чтобы начать работу. Может быть, вы хотите использовать хранилище плагинов для автоматизации вещей, чтобы вы могли увидеть, есть ли у вашей идеи какие-нибудь зубы.
Дайте нам знать, как это идет в комментариях.
С момента написания этого урока я отчаянно работал над базовыми библиотеками. Настолько, что теперь есть сайт, где размещен и продемонстрирован этот код: https://preprocess.io . Он все еще находится в альфа-состоянии, но демонстрирует весь код, о котором я говорил здесь, а затем немного. Есть также удобный REPL, на случай, если вы захотите попробовать любой из макросов.