8

从零实现一个 PHP 微框架 - IoC 容器

 3 years ago
source link: https://blog.ixk.me/post/implement-a-php-microframework-from-zero-3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
本文最后更新于 268 天前,文中所描述的信息可能已发生改变

差不多该写写该系列文章了,咕了好几天 ?。

在 XK-PHP 中 IoC 容器是框架的核心,其掌管着框架中实例的存储和初始化,并提供自动依赖注入等功能,我们可以把 IoC 容器看成一个拥有存储功能的工厂,当我们需要某个实例的时候,工厂会依靠需求将实例组装好并返回给需求者,如果实例是单例的,那么制作好的实例就可以存到仓库中,当需求者再次需要的时候就可以直接返回实例。需求者无需关心实例是如何制造的,只需要将需求提交给工厂即可。这看起来似乎就是工厂模式?IoC 容器和 工厂模式 很类似,但是工厂模式注入的依赖是定死的,而 IoC 容器可以依据需求按需注入依赖。

DI & IoC

由于我之前写过 DI 和 IoC 的介绍文章,这里就不重复写了,链接见下方:

IoC 容器

由于之前的文章已经说明了 IoC 容器的实现了,这里就不再讲解 IoC 容器内部的细节了,本文就只讲述将 IoC 容器集成到我们上次创建的项目之中。

首先,因为我们的容器需要兼容 PSR-11 ,那么就需要引入 psr/container 的包,来引入 ContainerInterface 接口:

1composer require psr/container

然后容器需要使用两个自定义函数,我们将其放到 app/Helper/functions.php 中,并修改 composer.json,使函数能被 Composer 自动导入并且全局生效

1<?php
2// functions.php
3
4// 解析 class@method 的字符串,返回 [class, method] 数组
5function str_parse_callback($callback, $default = null)
6{
7    if (is_array($callback)) {
8        return $callback;
9    }
10    if (strpos($callback, '@') !== false) {
11        return explode('@', $callback);
12    }
13    if (strpos($callback, '::') !== false) {
14        return explode('::', $callback);
15    }
16    return [$callback, $default];
17}
18
19// 解析 [class, method] 或 [class, split, method] 到 class@method 或 class::method
20function str_stringify_callback(
21    $callback,
22    $default = null,
23    bool $isStatic = false
24) {
25    $split = $isStatic ? '::' : '@';
26    if (is_array($callback)) {
27        return implode($split, $callback);
28    }
29    if (preg_match('/@|::/', $callback) > 0) {
30        return $callback;
31    }
32    if ($default === null) {
33        return $callback;
34    }
35    return "{$callback}{$split}{$default}";
36}
1{
2  "autoload": {
3    "psr-4": {
4      "App\\": "app/"
5    },
6    // 新增
7    "files": ["app/Helper/functions.php"]
8  }
9}

修改了 composer.jsonautoload 后需要运行以下命令后才能生效

1composer dump-auto

然后就可以写容器的代码了,首先创建 app/Kernel/Container.php 的文件,输入以下代码,本文的容器代码和之前的文章中的容器不一样,但是流程是一样的:

