DEV Community

菜皮日记
菜皮日记

Posted on

PHP Composer 的自动加载

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

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

__autoload 自动加载器

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

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

// we've writen this code where we need
function __autoload($classname) {
    $filename = "./". $classname .".php";
    include_once($filename);
}

// we've called a class ***
$obj = new myClass();
Enter fullscreen mode Exit fullscreen mode

__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"
*/
Enter fullscreen mode Exit fullscreen mode

二、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.php
require_once __DIR__ . '/composer/autoload_real.php';

// 返回了autoload_real文件中的类方法
return ComposerAutoloaderInit7e421c277f7e8f810a19524f0d771cdb::getLoader();

/* ------------- */

// vendor/composer/autoload_real.php
public 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;
}
Enter fullscreen mode Exit fullscreen mode

在代码 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',
    ...
);
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}

function includeFile($file)
{
    include $file;
}
Enter fullscreen mode Exit fullscreen mode

其中 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;
}
Enter fullscreen mode Exit fullscreen mode

对于类的加载十分简单,直接去 classmap 中取。如果取不到,则将目标类名追加 .php 后缀,去$prefixLengthsPsr4 和 $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;
Enter fullscreen mode Exit fullscreen mode

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

// autoload_static.php
public 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.php
if (!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); }
}
Enter fullscreen mode Exit fullscreen mode

至此 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

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

Top comments (0)

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay