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