入口程序
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
启动流程
- 基础
bootstrap
行为,如必要的常量定义、Composer加载器引入、配置读取... - 生成被所有
worker/task
进程共享的程序全局期的对象 - 启动时所有进程中只能执行一次的操作,如前置Process的启动...
-
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
配置的命名空间,另一部分是所有组件下的Command
、Bootstrap
、Aop
命名空间。
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)
打印查看。
未完待续...