为什么symfony框架需要生成容器相关的缓存文件,要从symfony的依赖注入特性看起。
先来看一下java生态中的实现方式:
php是解释型语言,无法在编译器实现,所以php生态内的依赖注入基本都是通过容器管理及反射机制实现的。但每次通过反射分析类结构会影响性能,所以symfony框架在此基础上将反射结果保存到php文件,这样只需要第一次访问时生成缓存文件即可通过缓存文件进行依赖注入。
那么symfony缓存是何时生成的呢?我们看下symfony的生命周期:
在初始化容器阶段(initializeContainer)会检查缓存容器代码是否有效,如果缓存已失效或不存在就会在此时生成容器并生成缓存
vendor/symfony/http-kernel/Kernel.php
protected function initializeContainer()
{
$class = $this->getContainerClass(); //获取容器classname
$cacheDir = $this->warmupDir ?: $this->getCacheDir();
$cache = new ConfigCache($cacheDir.'/'.$class.'.php', $this->debug);
$oldContainer = null;
if ($fresh = $cache->isFresh()) { //判断容器缓存是否有效
// Silence E_WARNING to ignore "include" failures - don't use "@" to prevent silencing fatal errors
$errorLevel = error_reporting(\E_ALL ^ \E_WARNING);
$fresh = $oldContainer = false;
try {
if (file_exists($cache->getPath()) && \is_object($this->container = include $cache->getPath())) { //缓存有效通过缓存文件获取容器
$this->container->set('kernel', $this);
$oldContainer = $this->container;
$fresh = true;
}
} catch (\Throwable $e) {
} finally {
error_reporting($errorLevel);
}
}
if ($fresh) {
return;
}
//......
//缓存失效
try {
$container = null;
$container = $this->buildContainer(); //生成容器
$container->compile(); //编译,通过反射分析容器依赖
} finally {
//....
}
if (null === $oldContainer && file_exists($cache->getPath())) {
$errorLevel = error_reporting(\E_ALL ^ \E_WARNING);
try {
$oldContainer = include $cache->getPath();
} catch (\Throwable $e) {
} finally {
error_reporting($errorLevel);
}
}
$oldContainer = \is_object($oldContainer) ? new \ReflectionClass($oldContainer) : false;
$this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass()); //保存容器缓存
$this->container = require $cache->getPath();
$this->container->set('kernel', $this);
if ($oldContainer && \get_class($this->container) !== $oldContainer->name) {
// Because concurrent requests might still be using them,
// old container files are not removed immediately,
// but on a next dump of the container.
static $legacyContainers = [];
$oldContainerDir = \dirname($oldContainer->getFileName());
$legacyContainers[$oldContainerDir.'.legacy'] = true;
foreach (glob(\dirname($oldContainerDir).\DIRECTORY_SEPARATOR.'*.legacy', GLOB_NOSORT) as $legacyContainer) {
if (!isset($legacyContainers[$legacyContainer]) && @unlink($legacyContainer)) {
(new Filesystem())->remove(substr($legacyContainer, 0, -7));
}
}
touch($oldContainerDir.'.legacy');
}
if ($this->container->has('cache_warmer')) {
$this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'));
}
}
通过容器初始化过程可以看到,关键流程在于容器缓存的有效性检查与容器缓存的保存。
缓存有效性检查时通过获取同名文件的meta信息(报错容器缓存时一同保存),将缓存依赖的相关信息进行检查,比如 var/cache/dev/srcApp_KernelDevDebugContainer.php.meta内包含生成缓存时依赖的类、配置文件、vendor等资源信息,将保存的信息与当前信息进行对比,最简单的是文件对比,meta信息中保存了文件生成时间,如果当前文件生成实际与缓存时间不一致则文件失效。vendor/symfony/config/Resource 目录下包含了FileResource等资源的检查实现
容器编译过程
vendor/symfony/http-kernel/Kernel.php
protected function buildContainer()
{
//生成目录
foreach (['cache' => $this->warmupDir ?: $this->getCacheDir(), 'logs' => $this->getLogDir()] as $name => $dir) {
if (!is_dir($dir)) {
if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
throw new \RuntimeException(sprintf("Unable to create the %s directory (%s)\n", $name, $dir));
}
} elseif (!is_writable($dir)) {
throw new \RuntimeException(sprintf("Unable to write in the %s directory (%s)\n", $name, $dir));
}
}
$container = $this->getContainerBuilder(); //获取容器编译容器
$container->addObjectResource($this);
$this->prepareContainer($container); //准备容器所需资源、配置等
if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) {
$container->merge($cont);
}
$container->addCompilerPass(new AddAnnotatedClassesToCachePass($this)); //添加编译器
return $container;
}
编译过程中有重要的两步:prepareContainer、compiler
vendor/symfony/http-kernel/Kernel.php
protected function prepareContainer(ContainerBuilder $container)
{
$extensions = [];
foreach ($this->bundles as $bundle) {
if ($extension = $bundle->getContainerExtension()) {
$container->registerExtension($extension); //获取扩展,一般在扩展代码的DependencyInjection/{$name}Extension.php
}
if ($this->debug) {
$container->addObjectResource($bundle);
}
}
foreach ($this->bundles as $bundle) {
$bundle->build($container); //编译扩展 对应扩展代码根目录的{$name}Bundle.php 对扩展需要的容器进行注册
}
$this->build($container); // 无
foreach ($container->getExtensions() as $extension) {
$extensions[] = $extension->getAlias();
}
// ensure these extensions are implicitly loaded
$container->getCompilerPassConfig()->setMergePass(new MergeExtensionConfigurationPass($extensions));
}
compiler 编译
public function compile(bool $resolveEnvPlaceholders = false)
{
$compiler = $this->getCompiler();
if ($this->trackResources) {
foreach ($compiler->getPassConfig()->getPasses() as $pass) {
$this->addObjectResource($pass);
}
}
$bag = $this->getParameterBag();
if ($resolveEnvPlaceholders && $bag instanceof EnvPlaceholderParameterBag) {
$compiler->addPass(new ResolveEnvPlaceholdersPass(), PassConfig::TYPE_AFTER_REMOVING, -1000);
}
$compiler->compile($this); //编译 内部循环上一步注册的编译器进行编译
foreach ($this->definitions as $id => $definition) {
if ($this->trackResources && $definition->isLazy()) {
$this->getReflectionClass($definition->getClass());
}
}
$this->extensionConfigs = [];
if ($bag instanceof EnvPlaceholderParameterBag) {
if ($resolveEnvPlaceholders) {
$this->parameterBag = new ParameterBag($this->resolveEnvPlaceholders($bag->all(), true));
}
$this->envPlaceholders = $bag->getEnvPlaceholders();
}
parent::compile();
foreach ($this->definitions + $this->aliasDefinitions as $id => $definition) {
if (!$definition->isPublic() || $definition->isPrivate()) {
$this->removedIds[$id] = true;
}
}
}
其中扩展的load方法便是在这一步通过vendor/symfony/dependency-injection/Compiler/MergeExtensionConfigurationPass.php 进行的调用
load方法内的processConfiguration会调用扩展代码/src/DependencyInjection/Configuration.php内的getConfigTreeBuilder
编译完成后会进行保存
vendor/symfony/http-kernel/Kernel.php :: dumpContainer 方法
实际通过vendor/symfony/dependency-injection/Dumper/PhpDumper.php进行文件的生成及保存
注解的解析依赖于
vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php
参考资料: