PHP 8 Attributes(注解)在框架中的应用:实现自定义路由与依赖注入的元数据驱动

好的,我们开始今天的讲座,主题是: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,Route Attribute只能用于方法上。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):获取方法上的所有Route Attribute实例。
  • $attribute->newInstance():创建Route Attribute的实例,方便访问属性。
  • 将解析后的路由信息存储在 $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,能够提升代码的可读性、可维护性和可扩展性,加速开发效率。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注