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新增的注释在实际应用中的简单使用方法