用 PHP 8 的 Attribute 实现 Spring 中的 FactoryBean

去年写了一年多 Java,今年重新回归 PHP,在使用 Java 的过程中,有一些非常方便的特性,比如依赖自动注入,在 Java 中只需要一个简单的 @Resource 注解,即可注入所需要的依赖。

public class UserService{
    @Resource
    private UserDAO userDAO;

    public UserEntity getUserById(Long id){
        return userDAO.queryUserById(id);
    }
}

而在 PHP 中,现框架基本都是依赖 __construct 方法注入的,属性的类型定义也是 PHP 7.4 版本才支持的。

class UserService{
    private $userQuery;

    public function __construct(UserQuery $userQuery){
        $this->userQuery = $userQuery;
    }

    public function getUserById($id){
        return $this->userQuery->queryUserById($id);
    }
}

动态代理

Spring 容器中的对象基本都是代理对象,之前有研究过 Java 动态代理的原理:博客:CGLIB代理

后面我根据 Java 中的动态代理,在 PHP 中实现了一番,发现连反射都用不上。

<?php
namespace Minor\Proxy\Main;

class ProxyFactory
{
    /**
     * 被代理的对象
     * @var string
     */
    private $class;
    /**
     * 拦截器
     * @var InterceptorInterface
     */
    private $interceptor;

    public function __construct($class, InterceptorInterface $interceptor)
    {
        $this->class = $class;
        $this->interceptor = $interceptor;
    }

    public function __call($name, $args)
    {
        // 调用代理类的方法时,转发到拦截器中
        return $this->interceptor->intercept($this->class, new MethodCaller($name), $args);
    }

     public static function create($class, InterceptorInterface $interceptor) 
     { 
          return new static($class, $interceptor); 
     } 
}
<?php
namespace Minor\Proxy\Main;
// 拦截器 interface
interface InterceptorInterface
{
    public function intercept($class, MethodInterface $method, array $args);
}
<?php
namespace Minor\Proxy\Main;
// 方法对象,这对象优点多余,在拦截器调用 call_user_func_array 就可以了
class MethodCaller
{

    /**
     * @var string
     */
    private $name;

    /**
     * MethodCaller constructor.
     * @param $name
     */
    public function __construct($name)
    {
        $this->name = $name;
    }

    public function invoke($class, array $args)
    {
        return call_user_func_array([$class, $this->name], $args);
    }

    public function __toString()
    {
        return $this->name;
    }
}

调用:

class A {
   public function run(){
      return "this is function A";
   }
}

class Interceptor implements InterceptorInterface{
   private function before(){
           echo "before A run\n";
   }

   private function after(){
           echo "after A run\n";
   }
   public function intercept($class, MethodInterface $method, array $args){
          $this->before();
          $result = $method->invoke($class,$args);
          $this->after();
          return $result;
   }
}

$a = ProxyFactory::create(new A(),new Interceptor());
$a->run();

以上是我之前仿照 Java 写的动态代理,动态代理某个类之后,这个类就可以对他进行切面,在 PHP 的框架底层设计都有切面相关的实现,具体切面作用就不展开讲了。接下来就是在动态代理的基础上实现属性的自动注入(@Resource)功能。

自动注入

class A {
   private B $b;

   public function run(){
      return $b->run();
   }
}

class B{
   public function run(){
      return "B run";
   }
}

在以上代码中 A 依赖 B,但是并没有对 $b 赋值,这个时候调用 run 方法肯定会报错,回到文章开头,因为类的属性类型定义是 PHP 7.4 才支持的,所以大部分框架还是依赖 __construct 进行依赖注入,现在我们修改一下 ProxyFactory 的代码,通过反射的方式实现自动注入。

先创建一个属性注入的工具类。

<?php
namespace Minor\Proxy\Proxy;

class PropertyInjector
{
    public function inject($class)
    {
        // 传入被代理对象,获取被代理对象的反射
        $reflectionClass = new \ReflectionClass($class);
        // 获取被代理对象的所有 property
        $properties = $reflectionClass->getProperties();
        foreach ($properties as $property) {
            // 获取 property 的 type,该方法 PHP 7.4 才支持 
            $type = $property->getType();
            // 如果没有定义 type ,先跳过,只针对定义了 type 的属性进行注入
            if (null === $type) {
                continue;
            }
            // 获取 type 名字,即 classname
            $name = $type->getName();
            // 这里图省事,直接 new 了这个 type,后续可加入依赖注入
            $propertyObject = new $name(); //todo di
            // 把创建好的对象赋值给 property
            $this->setProperty($property, $class, $propertyObject);
        }

        return $class;
    }
    /**
     * @param \ReflectionProperty $property
     * @param $class
     * @param mixed $propertyObject
     */
    protected function setProperty(\ReflectionProperty $property, $class, mixed $propertyObject): void
    {
        // 设置当前属性为可处理的
        $property->setAccessible(true);
        // 赋值
        $property->setValue($class, $propertyObject);
    }
}

ProxyFactory

    public function __construct($class, InterceptorInterface $interceptor = null)
    {
        // 增加了一个属性注入的过程
        $this->propertyInjector = new PropertyInjector();
        $this->class = $this->propertyInjector->inject($class);
        $this->interceptor = $interceptor ?? self::getGlobalInterceptor();
    }

以上是实现自动注入的简单过程,下面是注入工厂的实现。

注入工厂

就是在注入属性时,可自定义实现创建属性对象的功能。

比方说以下场景,A 依赖 B,B 是一个接口,并没有真正的实现类,只有在调用的时候才需要实现(比如RPC调用)

class A {
    private B $b;

    public function run(){
        return $this->b->run();
    }
}

interface B {
   public funtion run();
}

修改 PropertyInjector

public function inject($class)
{
        foreach ($properties as $property) {

            ... 省略使用 type 自动注入的代码
            // 利用 PHP 8 的 Attribute ,实现 PropertyFactory
            if ($this->setPropertyByAttribute($property, $class)) {
                continue;
            }

        }

        return $class;
}

/**
* @param \ReflectionProperty $property
* @param $class
*/
protected function setPropertyByAttribute(\ReflectionProperty $property, $class,$type): bool
{ 
    // getAttributes 方法是 PHP 8 新增的
    $attributes = $property->getAttributes();
    if (count($attributes) > 0) {
        // 获取 factory 的实例
        // 图省事,直接取第一个属性,后续完善需要遍历,newInstance 可能需要依赖注入,还没深入研究
        $factory = $attributes[0]->newInstance(); //todo di
        // PropertyFactory 是我们定义的一个工厂接口,里面有一个 create($class) 方法,返回属性对象
        if ($factory instanceof PropertyFactory) {
            // 给属性赋值
            $this->setProperty($property, $class, $factory->create($type));
        }
        return true;
    }
    return false;
}

修改 A

class A {
    #[RpcProperty]
    private B $b;

    public function run(){
      return $this->b->run();
    }
}


class RpcProperty implements PropertyFactory{
    public function create($class){
        // 这一步需要创建一个继承于 $class 的类,这里省略,具体实现可查看我仓库的源码
        return new RpcProxy($class);
    }
}

class RpcProxy {
    private $target;

    public function __construct($target){
        $this-target = $target;
    }

    public function __call($method,$args){
        // 伪代码,将被调用的类、方法名、参数,通过 RPC 发送
        return rpc_call($this->targetClass,$method,$args);
    }
}

源码:github.com/minororange/function-pr...

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!