引子

不知道大家开发时有没有好奇过以下这两个问题呢?

  1. 为什么方法的参数位置①是需要传入 2 个参数的,一个是 Request 类型的参数,一个是不定类型的 $id 参数,但路由只有一个$id 参数,那 $request 参数是哪里来的?
  2. UserService__construct 方法明确实例化需要一个 Cache 类型的参数,但②中并没有传入,为什么能使用呢?③为什么使用 new 不传参数就会报错呢?

    1. Route::get('/{id}','\App\Http\Controllers\IndexController@index');
    1. class IndexController extends Controller
    2. {
    3. public function index(Request $request, $id)①
    4. {
    5. app(UserService::class)②->getUserNameById($id);
    6. // TypeError: Too few arguments to function App/Services/UserService::__construct(), 0 passed in Psy Shell code on line 1 and exactly 1 expected
    7. (new UserService())->getUserNameById($id);
    8. }
    9. }
    1. class UserService
    2. {
    3. public $cache;
    4. public function __construct(Cache $cache)
    5. {
    6. $this->cache = $cache;
    7. }
    8. public function getUserNameById($id)
    9. {
    10. return $this->cache->get('user:id:' . $id);
    11. }
    12. }

    原来这叫 依赖注入 ,开始也不知道是个啥,那就抱着这两个疑问开始寻找答案。

接下来会围绕这 3 个点来讲

  1. 依赖注入
    1. 依赖:谁依赖谁
    2. 注入:注入什么
  2. 控制反转
    1. 控制:谁控制谁
    2. 反转:反转什么
  3. 什么是容器

    常规代码

    ```php class Index1Controller { public $userService;

    public function __construct() {

    1. /**
    2. * 因为我需要(依赖) UserService() 给我提供数据, 所以创建了一个 UserService() 对象
    3. *
    4. * 控制:我 (IndexController) 控制了 UserService() 对象的创建
    5. * 反转:我 (IndexController) 绝对控制 UserService() 对象的权利,创建对象的控制权没有发生转移,所以没有反转,一切都是亲力亲为。
    6. */
    7. $this->userService = new UserService();

    }

    public function index() {

    1. // 我 (index) 控制了 UserService() 对象的创建
    2. $userService = new UserService();
    3. $userName = $userService->getUserName();
    4. $userName2 = $this->userService->getUserName();
    5. return [$userName, $userName2];

    } }

(new IndexController())->index();

  1. <a name="FprSj"></a>
  2. ### 依赖注入和控制反转
  3. ```php
  4. class Index2Controller
  5. {
  6. public $userService;
  7. /**
  8. * 因为我需要(依赖) UserService() 给我提供数据, 所以我需要接收一个 UserService 类型的参数
  9. * 把依赖从外部传入进来,把需要的依赖传入进来了,就是依赖注入
  10. *
  11. * 控制:调用者控制了 UserService() 对象的创建
  12. * 反转:我 (IndexController) 控制 UserService 创建的权利已经没有了(转移了),那转移给谁了?这里的控制权转移给调用者了。
  13. */
  14. public function __construct(UserService $userService)
  15. {
  16. $this->userService = $userService;
  17. }
  18. public function index()
  19. {
  20. /**
  21. * 在方法中创建对象
  22. * 我 (index) 控制了 UserService() 对象的创建
  23. */
  24. $userService = new UserService();
  25. $userName = $userService->getUserName();
  26. $userName2 = $this->userService->getUserName();
  27. return [$userName, $userName2];
  28. }
  29. }
  30. // __construct() 中创建 new UserService() 转移到了这里
  31. $userService = new UserService();
  32. // 将 $userService 传入(注入) controller 中
  33. (new Index2Controller($userService))->index();

IoC 容器自动注入

上面的 依赖注入和控制反转 并没有解决开头引出的两个问题的答案,依赖还是需要手动创建,然后手动注入,如何实现依赖的自动注入呢?这个时候就需要一个 IoC 容器了

  1. 如何注入

使用 PHP 提供的 反射 (Reflection) 功能

  1. 我们需要注入哪里的参数

依赖注入是以构造函数参数的形式传入,所以我们需要自动注入构造函数指定的参数

  1. 我们需要注入哪些参数

我们只注入类实例,其他参数原样传入

