ThinkPHP容器源码深度解析

网友投稿 713 2022-05-30

本文主要针对框架内部容器的实现做为基准点来进行深度解析

ThinkPHP容器源码深度解析

前言

一、单例模式

二、注册树模式

三、如何理解控制反转和依赖注入

四、必会反射机制

五、玩转自己的容器类

六、Container容器类剖析之Countable巧用

七、Container容器类剖析

八、容器源码阅读后总结

前言

在这之前已经剖析过了类的自动加载、配置文件加载的源码解析,本文为第三期的文章,主要针对容器以及门面类的实现,解析源码。以及学习实现此功能的一些知识点。

第一期文章:ThinkPHP自动加载Loader源码分析

第二期文章:ThinkPHP配置文件源码分析

一、单例模式

在学习容器以及门面之前需要必须了解的俩个设计模式,单例模式、注册树模式。

先对单例模式做一个简单的说明。

拥有一个构造函数,并且属性为private

拥有一个静态成员变量来保存类的实例

拥有一个静态方法来访问这个实例

一下就是咔咔实现的一个简单的单例模式,对照一下上面的三大特性看是否一致。

静态变量为instance

拥有构造并且还是私有的

最后一个就是有一个getInstance这个静态方法

接下来进行一下简单的测试

还是在index控制器中做测试,为了证实其类只被实例化过一次,调用了其四次

访问这个方法来看一下

new-class只执行了一次,就直接证明了创建的类只实例化了一次。

在这里咔咔之前有过一个疑问就是,这里的构造函数为什么要使用私有的属性。

你之前有过这个疑问吗?咔咔带你一起来解答一下

在本类定义私有属性的构造方法是为了防止其类在外部被实例化。

当在外部实例化这个类就会报下图的错。

那么为什么会在这里提一嘴单例模式呢!是因为在接下来的学习容器的源码中会使用到

例如下图thinkphp/library/think/Container.php类中就存在一个获取当前容器的实例。

截止到这里单例模式就简单的了解完了,了解单例模式也是为了更好的理解容器。

二、注册树模式

为什么在这里说这个注册树模式,因为在框架中注册树模式就是一个主导位置,所以必须去了解它!

那什么是注册树模呢!

注册树模式就是将对象实例注册到一颗树上(这里的树可不是真的树啊!就是注册到一个全局的属性里边)

然后可以通过内部方法从全局的树上获取对应的对象实例。

这样说的话肯定也不能更好的理解,接下来咔咔带大家看一个简单的案例来简单的了解一下。

一个注册树模式需要的东西就是四个,注册树的池子,将对象挂载到注册池里,从注册池里获取对象,从注册池里卸载对象。

如下图是咔咔写的一个简单的注册树模式。

代码如果看不懂的就需要去补补基础了哈!

接下来在到同一目录创建一个TestTree文件

来到控制器测试写的注册树模式是否有问题

在做测试的时候一定要注意命名空间问题哈!这里的kaka目录是之前在类的自动加载哪里配置的,如有不会的可以去第一期文章查看。

这里就相当于先把TestTree这个类实例化出来

然后使用注册树模式把这个实例注册到object树池子中

最后使用get方式将这个类获取出来就可以直接调用TestTree中的方法了。

最后看一下最终打印结果,结果就是TestTree类中getTreeContent方法的返回值。

注册树模式就是以上咔咔说明的这些内容,就是不去针对源码学习,这些内容也是我们必须要去学会使用的。

三、如何理解控制反转和依赖注入

其实这俩个就是指的一个东西,就是一种编程思想而已,不要想的那么难以理解和高大上。

那么什么是容器,容器直面理解就是装东西的东西。在编程中,我们常见的变量、对象属性都是一个容器。一个容器里边能够装什么,完全取决于对该容器的定义。

然而现在我们讨论的是另外一种容器,它存储的既不是文本、数值,而是对象、类、接口通过这种容器,得以实现很多高级功能,最常用的就是代码之间的解耦、依赖注入。

