我不生产代码
我只是代码的搬运工

PHP8 注解的使用

PHP 8 引入了 属性(Attributes)作为新的元数据机制,用于替代传统的 PHPDoc 注解,使得代码更具类型安全性和结构化。

语法

PHP 8 的属性(Attributes)使用 #[...] 语法表示,并可以用于类、方法、属性、参数、常量等。其实语法跟实例化类非常相似,只是少了个 new 关键词而已:

#[Route]
#[Route()]
#[Route("/path", ["get"])]
#[Route(path: "/path", methods: ["get"])]

要注意的是,注解名不能是变量,只能是常量或常量表达式

// 实例化类
$route = new Route(path: “/path”, methods: [“get”]);
//(path: “/path”, methods: [“get”]) 是 php8 的新语法,在传参的时候可以指定参数名,不按照形参的顺序传参。

注解类作用范围

在定义注解类时,你可以使用内置注解类 #[Attribute] 定义注解类的作用范围,也可以省略,由 PHP 动态地根据使用场景自动定义范围。

注解作用范围列表:

Attribute::TARGET_CLASS
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_ALL
Attribute::IS_REPEATABLE

在使用时,#[Attribute] 等同于 #[Attribute(Attribute::TARGET_ALL)],为了方便,一般使用前者。

1~7 都很好理解,分别对应类、函数、类方法、类属性、类常量、参数、所有,前 6 项可以使用 | 或运算符随意组合,比如 Attribute::TARGET_CLASS | Attribute::TARGET_FUNCTION。(Attribute::TARGET_ALL 包含前 6 项,但并不包含 Attribute::IS_REPEATABLE)。

Attribute::IS_REPEATABLE 设置该注解是否可以重复,比如:

class IndexController
{
    #[Route('/index')]
    #[Route('/index_alias')]
    public function index()
    {
        echo "hello!world" . PHP_EOL;
    }
}

如果没有设置 Attribute::IS_REPEATABLE,Route 不允许使用两次。

示例

下面通过模拟路由请求、参数验证,演示如何使用注解

首先定义两个注解类:

/**
 * 路由注解,用于解析路由请求
 */
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Route
{
    public function __construct(private readonly string $url = '/', private readonly string $method = 'GET')
    {
    }

    public function getUrl(): string
    {
        return $this->url;
    }

    public function getMethod(): string
    {
        return $this->method;
    }
}


/**
 * 参数验证注解
 */
#[Attribute(Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)]
class Params
{
    public function __construct(
        private readonly string $name,
        private readonly bool $required = false,
        private readonly string $regex = '',
        private $callback = null
    )
    {
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getRequired(): bool
    {
        return $this->required;
    }

    public function getRegex(): string
    {
        return $this->regex;
    }

    public function getCallback()
    {
        return $this->callback;
    }

    /**
     * @param $value
     * @return void
     * @throws Exception
     */
    public function valid($value): void
    {
        if ($this->required && empty($value)) {
            throw new ParamsException("参数 {$this->name} 不能为空");
        }

        if (!empty($this->regex) && !preg_match($this->regex, $value)) {
            throw new ParamsException("参数 {$this->name} 不符合正则 {$this->regex}");
        }

        if (!empty($this->callback) && !call_user_func($this->callback, $value)) {
            throw new ParamsException("参数 {$this->name} 不符合回调函数 {$this->callback}");
        }
    }
}

定义异常类,用于在参数校验失败时抛出的异常:

/**
 * 参数校验异常类
 */
class ParamsException extends Exception
{
}

在当前目录下,创建 controller 目录,用于存在模拟处理 http 请求的控制器,其中上面创建的注解就作用在该类上:

#[Route('/home', 'POST')]
class HomeController
{
    public function url1()
    {
    }

    #[Route('/url2', 'GET')]
    #[Params(name: 'params2', required: true, regex: '/^\d{3,10}$/')]
    #[Params(name: 'params4', regex: '/^[a-z]+$/i', callback: 'validParams')]
    public function url2(array $params1, string $params2, bool $params3 = true, string $params4 = 'abc')
    {
        print_r(func_get_args());
        echo __METHOD__.PHP_EOL;
        return "success";
    }

    public function url3()
    {
    }
}

/**
 * 参数校验方法
 * @param $data
 * @return false
 */
function validParams($data): bool
{
    return true;
}

@Route 注解用于定期路由请求到的方法及请求方式,@Params 注解用于指定方法参数的验证规则

下面为核心处理逻辑,通过反射获取类的对象实例及方法对象示例,然后判断对象实例上是否有添加 @Route、@Params注解,如果有则执行相应的代码逻辑:

class App
{
    private array $routeMap = [];

    private array $controllerList = [];

    public function __construct(
        private readonly string $controllerDir = './controller',
        private readonly string $requestUrl = '/',
        private readonly string $requestMethod = 'GET'
    )
    {
    }

