PHP composer 基本原理

栏目: PHP · 发布时间: 5年前

public/index.php

// Register The Auto Loader
require __DIR__.'/../vendor/autoload.php';

vendor/composer/autoload_real.php'

autoload.php 不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。

autoload_real.php 中的类名为 ComposerAutoloaderInit... 这可能是为防止与用户自定义类名跟这个类重复冲突,加上了哈希值。

其实还有一个做法我们更加熟悉,那就是不直接定义类名,而是定义一个命名空间。这里为什么不定义一个命名空间呢?个人理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用哈希值更加合适。

autoload_real.php

autoload.php 主要调用了 getLoader()

public static function getLoader()
{
    // 单例模式,自动加载类只能有一个 1
    if (null !== self::$loader) {
        return self::$loader;
    }

    // 获得自动加载核心类对象 2
    spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);
    self::$loader = $loader = new \Composer\Autoload\ClassLoader();
    spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));

    // 初始化自动加载核心类对象 3
    $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
    if ($useStaticLoader) {
        require_once __DIR__ . '/autoload_static.php';

        call_user_func(\Composer\Autoload\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::getInitializer($loader));
    } else {
        $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);
        }
    }

    // 注册自动加载核心类对象 4
    $loader->register(true);

    // 自动加载全局函数 5
    if ($useStaticLoader) {
        $includeFiles = Composer\Autoload\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$files;
    } else {
        $includeFiles = require __DIR__ . '/autoload_files.php';
    }
    foreach ($includeFiles as $fileIdentifier => $file) {
        composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file);
    }

    return $loader;
}

单例模式 1

if (null !== self::$loader) {
    return self::$loader;
}

构造 ClassLoader 核心类 2

spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));
public static function loadClassLoader($class)
{
    if ('Composer\Autoload\ClassLoader' === $class) {
        require __DIR__ . '/ClassLoader.php';
    }
}

composer 先向 PHP 自动加载机制注册了一个函数,这个函数 requireClassLoader 文件。成功 new 出该文件中核心类 ClassLoader() 后,又销毁了该函数。

为什么不直接 require ?原因是:怕有的用户也定义了个 \Composer\Autoload\ClassLoader 命名空间,导致自动加载错误文件。

那为什么不跟引导类一样用个哈希值呢?原因是:这个类是可以复用的,框架允许用户使用这个类。

个人疑问:为什么这样就解决了与用户命名空间的冲突?

初始化核心类对象 3

对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。

初始化的方法有两种:

autoload_static

autoload_static 静态初始化

静态初始化只支持 PHP 5.6 以上版本、不支持 HHVM 虚拟机、不存在 Zend-encoded file

autoload_static.php

<?php

// autoload_static.php @generated by Composer

namespace Composer\Autoload;

// hash 防止冲突
class ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db
{
    public static $files = array (...);
    public static $prefixLengthsPsr4 = array (...);
    public static $prefixDirsPsr4 = array (...);
    public static $fallbackDirsPsr4 = array (...);
    public static $prefixesPsr0 = array (...);
    public static $classMap = array array (...);

    public static function getInitializer(ClassLoader $loader)
    {
        return \Closure::bind(function () use ($loader) {
            $loader->prefixLengthsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixDirsPsr4;
            $loader->fallbackDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$fallbackDirsPsr4;
            $loader->prefixesPsr0 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixesPsr0;
            $loader->classMap = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$classMap;

        }, null, ClassLoader::class);
    }
}

这个静态初始化类的核心就是 getInitializer() 函数,它将自己类中的顶级命名空间映射给了 ClassLoader 类。

值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是 ClassLoader 中的 prefixLengthsPsr4prefixDirsPsr4 等等方法都是 private 的。普通的函数没办法给类的 private 成员变量赋值。利用匿名函数的绑定功能就可以将把匿名函数转为 ClassLoader 类的成员函数。

关于匿名函数的 绑定功能

接下来就是 顶级命名空间 初始化的关键了。

classMap

public static $classMap = array (
    'App\\Api\\Middleware\\DeviceRecord' => __DIR__ . '/../..' . '/app/Api/Middleware/DeviceRecord.php',
    'App\\Api\\Middleware\\HeaderCheck' => __DIR__ . '/../..' . '/app/Api/Middleware/HeaderCheck.php',
    ...
)

直接命名空间全名与目录的映射,没有顶级命名空间。简单粗暴,也导致这个数组相当的大。

PSR0 顶级命名空间映射

public static $prefixesPsr0 = array (
    'P' =>
    array (
        'Prophecy\\' =>
        array (
            0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
        ),
        'Parsedown' =>
        array (
            0 => __DIR__ . '/..' . '/erusev/parsedown',
        ),
    ),
    ...
);

为了快速找到顶级命名空间,这里使用命名空间第一个字母作为前缀索引。这个映射的用法比较明显,假如我们有 Parsedown/example 这样的命名空间,首先通过首字母 P ,找到:

'P' => array (...)

这个数组,然后就会遍历这个数组来和 Parsedown/example 比较,发现第一个 Prophecy 不符合,第二个 Parsedown 符合,然后得到了映射目录(映射目录可能不止一个):

0 => __DIR__ . '/..' . '/erusev/parsedown',

接着遍历这个数组,尝试 __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php' 是否存在,如果不存在接着遍历数组(这个例子数组只有一个元素),如果数组遍历完都没有,就会加载失败。

PSR4 标准顶级命名空间映射

public static $prefixLengthsPsr4 = array (
    'p' =>
    array (
        'phpDocumentor\\Reflection\\' => 25,
    ),
    'Z' =>
    array (
        'Zend\\Diactoros\\' => 15,
    ),
    ...
);

public static $prefixDirsPsr4 = array (
    'phpDocumentor\\Reflection\\' =>
    array (
        0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
        1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
        2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
    ),
    'Zend\\Diactoros\\' =>
    array (
        0 => __DIR__ . '/..' . '/zendframework/zend-diactoros/src',
    ),
    ...
);

PSR4 标准 顶级命名空间 映射用了两个数组,第一个和 PSR0 一样用命名空间第一个字母作为前缀索引,然后是 顶级命名空间 ,但是最终并不是文件路径,而是 顶级命名空间 的长度。为什么呢?因为 PSR4 的文件目录更加灵活,更加简洁。

PSR0顶级命名空间 目录 直接加 到命名空间前面就可以得到路径:

                                        ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php
                                                         ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

PSR4 却是用顶级命名空间目录 替换 顶级命名空间,所以获得顶级命名空间的 长度 很重要:

                                        ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/example.php
                                                ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

举例:假如我们找 Symfony\\Polyfill\\Mbstring\\example 这个类,和 PSR0 一样通过前缀索引和字符串匹配我们得到了:

'Symfony\\Polyfill\\Mbstring\\' => 26,

这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4 获取它的映射目录数组(注意映射目录可能不止一条):

'Symfony\\Polyfill\\Mbstring\\' =>
array (
    0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),

Symfony\\Polyfill\\Mbstring\\example 前 26 个字母替换为 __DIR__ . '/..' . '/symfony/polyfill-mbstring 也就是:

__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php

先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。

自动加载核心类 ClassLoader 的静态初始化完成!

其实还有 $fallbackDirsPsr4 ,暂未研究

调用接口初始化

如果 PHP 版本低于 5.6 或者使用 HHVM 虚拟机环境或者存在 zend_loader_file_encoded ,那么就要使用核心类的接口进行初始化。

/*
PSR0 取出命名空间的第一个字母作为索引,一个索引对应多个顶级命名空间,一个顶级命名空间对应多个目录路径,具体形式可以查看上面的 autoload_static 的 $prefixesPsr0。

如果没有顶级命名空间,就只存储一个路径名,以便在后面尝试加载。
*/
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
    $loader->set($namespace, $path);
}

/*
PSR4 如果没有顶级命名空间,就直接保存目录。
如果有命名空间的话,要保证顶级命名空间最后是 \,然后分别保存
( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 )
( 顶级命名空间 -> 目录 )

这两个映射数组。具体形式可以查看上面我们讲的 autoload_static 的 prefixLengthsPsr4、$prefixDirsPsr4 。
*/
$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);
}

注册核心类对象 4

Composer 自动加载功能的启动与初始化,经过启动与初始化,自动加载核心类对象已经获得了顶级命名空间与相应目录的映射,换句话说,如果有命名空间 App\Console\Kernel ,我们已经知道了 App\ 对应的目录,接下来我们就要解决下面的就是 \Console\Kernel 这一段。

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

一行代码实现自动加载。核心在 ClassLoaderloadClass() 函数上,这个函数负责按照 PSR 标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将 App\Console\KernelConsole\Kernel 这一段转为目录。

自动加载全局函数 5

Composer 不止可以自动加载命名空间,还可以加载全局函数。就是把全局函数写到特定的文件里面去,在程序运行前挨个 require 就行了。

if ($useStaticLoader) {
    // 静态初始化
    $includeFiles = Composer\Autoload\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$files;
} else {
    // 普通初始化
    $includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file);
}
function composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file)
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        require $file;

        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

问题 1

为什么不直接 require $includeFiles 里面的每个文件名,而要用类外面的函数 composerRequire...

  • 避免和用户定义函数冲突
  • 防止有人在全局函数所在的文件写 $this 或者 self

假如 $includeFiles 有个 app/helper.php 文件,这个 helper.php 文件的函数外有一行代码: $this->foo() ,如果引导类在 getLoader() 函数直接 require($file) ,那么引导类就会运行这句代码,调用自己的 foo() 函数,这显然是错的。

