预处理php_如何使现代PHP更现代? 随着预处理!

预处理php

Let’s have a bit of fun. A while ago, I experimented with PHP macros, adding Python range syntax. Then, the talented SaraMG mentioned an RFC, and LordKabelo suggested instead adding C#-style getters and setters to PHP.

让我们玩得开心。 前一阵子, 我尝试了PHP宏 ,添加了Python范围语法。 然后,才华横溢的SaraMG 提到了RFC ,而LordKabelo建议改为在PHP中添加C#风格的getter和setter。

Aware of how painfully slow it can be for an outsider to suggest and implement a new language feature, I took to my editor…

意识到局外人建议并实施一项新的语言功能可能要花多长时间后,我带去了我的编辑…

The code for this tutorial can be found on Github. It’s been tested with PHP ^7.1, and the generated code should run on PHP ^5.6|^7.0.

可以在Github上找到本教程的代码。 它已经在PHP ^7.1上进行了测试,并且生成的代码应该在PHP ^5.6|^7.0

Vector illustration of cog with upward facing arrows running through it, indicating preprocessing, upgrading, improving

宏又如何工作? (How Do Macros Work Again?)

It’s been a while (and perhaps you’ve never heard of them) since I’ve talked about macros. To refresh your memory, they take code that looks like this:

自从我谈论宏以来已经有一段时间了(也许您从未听说过它们)。 为了刷新您的记忆,他们采用了如下代码:

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

…and turn custom PHP syntax, like this:

…然后启用自定义PHP语法,如下所示:

$few = many[1..3];

…into valid PHP syntax, like this:

…转换为有效PHP语法,如下所示:

$few = eval(
    '$list = ' . '$many' . ';'.
    '$lower = ' . explode('..', '1..3')[0] . ';' .
    '$upper = ' . explode('..', '1..3')[1] . ';' .
    'return array_slice($list, $lower, $upper - $lower);'
);

If you’d like to see how this works, head over to the the post I wrote about it.

如果您想了解它的工作原理,请转到我写的有关它的文章

The trick is to understand how a parser tokenizes a string of code, build a macro pattern, and then apply that pattern recursively to the new syntax.

诀窍是了解解析器如何对代码字符串进行标记,构建宏模式,然后将其递归地应用于新语法。

The macro library isn’t well documented, though. It’s difficult to know exactly what the pattern needs to look like, or what valid syntax to generate in the end. Every new application begs for a tutorial like this to be written, before others can understand what’s really going on.

不过,该宏库的文档并不完善。 很难确切知道模式需要什么样,或者到底要生成什么有效语法。 每个新的应用程序都要求编写这样的教程,然后其他人才能理解实际情况。

建立基地 (Building A Base)

So, let’s look at the application at hand. We’d like to add getter and setter syntax, resembling that of C#, to PHP. Before we can do that, we need to have a good base of code to work from. Perhaps something in the form of a trait that we can add to classes needing this new functionality.

因此,让我们看一下手边的应用程序。 我们想向PHP添加类似于C#的getter和setter语法。 在我们做到这一点之前,我们需要有一个良好的代码基础。 也许可以将特征形式的某些内容添加到需要此新功能的类中。

We need to implement code that will inspect a class definition and create these dynamic getter and setter methods for each special property or comment it sees.

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

Perhaps we can start by defining a special method name format, and magic __get and __set methods:

也许我们可以先定义一种特殊的方法名称格式,然后使用神奇的__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);
    }
  }
}

Each method starting with the name __get_ and __set_ needs to be connected to an as-yet undefined property. We can imagine this syntax:

每个以__get___set_需要连接到一个尚未定义的属性。 我们可以想象这样的语法:

namespace App;

class Sprocket
{
    private $type {
        get {
            return $this->type;
        }

        set {
            $this->type = strtoupper($value);
        }
    };
}

…being converted to something very much like:

…被转换成非常像的东西:

namespace App;

class Sprocket {
    use AccessorTrait;

    private $type;

    private function __get_type() {
        return $this->type;  
    }

    private function __set_type($value) {
        $this->type = strtoupper($value);   
    }
}

定义宏 (Defining Macros)

Defining the required macros is the hardest part of any of this. Given the lack of documentation (and widespread use), and with only a handful of helpful exception messages, it’s mostly a lot of trial and error.

定义所需的宏是其中最难的部分。 由于缺乏文档(并且广泛使用),并且只有少量有用的异常消息,因此,这大部分都是反复试验。

I spent a few hours coming up with the following patterns:

我花了几个小时提出以下模式:

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
  }
}

Ok, let’s look at what these two macros are doing:

