如何使用 php 写一个类似于 laravel 框架的服务容器?

这篇文章可能文字不会太多,毕竟说再多都还不如直接看代码来的实在 😀,以下我会把核心的代码都先贴出来,里面都有比较完善的注释信息,可以对着看。另外如果自己测试的话,可以直接下载我的源码,关于如何测试,源码中都有示例代码。

以下是实现容器的核心代码


<?php
/**
 * 实现一个简单的 php 容器
 *
 * Created by PhpStorm
 * User: Alex
 * Date: 2021-08-03 17:51
 * E-mail: <276558492@qq.com>
 */

class Container
{

    /**
     * 当前全局可用的容器(如果有)
     *
     * @var static
     */
    private static $instance;

    /**
     * 容器的绑定
     *
     * @var array[]
     */
    private $bindings = [];

    /**
     * 容器的共享实例
     *
     * @var object[]
     */
    private $instances = [];

    public function __construct()
    {
        $this->instances[Container::class] = $this;
    }

    public static function getInstance()
    {
        if (is_null(self::$instance)) {
            self::$instance = new self;
        }

        self::$instance->instances[Container::class] = self::$instance;

        return self::$instance;
    }

    /**
     * 在容器中注册共享绑定
     *
     * @param $abstract
     * @param $concrete
     */
    public function singleton($abstract, $concrete)
    {
        $this->bind($abstract, $concrete, true);
    }

    /**
     * 向容器注册绑定
     *
     * @param $abstract
     * @param $concrete
     * @param false $shared
     */
    public function bind($abstract, $concrete, $shared = false)
    {
        if ($concrete instanceof Closure) {
            $this->bindings[$abstract] = compact('concrete', 'shared');
        } else {
            if (! is_string($concrete) || ! class_exists($concrete)) {
                throw new InvalidArgumentException('Argument 2 must be callback or class.');
            }
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

    }

    /**
     * 将现有实例注册为容器中的共享实例
     *
     * @param string $abstract
     * @param mixed $instance
     * @return mixed
     */
    public function instance($abstract, $instance)
    {
        $this->instances[$abstract] = $instance;

        return $instance;
    }

    /**
     * 从容器解析给定类型
     *
     * @param string $abstract  目标类的名称
     * @param array $parameters  实例化目标类时所需要的参数(非对象类型约束参数数组)
     * @return mixed|object
     */
    public function make(string $abstract, array $parameters = [])
    {

        if (! isset($this->instances[$abstract]) && ! isset($this->bindings[$abstract])) {
            if (! class_exists($abstract)) throw new InvalidArgumentException("Target class [$abstract] does not exist.");
        }

        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        try {

            if (isset($this->bindings[$abstract])) {
                $concrete = $this->bindings[$abstract]['concrete'];
                if (is_callable($concrete)) {
                    $instance = $this->resolveCallable($concrete, $parameters);
                } else {
                    $instance = $this->resolveClass($concrete, $parameters);
                }
            } else {
                $instance = $this->resolveClass($abstract, $parameters);
            }

            if (isset($this->bindings[$abstract]) && $this->bindings[$abstract]['shared']) {
                $this->instances[$abstract] = $instance;
            }

            return $instance;
        } catch (\Exception $exception) {
            echo($exception->getMessage() . PHP_EOL);
            print_r($exception->getTraceAsString());
        }

    }

    /**
     * 解决回调函数时的依赖
     *
     * @param callable $callbackName  目标回调函数
     * @param array $realArgs
     * @return mixed
     * @throws ReflectionException
     */
    private function resolveCallable(callable $callbackName, array $realArgs = [])
    {
        $reflector = new ReflectionFunction($callbackName);

        // 获取回调函数的参数列表
        $parameters = $reflector->getParameters();
        $list = [];
        if (count($parameters) > 0) {
            $list = $this->resolveDependencies($parameters, $realArgs);
        }

        // 调用函数参数
        return $reflector->invokeArgs($list);
    }

    /**
     * 解决对象时的依赖
     *
     * @param string|object $className  目标类的名称
     * @param array $realArgs
     * @return object  目标类对应的实例对象
     * @throws ReflectionException
     */
    private function resolveClass($className, array $realArgs = [])
    {
        try {
            // 对目标类进行反射(解析其方法、属性)
            $reflector = new ReflectionClass($className);
        } catch (ReflectionException $e) {
            throw new RuntimeException("Target class [$className] does not exist.", 0, $e);
        }

        if (! $reflector->isInstantiable()) {  // 检查类是否可以实例化
            throw new RuntimeException("Target class [$className] is not instantiable.");
        }

        // 获取目标类的构造函数,当类不存在构造函数时返回 null
        $constructor = $reflector->getConstructor();
        // 没有构造函数,则直接实例化
        if (is_null($constructor)) {
            // return new $className;  // 或者也可以直接这样去实例化,因为目标类没有构造函数,不需要传参数
            return $reflector->newInstance();
        }

        // 获取构造函数的参数列表
        $parameters = $constructor->getParameters();
        // 递归解析构造函数的参数
        $list = $this->resolveDependencies($parameters, $realArgs);

        // 从给出的参数创建一个新的类实例
        return $reflector->newInstanceArgs($list);
    }

    /**
     * 递归解析依赖树
     *
     * @param array $dependencies  目标类的构造函数参数列表
     * @param array $parameters  实例化目标类时的其他参数(非类型提示参数)
     * @return array  实例化目标类时构造函数所需的所有参数
     */
    private function resolveDependencies(array $dependencies, array $parameters = [])
    {
        // 用于存储所有的参数
        $results = [];

        foreach ($dependencies as $dependency) {

            // 获取类型提示类
            $obj = $dependency->getClass();

            // 如果类为 null,则表示依赖项是字符串或其他类型
            if (is_null($obj)) {

                $parameterName = $dependency->getName();  // 获取参数的名称

                // 检查参数是否有默认值
                if (! $dependency->isDefaultValueAvailable()) {
                    if (! isset($parameters[$parameterName])) {
                        throw new RuntimeException($parameterName . ' has no value');
                    } else {
                        $results[] = $parameters[$parameterName];
                    }
                } else {  // 参数有默认值的时候
                    if (isset($parameters[$parameterName])) {
                        $results[] = $parameters[$parameterName];
                    } else {
                        $results[] = $dependency->getDefaultValue();  // 获取参数的默认值
                    }
                }

            } else {  // 类型提示确定是一个类时,则需要递归处理依赖项
                $objName = $obj->getName();  // 获取依赖项的类名
                if (! class_exists($objName)) {
                    throw new RuntimeException('Unable to load class: ' . $objName);
                } else {
                    $results[] = $this->make($objName);
                }
            }

        }

        return $results;
    }

}



左诗右码
85 声望11 粉丝

三观比五官更正,思想比套路更深。常用技术栈PHP、Go、Python,享受编程,平时爱好写点文章。V公主号:「左诗右码」,欢迎关注交流。