好的,我们开始今天的讲座,主题是:PHP 8 Attributes(注解)在框架中的应用:实现自定义路由与依赖注入的元数据驱动。
一、PHP 8 Attributes 简介:元数据的力量
在PHP 8之前,我们通常使用DocBlock注释来为类、方法、属性等添加元数据。虽然DocBlock可以提供很多信息,但它本质上是字符串,需要通过反射和字符串解析才能提取信息,效率较低且容易出错。PHP 8引入了Attributes,也称为注解,它提供了一种更结构化、类型安全的方式来添加元数据。
Attributes的优势:
- 类型安全: Attributes是类,可以定义属性类型,避免了字符串解析带来的错误。
- 结构化: Attributes是类实例,可以包含多个属性,更容易组织复杂的信息。
- 易于访问: PHP的反射API提供了方便的方法来访问Attributes。
- 性能: Attributes是编译时信息,访问速度比解析DocBlock更快。
二、自定义路由:Attribute驱动的路由配置
传统的路由配置通常是通过配置文件(如YAML、XML、PHP数组)或者手动编写代码来完成的。这种方式存在一些问题:
- 配置分散: 路由配置和控制器代码分离,维护成本较高。
- 代码冗余: 路由规则可能需要在多个地方定义和维护。
- 难以扩展: 添加新的路由规则可能需要修改多个文件。
使用Attributes,我们可以将路由信息直接添加到控制器的方法上,实现更清晰、更简洁的路由配置。
1. 定义路由Attribute:
首先,我们需要定义一个Attribute类,用于存储路由信息。
<?php
namespace AppAttributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET'
) {
}
}
#[Attribute(Attribute::TARGET_METHOD)]:这是关键,它告诉PHP,RouteAttribute只能用于方法上。Attribute::TARGET_METHOD是一个内置常量。public string $path:定义了路由的路径。public string $method = 'GET':定义了HTTP方法,默认为GET。
2. 在控制器中使用路由Attribute:
现在,我们可以在控制器的方法上使用Route Attribute来定义路由。
<?php
namespace AppControllers;
use AppAttributesRoute;
class UserController
{
#[Route(path: '/users', method: 'GET')]
public function index()
{
// 获取所有用户
return 'List of Users';
}
#[Route(path: '/users/{id}', method: 'GET')]
public function show(int $id)
{
// 获取指定用户
return 'User ID: ' . $id;
}
#[Route(path: '/users', method: 'POST')]
public function create()
{
// 创建新用户
return 'Creating a new user';
}
#[Route(path: '/users/{id}', method: 'PUT')]
public function update(int $id)
{
// 更新指定用户
return 'Updating user ID: ' . $id;
}
#[Route(path: '/users/{id}', method: 'DELETE')]
public function delete(int $id)
{
// 删除指定用户
return 'Deleting user ID: ' . $id;
}
}
#[Route(path: '/users', method: 'GET')]:将/users路径映射到index方法,并且只允许 GET 请求。#[Route(path: '/users/{id}', method: 'GET')]:将/users/{id}路径映射到show方法,{id}是一个参数。
3. 路由解析器:扫描和注册路由
我们需要一个路由解析器来扫描控制器,提取Attributes信息,并将路由注册到路由表中。
<?php
namespace AppRouting;
use AppAttributesRoute;
use ReflectionClass;
use ReflectionMethod;
class RouteResolver
{
private array $routes = [];
public function __construct(private string $controllerNamespace) {}
public function resolveRoutes(array $controllers): array
{
foreach ($controllers as $controllerClass) {
$className = $this->controllerNamespace . '\' . $controllerClass;
if (!class_exists($className)) {
throw new Exception("Controller class {$className} not found.");
}
$reflectionClass = new ReflectionClass($className);
foreach ($reflectionClass->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$routeInstance = $attribute->newInstance();
$this->routes[] = [
'path' => $routeInstance->path,
'method' => $routeInstance->method,
'controller' => $className,
'action' => $method->getName(),
];
}
}
}
return $this->routes;
}
public function getRoutes(): array
{
return $this->routes;
}
}
resolveRoutes(array $controllers): 接收一个控制器类名数组。new ReflectionClass($className):使用反射API获取控制器类的反射对象。$reflectionClass->getMethods():获取控制器类的所有方法。$method->getAttributes(Route::class):获取方法上的所有RouteAttribute实例。$attribute->newInstance():创建RouteAttribute的实例,方便访问属性。- 将解析后的路由信息存储在
$this->routes数组中。
4. 路由分发器:匹配和执行路由
路由分发器负责接收请求,匹配路由,并执行相应的控制器方法。
<?php
namespace AppRouting;
class RouteDispatcher
{
private array $routes;
public function __construct(array $routes)
{
$this->routes = $routes;
}
public function dispatch(string $uri, string $method): string
{
foreach ($this->routes as $route) {
if ($this->matchRoute($uri, $method, $route['path'], $route['method'])) {
return $this->executeRoute($route, $uri);
}
}
return '404 Not Found';
}
private function matchRoute(string $uri, string $method, string $routePath, string $routeMethod): bool
{
// Simple match, can be improved with regular expressions for dynamic routes.
$routeMethod = strtoupper($routeMethod);
$method = strtoupper($method);
if ($routeMethod !== $method) {
return false;
}
// Basic path matching. Expand this to handle route parameters
// e.g. matchRoute('/users/123', 'GET', '/users/{id}', 'GET')
// This current implementation is very simplistic.
return $uri === $routePath;
}
private function executeRoute(array $route, string $uri): string
{
$controllerClass = $route['controller'];
$action = $route['action'];
// Instantiate the controller and execute the action
$controller = new $controllerClass();
// Basic implementation. Expand this to extract route parameters
// and pass them to the controller method.
return $controller->$action();
}
}
dispatch(string $uri, string $method):接收URI和HTTP方法,遍历路由表,匹配路由。matchRoute(string $uri, string $method, string $routePath, string $routeMethod):匹配URI和HTTP方法与路由规则。 注意: 这是一个非常简化的匹配实现,没有处理路由参数。 实际应用中,你需要使用正则表达式来匹配带参数的路由,并提取参数。executeRoute(array $route, string $uri):实例化控制器,执行方法。 注意: 这是一个非常简化的执行实现,没有处理依赖注入和参数传递。 实际应用中,你需要使用依赖注入容器来创建控制器实例,并根据路由参数调用相应的方法。
5. 示例代码(完整流程):
<?php
require_once 'vendor/autoload.php'; // 假设你使用了Composer
use AppRoutingRouteResolver;
use AppRoutingRouteDispatcher;
// 1. 定义控制器类(如上例中的 UserController)
// 2. 创建路由解析器,指定控制器命名空间
$routeResolver = new RouteResolver('AppControllers');
// 3. 指定要解析的控制器
$controllers = ['UserController'];
// 4. 解析路由
$routes = $routeResolver->resolveRoutes($controllers);
// 5. 创建路由分发器
$routeDispatcher = new RouteDispatcher($routes);
// 6. 接收请求,分发路由
$uri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];
$response = $routeDispatcher->dispatch($uri, $method);
echo $response;
三、依赖注入:Attribute 驱动的依赖绑定
依赖注入(DI)是一种设计模式,用于解耦类之间的依赖关系。传统的依赖注入通常是通过构造函数注入或者Setter注入来实现。使用Attributes,我们可以更清晰、更简洁地定义依赖关系。
1. 定义依赖注入Attribute:
首先,我们需要定义一个Attribute类,用于标记需要注入的依赖。
<?php
namespace AppAttributes;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class Inject
{
public function __construct(public ?string $name = null) {}
}
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]:这个Attribute可以用于属性和构造函数参数。public ?string $name = null:允许指定绑定的名称。如果未指定,则使用类型提示作为名称。
2. 定义服务容器:管理依赖关系
服务容器负责管理依赖关系,并创建对象实例。
<?php
namespace AppContainer;
use PsrContainerContainerInterface;
use ReflectionClass;
use ReflectionProperty;
use AppAttributesInject;
use ReflectionParameter;
class Container implements ContainerInterface
{
private array $bindings = [];
public function bind(string $id, callable|string $concrete): void
{
$this->bindings[$id] = $concrete;
}
public function get(string $id): mixed
{
if ($this->has($id)) {
$concrete = $this->bindings[$id];
if (is_callable($concrete)) {
return $concrete($this);
}
return $this->resolve($concrete);
}
return $this->resolve($id);
}
public function has(string $id): bool
{
return isset($this->bindings[$id]);
}
public function resolve(string $id): mixed
{
try {
$reflectionClass = new ReflectionClass($id);
} catch (ReflectionException $e) {
throw new Exception("Class {$id} not found: " . $e->getMessage());
}
if (!$reflectionClass->isInstantiable()) {
throw new Exception("Class {$id} is not instantiable.");
}
$constructor = $reflectionClass->getConstructor();
if (!$constructor) {
return new $id();
}
$parameters = $constructor->getParameters();
$dependencies = $this->resolveDependencies($parameters);
return $reflectionClass->newInstanceArgs($dependencies);
}
private function resolveDependencies(array $parameters): array
{
$dependencies = [];
foreach ($parameters as $parameter) {
$dependency = $this->resolveDependency($parameter);
$dependencies[] = $dependency;
}
return $dependencies;
}
private function resolveDependency(ReflectionParameter $parameter): mixed
{
$type = $parameter->getType();
if ($type === null) {
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
throw new Exception("Unable to resolve dependency for parameter {$parameter->getName()}");
}
$name = $type->getName();
if ($parameter->getAttributes(Inject::class)) {
$injectAttribute = $parameter->getAttributes(Inject::class)[0]->newInstance();
if ($injectAttribute->name) {
$name = $injectAttribute->name;
}
}
return $this->get($name);
}
public function injectProperties(object $object): void
{
$reflectionClass = new ReflectionClass($object);
foreach ($reflectionClass->getProperties() as $property) {
$attributes = $property->getAttributes(Inject::class);
if (!empty($attributes)) {
$this->injectProperty($object, $property, $attributes[0]->newInstance());
}
}
}
private function injectProperty(object $object, ReflectionProperty $property, Inject $injectAttribute): void
{
$propertyName = $property->getName();
$propertyType = $property->getType();
if ($propertyType === null) {
throw new Exception("Cannot inject property {$propertyName} without a type hint.");
}
$typeName = $propertyType->getName();
$dependency = $injectAttribute->name ?? $typeName;
$property->setAccessible(true); // Allow access to private/protected properties
$property->setValue($object, $this->get($dependency));
}
}
bind(string $id, callable|string $concrete):绑定一个接口或类到一个具体的实现。$id是接口或类的名称,$concrete可以是一个闭包或者类的名称。get(string $id):获取一个实例。如果已经绑定,则使用绑定的实现,否则尝试自动解析。resolve(string $id):自动解析一个类的依赖关系,并创建实例。resolveDependencies(array $parameters):解析构造函数的参数,递归地解析依赖关系。injectProperties(object $object): 对对象的属性执行依赖注入injectProperty(object $object, ReflectionProperty $property, Inject $injectAttribute): 对单个属性执行依赖注入
3. 在类中使用依赖注入Attribute:
<?php
namespace AppServices;
class UserService
{
public function __construct(#[Inject] private UserRepository $userRepository)
{
}
public function getUser(int $id)
{
return $this->userRepository->find($id);
}
}
class OrderService
{
#[Inject]
private UserService $userService;
public function getOrder(int $id)
{
// 使用 UserService 获取用户信息
$user = $this->userService->getUser(1); // 假设获取用户ID为1的用户
// 获取订单信息
return 'Order ID: ' . $id . ' User: ' . $user;
}
}
#[Inject] private UserRepository $userRepository:将UserRepository注入到UserService的构造函数中。#[Inject] private UserService $userService:将UserService注入到OrderService的属性中。
4. 示例代码(完整流程):
<?php
require_once 'vendor/autoload.php';
use AppContainerContainer;
use AppServicesUserService;
use AppServicesOrderService;
use AppRepositoriesUserRepository;
// 1. 创建服务容器
$container = new Container();
// 2. 绑定接口到实现
$container->bind(UserRepository::class, UserRepository::class); // 可以绑定到不同的实现
// 3. 获取实例
$userService = $container->get(UserService::class);
// 4. 测试
$user = $userService->getUser(1);
echo "User: " . $user . "n";
// 5. 测试属性注入
$orderService = $container->get(OrderService::class);
$container->injectProperties($orderService); // 手动注入属性
$order = $orderService->getOrder(123);
echo $order . "n";
四、更进一步:组合路由和依赖注入
我们可以将路由和依赖注入结合起来,实现更强大的功能。例如,可以在路由分发器中,使用依赖注入容器来创建控制器实例,并将路由参数传递给控制器方法。
1. 修改路由分发器:
修改RouteDispatcher::executeRoute() 方法,使用依赖注入容器创建控制器实例,并将路由参数传递给控制器方法。 (需要大幅修改,略)
五、Attribute的更多应用场景
除了路由和依赖注入,Attributes还可以用于很多其他场景,例如:
- 验证: 可以使用Attributes来定义数据验证规则。
- 序列化/反序列化: 可以使用Attributes来控制对象的序列化和反序列化过程。
- 事件监听: 可以使用Attributes来定义事件监听器。
- 缓存: 可以使用Attributes来控制方法的缓存行为。
- 数据库映射(ORM): 可以使用Attributes来定义实体类的数据库映射关系。
六、总结:Attribute赋能框架开发
PHP 8 Attributes提供了一种更结构化、类型安全的方式来添加元数据,可以极大地简化框架开发。 通过Attribute,我们可以将配置信息直接添加到代码中,实现更清晰、更简洁的编程模型。 合理利用Attributes,能够提升代码的可读性、可维护性和可扩展性,加速开发效率。