那么为什么会存在俩种概念,为什么要说控制反转和依赖注入呢!在上文也提到过,它们其实指的就是一种东西,只是描述的角度不同而已。

就跟你是爸爸的儿子,你还是你爷爷的孙子,不管儿子还是孙子都指的是一个人。只是站在不同的角度看待问题而已。

控制反转

是站在容器的角度看待问题,容器控制着应用程序,由容器反向的向应用程序注入应用程序需要的外部资源。

依赖注入

是站在应用程序的角度看待问题,应用程序依赖容器创建并注入它所需要的外部资源。

作用

主要用来减少代码之间的耦合程度。

有效的分离对象和应用程序所需要的外部资源。

下面俩幅图就可以很清晰的说明问题

给大家整一个简单的案例

定义俩个类分别为Person、Car,在Person中实例并调用Car中的pay方法。

然后在控制器中调用,并且打印结果肯定就是Car返回的123,这个就不去打印了。

那这个时候我们把代码修改一下,把Car类直接传给Person类,在Person类中直接用传过来的对象去调用对应的方法。

这只是一个简单的实现过程,为了给阅读框架容器代码做一个铺垫,在后文中会详细说明框架中的容器注入。

四、必会反射机制

不知道大家有没有了解过GO的反射机制,咔咔在当时看了go的反射机制后说实话有点晕乎乎的。

但是在后来看了PHP的反射之后,不仅对go的反射有了一定的深入了解,并且对于PHP的反射也是更好的理解。

反射这一概念是在PHP5.0被引出来的,在目前使用的框架中咔咔知道的就有thinkphp和laravel都使用了反射来实现依赖注入。

对于反射的理解:其实就是从根获取根以外的东西,放在编程中讲就是只要知道一个类就可以知道这个类所有的属性和方法。

案例

这只是一个简单的实现案例,获取类的全部方法和属性。可以看下图中的打印结果跟TestReflection是否一致。

这个也从侧面表现出现一个问题,就是会暴露出来一些本不应该暴露出来的信息。

关于反射提供的接口还有很多,这里就介绍几个常用的,其余的在框架源码中解析。

使用反射执行一个类的方法

打印出来的结果就是咔咔

使用反射执行一个类中带参数的方法

使用反射执行一个类中不带参数的方法

其它的方法你们自己可以尝试尝试,因为这个反射的接口在平时基础开发是不怎么用的,这咔咔给大家介绍的都是后边在阅读源码都是可以用的到的。

既然了解到了反射,那么反射可以做什么事情呢!其中有一个功能点自动生成文档。

反射到这里就简单的了解一下,至于还想了解更多的接口使用可以去官方查看对应的接口信息。

在了解完反射之后就要开始进入正题了,就需要正式进入我们的容器环节了。只有上边的基础打好接下来的容器才能更好的理解。

五、玩转自己的容器类

经历了九九八十一难终于来到了容器这一环节,在这一环节我们先来实现一个自己的容器,将之前讲解的单例模式、注册树模式、反射进行一个串联,从而进行加深印象和更好的理解。

还记得之前在依赖注入里边说过这样一个方法dependency,这个方法就是进行了依赖注入,从而对代码进行解耦。

但是这次呢!会使用容器来解决这一问题。

首先先把需要的类定义好,这一个类就使用了单例模式和注册树模式,之前的文章没有好好看的,一定要仔细看一下,否则后文会很难理解的。

instances[$key] = $value; } public function get ($key) { return $this->instances[$key]; } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

为了方便以后查看方便,这里把每节的案例演示都放在对应的控制器中

这里把之前的依赖注入的代码移植过来,并且配置上注解路由进行访问,看最终结果是否为Car方法返回的123

测试一下打印结果,一切ok

使用单例模式和注册树模式配合后修改的这份代码

修改后打印出其结果,同样也是car返回的值123。

在这里需要注意一下就是在同一个方法中set和get方法是不会共存的,这里只是为了给大家做一个演示写到一起的。