IoC 容器其实就是一个普通的 class 类,实现了某些功能而已,不必想的太复杂。

  1. class Container{
  2. /**
  3. * ioc容器实现自动依赖注入
  4. * @param string $className 类名
  5. * @param array $param 参数数组
  6. * @return object
  7. * @throws ReflectionException
  8. */
  9. public static function make($className,$param = []){
  10. $reflect = new ReflectionClass($className);
  11. // 获取构造函数
  12. $construct = $reflect->getConstructor();
  13. // 保存实例化需要的参数
  14. $args = [];
  15. if ($construct){
  16. /**
  17. * 获取构造函数的参数
  18. * array(3) {
  19. * [0] => object(ReflectionParameter)#3 (1) {["name"]=> string(10) "userService"}
  20. * [1] => object(ReflectionParameter)#4 (1) {["name"]=> string(9) "userModel"}
  21. * [2] => object(ReflectionParameter)#5 (1) {["name"]=> string(2) "id"}
  22. * }
  23. */
  24. $consParams = $construct->getParameters();
  25. foreach ($consParams as $k => $v){
  26. /**
  27. * object(ReflectionClass)#6 (1) {["name"]=>string(10) "UserServer"}
  28. */
  29. $class = $v->getClass();
  30. if ($class){
  31. // $c = $class->getName();
  32. // $args[] = new $c;
  33. // 如果这样处理依赖的的 UserService() 还有依赖的话则无法兼顾,所以需要递归处理
  34. $args[] = self::make($class->getName());
  35. }
  36. }
  37. }
  38. // 合并参数
  39. $args = array_merge($args, $param);
  40. /**
  41. * ReflectionClass::newInstanceArgs — 从给出的参数创建一个新的类实例。
  42. * 相当于:return new UserController(new UserServer, new UserModel, $id)
  43. * IoC 控制反转:
  44. * 控制:容器控制了对象的创建
  45. * 反转:创建对象的权利已经转移到了容器中来了,不再是 UserController() 中的 __construct() 了。
  46. * DI 依赖注入:
  47. * 依赖:$args 保存了保存了需要那些依赖
  48. * 注入:把 $args 中的依赖作为参数传入(注入),返回实例
  49. */
  50. return $reflect->newInstanceArgs($args);
  51. }
  52. }
  53. try {
  54. //使用ioc容器可以自动注入 UserServer 和 UserModel 两个依赖,我们只需要传入剩下的参数即可
  55. $userController = Container::make(UserController::class, [55]);
  56. $userController->index();
  57. } catch (ReflectionException $e) {
  58. echo $e->getMessage();
  59. }
  60. class UserController{
  61. public $userServer;
  62. public $userModel;
  63. public $id;
  64. //依赖 UserServer 和 UserModel
  65. public function __construct(UserServer $userServer,UserModel $userModel,$id) {
  66. $this->userServer = $userServer;
  67. $this->userModel = $userModel;
  68. $this->id = $id;
  69. }
  70. public function index(){
  71. echo 'id:'.$this->id.PHP_EOL;
  72. $this->userServer->say();
  73. $this->userModel->say();
  74. }
  75. }
  76. class UserServer{
  77. //依赖 SayServer
  78. public function __construct(SayServer $sayServer){
  79. echo $sayServer->getName().PHP_EOL;
  80. }
  81. public function say(){
  82. echo 'UserServer'.PHP_EOL;
  83. }
  84. }
  85. class UserModel{
  86. //依赖 SayServer
  87. public function __construct(SayServer $sayServer){
  88. echo $sayServer->getName().PHP_EOL;
  89. }
  90. public function say(){
  91. echo 'UserModel'.PHP_EOL;
  92. }
  93. }
  94. class SayServer{
  95. protected $name;
  96. public function __construct(){
  97. $this->name = 'sayServer';
  98. }
  99. public function getName(){
  100. return $this->name;
  101. }
  102. }

回顾问题

  1. 路由中的 Request $request 参数是哪里来的

答:请求进入框架之后,框架解析 url 找到相对应的控制器类,调用容器写好的自动注入方法 (案例中是 make()),进行注入参数,这样就可以愉快又方便的使用啦。

  1. 使用 app() 和 new 有什么不同

答:其实 laravelapp() 就是使用 Container 实例化的一个助手函数

看看 laravel 中的助手函数

  1. function app($abstract = null, array $parameters = [])
  2. {
  3. if (is_null($abstract)) {
  4. return Container::getInstance();
  5. }
  6. return Container::getInstance()->make($abstract, $parameters);
  7. }

结尾

这个案例的 Container 中似乎没有太体现出 容器 这个词,因为还没有实现实例化对象的存储,具体可以看看相关的源码。