Skip to content

初始化依赖注入 (DI) 容器

php
(function () { 

    Hyperf\Di\ClassLoader::init();  

    /** @var Psr\Container\ContainerInterface $container */
    $container = require BASE_PATH . '/config/container.php';

    /**
     * @var \Symfony\Component\Console\Application $application
     */
    $application = $container->get(Hyperf\Contract\ApplicationInterface::class);
    $application->run();
})();

init方法有三个可选参数,默认是不传参数进来

php
public static function init(?string $proxyFileDirPath = null, ?string $configDir = null, ?ScanHandlerInterface $handler = null): void
{
    // 代理文件目录
    if (! $proxyFileDirPath) {
        $proxyFileDirPath = BASE_PATH . '/runtime/container/proxy/';
    }

    // 配置文件目录
    if (! $configDir) {
        $configDir = BASE_PATH . '/config/';
    }

    // 扫描器,用来扫描类文件并生成类映射
    // 利用 PHP 的 pcntl 扩展提供的功能来进行并发扫描,以加快扫描速度
    if (! $handler) {
        $handler = new PcntlScanHandler();
    }
    // 获取composer的自动加载器实例
    $composerLoader = Composer::getLoader();

    // 加载.env文件
    // 使用的是vlucas/phpdotenv库
    if (file_exists(BASE_PATH . '/.env')) {
        static::loadDotenv();
    }

    // 通过 ScanConfig 扫描并生成反射类映射
    $config = ScanConfig::instance($configDir);
    $composerLoader->addClassMap($config->getClassMap());

    $scanner = new Scanner($config, $handler);
    $composerLoader->addClassMap(
        $scanner->scan($composerLoader->getClassMap(), $proxyFileDirPath)
    );

    // Initialize Lazy Loader. This will prepend LazyLoader to the top of autoload queue.
    LazyLoader::bootstrap($configDir);
}

protected static function loadDotenv(): void
{
    $repository = RepositoryBuilder::createWithNoAdapters()
        ->addAdapter(Adapter\PutenvAdapter::class)
        ->immutable()
        ->make();

    Dotenv::create($repository, [BASE_PATH])->load();
}

读取配置目录

看这行,instance方法创建一个ScanConfig实例,用于加载并解析目录下的配置文件,并生成类映射等扫描相关的配置信息。

php
$config = ScanConfig::instance($configDir);

public static function instance(string $configDir): self
{
    // 如果该实例存在,则直接返回该实例
    if (self::$instance) {
        return self::$instance;
    }

    $configDir = rtrim($configDir, '/');

    [$config, $serverDependencies, $cacheable] = static::initConfigByFile($configDir);
    // 这里返回该实例
    return self::$instance = new self(
        $cacheable,
        $configDir,
        $config['paths'] ?? [],
        $serverDependencies ?? [],
        $config['ignore_annotations'] ?? [],
        $config['global_imports'] ?? [],
        $config['collectors'] ?? [],
        $config['class_map'] ?? []
    );
}

initConfigByFile方法中,在指定目录解析出对应的$config$serverDependencies$cacheable三个变量。该方法具体实现如下,

php
private static function initConfigByFile(string $configDir): array
{
    $config = [];
    $configFromProviders = [];
    $cacheable = false;
    if (class_exists(ProviderConfig::class)) { 
        $configFromProviders = ProviderConfig::load();
    }

    $serverDependencies = $configFromProviders['dependencies'] ?? [];
    if (file_exists($configDir . '/autoload/dependencies.php')) {
        $definitions = include $configDir . '/autoload/dependencies.php';
        $serverDependencies = array_replace($serverDependencies, $definitions ?? []);
    }

    $config = static::allocateConfigValue($configFromProviders['annotations'] ?? [], $config);

    // Load the config/autoload/annotations.php and merge the config
    if (file_exists($configDir . '/autoload/annotations.php')) {
        $annotations = include $configDir . '/autoload/annotations.php';
        $config = static::allocateConfigValue($annotations, $config);
    }

    // Load the config/config.php and merge the config
    if (file_exists($configDir . '/config.php')) {
        $configContent = include $configDir . '/config.php';
        $appEnv = $configContent['app_env'] ?? 'dev';
        $cacheable = value($configContent['scan_cacheable'] ?? $appEnv === 'prod');
        if (isset($configContent['annotations'])) {
            $config = static::allocateConfigValue($configContent['annotations'], $config);
        }
    }

    return [$config, $serverDependencies, $cacheable];
}