后边在看容器源码时就知道set和get方法到底是怎么使用的,这里只是让大家体验一下单例模式和注册树模式。

这里做一个小修改,修改上文中最后俩行代码

场景二

此时我们把Person 的文件修改一下

添加一个构造函数,把参数使用构造函数进行赋值,在buy方法中就不需要在进行传递参数,只需要使用this->obj即可。

此时如果还是直接运行dependency路由就会报下边一个错,那是因为在Person中构造函数有个参数,的但是我们没有传。

此时就需要在修改一处,就是在实例化Person时把Car的实例当参数给传进去就没有任何问题了。

但是你会发现上边这都是什么代码,本来简简单单的几行代码被复杂成这个样子,这个时候就已经弊大于利了,不管设计模式在好,盲目的使用对项目来说也是一种负担。

所以这个时候反射就来了,反射在上文中也进行简单的介绍过,一定要看哈!文章都是一环套着一环的。

反射之战优化代码

最终优化完成的代码就是这样的,接下来对这段代码进行简单的解析。

在之前代码的基础上只修改了kaka/container/Container.php这个类里边的get方法

判断这个名person是否在容器中

使用反射接口,然后获取传进去person类的构造方法

如果person没有构造方法就直接返回person这个实例即可

如存person在构造函数,则获取person构造函数的方法

由于person类里边的构造函数的参数不会仅限于一个

所以需要循环来获取每个参数的对象

最后使用反射的 newInstanceArgs接口创建对应的实例