好的,让我们看一下这两个宏在做什么:

  1. We begin by matching class MyClass { ... }, and inserting the AccessorTrait we built previously. This provides the __get and __set implementations, which links __get_bar to print $class->bar etc.

    我们首先匹配class MyClass { ... } ,然后插入之前构建的AccessorTrait 。 这提供了__get__set实现,它们链接__get_barprint $class->bar等。

  2. We match the accessor block syntax, and replace it with an ordinary property definition, followed by a couple of individual method definitions. We can wrap the exact contents of the get { ... } and set { ... } blocks within these functions.

    我们匹配访问器块语法,并将其替换为普通的属性定义,然后是几个单独的方法定义。 我们可以包装get { ... }的确切内容,并在这些函数中set { ... }块。

At first, when you run this code, you’ll get an error. That’s because the ··unvar function isn’t a standard part of the macro processor. It’s something I had to add, to convert from $type to type:

首先,运行此代码时,您会得到一个错误。 这是因为··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
      )
    )
  ;
}

I was able to copy (almost exactly) the ··stringify expander, which is included in the macro parser. You don’t need to understand much about the internals of Yay in order to see what this is doing. Casting a TokenStream to a string (in this context) means you’re getting the string value of whatever token is currently referenced – in this case it’s ··unvar(T_VARIABLE·var) – and perform string manipulations on it.

我能够(几乎完全)复制宏解析器中包含的··stringify扩展器。 您无需对Yay的内部知识有太多了解即可了解它在做什么。 将TokenStream转换为字符串(在这种情况下)意味着您将获取当前引用的任何令牌的字符串值-在这种情况下为··unvar(T_VARIABLE·var) –并对它执行字符串操作。

(string) $ts becomes "$type", as opposed to "T_VARIABLE·var".

(string) $ts变成"$type" ,而不是"T_VARIABLE·var"

Usually, these macros are applied when they are placed inside the script they are meant to apply to. In other words, we could create a script resembling:

通常,将这些宏放在要应用的脚本中时会应用它们。 换句话说,我们可以创建类似于以下内容的脚本:

<?php

macro ·unsafe {
  ...
} >> {
  ...
}

macro ·unsafe {
  ...
} >> {
  ...
}

namespace App;

trait AccessorTrait
{
  ...
}

class Sprocket
{
  private $type {
    get {
      return $this->type;
    }

    set {
      $this->type = strtoupper($value);
    }
  };
}

… then we could run it using a command like:

…然后我们可以使用以下命令运行它:

vendor/bin/yay src/Sprocket.pre >> src/Sprocket.php

Finally, we could use this code (with some Composer PSR-4 autoloading), using:

最后,我们可以使用以下代码(带有一些Composer PSR-4自动加载功能):

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

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

print $sprocket->type; // Acme Sprocket

自动转换 (Automating Conversion)

As a manual process, this sucks. Who wants to run that bash command every time they change src/Sprocket.pre? Fortunately, we can automate this!

作为手动过程,这很糟糕。 谁想要在每次更改src/Sprocket.pre时运行该bash命令? 幸运的是,我们可以使它自动化!

The first step is to define a custom autoloader:

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

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

You can save this file as autoload.php, and use files autoloading to include it through Composer’s autoloader, as explained in the documentation.

您可以将该文件另存为autoload.php ,并使用files自动加载功能通过Composer的自动加载器将其包括在内,如文档所述

The first part of this definition comes straight out of the example implementation of the PSR-4 specification. We fetch Composer’s PSR-4 definitions file, and for each prefix, we check whether it matches the class currently being loaded.

该定义的第一部分直接来自PSR-4规范示例实现 。 我们获取Composer的PSR-4定义文件,并检查每个前缀是否与当前正在加载的类匹配。

If it matches, we check each potential path, until we find a file.pre, in which our custom syntax is defined. Then we get the contents of a macros.pre file (in the project base directory), and create an interim file – using macros.pre contents + the matched file’s contents. That means the macros are available to the file we pass to Yay. Once Yay has compiled file.pre.interimfile.php, we delete file.pre.interim.

如果匹配,则检查每个可能的路径,直到找到file.pre ,在其中定义了自定义语法。 然后,我们获得macros.pre文件的内容(在项目基本目录中),并使用macros.pre contents +匹配文件的内容创建一个临时文件。 这意味着宏可用于我们传递给Yay的文件。 Yay编译file.pre.interimfile.php ,我们将删除file.pre.interim

The code for that process is:

该过程的代码是:

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;

Notice those two booleans at the end of the call to spl_autoload_register. The first is whether or not this autoloader should throw exceptions for loading errors. The second is whether this autoloader should be prepended to the stack. This puts it before Composer’s autoloaders, which means we can convert file.pre before Composer tries to load file.php!

请注意,在对spl_autoload_register的调用结束时,这两个布尔值。 首先是此自动加载器是否应引发加载错误的异常。 第二个问题是该自动装载器是否应该放在堆栈之前。 这会将其放在Composer的自动加载器之前,这意味着我们可以在Composer尝试加载file.php之前转换file.pre

创建一个插件框架 (Creating A Plugin Framework)

