突发奇想:用 PHPDoc 和 反射 来给 PHP 整一个类似 Python 的装饰器~~

装饰器简单解释:把一个函数传给装饰器,并返回一个函数,替换掉原函数。

举个 Python 的栗子:

import time


def time_recorder(fn):
    def wrapper():
        start = time.time()
        fn()
        print(time.time() - start)

    return wrapper


@time_recorder
def test():
    time.sleep(1)
    print('test')


# 上面的装饰器用法,相当于下面的语句
# test = time_recorder(test)

test()

输出:

test
1.00072383881

在 Laravel 中,我们就在控制器中尝试这个吧

首先,重写控制器的 callAction

// 获取注释中的各种参数以及对应的值
// 反射居然不能直接获取到各个参数,只能获取到整个注释的字符串
// 直接 copy 一个:https://tomlankhorst.nl/get-phpdoc-parameters-of-a-method/
public function phpdocParams(\ReflectionMethod $method): array
{
    // Retrieve the full PhpDoc comment block
    $doc = $method->getDocComment();

    if ($doc === false) {
        return [];
    }

    // Trim each line from space and star chars
    $lines = array_map(function ($line) {
        return trim($line, " *");
    }, explode("\n", $doc));

    // Retain lines that start with an @
    $lines = array_filter($lines, function ($line) {
        return strpos($line, "@") === 0;
    });

    $args = [];

    // Push each value in the corresponding @param array
    foreach ($lines as $line) {
        [$param, $value] = explode(' ', $line, 2);
        $args[$param][] = $value;
    }

    return $args;
}

public function callAction($method, $parameters)
{
    $methodReflection = new \ReflectionMethod(static::class, $method);
    $decorators = $this->phpdocParams($methodReflection)['@decorator'] ?? [];
    $decorators = array_reverse($decorators);

    $fn = [$this, $method];
    foreach ($decorators as $decorator) {
        $fn = call_user_func($decorator, $fn);
    }

    return call_user_func_array($fn, $parameters);
}

定义两个全局辅助函数随便试试:

function my_logger($fn)
{
    return function () use ($fn) {
        Log::info('start');
        $res = $fn(...func_get_args());
        Log::info('end');
        // 这里不搞个 return,会拿不到控制器的返回结果~~
        return $res;
    };
}

function time_recorder($fn)
{
    return function () use ($fn) {
        $start = microtime(true);
        $res = $fn(...func_get_args());
        Log::info('time spent: '.(microtime(true) - $start));
        return $res;
    };
}

在控制中使用装饰器:

/**
 * @param Request $request
 *
 * 这里用了两个装饰器
 * 包裹(装饰)顺序是从内(下)往外(上),执行顺序是从外(上)往内(下)
 *
 * @decorator my_logger
 * @decorator time_recorder
 *
 * @return mixed
 */
public function index(Request $request)
{
    return response()->json($request->input());
}

浏览器访问:xx.xx.xx/some-route?a=1&b=2

浏览器的结果:{"a":"1","b":"2"}

log 的结果:

[2020-04-07 08:50:47] local.INFO: start  
[2020-04-07 08:50:47] local.INFO: time spent: 0.013785123825073  
[2020-04-07 08:50:47] local.INFO: end  

其实有点像中间件~~

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 8

有点切面编程意思

4年前 评论
largezhou (楼主) 4年前
DotO 4年前

思路不错,不过这种只能在控制器中使用,其实也就和中间件没什么差别了。要真正像python那样的装饰器,还是需要官方支持才行。

4年前 评论

@Jiangqh

不一定控制器的,我只是为了快速测试,就在控制器尝试了。

要在其他地方用,必须得用第三方来调用,比如专门写个全局辅助方法叫 call_with_decorators

或者更好的是改写 app()->call 这个方法,还能用上 laravel 的服务容器,,

4年前 评论
Jiangqh 4年前
largezhou (作者) (楼主) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!