1<?php
2
3namespace App\Kernel;
4
5use Closure;
6use Psr\Container\ContainerInterface;
7use ReflectionClass;
8use ReflectionException;
9use ReflectionFunction;
10use ReflectionMethod;
11use ReflectionParameter;
12use RuntimeException;
13use function array_pad;
14use function class_exists;
15use function compact;
16use function count;
17use function is_array;
18use function is_bool;
19use function is_int;
20use function is_string;
21use function preg_match;
22use function str_parse_callback;
23use function strpos;
24
25/**
26 * IoC 容器,兼容 PSR-11
27 */
28class Container implements ContainerInterface
29{
30    /**
31     * 容器中存储依赖的数组
32     * 存储的是闭包,运行闭包会返回对应的依赖实例
33     *
34     * @var array
35     */
36    protected $bindings = [];
37
38    /**
39     * 绑定方法
40     *
41     * @var array
42     */
43    protected $methodBindings = [];
44
45    /**
46     * 已创建的单例实例
47     *
48     * @var array
49     */
50    protected $instances = [];
51
52    /**
53     * 自动通过类名绑定类
54     *
55     * @var bool
56     */
57    protected $autobind = true;
58
59    /**
60     * 依赖别名
61     *
62     * @var string[]
63     */
64    protected $aliases = [];
65
66    /**
67     * 绑定依赖
68     *
69     * @param string|array $abstract 依赖名或者依赖列表
70     * @param Closure|string|null $concrete 依赖闭包
71     *
72     * @param bool $shared
73     * @param bool|string $alias
74     * @param bool $overwrite
75     * @return  Container
76     */
77    public function bind(
78        $abstract,
79        $concrete = null,
80        bool $shared = false,
81        $alias = false,
82        bool $overwrite = false
83    ): Container {
84        // 同时绑定多个依赖
85        if (is_array($abstract)) {
86            foreach ($abstract as $_abstract => $value) {
87                if (is_int($_abstract)) {
88                    $_abstract = $value;
89                }
90                $_concrete = null;
91                $_shared = false;
92                $_alias = false;
93                $_overwrite = false;
94                if (is_bool($value)) {
95                    $_shared = $value;
96                } elseif (is_array($value)) {
97                    [$_concrete, $_shared, $_alias, $_overwrite] = array_pad(
98                        $value,
99                        3,
100                        false
101                    );
102                }
103                $this->bind(
104                    $_abstract,
105                    $_concrete === false ? null : $_concrete,
106                    $_shared,
107                    $_alias,
108                    $_overwrite
109                );
110            }
111            return $this;
112        }
113        [$abstract, $alias] = $this->getAbstractAndAliasByAlias(
114            $abstract,
115            $alias
116        );
117        // 为了方便绑定依赖,可以节省一个参数
118        if ($concrete === null) {
119            $concrete = $abstract;
120        }
121        $this->setBinding($abstract, $concrete, $shared, $overwrite);
122        if ($alias) {
123            $this->alias($abstract, $alias);
124        }
125        // 返回 this 使其支持链式调用
126        return $this;
127    }
128
129    // 设置 binding
130    protected function setBinding(
131        string $abstract,
132        $concrete,
133        bool $shared = false,
134        bool $overwrite = false
135    ): void {
136        $abstract = $this->getAbstractByAlias($abstract);
137        // 传入的默认是闭包,如果没有传入闭包则默认创建
138        if (!$concrete instanceof Closure) {
139            $concrete = function (Container $c, array $args = []) use (
140                $concrete
141            ) {
142                return $c->build($concrete, $args);
143            };
144        }
145        // 判断是否是单例,是否被设置过
146        if (!$overwrite && $shared && isset($this->bindings[$abstract])) {
147            throw new RuntimeException(
148                "Target [$abstract] is a singleton and has been bind"
149            );
150        }
151        // 设置绑定的闭包
152        $this->bindings[$abstract] = compact('concrete', 'shared');
153    }
154
155    // 获取 binding
156    protected function getBinding(string $abstract)
157    {
158        $abstract = $this->getAbstractByAlias($abstract);
159        if (!isset($this->bindings[$abstract])) {
160            // 尝试自动绑定
161            if (
162                $this->autobind &&
163                $abstract[0] !== '$' &&
164                class_exists($abstract)
165            ) {
166                $this->setBinding($abstract, $abstract);
167            } else {
168                throw new RuntimeException(
169                    "Target [$abstract] is not binding or fail autobind"
170                );
171            }
172        }
173        return $this->bindings[$abstract];
174    }
175
176    // 判断 binding 是否存在
177    protected function hasBinding(string $abstract): bool
178    {
179        $abstract = $this->getAbstractByAlias($abstract);
180        return isset($this->bindings[$abstract]);
181    }
182
183    /**
184     * 实例化对象
185     *
186     * @param string $abstract 对象名称
187     * @param array $args
188     *
189     * @return  mixed
190     */
191    public function make(string $abstract, array $args = [])
192    {
193        $abstract = $this->getAbstractByAlias($abstract);
194        $binding = $this->getBinding($abstract);
195        $concrete = $binding['concrete'];
196        $shared = $binding['shared'];
197        // 判断是否是单例,若是单例并且已经实例化过就直接返回实例
198        if ($shared && isset($this->instances[$abstract])) {
199            return $this->instances[$abstract];
200        }
201        // 构建实例
202        $instance = $concrete($this, $args);
203        // 判断是否是单例,若是则设置到容器的单例列表中
204        if ($shared) {
205            $this->instances[$abstract] = $instance;
206        }
207        return $instance;
208    }
209
210    /**
211     * 绑定单例
212     *
213     * @param string $abstract 依赖名称
214     * @param mixed $concrete 依赖闭包
215     * @param bool|string $alias
216     *
217     * @param bool $overwrite
218     * @return  Container
219     */
220    public function singleton(
221        string $abstract,
222        $concrete = null,
223        $alias = false,
224        bool $overwrite = false
225    ): Container {
226        $this->bind($abstract, $concrete, true, $alias, $overwrite);
227        return $this;
228    }
229
230    /**
231     * 绑定已实例化的单例
232     *
233     * @param string $abstract 依赖名称
234     * @param mixed $instance 已实例化的单例
235     * @param string|false $alias
236     *
237     * @param bool $overwrite
238     * @return  Container
239     */
240    public function instance(
241        string $abstract,
242        $instance,
243        $alias = false
244    ): Container {
245        [$abstract, $alias] = $this->getAbstractAndAliasByAlias(
246            $abstract,
247            $alias
248        );
249        $this->instances[$abstract] = $instance;
250        $this->bind(
251            $abstract,
252            function () use ($instance) {
253                return $instance;
254            },
255            true,
256            $alias,
257            true
258        );
259        return $this;
260    }
261
262    /**
263     * 构建实例
264     *
265     * @param Closure|string $class 类名或者闭包
266     * @param array $args
267     * @return  mixed
268     *
269     * @throws ReflectionException
270     */
271    public function build($class, array $args = [])
272    {
273        if ($class instanceof Closure) {
274            return $class($this, $args);
275        }
276        if (!class_exists($class)) {
277            return $class;
278        }
279        // 取得反射类
280        $reflector = new ReflectionClass($class);
281        // 检查类是否可实例化
282        if (!$reflector->isInstantiable()) {
283            // 如果不能,意味着接口不能正常工作,报错
284            throw new RuntimeException("Target [$class] is not instantiable");
285        }
286        // 取得构造函数
287        $constructor = $reflector->getConstructor();
288        // 检查是否有构造函数
289        if ($constructor === null) {
290            // 如果没有,就说明没有依赖,直接实例化
291            $instance = new $class();
292        } else {
293            // 返回已注入依赖的参数数组
294            $dependency = $this->injectingDependencies($constructor, $args);
295            // 利用注入后的参数创建实例
296            $instance = $reflector->newInstanceArgs($dependency);
297        }
298        return $instance;
299    }
300
301    /**
302     * 注入依赖
303     *
304     * @param ReflectionFunction|ReflectionMethod $method
305     * @param array $args
306     *
307     * @return  array
308     */
309    protected function injectingDependencies($method, array $args = []): array
310    {
311        $dependency = [];
312        $parameters = $method->getParameters();
313        foreach ($parameters as $parameter) {
314            if (isset($args[$parameter->name])) {
315                $dependency[] = $args[$parameter->name];
316                continue;
317            }
318            // 利用参数的类型声明,获取到参数的类型,然后从 bindings 中获取依赖注入
319            $dependencyClass = $parameter->getClass();
320            if ($dependencyClass === null) {
321                $dependency[] = $this->resolvePrimitive($parameter);
322            } else {
323                // 实例化依赖
324                $dependency[] = $this->resolveClass($parameter);
325            }
326        }
327        return $dependency;
328    }
329
330    /**
331     * 处理非类的依赖
332     *
333     * @param ReflectionParameter $parameter
334     *
335     * @return  mixed
336     */
337    protected function resolvePrimitive(ReflectionParameter $parameter)
338    {
339        $abstract = $parameter->name;
340        // 通过 bind 获取
341        if ($this->hasBinding('$' . $parameter->name)) {
342            $abstract = '$' . $parameter->name;
343        }
344        // 匹配别名
345        if ($this->isAlias($abstract)) {
346            $abstract = $this->getAbstractByAlias($abstract);
347        }
348        try {
349            $concrete = $this->getBinding($abstract)['concrete'];
350        } catch (RuntimeException $e) {
351            $concrete = null;
352        }
353        if ($concrete !== null) {
354            return $concrete instanceof Closure ? $concrete($this) : $concrete;
355        }
356        if ($parameter->isDefaultValueAvailable()) {
357            return $parameter->getDefaultValue();
358        }
359        throw new RuntimeException("Target [$$parameter->name] is not binding");
360    }
361
362    /**
363     * 处理类依赖
364     *
365     * @param ReflectionParameter $parameter
366     *
367     * @return  mixed
368     */
369    protected function resolveClass(ReflectionParameter $parameter)
370    {
371        try {
372            return $this->make($parameter->getClass()->name);
373        } catch (RuntimeException $e) {
374            if ($parameter->isDefaultValueAvailable()) {
375                return $parameter->getDefaultValue();
376            }
377            throw $e;
378        }
379    }
380
381    /**
382     * 设置自动绑定
383     *
384     * @param   bool  $use  是否自动绑定类
385     *
386     * @return  void
387     */
388    public function useAutoBind(bool $use): void
389    {
390        $this->autobind = $use;
391    }
392
393    /**
394     * 判断是否绑定了指定的依赖
395     *
396     * @param $id
397     * @return  bool
398     */
399    public function has($id): bool
400    {
401        return $this->hasBinding($id);
402    }
403
404    /**
405     * 同 make
406     *
407     * @param   string  $id  对象名称
408     *
409     * @return  mixed
410     */
411    public function get($id)
412    {
413        return $this->make($id);
414    }
415
416    public function hasMethod(string $method): bool
417    {
418        return isset($this->methodBindings[$method]);
419    }
420
421    public function bindMethod(string $method, $callback): void
422    {
423        $this->methodBindings[$method] = $callback;
424    }
425
426    protected function getMethodBind(string $method)
427    {
428        if (isset($this->methodBindings[$method])) {
429            return $this->methodBindings[$method];
430        }
431        throw new RuntimeException("Target [$method] is not binding");
432    }
433
434    public function call(
435        $method,
436        array $args = [],
437        $object = null,
438        $isStatic = false
439    ) {
440        if ($object !== null) {
441            return $this->callMethod($object, $method, $isStatic, $args);
442        }
443        if (
444            is_array($method) ||
445            (is_string($method) && preg_match('/@|::/', $method) > 0)
446        ) {
447            return $this->callClass($method, $args);
448        }
449        if (is_string($method)) {
450            $method = $this->getMethodBind($method);
451        }
452        return $this->callFunction($method, $args);
453    }
454
455    protected function callFunction($method, array $args = [])
456    {
457        $reflector = new ReflectionFunction($method);
458        $dependency = $this->injectingDependencies($reflector, $args);
459        return $reflector->invokeArgs($dependency);
460    }
461
462    /**
463     * @param string|array $target
464     * @param array $args
465     * @return mixed
466     */
467    protected function callClass($target, array $args = [])
468    {
469        $class = null;
470        $method = null;
471        $object = null;
472        $isStatic = false;
473        if (is_string($target)) {
474            $isStatic = strpos($target, '@') === false;
475            [$class, $method] = str_parse_callback($target);
476            $object = $this->bindAndMakeReflection($class, $isStatic);
477        } else {
478            if (count($target) === 3) {
479                [$class, $split, $method] = $target;
480                $isStatic = $split === '::';
481            } else {
482                [$class, $method] = $target;
483            }
484            $object = $this->bindAndMakeReflection($class, $isStatic);
485        }
486        return $this->callMethod($object, $method, $isStatic, $args);
487    }
488
489    protected function bindAndMakeReflection(
490        string $class,
491        bool $isStatic = false
492    ) {
493        if ($isStatic) {
494            return $class;
495        }
496        if (!$this->has($class)) {
497            $this->bind($class);
498        }
499        return $this->make($class);
500    }
501
502    protected function callMethod(
503        $object,
504        $method,
505        $isStatic = false,
506        array $args = []
507    ) {
508        $reflector = new ReflectionMethod($object, $method);
509        $dependency = $this->injectingDependencies($reflector, $args);
510        return $reflector->invokeArgs($isStatic ? null : $object, $dependency);
511    }
512
513    public function isAlias(string $name): bool
514    {
515        return isset($this->aliases[$name]);
516    }
517
518    public function alias(string $abstract, string $alias): void
519    {
520        if ($abstract === $alias) {
521            return;
522        }
523        $this->aliases[$alias] = $abstract;
524    }
525
526    public function getAlias($abstract)
527    {
528        foreach ($this->aliases as $alias => $value) {
529            if ($value === $abstract) {
530                return $alias;
531            }
532        }
533        return $abstract;
534    }
535
536    public function removeAlias($alias): void
537    {
538        unset($this->aliases[$alias]);
539    }
540
541    protected function getAbstractByAlias($alias)
542    {
543        return $this->aliases[$alias] ?? $alias;
544    }
545
546    protected function getAbstractAndAliasByAlias(
547        $alias,
548        $inAlias = false
549    ): array {
550        $abstract = $this->getAbstractByAlias($alias);
551        if ($alias === $abstract) {
552            return [$abstract, $inAlias];
553        }
554        if (!$inAlias) {
555            return [$abstract, $alias];
556        }
557        return [$abstract, $inAlias];
558    }
559}

