# 依赖注入
# 前言
好的设计会提高程序的可复用性和可维护性,也间接的提高了开发人员的生产力。我们就来说一下在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
依赖UserService
,UserService
依赖User
。从控制的角度来看, Controller
主动创建依赖UserService
,UserService
主动创建依赖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;
}
← 视图 Session 管理 →