    /**
     * @throws ReflectionException
     */
    public function run(): void
    {
        $this->getControllerList($this->controllerDir);

        foreach ($this->controllerList as $controller) {
            if (!class_exists($controller)) {
                continue;
            }

            $reflectionClass = new ReflectionClass($controller);
            $classAttributes = $reflectionClass->getAttributes(Route::class);

            $requestUrl = $this->requestUrl;
            $requestMethod = $this->requestMethod;

            //获取路由信息
            $routeParams = $this->getRouteParams($classAttributes);
            if (empty($routeParams)) {
                continue;
            }

            $requestUrl = $routeParams['url'] ?: $requestUrl;
            $requestMethod = $routeParams['method'] ?: $requestMethod;

            //获取方法
            $reflectionMethods = $reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC);

            foreach ($reflectionMethods as $reflectionMethod) {
                $methodAttributes = $reflectionMethod->getAttributes();
                $routeParams = $this->getRouteParams($methodAttributes);
                if (!empty($routeParams)) {
                    //方法路由信息
                    $requestUrl = rtrim($requestUrl, '/') . '/' . ltrim($routeParams['url'], '/');
                    $requestMethod = $routeParams['method'] ?? $requestMethod;

                    $methodParams = [];
                    foreach ($reflectionMethod->getParameters() as $reflectionParameter) {
                        $name = $reflectionParameter->getName();
                        $type = $reflectionParameter->getType()->getName();
                        try {
                            $methodParams[] = [
                                'name' => $name,
                                'type' => $type,
                                'default' => $reflectionParameter->getDefaultValue(),
                            ];
                        } catch (ReflectionException $e) {
                            //如果方法没有默认值,会报异常
                            $methodParams[] = [
                                'name' => $name,
                                'type' => $type,
                            ];
                        }
                    }

                    $this->routeMap[strtoupper($requestMethod)][$requestUrl] = [
                        'class' => $reflectionClass->newInstance(),
                        'method' => $reflectionMethod,
                        'params' => $methodParams,
                        'params_valid' => $this->getMethodParamsValid($methodAttributes),//方法参数校验规则
                    ];
                }
            }
        }
    }

    /**
     * 处理请求
     * @param string $url
     * @param array  $requestParams
     * @param string $requestMethod
     * @return mixed
     */
    public function handle(string $url, array $requestParams = [], string $requestMethod = 'GET'): mixed
    {
        $requestMethod = strtoupper($requestMethod);

        //判断路由是否存在
        if (!isset($this->routeMap[$requestMethod][$url])) {
            die("404\n");
        }

        //路由信息
        $route = $this->routeMap[$requestMethod][$url];

        //参数验证规则
        $paramsValid = $route['params_valid'] ?? [];

        $params = [];
        foreach ($route['params'] as $param) {
            if (!isset($requestParams[$param['name']]) && !isset($param['default'])) {
                die("参数 {$param['name']} 不能为空\n");
            }

            $params[$param['name']] = $requestParams[$param['name']] ?? $param['default'];

            //参数验证
            if (isset($paramsValid[$param['name']])) {
                foreach ($paramsValid[$param['name']] as $valid) {
                    $valid->valid($params[$param['name']]);
                }
            }
        }

        //执行方法
        return $route['method']->invoke($route['class'], ...$params);
    }

    /**
     * 获取路由url、请求方法
     * @param array $attributes
     * @return array|bool
     */
    private function getRouteParams(array $attributes): array|bool
    {
        foreach ($attributes as $attribute) {
            if ($attribute->getName() == Route::class && !empty($attribute->getArguments())) {
                $newInstance = $attribute->newInstance();
                return ['url' => $newInstance->getUrl(), 'method' => $newInstance->getMethod()];
            }
        }

        return false;
    }

    /**
     * 获取方法参数验证规则
     * @param array $attributes
     * @return array
     */
    function getMethodParamsValid(array $attributes): array
    {
        $data = [];
        foreach ($attributes as $attribute) {
            if ($attribute->getName() == Params::class && !empty($attribute->getArguments())) {
                $newInstance = $attribute->newInstance();
                $data[$newInstance->getName()][] = $newInstance;
            }
        }

        return $data;
    }

    /**
     * 获取控制器列表
     * @param string $controllerDir
     * @return void
     */
    private function getControllerList(string $controllerDir): void
    {
        if (!is_dir($controllerDir)) {
            die("目录 {$controllerDir} 不存在\n");
        }

        $dir = opendir($controllerDir);
        if (!$dir) {
            echo "目录 {$controllerDir} 无法打开\n";
            exit;
        }

        // 遍历目录中的文件
        while (($file = readdir($dir)) !== false) {
            if (in_array($file, ['.', '..'])) {
                continue;
            }

            $filePath = $controllerDir . '/' . $file;

            // 检查文件是否以 Controller.php 结尾
            if (str_ends_with($file, 'Controller.php')) {
                include_once $filePath;
                $this->controllerList[] = trim($file, '.php');
            } elseif (is_dir($filePath)) {
                $this->getControllerList($filePath);
            }
        }

        // 关闭目录
        closedir($dir);
    }

    /**
     * 获取路由映射表
     * @return array
     */
    public function getRouteMap(): array
    {
        return $this->routeMap;
    }
}

使用方式:

$app = new App();
$app->run();

$routeParams = [
    'params1' => [1,2,2,3,4,5],
    'params2' => '123',
];

$url = '/home/url2';

$response = $app->handle($url, $routeParams);
print_r($response);

/*
输出内容:
Array
(
    [0] => Array
        (
            [0] => 1
            [1] => 2
            [2] => 2
            [3] => 3
            [4] => 4
            [5] => 5
        )

    [1] => 123
    [2] => 1
    [3] => abc
)
HomeController::url2
success
*/

当路由不存在时,结果如下:

$url = '/home';

$response = $app->handle($url, $routeParams);
print_r($response);
/*
输出结果:
404
*/

以上为PHP8新增的注释在实际应用中的简单使用方法

本文章为本站原创,如转载请注明文章出处:https://www.sviping.com/archives/48

分享到:
上一篇: springboot 用FastJson2 替代默认的 jackjson 下一篇: 没有了
12