# 依赖注入

# 前言

好的设计会提高程序的可复用性和可维护性,也间接的提高了开发人员的生产力。我们就来说一下在Herosphp中都使用的依赖注入。

# 概念

要搞清楚什么是依赖注入如何依赖注入,首先需要明确一些概念。

DIP 依赖倒置原则: 程序要依赖于抽象接口,不要依赖于具体实现。

IOC 控制反转: 遵循依赖倒置原则的一种代码设计方案,依赖的创建 (控制) 由主动变为被动 (反转)。

DI 依赖注入: 控制反转的一种具体实现方法。通过参数的方式从外部传入依赖,将依赖的创建由主动变为被动 (实现了控制反转)。


class Controller
{
    protected UserService userService;

    public function __construct()
    {
        // 主动创建依赖
        $this->userService = new UserService(12, 13); 
    }       
}

class UserService
{
    protected User user;
    protected int $count;

    public function __construct($param1, $param2)
    {
        $this->count = $param1 + $param2;
        // 主动创建依赖
        $this->user = new User('test_table'); 
    }
}

class User
{
    protected $table;

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

$controller = new Controller;

上述的依赖关系是Controller依赖UserServiceUserService依赖User。从控制的角度来看, Controller主动创建依赖UserServiceUserService主动创建依赖User。依赖是由需求方内部产生的,需求方需要关心依赖的具体实现。这样的设计使代码耦合性变高,每次底层发生改变(如参数变动),顶层就必须修改代码。

接下来,使用依赖注入实现控制反转,使依赖关系倒置:


class Controller
{
    protected UserService $service;
    public function __construct(UserService $service)
    {
        $this->service = $service; 
    }       
}

class UserService
{
    protected User $user;
    protected int $count;
    public function __construct(User $user, int $param1, int $param2)
    {
        $this->count = $param1 + $param2;
        $this->user = $user; 
    }
}

class User
{
    protected string $table;

    public function __construct(string $table)
    {
        $this->table = $table;
    }
}

$user = new User('test_table');
$userService = new UserService($user, 12, 13);
$controller = new Controller($service);

将依赖通过参数的方式从外部传入(即依赖注入),控制的角度上依赖的产生从主动创建变为被动注入,依赖关系变为了依赖于抽象接口而不依赖于具体实现。此时的代码得到了解耦,提高了可维护性。

# 自动注入依赖

有了上面的一些理论基础,我们大致了解了依赖注入是什么,能干什么。 不过虽然上面的代码可以进行依赖注入了,但是依赖还是需要手动创建。我们可不可以创建一个容器类,用来帮我们进行自动依赖注入呢?OK,我们需要一个IOC容器。

# 实现一个简单的IOC容器

依赖注入是以构造函数参数的形式传入的,想要自动注入:

  • 我们需要知道需求方需要哪些依赖,使用反射来获得。
  • 只有类的实例会被注入,其它参数不受影响。

如何自动进行注入呢?当然是PHP自带的反射功能!

# 1. 雏形

首先,创建BeanContainer类,make 方法:

<?php

// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2014 The Herosphp Authors. All rights reserved.
// * Use of this source code is governed by a MIT-style license
// * that can be found in the LICENSE file.
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

declare(strict_types=1);

namespace herosphp\core;

use herosphp\annotation\Inject;
use herosphp\exception\HeroException;
use ReflectionClass;
use ReflectionException;

/**
 * BeanContainer class
 *
 * @author RockYang<yangjian102621@gmail.com>
 */
class BeanContainer
{
    protected static array $_instances = [];

    // create an instance with specified constructor args
    public static function make(string $name, array $constructor = []): mixed
    {
        if (!class_exists($name)) {
            throw new HeroException("Class '$name' not found");
        }
        if (isset(static::$_instances[$name])) {
            return static::$_instances[$name];
        }
        $value = new $name(...array_values($constructor));
        static::put($name, $value);
        return $value;
    }

