转载

如何用预处理让 PHP 更先进

先来点趣事。不久以前, 我尝试在 PHP 通过宏 来添加 Python 的 range 语法。然后, SaraMG 大虾 提到一个 RFC ,并且  LordKabelo 建议为 PHP 添加 C# 风格的 getter 和 setter。

我意识到对于一个局外人来说,建议和实现新的语言特性是件缓慢的事情,所以我打开了自己的编辑器……

这篇教程的代码可以在 Github 上找到。它在 PHP^7.1 版本测试,生成的代码可以运行在 PHP^5.6|^7.0。

如何用预处理让 PHP 更先进

宏是如何运行的?

从我上次谈及宏,已经有一段时间了(也许你从来没有听说过他们)。为了更新存储空间,他们会采用类似这样的代码:

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);'
);

如果你想了解这是如何运行的,可以查看我之前发布的 文章 。

秘诀是理解解析器的如何分割代码字符串,构建一个宏模式,然后将该模式递归地应用于新的语法之上的。

但是 宏库 没有很好的文档。我们很难知道模式究竟是什么样子的,或者最终生成什么样的有效语法。每个新的应用程序都要求编写一个类似这样的教程,其他人才能真正理解发生了什么。

创建基准代码

所以,让我们来看看手边的应用程序。我们模仿 C# 的语法向 PHP 添加 getter 和 setter 语法。在我们可以做到这一点之前,我们需要有一个好的基准代码,用于后续开发。 也许是某种形式的trait,我们可以将其添加到需要这个新功能的类中。

我们需要实现代码来检查类定义,并为每个特殊属性或注释动态创建 getter 和 setter 方法。

也许我们可以从定义一个特殊方法名称的格式开始,并且使用 __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
  }
}

好吧,让我们看看这两个宏是做什么的:

  1. 我们从匹配 class MyClass {...} 开始,并插入我们之前构建的 AccessorTrait。 这里提供了 _get 和 _set 的实现,其中将 _get_bar 链接到 print $class->bar 中。

  2. 我们匹配 accessor 块的语法,并将其替换为通用的属性定义,后面是几个独立的方法定义。 我们可以在这些函数中封装 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 如何实现的,你不需要了解很多关于 Yay 内部结构。将 TokenStream 转换为 string(在此上下文中)意味着你正在获取当前token所标记的字符串的值 - 在本例中为 ··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 autoloading):

require __DIR__ . "/vendor/autoload.php";

$sprocket = new App/Sprocket();
$sprocket->type = "acme sprocket";

print $sprocket->type; // Acme Sprocket

自动转换

手动过程就是这样子。在每次更改 src/Sprocket.pre 时谁会想去运行这个 bash 命令呢? 幸运的是,我们可以将其自动化!

第一步是定义自定义的自动加载器:

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 的 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 自动加载器之前,这意味着我们可以在 Composer 尝试加载 file.php 之前转换 file.pre!

创建一个插件框架

这种自动化实现很棒,但如果在每个项目中都重新操作是非常浪费的。 如果我们可以仅添加一个 composer require 依赖(为获得一个新的语言功能)就可以正常工作,这怎么样呢?让我们试试看......

首先,我们需要创建一个新的 repo,包含以下文件:

  • composer.json→ 自动加载下列文件

  • functions.php→ 创建宏路径函数(在其他库中可以动态添加自己的宏文件)

  • expanders.php→ 创建扩展器函数,比如 ··unvar

  • autoload.php→ augment Composer 的自动加载器,将每个其他库的宏文件加载到每个编译的 .prefile 中

{
  "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→ 将 accessor 宏挂接到基本插件系统中

  • 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

这个宏文件比以前的版本更冗长。可能有一个更优雅的方式来处理所有的关于 accessors 重定义的排列,但我目前还没有找到。

整合在一起

现在,一切都很好地打包了,你可以直接使用语言功能。 看看这个快速演示!

你可以在 Github 上找到这些插件库:

  • 基准插件

  • 类访问器插件

结语

和所有的东西一样,这可能被滥用。 宏也不例外。虽然它在概念上很酷, 但这个代码绝对不是产品级代码。

原文  https://www.oschina.net/translate/how-to-make-modern-php-more-modern-with-preprocessing
正文到此结束
Loading...