PHP的依赖管理工具Composer(2)

之前介绍过如何安装Composer和简单使用,出门左拐能找到。
PHP的依赖管理工具Composer(1)

现在更深入地学习Composer。在上一篇MyProject的app.php

<?php
require 'vendor/autoload.php';
$log = new Monolog\Logger('name');

...

上面的代码是为了 new Monolog\Logger('name') ,所以require 'vendor/autoload.php',作用是为了找到Monolog\Logger类的实现代码,然后require。
这应该很好理解,因为假如不用require 'vendor/autoload.php',那就必须require Logger.php源码。

PSR-4 规范

1、简介

在深入学习Composer的之前,了解一下Composer实现自动加载功能所参考的标准规范PSR。
FIG 制定的PHP规范,简称PSR,是PHP开发的事实标准。
PSR现在有6个规范,分别是:

  • PSR-0 自动加载
  • PSR-1 基本代码规范
  • PSR-2 代码样式
  • PSR-3 日志接口
  • PSR-4 自动加载
  • PSR-6 缓存
  • PSR-7 http消息

PSR-4可以说是PSR-0的演进版本吧,PSR-4和PSR-0最大的区别是对下划线(underscore)的定义不同。PSR-4中,在类名中使用下划线没有任何特殊含义。而PSR-0则规定类名中的下划线 _ 会被转化成目录分隔符。

PHP的包管理系统Composer已经支持PSR-4,同时也允许在 composer.json 中定义不同的prefix使用不同的自动加载机制。
Composer使用PSR-0风格

vendor/
    vendor_name/
        package_name/
            src/
                Vendor_Name/
                    Package_Name/
                        ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                Vendor_Name/
                    Package_Name/
                        ClassNameTest.php   # Vendor_Name\Package_Name\ClassName

Composer使用PSR-4风格

vendor/
    vendor_name/
        package_name/
            src/
                ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest

对比以上两种结构,明显可以看出PSR-4带来更简洁的文件结构。

2、PSR-4 规范说明

class的说明包括class、interface、traint或其他相似的结构。

一个规范的class命名如下面所示:

\<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>

a.规范的命名必须指定一个最上层的NamespaceName名字,比如vendor的名字。
b.规范的命名可能有一个或多个SubNamespaceNames名字。
c.规范的命名必须指定一个类名。
d.下划线在命名的任一部分都没有特别的意义。(我的理解是不对下划线进行处理)
e.可以由任意大小写字母组合。
f.字母大小写敏感。

举例

下面的表展示了对一个完全合规的类名, 命名空间前缀以及base目录对应的文件路径。
PyCharm remote debug
仔细分析一下所示的例子:
当加载完全合规的类名对应的文件时…
a.在完全合规的类名中, 不包含前面的命名空间分隔符,由一个顶级命名空间与一个或多个二级命名空间名称组成的命名空间前缀对应于至少一个Base目录
b.基目录之后的子目录名字必须(MUST)与命名空间前缀后的子命名空间名字匹配。
c.后面的类名对应于以.php为后缀的文件名,这个文件名必须(MUST)匹配到后面的类名。
d.自动加载实现一定不能(MUST NOT)抛出异常,一定不能(MUST NOT)引发任何级别的错误, 并且不应当(SHOULD NOT)返回值。

3、Monolog使用的是PSR-0,但不影响理解

分析require 'vendor/autoload.php'源码。

<?php
/*
autoload.php 中require了autoload_real.php,然后
调用ComposerAutoloaderInit::getLoader()静态方法。
*/
// autoload.php @generated by Composer

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

return ComposerAutoloaderInitcc5afff809d01782ea6cfda49dba67db::getLoader();

ComposerAutoloaderInit的实现。

<?php

// autoload_real.php @generated by Composer

class ComposerAutoloaderInitcc5afff809d01782ea6cfda49dba67db
{
    private static $loader;

    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

