写点什么

PHP Composer 的自动加载

作者:菜皮日记
  • 2023-09-08
    北京
  • 本文字数:5590 字

    阅读完需:约 18 分钟

PHP 的 autoload 机制,可以在使用一个未导入的类时动态加载该类,从而实现延迟加载和管理依赖类文件的目的。

一、没有 composer 时 PHP 是怎么做的

__autoload 自动加载器

PHP 中想要使用一个类,必须通过 require (指代 require_once, include_once 等) 的方式在文件开头声明要使用的类。当项目中类较多时,一个个声明加载显然不可行。

在 PHP5 版本,PHP 支持通过 __autoload 定义一个自动加载器,尝试加载未定义的类。 如:

// we've writen this code where we needfunction __autoload($classname) {    $filename = "./". $classname .".php";    include_once($filename);}
// we've called a class ***$obj = new myClass();
复制代码

__autoload 函数缺点比较明显:他只能定义一次,这样就会耦合所有依赖的类的自动加载逻辑,统统写到这个方法里,这时候就需要用到 spl_autoload_register 函数了。

使用 spl_autoload_register 注册多个自动加载器

spl 是 standard php library 的缩写。spl_autoload_register 最大的特点是支持注册多个自动加载器,这样就能实现将各个类库的自动加载逻辑分开,自己处理自己的加载逻辑。

function my_autoloader($class) {    var_dump("my_autoloader", $class);}
spl_autoload_register('my_autoloader');
// 静态方法class MyClass1 {    public static function autoload($className) {        var_dump("MyClass1 autoload", $className);    }}
spl_autoload_register(array('MyClass1', 'autoload'));
// 非静态方法class MyClass2 {    public function autoload($className) {        var_dump("MyClass2 autoload", $className);    }}
$instance = new MyClass2();spl_autoload_register(array($instance, 'autoload'));
new \NotDefineClassName();
/*输出string(32) "my_autoloader NotDefineClassName"string(36) "MyClass1 autoload NotDefineClassName"string(36) "MyClass2 autoload NotDefineClassName"*/
复制代码

二、PSR 规范

PSR 即 PHP Standards Recommendation 是一个社区组织:https://www.php-fig.org/psr/,声明一系列规范来统一开发风格,减少互不兼容的困扰。规范中的 PSR-4 代表:Autoloading Standard,即自动加载规范。

PSR-4

其中规定:一个类的完整类名应该遵循一下规范:

\<命名空间>(\<子命名空间>)*\<类名>,即:

  1. 完整的类名必须要有一个顶级命名空间,被称为 “vendor namespace”;

  2. 完整的类名可以有一个或多个子命名空间;

  3. 完整的类名必须有一个最终的类名;

  4. 完整的类名中任意一部分中的下滑线都是没有特殊含义的;

  5. 完整的类名可以由任意大小写字母组成;

  6. 所有类名都必须是大小写敏感的。

看看例子:

PSR-4

应用的效果简单来说就是:将命名空间前缀 Namespace Prefix 替换成 Base Directory 目录,并将 \ 替换成 / 。一句话,命名空间可以表明类具体的存放位置。

三、Composer 自动加载的过程

结合 spl_auto_register 和 PSR-4 的命名空间规范,可以想象,我们可以通过类的命名空间,来找到具体类的存放位置,然后通过 require 将其加载进来生效,composer 就是这么干的。

接下来我们分两步看 composer 是怎么做的。

第一步,建立类的命名空间和类存放位置的映射关系

首先看 vendor 目录下的 autoload.php 文件,所有项目启动必然要先 require 这个文件。

// autoload.php @generated by Composer// vendor/autoload.phprequire_once __DIR__ . '/composer/autoload_real.php';
// 返回了autoload_real文件中的类方法return ComposerAutoloaderInit7e421c277f7e8f810a19524f0d771cdb::getLoader();
/* ------------- */
// vendor/composer/autoload_real.phppublic static function getLoader(){    if (null !== self::$loader) {        return self::$loader;    }
    // P0 初始化ClassLoader    spl_autoload_register(array('ComposerAutoloaderInit7e421c277f7e8f810a19524f0d771cdb', 'loadClassLoader'), true, true);    self::$loader = $loader = new \Composer\Autoload\ClassLoader();    spl_autoload_unregister(array('ComposerAutoloaderInit7e421c277f7e8f810a19524f0d771cdb', 'loadClassLoader'));
    $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';
        // P1 向ClassLoader中set命名空间和文件路径映射关系        call_user_func(\Composer\Autoload\ComposerStaticInit7e421c277f7e8f810a19524f0d771cdb::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);        }    }
    // P2 将ClassLoader中的loadClass方法,注册为加载器    $loader->register(true);
    if ($useStaticLoader) {        $includeFiles = Composer\Autoload\ComposerStaticInit7e421c277f7e8f810a19524f0d771cdb::$files;    } else {        $includeFiles = require __DIR__ . '/autoload_files.php';    }    foreach ($includeFiles as $fileIdentifier => $file) {        composerRequire7e421c277f7e8f810a19524f0d771cdb($fileIdentifier, $file);    }
    return $loader;}