完成以上步骤后就可以测试下容器是否可以正常工作了,首先创建几个测试类:

1<?php
2
3namespace App\Entry;
4
5// app/Entry/Cat.php
6class Cat
7{
8    public function name(): string
9    {
10        return "Cat";
11    }
12}
13
14// app/Entry/Dog.php
15class Dog
16{
17    public function name(): string
18    {
19        return "Dog";
20    }
21}
22
23// app/Entry/CatShop.php
24class CatShop
25{
26    /**
27     * @var Cat
28     */
29    protected $cat;
30
31    public function __construct(Cat $cat)
32    {
33        $this->cat = $cat;
34    }
35
36    public function getName(): string
37    {
38        return $this->cat->name();
39    }
40}
41
42// app/Entry/DogShop.php
43class DogShop
44{
45    /**
46     * @var Dog
47     */
48    protected $dog;
49
50    public function __construct(Dog $dog)
51    {
52        $this->dog = $dog;
53    }
54
55    public function getName(): string
56    {
57        return $this->dog->name();
58    }
59}

然后修改 public/index.php 文件,把之前 Test 相关的代码都删了,然后添加以下代码:

1<?php
2// public/index.php
3
4$container = new Container();
5
6$container->bind(Cat::class, null, 'cat');
7$container->bind(Dog::class, null, 'dog');
8
9$container->singleton(CatShop::class);
10$container->singleton(DogShop::class);
11
12/* @var CatShop $cat_shop */
13$cat_shop = $container->make(CatShop::class);
14/* @var DogShop $dog_shop */
15$dog_shop = $container->make(DogShop::class);
16
17echo $cat_shop->getName() . "\n"; // Cat
18echo $dog_shop->getName() . "\n"; // Dog

添加完毕后就可以进行测试了,运行 index.php

eb82605c 82fd 47b7 b015 18abda0f8c3a

可以看到,我们并没有写赋值 CatDog 的代码,按理使用的时候应该为 null,而 CatShopDogShop 却可以正常使用,这是因为 IoC 容器中为我们完成了赋值的工作,我们只需要关心需要使用什么而不需要关心依赖是如何来的,这样就可以很好的解耦代码,同时也简化了代码的编写。

结语。。。实在不知道写什么了 ?。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK