开头语:TP5.1容器是整个框架的核心之一,理解容器代码,会对阅读tp5.1代码有很大帮助,如果容器这一关没有学好,下面的代码会越看越懵逼。

要看懂容器这块,首先要有4个知识点储备: ArrayAccess, IteratorAggregate, Countable以及反射。这4块地方可以看手册,这里进行简要说明

ArrayAccess:提供将对象当成数组一样使用的能力。继承ArrayAccess需要实现其4个方法,下面将结合代码分析。

 

IteratorAggregate:提供像循环数组一样循环对像的能力,继承这个接口必须要实现getIterator这个方法,这个方法将返回一个ArrayIterator对象(对象必须传入一个数组,通常一般都是成员属性作为这个数组参数),这样foreach就会循环这个对象,实际上也就是循环这个成员属性数组的内容。

 

Countable:提供了对象使用count()函数的能力,继承Countable并且实现Countable的count()方法,在使用count(obj)时,会调用对象的count方法。

 

反射:反射API提供了类在运行时操作类内部的能力。使用反射主要是用来对类以及类的参数进行分析。

 

下面先看容器类继承ArrayAccess, IteratorAggregate, Countable的代码:

//如果是对象则绑定到注册池,如果是类名则放入类名池(bind池),如果是闭包也放入类名池(bind池),bind池是key(名称索引)-value(类名/闭包)
    public function __set($name, $value)
    {
        $this->bindTo($name, $value);
    }

    //获取一个不存在或者私有变量时调用,这样可以将成员属性不写在代码里
    public function __get($name)
    {
        return $this->make($name);
    }

    //检查一个不可被外部访问的成员属性是否设置
    public function __isset($name)
    {
        return $this->bound($name);
    }

    //销毁一个不可被外部访问的成员属性的值
    public function __unset($name)
    {
        $this->delete($name);
    }

    public function offsetExists($key)
    {
        return $this->__isset($key);
    }

    public function offsetGet($key)
    {
        return $this->__get($key);
    }

    public function offsetSet($key, $value)
    {
        $this->__set($key, $value);
    }

    public function offsetUnset($key)
    {
        $this->__unset($key);
    }

    //Countable
    public function count()
    {
        return count($this->instances);
    }

    //IteratorAggregate,可以使用foreach遍历注册池中的对象
    public function getIterator()
    {
        return new ArrayIterator($this->instances);
    }

    //var_dump调用时调用
    public function __debugInfo()
    {
        $data = get_object_vars($this);
        unset($data['instances'], $data['instance']);

        return $data;
    }

1.count()方法用来计算容器注册池中实例的个数

2.getIterator()方法用来遍历注册池的每一个实例

3.__debugInfo()方法用来自定义var_dump方法的内容,这个方法应该是用于放便调试。

4.ArrayAccess和魔术方法__set、__get、__isset()、__unset()一起搭配使用,比方说__set()调用场景是当一个私有属性name需要被设置时:$obj->name,会触发__set(),而搭配上

ArrayAccess,就可以这么用:$obj['name'] = xx; 因此ArrayAccess提供的是一个允许像使用数组那样,使用对象的能力。在这个例子中,__set()需要被写到对应的ArrayAccess方法中去,如代码中的offsetSet(),因为ArrayAccess中触发offsetSet()的就是这样的赋值形式__isset()。其余的几个搭配类比。

5.之所以使用这些魔术方法,是因为容器是封闭的(protected),所有成员属性不对外开放.但又需要暴露一些可操作的接口。

 

__get()方法作为容器对外开发的接口,它是实例化对象的关键,有这样一段代码,该代码是app类的initialize函数一部分:

$this->configExt = $this->env->get('config_ext', '.php');

        // 加载惯例配置文件,优先级最低
        $this->config->set(include $this->thinkPath . 'convention.php');

        //设置路径环境变量
        $this->env->set([
            'think_path'   => $this->thinkPath,
            'root_path'    => $this->rootPath,
            'app_path'     => $this->appPath,
            'config_path'  => $this->configPath,
            'route_path'   => $this->routePath,
            'runtime_path' => $this->runtimePath,
            'extend_path'  => $this->rootPath . 'extend' . DIRECTORY_SEPARATOR,
            'vendor_path'  => $this->rootPath . 'vendor' . DIRECTORY_SEPARATOR,
        ]);

使用代码追踪工具会发现容器里根本没有设置config或者env的属性,这就是魔术方法__get()在起作用,访问一个不存在的属性会触发__get方法。__get方法就会调用this->make

去创建这个属性名称的实例。这样只要写上$this->config这种不存在的属性,就启动后容器去获取实例。后面的 $this->config->get() 中的get方法是该实例的成员方法.

查看容器类可以发现,这些属性全部写成了注释放在了类的上头,都没有在类中设置:

/**
 * @package think
 * @property Build          $build
 * @property Cache          $cache
 * @property Config         $config
 * @property Cookie         $cookie
 * @property Debug          $debug
 * @property Env            $env
 * @property Hook           $hook
 * @property Lang           $lang
 * @property Middleware     $middleware
 * @property Request        $request
 * @property Response       $response
 * @property Route          $route
 * @property Session        $session
 * @property Template       $template
 * @property Url            $url
 * @property Validate       $validate
 * @property View           $view
 * @property route\RuleName $rule_name
 * @property Log            $log
 */
class Container implements ArrayAccess, IteratorAggregate, Countable

容器主要属性归纳:

容器 mounts属性 容器类型代码_获取参数

下面再来看看容器使用反射的方法:

/**
     * 执行函数或者闭包方法 支持参数调用
     * @access public
     * @param  mixed  $function 函数或者闭包
     * @param  array  $vars     参数
     * @return mixed
     */
    public function invokeFunction($function, $vars = [])
    {
        try {
            $reflect = new ReflectionFunction($function);

            $args = $this->bindParams($reflect, $vars);

            return call_user_func_array($function, $args);
        } catch (ReflectionException $e) {
            throw new Exception('function not exists: ' . $function . '()');
        }
    }

    /**
     * 调用反射执行类的方法 支持参数绑定
     * @access public
     * @param  mixed   $method 方法
     * @param  array   $vars   参数
     * @return mixed
     */
    public function invokeMethod($method, $vars = [])
    {
        try {
            if (is_array($method)) {
                $class   = is_object($method[0]) ? $method[0] : $this->invokeClass($method[0]);
                $reflect = new ReflectionMethod($class, $method[1]);
            } else {
                // 静态方法
                $reflect = new ReflectionMethod($method);
            }

            $args = $this->bindParams($reflect, $vars);

            return $reflect->invokeArgs(isset($class) ? $class : null, $args);
        } catch (ReflectionException $e) {
            if (is_array($method) && is_object($method[0])) {
                $method[0] = get_class($method[0]);
            }

            throw new Exception('method not exists: ' . (is_array($method) ? $method[0] . '::' . $method[1] : $method) . '()');
        }
    }

    /**
     * 调用反射执行类的方法 支持参数绑定
     * @access public
     * @param  object  $instance 对象实例
     * @param  mixed   $reflect 反射类
     * @param  array   $vars   参数
     * @return mixed
     */
    public function invokeReflectMethod($instance, $reflect, $vars = [])
    {
        $args = $this->bindParams($reflect, $vars);

        return $reflect->invokeArgs($instance, $args);
    }

    /**
     * 调用反射执行callable 支持参数绑定
     * @access public
     * @param  mixed $callable
     * @param  array $vars   参数
     * @return mixed
     */
    public function invoke($callable, $vars = [])
    {
        if ($callable instanceof Closure) {
            return $this->invokeFunction($callable, $vars);
        }

        return $this->invokeMethod($callable, $vars);
    }

    /**
     * 调用反射执行类的实例化 支持依赖注入
     * @access public
     * @param  string    $class 类名
     * @param  array     $vars  参数
     * @return mixed
     */
    public function invokeClass($class, $vars = [])
    {
        try {
            $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);
                }
            }

            $constructor = $reflect->getConstructor();

            $args = $constructor ? $this->bindParams($constructor, $vars) : [];

            return $reflect->newInstanceArgs($args);

        } catch (ReflectionException $e) {
            throw new ClassNotFoundException('class not exists: ' . $class, $class);
        }
    }

    /**
     * 绑定参数
     * @access protected
     * @param  \ReflectionMethod|\ReflectionFunction $reflect 反射类
     * @param  array                                 $vars    参数
     * @return array
     */
    protected function bindParams($reflect, $vars = [])
    {
        //若无参数则不需要绑定参数
        if ($reflect->getNumberOfParameters() == 0) {
            return [];
        }

        // 判断数组类型 数字数组时按顺序绑定参数
        reset($vars);
        $type   = key($vars) === 0 ? 1 : 0;
        $params = $reflect->getParameters();         //获取参数数组列表

        //对每个参数进行分析
        foreach ($params as $param) {
            $name      = $param->getName();          //获取参数名
            $lowerName = Loader::parseName($name);   //名字转换
            $class     = $param->getClass();         //获取参数的类型提示类,类型为ReflectionClass对象。

            if ($class) {
                $args[] = $this->getObjectParam($class->getName(), $vars);
            } elseif (1 == $type && !empty($vars)) {
                $args[] = array_shift($vars);
            } elseif (0 == $type && isset($vars[$name])) {
                $args[] = $vars[$name];
            } elseif (0 == $type && isset($vars[$lowerName])) {
                $args[] = $vars[$lowerName];
            } elseif ($param->isDefaultValueAvailable()) {
                $args[] = $param->getDefaultValue();
            } else {
                throw new InvalidArgumentException('method param miss:' . $name);
            }
        }

        return $args;
    }

    /**
     * 获取对象类型的参数值
     * @access protected
     * @param  string   $className  类名
     * @param  array    $vars       参数
     * @return mixed
     */
    protected function getObjectParam($className, &$vars)
    {
        $array = $vars;
        $value = array_shift($array);

        if ($value instanceof $className) {
            $result = $value;
            array_shift($vars);
        } else {
            $result = $this->make($className);
        }

        return $result;
    }

怎么理解这段代码呢?来看看图:

容器 mounts属性 容器类型代码_容器 mounts属性_02

一个类的构造函数列表有可能包含两种参数,普通和对象参数。因此,要实现类似自动绑定,就必须要使用反射来分析,那么就会有无限递归的可能。

函数执行的流程:make  ------>  invokeClass  ----> bindParams (如果是对象参数)  【--------->getObjectParam----------->make 如果后面还有对象参数再从重复这个步骤】,可见这样的写法可以无限分析并且自动绑定,但是它这个东西有个缺陷