Composer探索之旅

问题

包依赖管理几乎是每门计算机编程语言都需要面对的问题,php是怎么解决的呢?Composer解决了什么问题?

上古

在语言层面,php提供了require, include, require_once, include_once来引入另外文件里的内容。

现代

在现代的php工程(PHP 5.3.2+)中都会使用composer做项目依赖包的统一管理。

What is composer:

Composer is a tool for dependency management in PHP. 
It allows you to declare the libraries your project depends on and it will manage (install/update) them for you.

它是一个依赖管理工具,帮助你统一定义项目中依赖的包,并提供对依赖的包的安装和更新管理,角色类似于js中的npm, ruby中的rubygems, python中的pipi。

composer的原理:

起点文件:

require __DIR__.'/../vendor/autoload.php';

在项目的入口文件index.php一般都会先执行上面的语句,看看里面的结构:

// autoload.php @generated by Composer

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInitf25e26ff97914aea3d103b42cdacc886::getLoader();

getLoader函数执行最后返回的是\Composer\Autoload\ClassLoader的实例。再看ClassLoader.php:

/**
 * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
 *
 *     $loader = new \Composer\Autoload\ClassLoader();
 *
 *     // register classes with namespaces
 *     $loader->add('Symfony\Component', __DIR__.'/component');
 *     $loader->add('Symfony',           __DIR__.'/framework');
 *
 *     // activate the autoloader
 *     $loader->register();
 *
 *     // to enable searching the include path (eg. for PEAR packages)
 *     $loader->setUseIncludePath(true);
 *

直接打印一下getLoader返回的ClassLoader实例:

Composer\Autoload\ClassLoader Object
(
    [prefixLengthsPsr4:Composer\Autoload\ClassLoader:private] => Array
        (
            [W] => Array
                (
                    [Whoops\] => 7
                )

            ...

        )

    [prefixDirsPsr4:Composer\Autoload\ClassLoader:private] => Array
        (
            [Whoops\] => Array
                (
                    [0] => /webser/www/simulator/app/homestead/vendor/composer/../../../vendor/filp/whoops/src
                )

            ...
        )

    [fallbackDirsPsr4:Composer\Autoload\ClassLoader:private] => Array
        (
        )

    [prefixesPsr0:Composer\Autoload\ClassLoader:private] => Array
        (
            [J] => Array
                (
                    [JakubOnderka\PhpConsoleHighlighter\] => Array
                        (
                            [0] => /webser/www/simulator/app/homestead/vendor/composer/../../../vendor/jakub-onderka/php-console-highlighter/src
                        )

                    ...

                )
            ...
        )

    [fallbackDirsPsr0:Composer\Autoload\ClassLoader:private] => Array
        (
        )

    [useIncludePath:Composer\Autoload\ClassLoader:private] => 
    [classMap:Composer\Autoload\ClassLoader:private] => Array
        (
            [App\Controller] => /webser/www/simulator/app/homestead/vendor/composer/../../app/Controller.php
            [App\Exceptions\Handler] => /webser/www/simulator/app/homestead/vendor/composer/../../app/Exceptions/Handler.php
            ...
        )

    [classMapAuthoritative:Composer\Autoload\ClassLoader:private] => 
    [missingClasses:Composer\Autoload\ClassLoader:private] => Array
        (
        )

    [apcuPrefix:Composer\Autoload\ClassLoader:private] => 
)

从上面的实例结构中,可以看到实例内部持有几个私有属性:

// PSR-4
    // 按照类名第一个字母做索引生成的映射map,map键是根命名空间,值是命名空间的字符长度
    private $prefixLengthsPsr4 = array();
    // psr-4根命名空间与物理文件目录对应的位置映射
    private $prefixDirsPsr4 = array();
    private $fallbackDirsPsr4 = array();

    // PSR-0
    // psr-4根命名空间与物理文件目录对应的位置映射
    private $prefixesPsr0 = array();
    private $fallbackDirsPsr0 = array();

    // 包含的文件路径
    private $useIncludePath = false;
    // 类名与实际物理文件的位置映射
    private $classMap = array();
    private $classMapAuthoritative = false;
    private $missingClasses = array();
    private $apcuPrefix;

    ...
    // 作为关键的一步: 注册类的自动加载处理函数是loadClass
    /**
     * Registers this instance as an autoloader.
     *
     * @param bool $prepend Whether to prepend the autoloader or not
     */
    public function register($prepend = false)
    {
        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
    }

    ...
    // 这个是实际的类的加载处理函数,调用了findFile方法
    /**
     * Loads the given class or interface.
     *
     * @param  string    $class The name of the class
     * @return bool|null True if loaded, null otherwise
     */
    public function loadClass($class)
    {
        if ($file = $this->findFile($class)) {
            includeFile($file);

            return true;
        }
    }

    ...
    // findFile是实际的查找类文件的执行逻辑,在内部可以清楚的看到里面的查找逻辑,就是在前面生成的各个私有属性中去查询
    // 所以前面生成的各种映射,也是最终方便我们去查询对应的文件,在执行include加载
     /**
     * Finds the path to the file where the class is defined.
     *
     * @param string $class The name of the class
     *
     * @return string|false The path if found, false otherwise
     */
    public function findFile($class)
    {
        // class map lookup
        if (isset($this->classMap[$class])) {
            return $this->classMap[$class];
        }
        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
            return false;
        }
        if (null !== $this->apcuPrefix) {
            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
            if ($hit) {
                return $file;
            }
        }

        $file = $this->findFileWithExtension($class, '.php');

        // Search for Hack files if we are running on HHVM
        if (false === $file && defined('HHVM_VERSION')) {
            $file = $this->findFileWithExtension($class, '.hh');
        }

        if (null !== $this->apcuPrefix) {
            apcu_add($this->apcuPrefix.$class, $file);
        }

        if (false === $file) {
            // Remember that this class does not exist.
            $this->missingClasses[$class] = true;
        }

        return $file;
    }