    public static function getLoader()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }
        /*注册给定的函数作为 __autoload 的实现,
        ComposerAutoloaderInit::loadClassLoader()
        主要是为了调用上面的loadClassLoader()函数,
        require ClassLoader.php,实例化一个$loader。*/
        spl_autoload_register(array('ComposerAutoloaderInitcc5afff809d01782ea6cfda49dba67db', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        spl_autoload_unregister(array('ComposerAutoloaderInitcc5afff809d01782ea6cfda49dba67db', 'loadClassLoader'));

        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInitcc5afff809d01782ea6cfda49dba67db::getInitializer($loader));
        } else {
        /*然后就是通过$loader的方法,遍历autoload_namespaces.php/
        autoload_psr4.php/autoload_classmap.php中的数组变量
        以PSR-0为例,看看autoload_namespaces.php中具体的值
        'Monolog' => array($vendorDir . '/monolog/monolog/src')
        再看看$loader->set(Monolog,array($vendorDir . '/monolog/monolog/src'))
        $this->fallbackDirsPsr0 = (array) $paths;
        即$loader->fallbackDirsPsr0 = array($vendorDir . '/monolog/monolog/src')*/
            $map = require __DIR__ . '/autoload_namespaces.php';
            foreach ($map as $namespace => $path) {
                $loader->set($namespace, $path);
            }

            $map = require __DIR__ . '/autoload_psr4.php';
            foreach ($map as $namespace => $path) {
                $loader->setPsr4($namespace, $path);
            }

            $classMap = require __DIR__ . '/autoload_classmap.php';
            if ($classMap) {
                $loader->addClassMap($classMap);
            }
        }

        // activate the autoloader
        // 调用$loader->register(true),执行autoloader
        $loader->register(true);

        return $loader;
    }
}

// autoload_namespaces.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Monolog' => array($vendorDir . '/monolog/monolog/src'),
);

//ClassLoader.php
/**
     * Registers a set of PSR-0 directories for a given prefix,
     * replacing any others previously set for this prefix.
     *
     * @param string       $prefix The prefix
     * @param array|string $paths  The PSR-0 base directories
     */
    public function set($prefix, $paths)
    {
        if (!$prefix) {
            $this->fallbackDirsPsr0 = (array) $paths;
        } else {
            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
        }
    }

  /**
     * Registers this instance as an autoloader.
     *
     * @param bool $prepend Whether to prepend the autoloader or not
     */
    public function register($prepend = false)
    {
        //当找不到类抛出异常前,会调用$loader->loadClass()
        //即进行$log = new Monolog\Logger('name');时,
        //不知道Monolog\Logger在哪,就去调用这个函数取寻找,然后再include
        //当然,再找不到就会抛出异常了。
        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
    }

    /**
     * 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;
        }
    }

下面的代码就是寻找Monolog\Logger类源码的实现,简单分析一下。

//ClassLoader.php
 /**
     * 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)
    {
        // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
        //去掉命名空间前的斜杠,即如果是\Monolog\Logger,那么就变成Monolog\Logger
        if ('\\' == $class[0]) {
            $class = substr($class, 1);
        }

        // class map lookup
        if (isset($this->classMap[$class])) {
            return $this->classMap[$class];
        }
        if ($this->classMapAuthoritative) {
            return false;
        }
        //找到Monolog\Logger的完整路径,包括php后缀名
        $file = $this->findFileWithExtension($class, '.php');

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

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

        return $file;
    }

    private function findFileWithExtension($class, $ext)
    {
        // PSR-4 lookup
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

        $first = $class[0];
        if (isset($this->prefixLengthsPsr4[$first])) {
            foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
                if (0 === strpos($class, $prefix)) {
                    foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
                            return $file;
                        }
                    }
                }
            }
        }

        // PSR-4 fallback dirs
        foreach ($this->fallbackDirsPsr4 as $dir) {
            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
                return $file;
            }
        }

        // PSR-0 lookup
        if (false !== $pos = strrpos($class, '\\')) {
            // namespaced class name
            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
        } else {
            // PEAR-like class name
            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
        }
        
        //执行这一段程序
        if (isset($this->prefixesPsr0[$first])) {
            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
                if (0 === strpos($class, $prefix)) {
                    foreach ($dirs as $dir) {
                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                            return $file;
                            //$file = __DIR__."\vendor\monolog\monolog\src\Monolog\Logger.php"
                        }
                    }
                }
            }
        }

        // PSR-0 fallback dirs
        foreach ($this->fallbackDirsPsr0 as $dir) {
            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                return $file;
            }
        }

        // PSR-0 include paths.
        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
            return $file;
        }
    }

找到$file = DIR.”\vendor\monolog\monolog\src\Monolog\Logger.php”之后include执行,然后就是Logger实例化的过程了。
Composer的运行过程就是酱紫了。

4、还有一个问题

autoload_namespaces.php、autoload_psr4.php、autoload_classmap.php中的数组变量怎么来的?

Monolog库中也有一个composer.json文件。

  "autoload": {
        "psr-0": {"Monolog": "src/"}
    },

再看看autoload_namespaces.php文件,存在映射关系的。

 'Monolog' => array($vendorDir . '/monolog/monolog/src')

Composer的学习目前就先到这里了,往后如果需要制作一个库,再往更深一层学习吧。

参考

  1. Composer中文文档
  2. FIG-PHP PSR规范系列4-自动加载