Swoft2 框架精华教程:Swoft 的启动过程与核心源代码解析

框架初始化阶段

日志模块加载

路径相关初始化

  • 初始化 basePath路径,如果未提供,会根据当前执行文件地址进行寻找项目主目录。

  • 初始化系统别名

    别名 路径
    @base swoft 项目目录
    @app swoft/app
    @config swoft/config
    @runtime swoft/runtime
    @resource swoft/resource

主要进程初始化

获取框架启动相关的各个主要进程,:

实例化应用进程实例对象,应用进程对象包含以下主要进程实例对象(每个进程均持有应用的对象实例)。

  • 环境信息进程实例对象
  • 配置信息进程实例对象
  • 注解信息进程实例对象
  • Bean 进程实例对象
  • 控制台进程实例对象
php 复制
protected function processors(): array
{
    return [
        new EnvProcessor($this),
        new ConfigProcessor($this),
        new AnnotationProcessor($this),
        new BeanProcessor($this),
        new EventProcessor($this),
        new ConsoleProcessor($this),
    ];
}

运行阶段

运行阶段,指的是应用进程对象处理的过程。这个过程又分为以下六个更详细的阶段。

这六个阶段,是对初始化过程中的各个进程实例对象的执行过程。

环境信息进程实例处理阶段

获取 @base/.env 文件

php 复制
// Load env info
$factory = new DotenvFactory([
    new EnvConstAdapter,
    new PutenvAdapter,
    new ServerConstAdapter
]);

类图

    
        
            classDiagram
	direction LR
	class AdapterInterface {
		<<interface>>
		+ isSupport(): bool;
		+ get(string $name): PhpOption;
		+ set(string $name, mixed? $val = null): void;
		+ clear(string $name): void;
	}
	class EnvConstAdapter
	class PutenvAdapter
	class ServerConstAdapter
	
	EnvConstAdapter ..|> AdapterInterface
    PutenvAdapter ..|> AdapterInterface
    ServerConstAdapter ..|> AdapterInterface

        
    
  • EnvConstAdapter($_ENV环境变量)
  • PutenvAdapter(系统环境变量中获取信息)
  • ServerConstAdapter($_SERVER变量中获取信息)

从以上不同的环境变量来源中,获取所有的环境变量信息。保存在 Dotenv 对象内部 loader 加载器中。

配置信息进程实例处理阶段

设置两个常量,如果环境变量中有这两变量的配置,则使用他们,没有配置,默认都是0。

php 复制
define('APP_DEBUG', (int)env('APP_DEBUG', 0));
define('SWOFT_DEBUG', (int)env('SWOFT_DEBUG', 0));

注解信息进程实例处理阶段

实例化相关对象

获取注解要加载的相关信息,根路径,通知处理函数,禁用的自动加载器,不包含的 Psr4Prefixes 目录(在每个组件的 Autoload 自动加载类中,有获取当前组件的 Psr4Prefixes 函数)。

php 复制
// Find AutoLoader classes. Parse and collect annotations.
AnnotationRegister::load([
    'inPhar'               => IN_PHAR,
    'basePath'             => $app->getBasePath(),
    'notifyHandler'        => [$this, 'notifyHandle'],
    // TODO force load framework components: bean, error, event, aop
    'disabledAutoLoaders'  => $app->getDisabledAutoLoaders(),
    'excludedPsr4Prefixes' => $app->getDisabledPsr4Prefixes(),
]);
// 获取自动加载所有组件类后,对注解信息的一些统计信息,并且打印到控制台
$stats = AnnotationRegister::getClassStats();
CLog::info(
    'Annotations is scanned(autoloader %d, annotation %d, parser %d)',
    $stats['autoloader'],
    $stats['annotation'],
    $stats['parser']
);

AnnotationRegister 注解注册器中又实例化了一个注解资源对象(AnnotationResource),加载器默认注册类加载,通过 class_exists 检测来加载类。

检测自动类加载器,是否默认为 composer 的类加载器。如果是,将 AnnotationResource 对象的 classLoader 属性绑定 composer 的类加载器实例。

获取包含文件,将包含文件放到 AnnotationResource 对象的 includedFiles 属性中。

自动加载所有组件相关的类

通过 AnnotationResource 中的 load 方法,首先通过 composer 类加载器,可以获取到当前主项目(swoft应用)的路径前缀(composer.json 中,autoload/autolaod-dev 对应的目录),通过对这些目录进行遍历,可以获取主项目中每个命名空间和目录绑定的关系。

php 复制
public function load(): void
{
    // 获取主项目中相关的 psr4 标准对应的信息
    $prefixDirsPsr4 = $this->classLoader->getPrefixesPsr4();

    // 遍历主项目中的 psr4 相关信息
    foreach ($prefixDirsPsr4 as $ns => $paths) {
        // Only scan namespaces
        // 是否配置了特定的扫描命名空间,如果是只扫描这个命名空间,其他空间直接跳过
        if ($this->onlyNamespaces && !in_array($ns, $this->onlyNamespaces, true)) {
            $this->notify('excludeNs', $ns);
            continue;
        }

        // It is excluded psr4 prefix
        // 判断当前 psr4 是否配置了排除规则,如果配置过,也跳过不扫
        if ($this->isExcludedPsr4Prefix($ns)) {
            AnnotationRegister::addExcludeNamespace($ns);
            $this->notify('excludeNs', $ns);
            continue;
        }

        // Find package/component loader class
        // 在需要扫描的命名空间对应的文件目录中,扫描是否有自动加载类 AutoLoader.php
        foreach ($paths as $path) {
            // 获取 AutoLoader.php 文件的路径(这里注意不要写成 Autoload.php)
            $loaderFile = $this->getAnnotationClassLoaderFile($path);
            // 如果自动加载文件不存在,交给自动加载不存在的处理函数,跳过本次路径扫描
            if (!file_exists($loaderFile)) {
                $this->notify('noLoaderFile', $this->clearBasePath($path), $loaderFile);
                continue;
            }
      			// 自动加载类存在,则获取 AutoLoader 的带命名空间的完全的类名
            $loaderClass = $this->getAnnotationLoaderClassName($ns);
            // 检测自动加载类是否存在,防止文件名和类名不一致情况
            if (!class_exists($loaderClass)) {
                $this->notify('noLoaderClass', $loaderClass);
                continue;
            }

            $isEnabled  = true;
            $autoLoader = new $loaderClass();
      			// 检测实例化的自动加载类是否实现类 swoft 要求的自动加载类的接口
            if (!$autoLoader instanceof LoaderInterface) {
                $this->notify('invalidLoader', $loaderFile);
                continue;
            }

            $this->notify('findLoaderClass', $this->clearBasePath($loaderFile));

            // If is disable, will skip scan annotation classes
            // 最后再次检查是否是被禁用的自动加载类,或者自动加载类自己配置为不启用
            if (isset($this->disabledAutoLoaders[$loaderClass]) || !$autoLoader->isEnable()) {
                $isEnabled = false;

                $this->notify('disabledLoader', $loaderFile);
            } else {
                // 正常的类加载器,保存下类加载器对应的文件路径
                AnnotationRegister::addAutoLoaderFile($loaderFile);
                $this->notify('addLoaderClass', $loaderClass);

                // Scan and collect class bean s
                // 从一个组件的加载配置中,加载注解相关信息
                $this->loadAnnotation($autoLoader);
            }

            // Storage autoLoader instance to register
            // 存储命名空间,自动加载器实例,是否启用信息到注解注册器
            AnnotationRegister::addAutoLoader($ns, $autoLoader, $isEnabled);
        }
    }
}

以下为另一段关键函数:$this->loadAnnotation($autoLoader)

php 复制
/**
 * Load annotations from an component loader config.
 *
 * @param LoaderInterface $loader
 *
 * @throws AnnotationException
 * @throws ReflectionException
 */
private function loadAnnotation(LoaderInterface $loader): void
{
    // 获取当前组件的命名空间和目录的绑定关系
    $nsPaths = $loader->getPrefixDirs();
	// 遍历当前组件命名空间
    foreach ($nsPaths as $ns => $path) {
        // 递归获取当前目录中的子目录和文件,获取一个迭代器
        $iterator = DirectoryHelper::recursiveIterator($path);

        /* @var SplFileInfo $splFileInfo */
        foreach ($iterator as $splFileInfo) {
            // 判断本次迭代的文件到底是文件还是目录,如果是目录则跳过
            $filePath = $splFileInfo->getPathname();
            // $splFileInfo->isDir();
            if (is_dir($filePath)) {
                continue;
            }

            $fileName  = $splFileInfo->getFilename();
            $extension = $splFileInfo->getExtension();
			// 判断当前文件是否是个php文件,不是也跳过
            if ($this->loaderClassSuffix !== $extension || strpos($fileName, '.') === 0) {
                continue;
            }

            // It is exclude filename
            // 判断当前文件是否是排除的文件,如果要排除,也加到注解注册器中的排除文件列表中
            if (isset($this->excludedFilenames[$fileName])) {
                AnnotationRegister::addExcludeFilename($fileName);
                continue;
            }

            $suffix    = sprintf('.%s', $this->loaderClassSuffix);
            $pathName  = str_replace([$path, '/', $suffix], ['', '\\', ''], $filePath);
            $className = sprintf('%s%s', $ns, $pathName);

            // Fix repeat included file bug
            // 如果当前文件已经在包含文件列表中
            $autoload = in_array($filePath, $this->includedFiles, true);

            // Will filtering: interfaces and traits
            // 对于没有包含过的文件,通过 class_exists 的方式自动加载到内存
            if (!class_exists($className, !$autoload)) {
                $this->notify('noExistClass', $className);
                continue;
            }

            // Parse annotation
            // 解析当前类的注解信息,保存到注解注册器中
            $this->parseAnnotation($ns, $className);
        }
    }
}

以上过程,通过遍历的方式,将所有的组件进行遍历一遍,这样所有组件中需要包含的类都会加载到内存,相关的注解信息,也都会存储到 AnnotationRegister 注解注册器中。

Bean 进程实例对象处理阶段

此部分主要是关于 BeanFactory 的使用,BeanFactory 的核心部分为对象容器 Container。

BeanFactory 需要收集以下信息,来进行所有 bean 对象的初始化:

  • 核心 bean 的定义
  • 注解解析对象(Parser)
  • 注解对象(Annotation)
  • bean 处理器对象

BeanHandler 对象解析

生成一个 BeanHandler 处理对象。此处将 Bean 处理过程抽象化,为的是方便扩展不同的 Bean 处理过程。

BeanHandler 对象中的三个方法:

  • beforeInit 方法中,已经将 Aop 切面编程相关的注解信息,跟当前正在处理的 bean 对象进行了绑定,这样,可以让 Bean 后续的实际执行过程中,触发切面中的一些操作。
  • classProxy 方法,生成给定类的代理类的相关代码,通过 require 对类进行包含,然后删除生成的代码,再通过 class_exists 方法检测以保证代理类正常加载成功,然后将原类和代理类的关联信息,存放到 Proxy 的 cache 属性中。代理类,生成了类的名字一种为带命名空间的($newClassName),一种为只有类名,不带命名空间的($proxyName
  • getReferenceValue 方法,获取 config 对象中,给定参数的名字的值

获取核心 Bean 对象定义

核心 bean 对象主要是指:

  • 组件 Autoloader 中 beans 方法定义的 bean
  • 主项目 app 目录下 bean.php 文件中定义的 bean

将以上 bean 定义的数组进行合并,将核心 Bean 定义传参给 BeanFactory 。优先获取组件中定义的 bean 定义,然后在用 app/bean.php 文件中的定义进行覆盖。所有 bean.php 中的定义优先级要高于组件中的 bean 定义,组件中的 bean 可以认为是默认定义。

注解注册器中获取解析对象和注解对象

将上一个注解信息实例进程中获得的注解相关的信息,注解解析器对象和注解信息对象放入 BeanFactory,用来初始化相关 bean 实例。

在容器的对bean 的实例化代码中,可以看到 parser 是如何和 annotation 工作的。

php 复制
$handler     = new BeanHandler();
$definitions = $this->getDefinitions();
$parsers     = AnnotationRegister::getParsers();
$annotations = AnnotationRegister::getAnnotations();

BeanFactory::addDefinitions($definitions);
BeanFactory::addAnnotations($annotations);
BeanFactory::addParsers($parsers);
BeanFactory::setHandler($handler);
// 将所有存储到 BeanFactory 的核心bean、注解实例化为 bean 对象,以供取用
BeanFactory::init();

事件进程实例对象处理阶段

获取核心 bean 对象 eventManager,将 eventManager 对象注册到 ListenerRegister 监听器注册对象上。

注册过程:

  • 遍历所有监听器,事件管理器逐个添加事件监听器。
  • 遍历所有订阅者,事件管理器逐个添加事件订阅者。
php 复制
/** @var EventManager $eventManager */
$eventManager = bean('eventManager');
// 注册事件管理器对象,返回事件管理器添加的监听器和订阅者数量
[$count1, $count2] = ListenerRegister::register($eventManager);
// 打印目前事件管理器关联的 Listener 和 subscriber
CLog::info('Event manager initialized(%d listener, %d subscriber)', $count1, $count2);

// 触发一个 app 启动的事件(这个事件会被关联到事件管理器上的处理者观察到并处理相关业务)
Swoft::trigger(SwoftEvent::APP_INIT_COMPLETE);

这里注意:最初的监听器(Listener)和订阅者(subscriber)的来源,也就是 ListenerRegister$listeners$subscribers属性中的数据,其主要是通过 ListenerRegister::addListener 添加到这两个列表中。通过进一步跟踪,可以看到在 event 组件的 src/Annotation/Parser/ 目录下的 SubscriberParser.phpListenerParser.php 两个注解解析器中看到添加监听的代码,这里就是通过类上的 @listener@subscriber 注解来识别的。如下示例:

php 复制
/**
 * Class AppInitCompleteListener
 *
 * @since 2.0
 *
 * @Listener(SwoftEvent::APP_INIT_COMPLETE)
 */

在注解进程阶段,这些监听器和事件关系数据,暂时会保存在 ListenerRegister 的两个监听器属性中;在通过 bean 进程阶段后,相关监听器就会实例化,然后存储到 Container 中;再通过事件进程阶段的时候,这些监听器就会在遍历 ListenerRegister 的两个监听器属性的时候,加载到事件管理器中。便于后续系统运行过程中,通过捕捉匹配的事件,来触发相应的监听器来处理。

另外,这里要说一下,这个事件管理器相当于一个自定义的同步事件管理器,就是为了同步处理对应事件。可能后边 http 服务的启动过程中,事件用的 swoole 的事件监听处理,而没有用事件管理处理,也许会让你感到疑惑。其实这个没冲突,swoole 自带自己的事件处理器,所以针对 swoole 已经支持的事件,用他自带的 on 函数,更直接。而对一些 swoole 不支持的事件,也要处理,所以才有了 swoft 的事件管理器。

例如:httpServer 的启动,它的监听器的加载等过程在执行cli 命令后,php bin/swoft http:start ,相当于执行了 HttpServer::class 的 start() 方法,这个过程中,加载了http 服务相关的事件监听。

控制台进程实例对象处理阶段

获取路由实例 cliRouter,将路由注册到命令行注册器。

cliApp 对象在 console 组件的 Autoloader::beans 方法中可以找到相应类,也就是 console 包下的 src/Application.php 文件,查看其中的 run 方法,可以找到 console 命令执行的方式。

php 复制
/** @var Router $router */
$router = bean('cliRouter');

// Register console routes
CommandRegister::register($router);

CLog::info('Console command route registered (group %d, command %d)', $router->groupCount(), $router->count());

// Run console application
if ($this->application->isStartConsole()) {
    // 在 console 组件的 Autoloader::beans 方法中可以找到相应类
    bean('cliApp')->run();
}

这部分相当于是串行执行的,这里不做更具体的说明。