php bin/hyperf.php start 发生了啥
php bin/hyperf.php start 发生了啥
用了这么久的hyperf框架,还没细细研究过源码,先看看start,入个门
先安装一个脚手架吧
- php 7.4
- swoole 4.8.5
- hyperf 2.2 - https://hyperf.wiki/2.2/#/
1
composer create-project hyperf/hyperf-skeleton:2.2 study-hyperf
先直接运行一下,看看有啥变化
1 | $ php bin/hyperf.php |
可以看到整个项目有一些些变化,在runtime下面生成了几个文件
1 | runtime |
得到几个疑问与猜想:
- aspects 切面?收集了项目内的切面信息?
- classes 类?收集了类的什么信息?什么类?
- scan 扫描?扫描了啥?结果又是啥?
- proxy 代理?难道说有文件被代理了?
- hyperf.pid pid?进程id吧?
入口文件
bin/hyperf.php
1 | #!/usr/bin/env php |
说明
error_reporting(E_ALL)
报告所有PHP错误BASE_PATH
太重要了,大部分文件扫描都用到它SWOOLE_HOOK_FLAGS
一键协程化的常量配置,后面start用到 官方说明composer
的自动加载
朴实无华的4步走
- di加载器初始化
- 加载容器container
- get app
- app run
这里使用了(function() {})();
的写法,还没看懂为啥,上面说的是 创建自己的作用域并保持全局命名空间干净
Hyperf\Di\ClassLoader::init() di加载器初始化
说明
- 定义代理类目录
$proxyFileDirPath
- 定义配置目录
$configDir
- 定义扫描器
$handler
- 代理composer的加载器
\Hyperf\Di\ClassLoader
- 初始化懒加载
\Hyperf\Di\LazyLoader\LazyLoader
不看细节,先看结果
万物皆可 var_dump($loaders);
以下内容做了美化与省略,只保留了一些关键点
composer被代理前
1 | Array |
当前被spl_autoload_register
的
Composer\Autoload\ClassLoader@loadClass()
PHPStan\PharAutoloader@loadClass()
composer被代理后
1 | Array |
当前被spl_autoload_register
的
Hyperf\Di\LazyLoader\LazyLoader@load()
Hyperf\Di\ClassLoader@loadClass()
PHPStan\PharAutoloader@loadClass()
可以明显的看到,spl_autoload_register
注册的东西被替换了,多了一个proxies的classmap,这不就是被代理的那两个类,看来以后加载这俩的时候,都去到了代理类
先看Hyperf\Di\ClassLoader@loadClass()
1 | public function loadClass(string $class): void |
- 果然,在进行自动加载的时候,先判断了是否在proxies里面。如果不在,走composer的老路。如果存在,则加载代理类
深入Proxy the composer class loader
1 | foreach ($loaders as &$loader) { |
这里只对ComposerClassLoader
做了处理,AnnotationRegistry::registerLoader
是为了doctrine/annotations
注解扫描做准备的
在hyperf3的版本,弃用了phpdoc的注解模式,所以AnnotationRegistry::registerLoader也就没有了
1 | public function __construct(ComposerClassLoader $classLoader, string $proxyFileDir, string $configDir, ScanHandlerInterface $handler) |
1. 设置composer的老路
- 这里除了保留composer原有的loader,还初始化了
\Hyperf\Utils\Composer
,后续再看
2. 加载 .env
- 也就是说,env函数,要在这一行之后才会生效,env的加载,使用了
vlucas/phpdotenv
- 如果我们的项目需要在不同的时候,读取不同的env,看了这里,就简单了,比如,BASE_PATH . ‘/.env’ 可以不存在,然后在启动文件自己一个Dotenv加载就行
3. 获取配置,每个包的ConfigProvider
和config
目录下 合并成一个Hyperf\Di\Annotation\ScanConfig
每一个ConfigProvider都需要有__invoke才能生效,也就是说,这里可以去执行一些东东
\Hyperf\Di\ConfigProvider
果然di包,就写了一些特殊执行,但我觉得这1
2
3
4
5
6
7
8
9
10
11// Register AST visitors to the collector.
if (! AstVisitorRegistry::exists(PropertyHandlerVisitor::class)) {
AstVisitorRegistry::insert(PropertyHandlerVisitor::class, PHP_INT_MAX / 2);
}
if (! AstVisitorRegistry::exists(ProxyCallVisitor::class)) {
AstVisitorRegistry::insert(ProxyCallVisitor::class, PHP_INT_MAX / 2);
}
// Register Property Handler.
RegisterInjectPropertyHandler::register();- 往
AstVisitorRegistry
优先队列里面丢了两个代理类处理器 PropertyHandlerVisitor
在__construct里处理属性的注解use \Hyperf\Di\Aop\PropertyHandlerTrait;
ProxyCallVisitor
每个方法添加特殊的__proxyCall
,use \Hyperf\Di\Aop\ProxyTrait;
- 往
PropertyHandlerManager
注册了一个Inject
的回调函数,后面有大用。简单的说,就是把当前注解里面映射的类,从容器里面取出来
合并ConfigProvider和config的配置
- 获取所有的annotations配置,即
paths 需要扫描的目录
,ignore_annotations 忽略的注解
,collectors 收集器
,class_map 替换加载的类
,global_imports
- 获取所有定义的
dependencies DI的依赖关系和类对应关系
- 获取
scan_cacheable
,扫描时是否使用缓存
这里内置的collectors有
- Hyperf\Cache\CacheListenerCollector
- Hyperf\Di\Annotation\AnnotationCollector 注解收集
- Hyperf\Di\Annotation\AspectCollector 切面收集
- Hyperf\ModelListener\Collector\ListenerCollector
将上面获取到的数据,初始化\Hyperf\Di\Annotation\ScanConfig
4. 添加composer的ClassMap
4. 初始化注解扫描器\Hyperf\Di\Annotation\Scanner
- 添加忽略注解与全局导入
5. 扫扫扫
使用缓存
- 文件存在,并且缓存配置是开启的,那么直接读取
scan.cache
- 给每个
collector
添加扫描结果
fork子进程进行扫描
通过子进程来完成缓存文件的生成,巧妙的避开了spl_autoload_register下会污染当前父进程的autoload的弊端。通过子进程完成扫描动作,传递结果文件给父进程,父进程直接进行自动加载即可。
1 | $scanned = $this->handler->scan(); |
当不传入handler时,使用默认的\Hyperf\Di\ScanHandler\PcntlScanHandler
,先进行 pcntl_fork
之后父进程一直等待pcntl_wait
子进程结束。也就是说,这个地方的$scanned,其实会返回两次,一次在父进程里(true),一次在子进程里(false)
$scanned === false 子进程
再次运行$this->deserializeCachedScanData($collectors);
- 没有
scan.cache
时,不做处理 - 有
scan.cache
时,$collectors存入上次扫描的结果
初始化 \Hyperf\Di\Annotation\AnnotationReader
- todo
获取所有的反射类
1 | $classes = ReflectionManager::getAllClasses($paths); |
- 扫描目录下的所有php文件
- 解析文件内容,如果是class,就反射
- 形成一个所有反射类的数组映射
清理$collectors被移除的class
1 | $this->clearRemovedClasses($collectors, $classes); |
- 这个地方总是生成最新反射类扫描结果
classes.cache
- 没有
classes.cache
时,array_diff不会有结果,也就不做处理 - 有
classes.cache
时,比较上次与现在的区别,$collectors清理被移除的类
清理修改过的类文件
- 循环所有的反射类 $classes
- 如果文件修改时间大于
scan.cache
文件修改时间,即上次扫描时间,那么就清理$collectors中的当前类,重新收集该类的信息
收集注解的信息
1 | $this->collect($annotationReader, $reflectionClass); |
普通注解\Hyperf\Di\Annotation\AbstractAnnotation
- 只是保存注解到
AnnotationCollector
- AnnotationCollector::collectClass($className, static::class, $this);
- AnnotationCollector::collectProperty($className, $target, static::class, $this);
- AnnotationCollector::collectMethod($className, $target, static::class, $this);
1. 解析class的注解
public function collectClass(string $className): void;
用\Hyperf\Di\Annotation\Aspect
作为特殊例子
- 先保存注解到
AnnotationCollector
注解收集器 [_c] AspectLoader::load($className);
用于获取注解里面的配置信息- 通过代码可以发现,类的配置优先于注解的配置
- 保存注解的配置到
AspectCollector
切面收集器
2. 解析property的注解
public function collectProperty(string $className, ?string $target): void;
用\Hyperf\Di\Annotation\Inject
作为特殊例子
- 获取注入的类,优先通过反射获取
$reflectionProperty->getType()->getName()
,再通过phpdoc去获取 - 如果是lazy===true,那么在注入类前面加上
'HyperfLazy\\'
前缀,用于标记该类为懒加载 - 保存注解到
AnnotationCollector
注解收集器 [_p]
3. 解析method的注解
public function collectMethod(string $className, ?string $target): void;
用\Hyperf\Cache\Annotation\Cacheable
作为特殊例子
- 如果有listener,则添加
CacheListenerCollector
- 保存注解到
AnnotationCollector
注解收集器 [_m]
加载配置文件的切面
1 | $this->loadAspects($lastCacheModified); |
- 先合并ConfigProvider、config.php、config/aspect.php的配置,得到最终的
$aspects
getChangedAspects
出现了aspects.cache
,可以看出config/aspect.php配置的无法定义优先级- 获取移除和修改的切面
- 接下来跟
\Hyperf\Di\Annotation\Aspect
注解的收集是一样的
生成代理类
1. 通过反射类初始化代理
1 | $this->initProxiesByReflectionClassMap($this->classMap) |
需要生成代理类的有两种文件
- 文件是被切面的class,如
- 文件内有被切面的annotations
遍历所有的classmap,符合上述条件的类,最后都放到$proxies
中,示例:
1 | array(2) { |
2. 生成代理文件
1 | $this->generateProxyFiles() |
- 这里的是使用
nikic/php-parser
生成代理类文件,贼强大 - https://www.bilibili.com/video/BV1ST411u7rk 李铭昕大佬的aop讲解
生成扫描缓存 scan.cache
- 生成扫描缓存文件,父进程要读这个文件
- exit标志着该子进程结束
$scanned === true 父进程
- 读取
scan.cache
- 给每个
collector
添加扫描结果
LazyLoader
- 注册了一个自动加载,优先级置顶
\Hyperf\Di\LazyLoader\LazyLoader::load
- 加载类时,如果配置文件存在或者是HyperfLazy开头的类,才会进行代理类的生成,而不是一开始就生成,大部分情况下,是请求过来的时候,这个类才会走上面的一些步骤去生成代理
总结
- 代理composer加载器
- 注解收集器
- 切面收集器
- 生成代理类。这一步决定着hyperf的注解与切面的实现
- 懒加载优先
加载容器container
定义源工厂
1 | (new DefinitionSourceFactory(true))() |
先看结果
这里是把ConfigProvider和config/auto/dependencies.php合并起来,进行DI源的初始化
看下source,这里只截取了ApplicationInterface
和StdoutLoggerInterface
1 | array(22) { |
经过autowire之后
1 | array(22) { |
猜想:通过上面的结果可以看到,di做了一个映射,后续再get的时候,应该就是用里面的值
normalizeDefinition 规范化di
- 类里面存在
__invoke
或者是匿名函数
,那使用\Hyperf\Di\Definition\FactoryDefinition
初始化 - 其余都是
\Hyperf\Di\Definition\ObjectDefinition
- 类里面的__construct,会生成
\Hyperf\Di\Definition\MethodInjection
- __construct里面的参数,会生成
Hyperf\Di\Definition\Reference
将规范化的di,注入到容器
- definitionSource 上面的normalizeDefinition
- definitionResolver 解析调度器,那几个Definition的调度器
- resolvedEntries 已经解决的依赖
总结
- 初始化di,定义好dependencies源
- 利用初始化的di生成容器
- 容器很强大,等会用ApplicationInterface来示例是如何从容器中get
- 在这之后,就已经可以通过容器去get其他类了,di会帮你完成一切
get 获取application
这里将会展示di是如何运行的
- 了解过依赖注入相关知识的,其实每个框架都差不多
- https://learnku.com/docs/laravel-core-concept/5.5/%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5,%E6%8E%A7%E5%88%B6%E7%BF%BB%E8%BD%AC,%E5%8F%8D%E5%B0%84/3017 可以看下laravel的一些示例
- 简单的来说,就是拿到构造中的类去反射出来,递归实现make
get()
判断是否已经解决过依赖了,如果有,就直接返回
1 | if (isset($this->resolvedEntries[$name]) || array_key_exists($name, $this->resolvedEntries)) { |
这里的写法很有意思,isset和array_key_exists都有用上
- https://segmentfault.com/a/1190000016613073
- https://stackoverflow.com/questions/3210935/whats-the-difference-between-isset-and-array-key-exists
- 简单的说,就是isset性能优于array_key_exists,但是isset为null时,就false了,所以为了保证数据的有效性与性能,中和使用得出的方案
make()
1. 先获取di源,主要是为了那些未加载到dependencies源的类
1 | $definition = $this->getDefinition($name); |
2. 依赖注入解决
1 | $this->resolveDefinition($definition, $parameters); |
先是\Hyperf\Di\Resolver\ResolverDispatcher::resolve,这里了定义一个
DepthGuard 递归深度
是为了防止出现死循环,依赖注入的时候,有时候会写出循环依赖的问题,这里定义了最大深度为500,也就是说,我们项目里面如果有超过500层的di,也不会执行成功。哈哈哈哈,项目里面应该不会出现吧进行强大的di,细节就不展示了,反射拿到定义源,继续get的时候,又会回到ObjectDefinition,递归一直new下去,实例化所有的__construct参数,直到构造里面再也没有DefinitionInterface为止
如果是一个FactoryDefinition形式的类,那么在解析的时候,是执行了factory的__invoke
简单的说,di就是一个递归、反射、实例的过程,利用程序,实现构造自动注入
在hyperf里面,aop和annotations,是利用代理类进行操作的,代理类里面的__construct也会定义好依赖的类,再利用di进行一个get
所以ApplicationInterface在get的时候,实际上是运行了\Hyperf\Framework\ApplicationFactory::__invoke()方法来生成一个Application
ApplicationFactory的创建
1. 触发\Hyperf\Framework\Event\BootApplication事件
搜索源码发现,有这几个内置的监听
- \Hyperf\Config\Listener\RegisterPropertyHandlerListener 注册了一个Value注解的回调
- \Hyperf\DbConnection\Listener\RegisterConnectionResolverListener 注册数据库连接解析器
- \Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler 设置用户自定义的错误处理函数,规定发生错误时运行的函数
- \Hyperf\ExceptionHandler\Listener\ExceptionHandlerListener 利用优先队列注册了异常处理器
2. command命令
- 注解和配置文件合并
- 利用symfony/console组件,注册所有的command
这一步骤,已经结束了php bin/hyperf.php
总结
- 从容器中获取Application
- 注册命令行
Start
从command命令得知,start的文件为Hyperf\Server\Command\StartServer
1 | protected function execute(InputInterface $input, OutputInterface $output) |
服务配置 \Hyperf\Server\ServerConfig
相当于把 config/autoload/server.php 转化为类 (实体?),方便获取与操作
具体要启动什么服务,都是靠着这个配置文件进行,默认的server启动方式是\Hyperf\Server\Server
初始化服务 \Hyperf\Server\Server::initServers
遍历服务配置,一个一个实例相应的类
1 | switch ($type) { |
注册一些swoole的事件
- start事件 [Hyperf\Framework\Bootstrap\StartCallback, onStart]
- managerStart事件 [Hyperf\Framework\Bootstrap\ManagerStartCallback, onManagerStart]
- workerStart事件 [Hyperf\Framework\Bootstrap\WorkerStartCallback, onWorkerStart]
- workerStop事件 [Hyperf\Framework\Bootstrap\WorkerStopCallback, onWorkerStop]
- workerExit事件 [Hyperf\Framework\Bootstrap\WorkerExitCallback, onWorkerExit]
- pipeMessage事件 [Hyperf\Framework\Bootstrap\PipeMessageCallback, onPipeMessage]
- request事件 [Hyperf\HttpServer\Server, onRequest]
request事件 [Hyperf\HttpServer\Server, onRequest]
除了注册该事件,还会触发\Hyperf\HttpServer\Server::initCoreMiddleware
1 | public function initCoreMiddleware(string $serverName): void |
createCoreMiddleware
关注点:\Hyperf\HttpServer\Router\DispatcherFactory::__construct
初始化路由的时候,也会顺便把中间件也一起定义了
1 | // 初始化注解路由,即AutoController和Controller |
总结
- 通过配置文件来定义启动什么服务
- 初始化路由调度器、中间件,用于请求分发到各个controller