事实上 helper.php 就不应该出现 $thisself 这样的代码,这样写一般都是用户写错了的,一旦这样的事情发生:

  • 第一种情况:引导类恰好有 foo() 函数,那么就会莫名其妙执行了引导类的 foo()
  • 第二种情况:引导类没有 foo() 函数,但是却甩出来引导类没有 foo() 方法这样的错误提示,用户不知道自己哪里错了。把 require 语句放到 引导类的外面 ,遇到 $this 或者 self ,程序就会告诉用户根本没有类, $thisself 无效,错误信息更加明朗。

问题 2

为什么要用 hash 作为 $fileIdentifier

这个变量是用来控制全局函数只被 require 一次的,那为什么不用 require_once 呢?事实上 require_oncerequire 效率低很多,使用全局变量 $GLOBALS 这样控制加载会更快。猜测另一个原因应该是 require_once 对相对路径的支持并不理想,所以 composer 尽量少用 require_once

运行

ClassLoaderloadClass() 函数注册到 PHP SPL 中的 spl_autoload_register() 里面去。这样,每当 PHP 遇到一个不认识的命名空间的时候,PHP 会自动调用注册到 spl_autoload_register() 里面的函数堆栈,运行其中的每个函数,直到找到命名空间对应的文件。

/**
 * 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); // include $file; Prevents access to $this/self from included files.

        return true;
    }
}

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

    // classMapAuthoritative 关闭搜索未在类映射中注册的类的 prefix and fallback directories。- 不清楚干啥的 暂没研究
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
        return false;
    }

    // 如果启用扩展名,则使用 APCu 前缀来缓存已找到/未找到的类。 - 不清楚干啥的 暂没研究
    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;
}

loadClass() 主要调用 findFile() 函数。 findFile() 在解析命名空间的时候主要分为两部分:

  • classMap 直接看命名空间是否在映射数组
  • findFileWithExtension() 包含了 PSR0PSR4

如果我们在代码中写 'phpDocumentor\Reflection\example ,PHP 会通过 SPL 调用 loadClass -> findFile -> findFileWithExtension

  • 首先默认用 .php 后缀名调用 findFileWithExtension 函数里,利用 PSR4 标准尝试解析目录文件,如果文件不存在则继续用 PSR0 标准解析
  • 如果解析出来的目录文件仍然不存在,但是环境是 HHVM 虚拟机,继续用后缀名 .hh 再次调用 findFileWithExtension 函数,如果不存在,说明此命名空间无法加载,放到 classMap 中设为 false ,以便以后更快地加载

PSR4

对于 phpDocumentor\Reflection\example ,当尝试利用 PSR4 标准映射目录时,步骤如下:

// $class: phpDocumentor\Reflection\example

// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
// $logicalPathPsr4: phpDocumentor/Reflection/example.php(hh)`

$first = $class[0];
// $first: p

if (isset($this->prefixLengthsPsr4[$first])) {
    /* 'p' =>
    array (
        'phpDocumentor\\Reflection\\' => 25,
    ),
    */
    $subPath = $class;
    // $subPath: phpDocumentor\Reflection\example
    while (false !== $lastPos = strrpos($subPath, '\\')) {
        // $lastPos 13
        $subPath = substr($subPath, 0, $lastPos);
        $search = $subPath.'\\';
        if (isset($this->prefixDirsPsr4[$search])) {
            // search phpDocumentor\\Reflection\\
            // $lastPos 25

            /* 'phpDocumentor\\Reflection\\' =>
                array (
                    0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
                    1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
                    2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
                ),
            */
            $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
            // $pathEnd /example.php(hh)

            foreach ($this->prefixDirsPsr4[$search] as $dir) {
                // 遍历 3 个
                if (file_exists($file = $dir . $pathEnd)) {
                    // $file __DIR__ . '/..' . /phpdocumentor/type-resolver/src/example.php(hh)`
                    return $file;
                }
            }
        }
    }
}

PSR0

如果 PSR4 标准加载失败,则要进行 PSR0 标准加载。对于 phpDocumentor\Reflection\example ,当尝试利用 PSR0 标准映射目录时,步骤如下:

// $class: phpDocumentor\Reflection\example

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

// $logicalPathPsr0: phpDocumentor/Reflection/example.php(hh)`
if (isset($this->prefixesPsr0[$first])) {
    foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
        /* 'P' =>
        array (
            'Prophecy\\' =>
            array (
                0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
            ),
            'Parsedown' =>
            array (
                0 => __DIR__ . '/..' . '/erusev/parsedown',
            ),
        ), */
        if (0 === strpos($class, $prefix)) {
            foreach ($dirs as $dir) {
                if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                    // $file __DIR__ . '/..' . '/phpspec/prophecy/src' . phpDocumentor/Reflection/example.php(hh)
                    return $file;
                }
            }
        }
    }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Art and Science of CSS

The Art and Science of CSS

Jonathan Snooks、Steve Smith、Jina Bolton、Cameron Adams、David Johnson / SitePoint / March 9, 2007 / $39.95

Want to take your CSS designs to the next level? will show you how to create dozens of CSS-based Website components. You'll discover how to: # Format calendars, menus and table of contents usin......一起来看看 《The Art and Science of CSS》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具