获取项目配置信息

使用ProviderConfig::load()方法,获取项目配置信息,该方法实现如下,

php
namespace Hyperf\Config;

use Hyperf\Di\Definition\PriorityDefinition;
use Hyperf\Support\Composer;

use function class_exists;
use function is_string;
use function method_exists;
/**
 * Provider config allow the components set the configs to application.
 */
class ProviderConfig
{
    // 项目配置数组
    private static array $providerConfigs = [];

    /**
     * 从组件中加载和合并所有的服务提供者
     */
    public static function load(): array
    {
        if (! static::$providerConfigs) {
            $providers = Composer::getMergedExtra('hyperf')['config'] ?? [];
            static::$providerConfigs = static::loadProviders($providers);
        }
        return static::$providerConfigs;
    }

    public static function clear(): void
    {
        static::$providerConfigs = [];
    }

    protected static function loadProviders(array $providers): array
    {
        $providerConfigs = [];
        foreach ($providers as $provider) {
            if (is_string($provider) && class_exists($provider) && method_exists($provider, '__invoke')) {
                $providerConfigs[] = (new $provider())();
            }
        }

        return static::merge(...$providerConfigs);
    }

    protected static function merge(...$arrays): array
    {
        if (empty($arrays)) {
            return [];
        }
        $result = array_merge_recursive(...$arrays);
        if (isset($result['dependencies'])) {
            $result['dependencies'] = [];
            foreach ($arrays as $item) {
                foreach ($item['dependencies'] ?? [] as $key => $value) {
                    $depend = $result['dependencies'][$key] ?? null;
                    if (! $depend instanceof PriorityDefinition) {
                        $result['dependencies'][$key] = $value;
                        continue;
                    }

                    if ($value instanceof PriorityDefinition) {
                        $depend->merge($value);
                    }
                }
            }
        }

        return $result;
    }
}

这里用到了ConfigProvider 机制,参考官网讲解。

简单来说,Hyperf会读取每个组件包composer.json中指定的信息,作为数组返回。Composer::getMergedExtra获取composer.json文件中的配置信息 ,打印获取的内容如下

接下来查看loadProviders方法,

php
protected static function loadProviders(array $providers): array
{
    $providerConfigs = [];
    // 循环所有服务提供者
    foreach ($providers as $provider) {
        // 如果是字符串,且存在该类,且该类存在__invoke方法
        if (is_string($provider) && class_exists($provider) && method_exists($provider, '__invoke')) {
            // 当通过方法调用类时,会触发__invoke魔术方法
            // 所以(new $provider())()相当于执行服务提供者的__invoke方法
            $providerConfigs[] = (new $provider())();
        }
    }

    return static::merge(...$providerConfigs);
}

我们以hyperf/config包为例,打开目录下ConfigProvider类,打开__invoke方法,会发现,这里返回的是一个配置数组,代码如下,

php
class ConfigProvider
{
    public function __invoke(): array
    {
        return [
            'dependencies' => [
                ConfigInterface::class => ConfigFactory::class,
            ],
            'aspects' => [
                ValueAspect::class,
            ],
            'listeners' => [
                RegisterPropertyHandlerListener::class,
            ],
        ];
    }
}

$providerConfigs数组格式如下,每个服务提供者都会生成一个数组。

php
 array(3) {
    ["dependencies"]=>
        array(1) {
            ["Hyperf\Contract\ConfigInterface"]=>
            string(27) "Hyperf\Config\ConfigFactory"
        }
    ["aspects"]=>
        array(1) {
            [0]=>
                string(36) "Hyperf\Config\Annotation\ValueAspect"
        }
    ["listeners"]=>
        array(1) {
            [0]=>
                string(54) "Hyperf\Config\Listener\RegisterPropertyHandlerListener"
        }
}

最后通过static::merge方法,将所有服务提供者的配置数组进行合并,最终返回一个数组。

php
protected static function merge(...$arrays): array
{
    if (empty($arrays)) {
        return [];
    }
    // 递归地合并一个或多个数组,
    // 如果输入的数组中有相同的字符串键名,则这些值会被合并到一个数组中去
    $result = array_merge_recursive(...$arrays);
    // 如果存在dependencies key,则对依赖进行合并
    if (isset($result['dependencies'])) {
        $result['dependencies'] = [];
        foreach ($arrays as $item) {
            foreach ($item['dependencies'] ?? [] as $key => $value) {
                $depend = $result['dependencies'][$key] ?? null;
                if (! $depend instanceof PriorityDefinition) {
                    $result['dependencies'][$key] = $value;
                    continue;
                }

                if ($value instanceof PriorityDefinition) {
                    $depend->merge($value);
                }
            }
        }
    }
    // 返回合并后的配置数组
    return $result;
}

以上,ProviderConfig::load()方法走完。

回到initConfigByFile方法,往下看,将config目录下配置与上面获取的配置进行合并,存在重复则配置文件优先

php
private static function initConfigByFile(string $configDir): array
{
    $config = [];
    $configFromProviders = [];
    $cacheable = false;
    if (class_exists(ProviderConfig::class)) { 
        $configFromProviders = ProviderConfig::load();
    }
    // 加载 autoload 目录下的 dependencies.php 文件,并合并依赖配置
    $serverDependencies = $configFromProviders['dependencies'] ?? []; 
    if (file_exists($configDir . '/autoload/dependencies.php')) {
        $definitions = include $configDir . '/autoload/dependencies.php';
        $serverDependencies = array_replace($serverDependencies, $definitions ?? []);
    }
    // 将 Provider 配置中的 annotations 值分配给 config
    $config = static::allocateConfigValue($configFromProviders['annotations'] ?? [], $config);

    // 加载 autoload 目录下的 annotations.php 文件,并合并配置
    if (file_exists($configDir . '/autoload/annotations.php')) {
        $annotations = include $configDir . '/autoload/annotations.php';
        $config = static::allocateConfigValue($annotations, $config);
    }

    // 加载 config 目录下的 config.php 文件,合并
    if (file_exists($configDir . '/config.php')) {
        $configContent = include $configDir . '/config.php';
        $appEnv = $configContent['app_env'] ?? 'dev';
        $cacheable = value($configContent['scan_cacheable'] ?? $appEnv === 'prod');
        if (isset($configContent['annotations'])) {
            $config = static::allocateConfigValue($configContent['annotations'], $config);
        }
    }

    return [$config, $serverDependencies, $cacheable];
}
  • $config 包含合并后的注解配置数组
  • serverDependencies 包含合并后的依赖配置数组
  • cacheable 布尔值,表示扫描结果是否可缓存(由config文件中的scan_cacheable参数决定)

接下来返回自身实例

php
return self::$instance = new self(
    $cacheable,
    $configDir,
    $config['paths'] ?? [],
    $serverDependencies ?? [],
    $config['ignore_annotations'] ?? [],
    $config['global_imports'] ?? [],
    $config['collectors'] ?? [],
    $config['class_map'] ?? []
);

现在,config文件配置已经得到,

php
$config = ScanConfig::instance($configDir);

扫描并更新 composerLoader 的类映射表

php
public static function init(?string $proxyFileDirPath = null, ?string $configDir = null, ?ScanHandlerInterface $handler = null): void
{
     if (! $proxyFileDirPath) {
         // This dir is the default proxy file dir path of Hyperf
         $proxyFileDirPath = BASE_PATH . '/runtime/container/proxy/';
     }

     if (! $configDir) {
         // This dir is the default proxy file dir path of Hyperf
         $configDir = BASE_PATH . '/config/';
     }

     if (! $handler) {
         $handler = new PcntlScanHandler();
     }

     $composerLoader = Composer::getLoader();

     if (file_exists(BASE_PATH . '/.env')) {
         static::loadDotenv();
     }

     $config = ScanConfig::instance($configDir); 
    // 将提供的类映射添加到 Composer 的自动加载器中
     $composerLoader->addClassMap($config->getClassMap());
     // 实例化Scanner扫描器
     $scanner = new Scanner($config, $handler);
     $composerLoader->addClassMap(
         $scanner->scan($composerLoader->getClassMap(), $proxyFileDirPath)
     );

     // Initialize Lazy Loader. This will prepend LazyLoader to the top of autoload queue.
     LazyLoader::bootstrap($configDir);
}

由上面配置文件可知,默认情况下,class_map为空。