复制代码

在代码 P0 处,上来先实例化一个 \Composer\Autoload\ClassLoader 类,这个类里面维护了所有命名空间到类具体存放位置的映射关系。

接下来在 P1 处,根据 PHP 版本和运行环境,如是否运行在 HHVM 环境下,来区分如何向 ClassLoader 中载入映射关系。

autoload_static.php 文件中定义的映射关系有三种:

public static $prefixLengthsPsr4 = array (    'p' =>     array (        'phpDocumentor\\Reflection\\' => 25,    ),    'W' =>     array (        'Webmozart\\Assert\\' => 17,    ),    'S' =>     array (        'Symfony\\Polyfill\\Ctype\\' => 23,    ),    'R' =>     array (        'RefactoringGuru\\' => 16,    ),    'P' =>     array (        'Prophecy\\' => 9,    ),    'D' =>     array (        'Doctrine\\Instantiator\\' => 22,        'DeepCopy\\' => 9,    ),);
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',    ),    'Webmozart\\Assert\\' =>     array (        0 => __DIR__ . '/..' . '/webmozart/assert/src',    ),    'Symfony\\Polyfill\\Ctype\\' =>     array (        0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',    ),    'RefactoringGuru\\' =>     array (        0 => __DIR__ . '/../..' . '/',    ),    'Prophecy\\' =>     array (        0 => __DIR__ . '/..' . '/phpspec/prophecy/src/Prophecy',    ),    'Doctrine\\Instantiator\\' =>     array (        0 => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator',    ),    'DeepCopy\\' =>     array (        0 => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy',    ),);
public static $classMap = array (  'File_Iterator' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Iterator.php',  'File_Iterator_Facade' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Facade.php',  'File_Iterator_Factory' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Factory.php',  'PHPUnit\\Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Exception.php',    ...);
复制代码

classMap 是完整映射关系,prefixLengthsPsr4 和 prefixDirsPsr4 是当通过完整命名空间找不到时,通过在目标类名后加上 .php 再次寻找用。

到此,建立命名空间到类存放路径的关系已经完成了。

第二步,如何找到类并加载

在上面代码中,将 ClassLoader 的 loadClass 方法注册成加载器:

public function loadClass($class){    if ($file = $this->findFile($class)) {        includeFile($file);
        return true;    }}
function includeFile($file){    include $file;}
复制代码

其中 findFile 方法,就是通过类名,去寻找文件实际的位置,如果找到了,就通过 includeFile 将文件加载进来。主要看看 findFile 中的逻辑:

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;}
复制代码

对于类的加载十分简单,直接去 classmap 中取。如果取不到,则将目标类名追加 .php 后缀,去和 prefixDirsPsr4 中查找。

第三步,如何加载全局函数

if ($useStaticLoader) {    $includeFiles = Composer\Autoload\ComposerStaticInit7e421c277f7e8f810a19524f0d771cdb::$files;} else {    $includeFiles = require __DIR__ . '/autoload_files.php';}foreach ($includeFiles as $fileIdentifier => $file) {    composerRequire7e421c277f7e8f810a19524f0d771cdb($fileIdentifier, $file);}
return $loader;
复制代码

还是通过 autoload_static.php 中定义的数据去加载:

// autoload_static.phppublic static $files = array (    '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',    '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',);
// vendor/symfony/polyfill-ctype/bootstrap.phpif (!function_exists('ctype_alnum')) {    function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); }}if (!function_exists('ctype_alpha')) {    function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); }}
复制代码

至此 composer 自动加载的逻辑基本就过了一遍。

四、composer 的 ClassLoader 中的 classMap 是怎么生成出来的?

答案就在 composer 的源码中:https://github.com/composer/composer/blob/d0aac44ed210e13ec4a4370908a5b36553a2f16c/src/Composer/Autoload/AutoloadGenerator.php

扫描所有包中的类,然后生成一个 php 文件,例如:getStaticFile 方法

参考:https://segmentfault.com/a/1190000014948542

PHP 官网对类自动加载的说明:https://www.php.net/manual/zh/language.oop5.autoload.php

用户头像

菜皮日记

关注

全干程序员 2018-08-08 加入

还未添加个人简介

评论

发布
暂无评论
PHP Composer 的自动加载_php_菜皮日记_InfoQ写作社区