This automation is great, but it’s wasted if one has to repeat it for every project. What if we could just composer require a dependency (for a new language feature), and it would just work? Let’s do that…

这种自动化很棒,但是如果每个项目都要重复一次,那是浪费的。 如果我们只是composer require一个依赖项(对于一种新的语言功能),那该怎么办呢? 来做吧...

First up, we need to create a new repo, containing the following files:

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

  • composer.json → autoload the following files

    composer.json →自动加载以下文件

  • functions.php → create macro path functions (to other libraries can add their own macro files dynamically)

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

  • expanders.php → create expander functions, like ··unvar

    expanders.php →创建扩展器功能,例如··unvar

  • autoload.php → augment Composer’s autoloader, loading each other library’s macro files into each compiled .pre file

    autoload.php →增强Composer的自动加载器,将彼此库的宏文件加载到每个已编译的.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
}

This is from composer.json

这是来自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];
}

This is from functions.php

这是来自functions.php

You may be cringing at the thought of using $GLOBALS as a store for the macro file paths. It’s unimportant, as we could store these paths in any number of other ways. This is just the simplest approach to demonstrate the pattern.

您可能不禁想到将$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
      )
    )
  ;
}

This is from expanders.php

这是来自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);

This is from autoload.php

这是来自autoload.php

Now, additional macro plugins can use these functions to hook their own code into the system…

现在,其他宏插件可以使用这些功能将自己的代码挂接到系统中……

创建新的语言功能 (Creating A New Language Feature)

With the plugin code built, we can refactor our class accessors to be a stand-alone, automatically applied feature. We need to create a few more files to make this happen:

通过构建插件代码,我们可以将类访问器重构为独立的,自动应用的功能。 我们需要创建更多文件来实现此目的:

  • composer.json → needs to require the base plugin repository and autoload the following files

    composer.json →需要基本的插件存储库并自动加载以下文件

  • macros.pre → macro code for this plugin

    macros.pre →此插件的宏代码

  • functions.php → place to hook the accessor macros into the base plugin system

    functions.php →将访问器宏挂接到基本插件系统中的位置

  • src/AccessorsTrait.php → largely unchanged from before

    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
}

This is from composer.json

这是来自composer.json

namespace Pre;

addMacroPath(__DIR__ . "/macros.pre");

This is from functions.php

这是来自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
    }
  };
} >> {
  // ...
}

This is from macros.pre

这是来自macros.pre

This macro file is a little more verbose compared to the previous version. There’s probably a more elegant way of handling all the arrangements the accessors could be defined in, but I haven’t found it yet.

与以前的版本相比,该宏文件有些冗长。 处理访问程序可以在其中定义的所有安排的方法可能是更优雅的方式,但是我还没有找到。

放在一起 (Putting it all together)

Now that everything is so nicely packaged, it’s rather straightforward to use the new language feature. Take a look at this quick demonstration!

现在,所有内容都打包得很好,使用新的语言功能非常简单。 看一下这个快速演示!

Demonstration

You can find these plugin repositories on Github:

您可以在Github上找到以下插件存储库:

结论 (Conclusion)

As with all things, this can be abused. Macros are no exception. This code is definitely not production-ready, though it is conceptually cool.

与所有事物一样,这可能会被滥用。 宏也不例外。 尽管从概念上讲,这段代码绝对不是生产就绪的。

Please don’t be that person who comments about how bad you think the use of this code would be. I’m not actually recommending you use this code, in this form.

请不要成为评论您认为使用该代码有多糟糕的人 。 我实际上不建议您以这种形式使用此代码。

Having said that, perhaps you think it’s a cool idea. Can you think of other language features you’d like PHP to get? Maybe you can use the class accessors repository as an example to get you started. Maybe you want to use the plugin repository to automate things, to the point where you can see if your idea has any teeth.

话虽如此,也许您认为这是一个好主意。 您能想到您想要PHP获得的其他语言功能吗? 也许您可以使用类访问器存储库作为示例来开始。 也许您想使用插件存储库来自动化事情,以至于您可以查看您的想法是否有任何想法。

Let us know how it goes in the comments.

让我们知道评论中的情况。

Since writing this tutorial, I’ve been frantically working on the underlying libraries. So much that there’s now a site where this code is hosted and demonstrated: https://preprocess.io. It’s still in an alpha state, but it showcases all the code I’ve spoken about here and then some. There’s also a handy REPL, in case you’d like to try any of the macros.

自编写本教程以来,我一直在疯狂地研究基础库。 如此之多,现在有一个托管和演示此代码的站点: https : //preprocess.io 。 它仍然处于Alpha状态,但是它展示了我在这里谈论过的所有代码,然后再展示一些。 如果您想尝试任何宏,也可以使用REPL。

翻译自: https://www.sitepoint.com/how-to-make-modern-php-more-modern-with-preprocessing/

预处理php

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值