instances[$key] = $value; } /** * User : 咔咔 * Notes: 获取容器里边的实例 使用反射 * Time :2020/9/21 22:04 * @param $key * @return mixed */ public function get ($key) { if(!empty($this->instances[$key])){ $key = $this->instances[$key]; } $reflect = new \ReflectionClass($key); // 获取类的构造函数 $c = $reflect->getConstructor(); if(!$c){ return new $key; } // 获取构造函数的参数 $params = $c->getParameters(); foreach ($params as $param) { /** ReflectionClass Object ( [name] => container\dependency\Car ) */ $class = $param->getClass(); if(!$class){ }else{ // container\dependency\Car $args[] = $this->get($class->name); } } // 从给出的参数创建一个新的类实例 return $reflect->newInstanceArgs($args); } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

ThinkPHP容器源码深度解析

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

文件application/index/controller/Container.php这里就是修改之后的变动

问题一:kaka/container/dependency/Person.php里边的参数Car是什么意思

这个问题其实很简单,你可以看到这个Car就是同目录的Car.php文件。你就可以直接理解为同命名空间下的文件。

问题二:文件application/index/controller/Container.php为什么可以直接调用buy方法

首先看一下obj的值,返回的这个对象里边就已经把Car的类实例化好了,所以无需在实例化,可直接调用buy方法,因为参数会直接传递过去

以上就是咔咔实现的一个简单的容器,如有不明白或者问题可以直接评论区回复即可。

接下来就是针对框架里边的容器进行剖析,一步一步的追溯到根源。

六、Container容器类剖析之Countable巧用

关于Countable这块内容一直没想好是否是文章的形式写出展现给大家,但是在后期阅读源码时大量的出现了Countable的应用。

为了大家能看懂每一个技术点,咔咔还是写了出来。

在文件thinkphp/library/think/Container.php中,就可以直接看到使用了Countable接口,并且实现了它!

来到Countable这接口中,我们只能看到一个方法就是count().

根据代码中Count elements of an object这行注释可以了解到,这个接口是计算对象的元素

根据PHP文档的说明在深入了解一下。

文档说明当你执行count()方法时就相当于在执行上边的abstract public Countable::count ( void ) : int抽象方法。

实战案例

光说不干,事事落空;又说又干,马到成功。直接开干

新建文件kaka/container/countableTest.php,并且添加以下内容

接着在文件application/index/controller/Container.php中学会使用Countable。

这里注意一下用法,是直接使用count();

Countable中的count()跟平时使用count()方法有什么区别

顺便看一下PHP源码中的解释

可以看到第一个参数可以是数组也可是是countable

咔咔的理解是Countable只是重写了SPL中的count方法,为了就是方便定制自己需要的统计规则而已。

int count ( mixed $array_or_countable [, int $mode = COUNT_NORMAL ] )

1

count你不知道的用法

既然说到了这里,咔咔给大家在普及一个count不是很常用的一个用法。

在平时开发的过程中,这样的用法是最普遍的,也是大家最经常见到的一个使用案例。

但是如果这时给你一个多维数组,例如下图这样,让你统计这个多维数组,你该怎么统计呢!

这个时候估计大多数小伙伴的想法就是循环然后定义一个计数器累计。

其实count()函数在这一块就已经解决了这个需求。

下方打印结果就是"4----6"

直接使用count()函数一个数组得到的就是第一层数组的长度。

但是count()函数还有第二个参数,设置为1就是递归地计数数组中元素的数目(计算多维数组中的所有元素)

所以你这时在去看文档就会发现,count()函数本身就有俩个参数

第一个参数是必须饿,选择是数组

第二个参数默认是0就是不对多维数组中的所有元素进行计数

当第二个参数为1时就是递归的计算多维数组中的所有元素。

七、Container容器类剖析

上文中实现了一个自己创建的容器,接下来看看源码中的容器,经过了上文容器中出现的技术点都已经囊括完了。

在接下里阅读容器源码就不会很吃力,如果之前的文章没看,一定要大概过一遍哈!

大家无数次打开的一个文件public/index.php。

曾有多少次打开这个文件想对源码进行一探究竟,但是看着看着就放弃了。

经过之前的注册树模式之后,你肯定就会明白这行代码会返回什么Container::get('app')

这行代码返回就是app的实例,可以进行简单的断点一下。

可以看到返回就是app类里边的众多属性。

所以说注册树模式不会的在继续返回去看之前写的,要不越看越迷糊。

那么框架中的容器是怎么定义的呢!它到底是怎么实现的呢!

也就是只需要去关注这个get()方法做的事情就可以了。

代码就会追踪到文件thinkphp/library/think/Container.php中的get()方法

这里的getInstance()方法不陌生了吧!这就是上文说过的单例模式。

可以进行代码追踪getInstance()这个方法,你就会在同文件中看到这个单例模式的方法,返回Container实例。

Container实例调用make方法

代码static::getInstance()返回了Container的实例后,就会去调用本类的make方法,接下来就是对make方法进行详解了。

在开始阅读make方法里边的源码之前,我们需要先对几个属性进行简单的梳理一下。

这四个属性一定要有点印象,并且一定要区别instance和instances。

这俩个属性一个是单例模式返回当前类的实例,一个是容器中的所有的实例。

第一次执行结果

/** * 创建类的实例 * @access public * @param string $abstract 类名或者标识 * @param array|true $vars 变量 * @param bool $newInstance 是否每次创建新的实例 * @return object */ public function make($abstract, $vars = [], $newInstance = false) { // 判断$vars这个变量是否为true if (true === $vars) { // 总是创建新的实例化对象 $newInstance = true; $vars = []; } // app 这里就是在容器别名里获取传递过来的app 如果没有则就是app $abstract = isset($this->name[$abstract]) ? $this->name[$abstract] : $abstract; // 从容器实例中获取 如果存在则直接返回对应的实例 也就是使用注册树模式 if (isset($this->instances[$abstract]) && !$newInstance) { return $this->instances[$abstract]; } // think\App 从容器标识中获取 if (isset($this->bind[$abstract])) { // 将think\App 复制给$concrete变量 $concrete = $this->bind[$abstract]; // 用于代表匿名函数的类 判断是不是闭包 if ($concrete instanceof Closure) { $object = $this->invokeFunction($concrete, $vars); } else { // $this->name['app'] = think\App $this->name[$abstract] = $concrete; // 在执行一次本类的make方法,也就是本方法 return $this->make($concrete, $vars, $newInstance); } } else { $object = $this->invokeClass($abstract, $vars); } if (!$newInstance) { $this->instances[$abstract] = $object; } return $object; }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

这是第二次执行流程

public function make($abstract, $vars = [], $newInstance = false) { // 判断$vars这个变量是否为true if (true === $vars) { // 总是创建新的实例化对象 $newInstance = true; $vars = []; } // app 这里就是在容器别名里获取传递过来的app 如果没有则就是app // 第二次执行时 $abstract = think\App $abstract = isset($this->name[$abstract]) ? $this->name[$abstract] : $abstract; // 从容器实例中获取 如果存在则直接返回对应的实例 也就是使用注册树模式 if (isset($this->instances[$abstract]) && !$newInstance) { return $this->instances[$abstract]; } // think\App 从容器标识中获取 // 第二次执行$this->bind['think\App']不存在走else if (isset($this->bind[$abstract])) { // 将think\App 复制给$concrete变量 $concrete = $this->bind[$abstract]; // 用于代表匿名函数的类 判断是不是闭包 if ($concrete instanceof Closure) { $object = $this->invokeFunction($concrete, $vars); } else { // $this->name['app'] = think\App $this->name[$abstract] = $concrete; // 在执行一次本类的make方法,也就是本方法 // think\App return $this->make($concrete, $vars, $newInstance); } } else { // think\App $object = $this->invokeClass($abstract, $vars); } if (!$newInstance) { // 把创建的容器存起来 //$this->instances['think\App'] = $object; $this->instances[$abstract] = $object; } return $object; }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

public function invokeClass($class, $vars = []) { try { /** * ReflectionClass Object ( [name] => think\App ) */ // 这里就是之前文章提到的反射 $reflect = new ReflectionClass($class); if ($reflect->hasMethod('__make')) { $method = new ReflectionMethod($class, '__make'); if ($method->isPublic() && $method->isStatic()) { $args = $this->bindParams($method, $vars); return $method->invokeArgs(null, $args); } } // 通过反射获取think\App的构造函数 $constructor = $reflect->getConstructor(); $args = $constructor ? $this->bindParams($constructor, $vars) : []; // 从给出的参数创建一个新的类实例 return $reflect->newInstanceArgs($args); } catch (ReflectionException $e) { throw new ClassNotFoundException('class not exists: ' . $class, $class); } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

执行流程图

既然把代码都理清楚了,这时来理一下执行的流程图可以看的更清晰。

invokeClass方法详细解析

不管是阅读完上边的代码流程,还是上图的流程图,肯定都知道了最终代码会走向一个方法invokeClass,就是这个方法。

这个方法中全部都是利用反射的知识点,不会的在去看上文或者之前的文章吧!

在invokeClass方法中,最重要的就是绑定参数的这个方法bindParams,这个方法里边也全部运用的是反射。

所以在容器中反射起到的作用有多大就不用在去做过多的说明了。

在这之前需要把这块说明一下,看到这个__make方法,咔咔是记忆尤深哈!

这个方法在之前学习config源码配置那一篇文章中咔咔说暂时略过,因为当时所储备的知识点和框架代码执行流程还没到说明__make这个方法的阶段。

为了就是在容器这里详细的说明__make这个方法的作用。

当你打印reflect这个变量的值时会返回俩个反射类的对象,如下图。

代码$reflect->hasMethod('__make')就是判断此反射类里边是否存在__make函数

代码$method = new ReflectionMethod($class, '__make');就是执行反射类的一个方法 这里就指的是__make方法

当断点这个method就会返回俩个存在__make反射类,这里是因为断点了只有显示了俩个反射类。

这里主要谈论think\Config.

最后一行代码$method->isPublic() && $method->isStatic()就是判断方法是不是公公共的 判断方法是不是静态的

直到运行到$args = $this->bindParams($method, $vars);这行才会进入到bindParams方法,这个方法也会在下文给出详细的解析。

解析bindParams方法

接下来就解析一下bindParams这个方法。

关于参数传递的就是一个反射类 第二个参数暂时不做说明,目前还没有遇到响应的场景。

第一个参数值$reflect

使用反射方法$reflect->getNumberOfParameters()获取反射类中对应的方法中的参数数目。按照上文的就是__make方法。容器代码中只获取过俩个方法的参数数目,一个是__make方法,一个是就是反射类中的构造函数。

由于目前还没有传递vars变量的场景,所以这块的内容暂时不去研究它直接略过。

代码$params = $reflect->getParameters();也是使用反射获取方法的参数。

打印出来可以看到的结果是俩组数据。

那么这这组数据是从哪里来的呢!往上翻一下,看一下$reflect这个参数是什么就明白了。

think\App这个反射类是没有__make方法的,所以会获取构造函数中的参数。

然后think\Log反射类中存在__make方法,于是就会返回__make的参数,如下图。

就像类似于think\Log这样的类,既有__make方法,也存在构造函数,就会走俩次bindParams方法,这个应该都明白,正是下图逻辑。

在接下来就是循环反射类中获取的参数。

获取参数名、和获取对应的反射类

最后将获取出来的反射类传递给getObjectParam方法。

在这个getObjectParam方法中并没有多少内容。

由于$vars从头到尾都是空数组所以去除数组第一个的操作和判断是否为闭包都不会执行。

最终会在返回去执行make方法

然后make方法会直接从容器中返回这个实例

当一个反射类存在__make方法时,最终就会执行return $method->invokeArgs(null, $args);,带参数执行反射类方法

使用容器来调用配置类

既然已经把容器源码读了一次了,可不可以使用容器来实现呢!

那当然是可以的了,这里需要注意一下咔咔的命名空间,这里由于为了以后回顾方便把类名也起成了Container了,所以给加了一个别名,你们在使用的时候是不需要的哈!

截止到这里容器的源码就讲解的差不多了,后边咔咔会做一个完整的流程图,提供改大家查看。

八、容器源码阅读后总结

注册模式

本文先从俩个设计模式开头,分别为单例模式和注册树模式。

单例模式简单理解就是在应用程序声明周期内只会返回一个实例对象,不会再去创建新的对象。

注册树模式理解就是会把程序中使用的对象都会存放在一颗树上,使用的时候直接从树上获取对象直接使用即可。

控制反转依赖注入

控制反转和依赖注入千万不要让名字把人虎住了,俩个看待一个事件的问题不同,一个是站在容器角度,一个是站在应用程序角度。

从容器角度来看,容器控制着应用程序,由容器反向的向应用程序注入外部资源

从应用程序的角度来看,应用程序依赖容器创建并注入它所需的外部资源。

反射

反射没有什么需要总结的,打开文档看一下就明白了,重要的要学会使用并且知道各自什么意思学会灵活运用即可。

容器源码解析

容器的源码看完后你会发现用的东西就是上边说的三个知识点形成的,运用注册模式来对容器中的对象管理。

对于这个图需要牢牢记住,在源码中就使用的这四个属性走来走去的。

在一个就是代码的执行流程

在容器中最重要的方法就是invokeClass和bindParams这俩个方法跟这咔咔的思路走就没有什么问题,跟这断点的流程一点一点执行。

这块看的时候估计有点绕,但是仔细看完之后你会发现可以学到很多东西

坚持学习、坚持写博、坚持分享是咔咔从业以来一直所秉持的信念。希望在偌大互联网中咔咔的文章能带给你一丝丝帮助。我是咔咔,下期见。

Java ThinkPHP 容器

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:分享一些整理的HTTP状态码及其详解
下一篇:【2020华为云AI实战营】基于华为云ModelArts——物体检测YOLOv3实践笔记分享
相关文章