至此探索,大体能明白composer作为依赖管理器,如何自动实现类的自动加载和使用的了:

dump-autoload命令就是扫描vendor包下的文件,生成各种映射数组,最后通过\Composer\Autoload\ClassLoader类实例的几个私有属性记录下这些 映射关系,最后注册类加载的处理函数。

Namespace命名空间

php官方是这么解释的:

(PHP 5 >= 5.3.0, PHP 7)

什么是命名空间?从广义上来说,命名空间是一种封装事物的方法。在很多地方都可以见到这种抽象概念。例如,在操作系统中目录用来将相关文件分组,对于目录中的文件来说,它就扮演了命名空间的角色。具体举个例子,文件 foo.txt 可以同时在目录/home/greg 和 /home/other 中存在,但在同一个目录中不能存在两个 foo.txt 文件。另外,在目录 /home/greg 外访问 foo.txt 文件时,我们必须将目录名以及目录分隔符放在文件名之前得到 /home/greg/foo.txt。这个原理应用到程序设计领域就是命名空间的概念。

在PHP中,命名空间用来解决在编写类库或应用程序时创建可重用的代码如类或函数时碰到的两类问题:

用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/

为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个别名(或简短)的名称,提高源代码的可读性。

另外, 个人觉得命名空间的约定完成了类名和类文件物理路径的一对一映射。我们在实际的开发中,只与类名打交道,无需关系类到底是在哪个文件目录之下,这极大的减轻了开发者的 心智负担,从对composer的探索式分析,知道composer完美的解决了类名到类文件的映射。这有点像HostName和IP的关系,我们不会去记忆IP,我们只会去记忆HostName,他们之间的映射 关系是通过DNS服务器的解析来完成的。在这里,composer就是DNS服务器的角色。

结论

通过对composer生成文件的分析,大致了解了composer解决包依赖,以及包类文件的查找的运作原理,composer在推动php朝现代工程范式的转型中其实扮演了很重要的角色。有很多的设计 和实践值得与学习和探索。