一点 AST 研究
前言
Laravel 中的表单验证对于我们开发是十分便利的,使用的方式有很多,最常用的就是定义 Request 类:
class DemoRequest extends FormRequest {
public function rules(): array
{
return [
'id' => ['required','integer'],
'name' => ['required']
];
}
}
class DemoController extends Controller {
public function index(DemoRequest $request){
$id = $request->input('id');
$name = $request->input('name');
}
}
前段时间参与过一些 Java 开发,对代码有了更多的思考,比如在当前的 DemoController
中,DemoRequest
中有哪些元素,是不是就像盲盒一样,想知道 $request->input()
能获取哪些合法参数,还得回过头去看看 rules
方法,能不能在控制器中就能够知道 $request
可以提供哪些合法参数呢。
Attribute + 反射
定义一个实体类,rules
通过 Attribute
定义,最终使用反射完成表单验证,控制器直接拿到实体。
#[Attribute]
class Validate {
public function __construct(protected array $rules){}
public function getRules(){
return $this->rules;
}
}
abstract class AbstractReuest extends FormRequest {
public function rules(): array
{
$ref = new ReflectionClass($this);
foreach($ref->getProperties() as $property){
$attributes = $property->getAttributes(Validate::class);
if(empty($attributes[0])){
continue;
}
/** @var Validate $validate */
$validate = $attributes[0]->newInstance();
$rules[$property->getName()] = $validate->getRules();
}
return $rules ?? [];
}
/**
* Laravel 提供的,验证通过之后的钩子函数
*
* @return void
*/
protected function passedValidation()
{
foreach($this->validationData() as $key => $value){
$setMethod = 'set'.ucfirst($key);
if(method_exists($this,$setMethod)){
$this->{$setMethod}($value);
}
}
}
}
class DemoRequst extends AbstractRequest{
#[Validate(rules:['required'])]
private int $id;
#[Validate(rules:['required','integer'])]
private string $name;
// set 和 get 方法在 IDE 中可以使用快捷键生成
public function getId(): int
{
return $this->id;
}
public function setId(int $id): void
{
$this->id = $id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}
class DemoController extends Controller {
public function index(DemoRequest $request){
$id = $request->getId();
$name = $request->getName();
}
}
通过以上代码,就可以在控制器中清楚的知道当前的 request
可以获取哪些合法参数了,但是每次请求的时候都需要通过反射去获取,规则都是静态的,能不能通过缓存文件提前生成好 rules
方法呢。
AST
需求分析
讲 AST 之前,先回顾一下一个 class 文件是如何被加载的:通过 composer 生成 classmap 文件,classmap 中定义了命名空间与文件路径的关联关系,我们这里的思路就是,替换 DemoRequst
的文件路径为我们生成好的缓存文件。
之前 DemoRequest
在 classmap 中可能是这样:
"\\App\\Http\\Requests\\DemoRequest" => $baseDir . './app/Http/Requests/DemoRequest.php'
我们需要替换成:
"\\App\\Http\\Requests\\DemoRequest" => $baseDir . './cache/Http_Requests_DemoRequest.cache.php'
app/Http/Requests/DemoRequest.php
中只需要定义 property 和规则的 Attribute:
class DemoRequst extends AbstractRequest {
#[Validate(rules:['required'])]
private int $id;
#[Validate(rules:['required','integer'])]
private string $name;
}
而 cache/Http_Requests_DemoRequest.cache.php
文件中,则会生成一个 rules 方法以及 get 和 set 方法:
class DemoRequst extends AbstractRequest {
public function rules(): array
{
return [
'id' => ['required','integer'],
'name' => ['required']
];
}
// property 和 get set 方法省略
}
AST 相关处理类介绍
ParserFactory
将源码解析成 AST 对象
Standard
将 AST 对象转化为代码
NodeTraverser
遍历 AST 节点
NodeVisitorAbstract
节点修改器
通过 AST 生成 cache 类
class MakeRequestCache extends Command
{
protected $signature = 'app:make-request-cache';
public function handle()
{
// 获取所有自动加载的 classmap,key 为 class ,value 为 class 的文件路径
// 可查看 vendor/composer/autoload_classmap.php
$classMap = $this->getClassLoader()->getClassMap();
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
$printer = new Standard();
foreach ($classMap as $class => $file) {
if (!str_starts_with($class, 'App\\Http\\Requests\\')) {
continue;
}
// 创建 rules 方法的 AST 对象
$rulesMethod = new ClassMethod('rules', ['flags' => 1, 'returnType' => 'array']);
// 定义 rules 方法的 return 值
$rulesMethod->stmts[] = new Return_($this->buildArrayNode($this->getRules($class)));
// 将源码转化为 AST 对象
$ast = $parser->parse(file_get_contents($file));
// 定义修改器
// 这里是在进入 Class_ 节点时,给 Class_ 增加 rules 方法节点
$visitor = new class([$rulesMethod]) extends NodeVisitorAbstract {
public function __construct(protected array $methods)
{
}
public function enterNode(Node $node): void
{
if ($node instanceof Class_) {
$node->stmts = array_merge($node->stmts, $this->methods);
}
}
};
// 节点遍历器
$traverser = new NodeTraverser();
// 添加修改器
$traverser->addVisitor($visitor);
// 遍历源码的 AST,完成修改
$proxy = $traverser->traverse($ast);
// 将修改后的 AST 转化为代码
$proxyCode = $printer->prettyPrintFile($proxy);
// 将代码写入缓存文件中
$classFile = str_replace('\\', '_', $class . '.cache.php');
$cachePath = storage_path($classFile);
file_put_contents($cachePath, $proxyCode);
$this->output->info("生成缓存文件:{$cachePath}");
// 将 classmap 中的关联更新为缓存文件
$classMap[$class] = $cachePath;
}
// 重新刷新 classmap,具体刷新方式也是使用 AST 对 classmap 文件进行修改
ClassCache::reloadClassMap($classMap);
}
private function getRules(string $class): array
{
$ref = new \ReflectionClass($class);
foreach ($ref->getProperties() as $property) {
$attributes = $property->getAttributes(Validate::class);
if (empty($attributes[0])) {
continue;
}
/** @var Validate $validate */
$validate = $attributes[0]->newInstance();
$rules[$property->getName()] = $validate->getRules();
}
return $rules ?? [];
}
private function getClassLoader(): ClassLoader
{
$loaders = spl_autoload_functions();
foreach ($loaders as $loader) {
if (is_array($loader) && $loader[0] instanceof ClassLoader) {
return $loader[0];
}
}
throw new \Exception('Composer loader not found.');
}
public function buildArrayNode(array $array): Array_
{
foreach ($array as $key => $value) {
$arrayItems[] = new ArrayItem($this->cast($value), $this->cast($key));
}
return new Array_($arrayItems ?? []);
}
private function cast(mixed $value): ?Expr
{
return match (gettype($value)) {
'boolean' => new LNumber(intval($value)),
'integer' => new LNumber($value),
'double' => new String_(strval($value)),
'string' => new String_($value),
'array' => $this->buildArrayNode($value),
default => null
};
}
}
通过以上命令,就把 classmap 中的关联重新定向到了缓存文件,而缓存文件中就有 rules
方法,后续请求就不用再通过反射来动态创建 rules
方法了。
具体代码实现可查看:github.com/minororange/ast-demo
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: