Swoft 启动流程

入口程序

Swoft入口是使用命令php bin/swoft start启动HTTP服务器

$ php bin/swoft start
                          Server Information
 ********************************************************************
 * HTTP | host: 0.0.0.0, port: 80, type: 1, worker: 1, mode: 3
 * TCP  | host: 0.0.0.0, port: 8099, type: 1, worker: 1 (Enabled)
 ********************************************************************
 Server has been started. (master PID: 1, manager PID: 6)
 You can use CTRL + C to stop run.
docker@default: ~$ docker exec -it myswoft bash

root@f92d826e0248:/var/www/swoft# apt-get install procps
root@f92d826e0248:/var/www/swoft# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.5 31.7 585500 321740 pts/0   Ssl+ 17:21   0:01 php-swoft master process (/var/www/swoft/bin/swoft)
root         6  0.0  0.8 511164  8352 pts/0    S+   17:21   0:00 php-swoft manager process
root         8  2.3  1.6 518440 16920 pts/0    S+   17:21   0:06 php-swoft task process
root         9  2.3  1.6 518900 17064 pts/0    S+   17:21   0:06 php-swoft worker process
root        10  3.3  1.8 520924 18948 pts/0    S+   17:21   0:09 php-swoft reload process
root        11  0.0  0.2  18136  2524 pts/1    Ss   17:23   0:00 bash
root        73  0.0  0.2  36640  2804 pts/1    R+   17:25   0:00 ps aux

启动流程

启动流程
  1. 基础bootstrap行为,如必要的常量定义、Composer加载器引入、配置读取...
  2. 生成被所有worker/task进程共享的程序全局期的对象
  3. 启动时所有进程中只能执行一次的操作,如前置Process的启动...
  4. Bean容器基本初始化以及项目启动流程需要的coreBean的加载

启动入口

启动脚本

Swoft的启动入口是bin/swoft脚本文件

$ vim bin/swoft

#!/usr/bin/env php
<?php
require_once __DIR__ . '/bootstrap.php';
$console = new \Swoft\Console\Console();
$console->run();

启动文件

swoft脚本加载当前目录下的bootstrap.php启动文件

$ vim bin/bootstrap.php
<?php
// 加载Composer的autoload文件
require_once dirname(__DIR__) . '/vendor/autoload.php';
// 加载自定义的常量与别名配置
require_once dirname(__DIR__) . '/config/define.php';

// 初始化Bean对象工厂(种子工厂)
\Swoft\Bean\BeanFactory::init();

/* @var \Swoft\Bootstrap\Boots\Bootable $bootstrap*/
$bootstrap = \Swoft\App::getBean(\Swoft\Bootstrap\Bootstrap::class);
$bootstrap->bootstrap();

启动文件的核心也就是框架的核心是BeanFactory对象工厂

什么是Bean呢?Swoft中的Bean是类的对象实例,详情可参见《Swoft Bean》

启动阶段主要完成两件事

  • 根据默认的注解扫描机制实例化Bean对象
  • 根据配置对Bean对象进行设置

这种通过配置来实例化类并设置对象属性的方式在PHP框架中广泛被应用

常量配置

查看自定义配置文件config/define.php文件

$ vim config/define.php
<?php
// 定义路径分隔符常量
! defined('DS') && define('DS', DIRECTORY_SEPARATOR);
// 定义应用名称常量
! defined('APP_NAME') && define('APP_NAME', 'swoft');
// 定义项目基础路径常量
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));

// 注册别名
$aliases = [
    '@root'       => BASE_PATH,
    '@env'        => '@root',
    '@app'        => '@root/app',
    '@res'        => '@root/resources',
    '@runtime'    => '@root/runtime',
    '@configs'    => '@root/config',
    '@resources'  => '@root/resources',
    '@beans'      => '@configs/beans',
    '@properties' => '@configs/properties',
    '@console'    => '@beans/console.php',
    '@commands'   => '@app/command',
    '@vendor'     => '@root/vendor',
    '@public'     => '@root/public'
];
// 为应用设置别名
\Swoft\App::setAliases($aliases);

define.php自定义配置文件主要完成两件事:定义PHP常量、为应用设置别名

别名机制

Swoft提供了别名机制,别名本质上是字符串的替换,主要用于文件目录或路径。

可通过系统应用简写类App提供的方法对别名进行设置和获取:

$ vi /vendor/swoft/framework/src/App.php
  • App::getAlias(string $alias): string 根据别名获取实际值,返回字符串。
  • App::setAlias(string $alias, string $path = null) 设置单个别名,参数为字符串别名与路径
  • App::setAliases(array $aliases) 同时设置多个别名,传入用作配置的关联数组。

简单来说,Swoft别名机制是用来解决路径问题的。

初始化对象

BeanFactory工厂用于对Bean种子的初始化,简单来说就是统一地进行对象的实例化。

$ vim /vendor/swoft/framework/src/Bean/BeanFactory.php
class BeanFactory implements BeanFactoryInterface
{
    public static function init()
    {
        // 获取property配置,读取config/properties/目录下的配置文件,merge到同一个数组。
        $properties = self::getProperties();

        // 实例化依赖注入DI或控制反转IoC的容器
        self::$container = new Container();
        // 设置依赖注入容器的属性
        self::$container->setProperties($properties);
        // 自动加载服务器注解
        self::$container->autoloadServerAnnotation();
        
        // 获取服务定义的Bean
        $definition = self::getServerDefinition();
        // IoC容器添加Bean的定义
        self::$container->addDefinitions($definition);
        // IoC容器初始化所有Bean
        self::$container->initBeans();
    }
}

种子工厂BeanFactory初始化主要是为了实例化Bean实例对象,Bean又从哪里来呢?

Bean的来源有两处:使用Annotation注解定义的Bean、服务定义的Bean

为了获取使用注解定义的Bean,采取的方式是

  • 读取属性配置
  • 实例化依赖注入DI或控制反转IoC的容器

为什么要这么做呢?通过配置来实例化类并设置对象属性,这种做法在PHP框架中广泛被应用。

1. 读取属性配置

$properties = self::getProperties();
/**
 * @return array
 */
private static function getProperties()
{
    $properties = [];
    $config     = new Config();
    $dir        = App::getAlias('@properties');
    // 判断属性配置文件夹是否可读
    if (is_readable($dir)) {
        $config->load($dir);
        $properties = $config->toArray();
    }

    return $properties;
}

属性别名配置文件夹

'@properties' => '@configs/properties'

读取配置的重点是加载配置文件,使用全局配置管理器Config提供的load方法。

$ vim /vendor/swoft/framework/src/Core/Config.php

load方法简单来说就是根据配置文件夹的文件路径,循环所有读取配置文件,合并为一个配置文件。

public function load(
    string $dir,
    array $excludeFiles = [],
    string $strategy = DirHelper::SCAN_BFS,
    string $structure = self::STRUCTURE_MERGE
): self {
    $mapping = [];
    if (StringHelper::contains($dir, ['@'])) {
        $dir = App::getAlias($dir);
    }
    if (!is_dir($dir)) {
        throw new \InvalidArgumentException('Invalid dir parameter');
    }

    $dir = DirHelper::formatPath($dir);
    $files = DirHelper::glob($dir, '*.php', $strategy);
    foreach ($files as $file) {
        if (! is_readable($file) || ArrayHelper::isIn($file, $excludeFiles)) {
            continue;
        }
        $loadedConfig = require $file;
        if (!\is_array($loadedConfig)) {
            throw new \InvalidArgumentException('Syntax error find in config file: ' . $file);
        }
        $fileName = DirHelper::basename([$file]);
        $key = current(explode('.', current($fileName)));
        switch ($structure) {
            case self::STRUCTURE_SEPARATE:
                $configMap = [$key => $loadedConfig];
                break;
            case self::STRUCTURE_MERGE:
            default:
                $configMap = $loadedConfig;
                break;
        }
        $mapping = ArrayHelper::merge($mapping, $configMap);
    }
    $this->properties = $mapping;
    return $this;
}

2. 设置依赖注入容器

启动流程的核心在于容器,关于容器需理解什么是依赖注入(DI)和控制反转(IoC),详情参见《IoC 控制反转》

// 实例化依赖注入DI或控制反转IoC的全局容器
self::$container = new Container();
// 设置依赖注入容器的属性
self::$container->setProperties($properties);
// 自动加载服务器注解
self::$container->autoloadServerAnnotation();

这里的Container指的是全局容器,容器里面是装的是什么东西呢?是Bean,是实例化的对象。容器有什么用呢?管理对象的依赖关系。如何管理的呢?

$ vim /vendor/swoft/framework/src/Bean/Container.php

注解的生命周期

关于读取配置并设置容器的配置,上面已经分析过,这里重点分析容器自动加载注解autoloadServerAnnotation(),全局容器Container提供了autoloadServerAnnotation()方法用于注册服务注解,这里为什么会使用到注解,注解的作用是为了解耦,注解的详情参见《Swoft Annotation 注解》

/**
 * 注册服务器的注解
 */
public function autoloadServerAnnotation()
{
    // 从属性配置文件中的bootScan选项中获取待扫描的命名空间
    $bootScan = $this->getScanNamespaceFromProperties('bootScan');
    // 根据配置实例化服务注解资源
    $resource = new ServerAnnotationResource($this->properties);
    // 添加扫描的命名空间
    $resource->addScanNamespace($bootScan);
    // 获取已解析的配置beans
    $definitions = $resource->getDefinitions();

    $this->definitions = array_merge($definitions, $this->definitions);
}

从属性配置文件中的bootScan选项中获取待扫描的命名空间

// 从属性配置文件中的bootScan选项中获取待扫描的命名空间
$bootScan = $this->getScanNamespaceFromProperties('bootScan');

从属性配置文件夹config/properties/app.php会发现有一段bootScan应用启动时扫描的配置

'bootScan'     => [
    'App\Commands',
    'App\Boot',
],

配置中存放的命名空间,默认是应用的命令行和启动两个命名空间,有什么用呢?

// 根据配置实例化服务注解资源
$resource = new ServerAnnotationResource($this->properties);
// 添加需要扫描的命名空间
$resource->addScanNamespace($bootScan);
// 获取已解析的配置beans
$definitions = $resource->getDefinitions();

这里重点分析getDefinitions()方法是如何获取已经解析的配置Beans对象。

/**
 * 获取已解析的配置beans
 *
 * @return array
 * <pre>
 * [
 *     'beanName' => ObjectDefinition,
 *      ...
 * ]
 * </pre>
 */
public function getDefinitions()
{
    // 获取扫描的PHP文件,即扫描上一步注册进来的命名空间。
    $classNames     = $this->registerLoaderAndScanBean();
    // 获取自定义配置的扫描文件即PHP类库文件
    $fileClassNames = $this->scanFilePhpClass();
    // 将系统内置的类库和用户自定义的类库合并以获取所有需要扫描的类
    $classNames     = array_merge($classNames, $fileClassNames);
    // 循环遍历所有类库
    foreach ($classNames as $className) {
        // 解析每个类库文件中的Bean注解
        $this->parseBeanAnnotations($className);
    }
    // 解析注解数据并存放到definitions成员属性中
    $this->parseAnnotationsData();
    // 返回所有的注解数据
    return $this->definitions;
}

这里总结下,Swoft使用的组件化方式,将不同的组件使用IoC容器进行统一管理实例化对象(Bean)的依赖关系。然后通过注解的进行使用。在容器中首先会根据命名空间扫描注解,Swoft中的注解主要包括两部分,一部分是属性配置文件夹下config/properties/app.php应用属性配置中bootScan配置的命名空间,另一部分是所有组件下的CommandBootstrapAop命名空间。

3. 初始化对象

// 获取服务注解数据
$definition = self::getServerDefinition();
// IoC容器添加注解数据
self::$container->addDefinitions($definition);
// IoC容器初始化Bean
self::$container->initBeans();

这里需要注意下关于definition是什么,根据注释上看是已解析Bean的规则。

首先来看下是如何获取服务的注解数据的

/**
 * @return array
 * @throws \InvalidArgumentException
 */
private static function getServerDefinition(): array
{
    // 通过常量配置文件config/define.php中的控制台别名@console获取对应文件保存路径
    $file             = App::getAlias('@console');

    // 判断配置中的文件是否可读进而引入
    $configDefinition = [];
    if (\is_readable($file)) {
        $configDefinition = require_once $file;
    }

    // 获取框架核心的Bean
    $coreBeans  = self::getCoreBean(BootBeanCollector::TYPE_SERVER);

    // 将框架核心Bean和@console中定义的注解合并后返回
    return ArrayHelper::merge($coreBeans, $configDefinition);
}

这里的@console是干什么用的呢?字面意思是控制台,与其相关的应该是一些自定义命令的功能。

/**
 * 定义配置bean
 *
 * @param array $definitions
 */
public function addDefinitions(array $definitions)
{
    $resource          = new DefinitionResource($definitions);
    $this->definitions = array_merge($resource->getDefinitions(), $this->definitions);
}
/**
 * @throws \InvalidArgumentException
 * @throws \ReflectionException
 */
public function initBeans()
{
    $autoInitBeans = $this->properties['autoInitBean'] ?? false;
    if (!$autoInitBeans) {
        return;
    }

    // 循环初始化
    foreach ($this->definitions as $beanName => $definition) {
        $this->get($beanName);
    }
}

autoInitBean配置位于config/properties/app.php属性配置文件中,用于配置是否初始化Bean,为什么要有这个配置选项呢?

/**
 * 获取一个bean
 *
 * @param string $name 名称
 *
 * @return mixed
 * @throws \ReflectionException
 * @throws \InvalidArgumentException
 */
public function get(string $name)
{
    // 已经创建
    if (isset($this->singletonEntries[$name])) {
        return $this->singletonEntries[$name];
    }

    // 未定义
    if (!isset($this->definitions[$name])) {
        throw new \InvalidArgumentException(sprintf('Bean %s not exist', $name));
    }

    /* @var ObjectDefinition $objectDefinition */
    $objectDefinition = $this->definitions[$name];

    return $this->set($name, $objectDefinition);
}

Swoft是如何根据Bean的名称来对Bean进行初始化的呢?

/**
 * 创建bean
 *
 * @param string           $name             名称
 * @param ObjectDefinition $objectDefinition bean定义
 *
 * @return object
 * @throws \ReflectionException
 * @throws \InvalidArgumentException
 */
private function set(string $name, ObjectDefinition $objectDefinition)
{
    // bean创建信息
    $scope             = $objectDefinition->getScope();
    $className         = $objectDefinition->getClassName();
    $propertyInjects   = $objectDefinition->getPropertyInjections();
    $constructorInject = $objectDefinition->getConstructorInjection();

    if ($refBeanName = $objectDefinition->getRef()) {
        return $this->get($refBeanName);
    }

    // 构造函数
    $constructorParameters = [];
    if ($constructorInject !== null) {
        $constructorParameters = $this->injectConstructor($constructorInject);
    }

    $reflectionClass = new \ReflectionClass($className);
    $properties      = $reflectionClass->getProperties();

    // new实例
    $isExeMethod = $reflectionClass->hasMethod($this->initMethod);
    $object      = $this->newBeanInstance($reflectionClass, $constructorParameters);

    // 属性注入
    $this->injectProperties($object, $properties, $propertyInjects);

    // 执行初始化方法
    if ($isExeMethod) {
        $object->{$this->initMethod}();
    }

    if (!$object instanceof AopInterface) {
        $object = $this->proxyBean($name, $className, $object);
    }

    // 单例处理
    if ($scope === Scope::SINGLETON) {
        $this->singletonEntries[$name] = $object;
    }

    return $object;
}

Bean初始化

  • 注解解析后获取类相关信息
  • 注入构造函数
  • 初始化类并执行构造函数
  • 注入属性
  • 执行初始化方法
  • AOP处理找到实际代理的类
  • 单例处理
  • 返回生成的Bean对象

总结下,Swoft的核心是使用IoC去扫描注解并通过注解注解初始化Bean,需要关注的是启动时是如何扫描注解文件,另外扫描到的注解是如何进行初始化成Bean的。

通过BeanFactory::init()初始化后就可以直接使用Bean

BeanFactory::getBean($bean_name)

App::getBean($bean_name)

启动

现在回到bin/bootstrap.php文件中,继续分析最后的环节,分析如何启动应用。

/* @var \Swoft\Bootstrap\Boots\Bootable $bootstrap*/
$bootstrap = \Swoft\App::getBean(\Swoft\Bootstrap\Bootstrap::class);
$bootstrap->bootstrap();

通过IoC容器根据Bootstrap类生成启动对象

$bootstrap = \Swoft\App::getBean(\Swoft\Bootstrap\Bootstrap::class);

启动对象执行启动方法

$bootstrap->bootstrap();

获取启动项,根据启动项中设置的排序值进行排序后,循环遍历依次通过每个启动获取启动的Bean对象,并执行每个对象中的启动方法。

public function bootstrap()
{
    $bootstraps = BootstrapCollector::getCollector();
    $temp = \array_column($bootstraps, 'order');

    \array_multisort($temp, SORT_ASC, $bootstraps);

    foreach ($bootstraps as $bootstrapBeanName => $name){
        /* @var Bootable $bootstrap*/
        $bootstrap = App::getBean($bootstrapBeanName);
        $bootstrap->bootstrap();
    }
}

通过框架源码中可以观察到需要项目启动所需的类库,可以使用var_dump($bootstraps)打印查看。

启动文件

未完待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容