    /**
     * build a instance with specified class path
     * auto-inject the properties and put it to bean container.
     * @throws ReflectionException
     */
    public static function build(string $class): object
    {
        $obj = static::get($class);
        if ($obj != null) {
            return $obj;
        }

        $clazz = new ReflectionClass($class);
        $obj = $clazz->newInstance();
        // scan Inject getProperties
        foreach ($clazz->getProperties() as $property) {
            $_attrs = $property->getAttributes(Inject::class);
            if (empty($_attrs)) {
                continue;
            }

            // find property class name
            $_attr = $_attrs[0];
            $name = $property->getType()->getName();
            $_args = $_attr->getArguments();
            if (!empty($_args)) {
                $name = array_shift($_args);
            }

            // set property accessibility
            // @Note: As of PHP 8.1.0, calling this method has no effect; all properties are accessible by default.
            // $property->setAccessible(true);
            $property->setValue($obj, static::build($name));
        }

        // register object to bean pool
        static::register($clazz->getName(), $obj);
        return $obj;
    }
}

这里我们获取构造方法参数时用到了ReflectionClass类,大家可以到官方文档了解一下该类包含的方法和用法,这里就不再赘述。

ok,有了 make 方法,我们可以试一下自动注入依赖了:


class A
{
    public $count = 100;
}

class B
{
    protected $count = 1;

    public function __construct(A $a, $count)
    {
        $this->count = $a->count + $count;
    }

    public function getCount()
    {
        return $this->count;
    }
}

$a = BeanContainer::make(A::class);
$b = BeanContainer::make(B::class, [$a,10]);
var_dump($b->getCount()); // result is 110

# 2.进阶

有些类会贯穿在程序生命周期中被频繁使用,为了在依赖注入中避免不停的产生新的实例,我们需要IOC容器支持单例模式,已经是单例的依赖可以直接获取,节省资源。

<?php

// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2014 The Herosphp Authors. All rights reserved.
// * Use of this source code is governed by a MIT-style license
// * that can be found in the LICENSE file.
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

declare(strict_types=1);

namespace herosphp\core;

use herosphp\annotation\Inject;
use herosphp\exception\HeroException;
use ReflectionClass;
use ReflectionException;

/**
 * BeanContainer class
 *
 * @author RockYang<yangjian102621@gmail.com>
 */
class BeanContainer
{
    protected static array $_instances = [];

    // get beans
    public static function get(string $name): mixed
    {
        if (isset(static::$_instances[$name])) {
            return static::$_instances[$name];
        }

        return null;
    }

    // check bean is exist
    public static function exist(string $name): bool
    {
        return isset(static::$_instances[$name]);
    }

    // register a new instance
    public static function register(string $name, object $value): void
    {
        if (isset(static::$_instances[$name])) {
            return;
        }

        static::$_instances[$name] = $value;
    }

    // add or update a new object
    public static function put(string $name, object $value): void
    {
        static::$_instances[$name] = $value;
    }
    ....省略代码
}

# 3.运行方法

类之间的依赖注入解决了,我们还需要一个以依赖注入的方式运行方法的功能,可以注入任意方法的依赖。在框架启动的阶段,会扫描类的注解,主要是以下注解在启动的时候进行扫描并实例化存在容器内。

  • Component
  • Service
  • Bootstrap
  • Controller 仅在RUN_WEB_MODE为TRUE。
  • Command 仅在Command为TRUE

在实例化的时候,扫描类上的属性通过Inject的注解进行动态注入。


    /**
     * build a instance with specified class path
     * auto-inject the properties and put it to bean container.
     * @throws ReflectionException
     */
    public static function build(string $class): object
    {
        $obj = static::get($class);
        if ($obj != null) {
            return $obj;
        }

        $clazz = new ReflectionClass($class);
        $obj = $clazz->newInstance();
        // scan Inject getProperties
        foreach ($clazz->getProperties() as $property) {
            $_attrs = $property->getAttributes(Inject::class);
            if (empty($_attrs)) {
                continue;
            }

            // find property class name
            $_attr = $_attrs[0];
            $name = $property->getType()->getName();
            $_args = $_attr->getArguments();
            if (!empty($_args)) {
                $name = array_shift($_args);
            }

            // set property accessibility
            // @Note: As of PHP 8.1.0, calling this method has no effect; all properties are accessible by default.
            // $property->setAccessible(true);
            $property->setValue($obj, static::build($name));
        }

        // register object to bean pool
        static::register($clazz->getName(), $obj);
        return $obj;
    }
上次更新: 10/27/2022, 11:18:25 AM