symfony原始碼分析之容器的生成與使用
symfony 的容器是有一個編譯過程的,框架初始化的時候會執行Symfony\Component\HttpKernel\Kernel::initializationContainer ,這個方法會對程式碼進行檢查,看是否需要生成新的容器程式碼。如果需要 Symfony 會將各個類的依賴關係通過編譯生成靜態的類並存儲在快取檔案中var/cache/[ENV]/appProjectContainer。
其中很多是框架自己的依賴關係,這些依賴關係類似java的方式,通過xml檔案的形式進行宣告,這些xml存在與框架的程式碼中,Syfmony\Bundle\FrameworkBundle\Resource\config\*.xml。
0. 程式碼流程
容器相關的程式碼大致如下流程
# Symfony\Component\HttpKernel\Kernel protected function initializeContainer() { // 獲取生成的容器程式碼檔案 類名 $class = $this->getContainerClass(); $cache = new ConfigCache($this->getCacheDir().'/'.$class.'.php', $this->debug); $fresh = true; // 檢查是否需要重新生成 容器 快取類 if (!$cache->isFresh()) { // ..... try { $container = null; // 重新生成容器快取類 // 例項化生成容器快取程式碼的類 $container = $this->buildContainer(); // 呼叫方法開始生成容器程式碼 $container->compile(); } finally { // ...... } // 將生成的程式碼寫入檔案 $this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass()); $fresh = false; } //載入生成的容器檔案 例項化容器類 並且複製給 Kernel::$container require_once $cache->getPath(); $this->container = new $class(); $this->container->set('kernel', $this); // ...... }
下面著重分析 容器程式碼生成類的建立過程
protected function buildContainer() { // 建立容器快取程式碼的目錄 $container = $this->getContainerBuilder(); $container->addObjectResource($this); // 容器預處理,在開始編譯容器前,會給容器新增一些 pass (這個問題就是在2017phpcon大會上提過的,當時沒解決,這裡看程式碼解決了) // 這些pass主要處理框架配置中宣告的容器關係 $this->prepareContainer($container); // 解析容器配置檔案 我們自己注入容器的類就要宣告在配置檔案 app/config/services.yml 中 // 這裡就是對這個配置檔案進行解析,並將其中的關係生成程式碼到容器中 if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) { $container->merge($cont); } $container->addCompilerPass(new AddAnnotatedClassesToCachePass($this)); $container->addResource(new EnvParametersResource('SYMFONY__')); return $container; }
如上的分析可以將容器程式碼生成的過程精簡到如下3個步驟
# Symfony\Component\HttpKernel\Kernel protected function initializeContainer() { // ... try { $container = null; $container = $this->buildContainer(); 3. 編譯生成容器程式碼 $container->compile(); } finally { // ...... } // ...... } protected function buildContainer() { // 1. 例項化建立容器程式碼類的時候,預處理一些內容 $this->prepareContainer($container); // 2. 載入專案容器的配置檔案進行處理 app/config/services.yml if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) { $container->merge($cont); } }
1. 容器預處理
# Symfony\Component\HttpKernel\Kernel protected function prepareContainer(ContainerBuilder $container) { $extensions = array(); // 這裡的 bundles 是在 initializeBundles 方法中例項化AppKernel中註冊的Bundles 進行的賦值 foreach ($this->bundles as $bundle) { // 這裡將註冊的Bundle進行迴圈處理 // 取出bundle的extension 並 註冊到 container中 if ($extension = $bundle->getContainerExtension()) { $container->registerExtension($extension); $extensions[] = $extension->getAlias(); } if ($this->debug) { $container->addObjectResource($bundle); } } foreach ($this->bundles as $bundle) { // 觸發bundle的build方法 $bundle->build($container); } $this->build($container); // 這裡的程式碼是為了確定將 MergeExtensionConfigurationPass 這個Pass類新增到容器的Pass中 $container->getCompilerPassConfig()->setMergePass(new MergeExtensionConfigurationPass($extensions)); }
我們可以在 AppKernel::registerBundles 中檢視到註冊的 Bundles
public function registerBundles() { $bundles = [ new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(), new Symfony\Bundle\MonologBundle\MonologBundle(), new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(), new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), new AppBundle\AppBundle(), ]; return $bundles; }
我們以 Symfony\Bundle\FrameworkBundle\FrameworkBundle 作為目標進行程式碼的繼續跟蹤
$extension = $bundle->getContainerExtension(); $container->registerExtension($extension);
跟蹤方法 getContainerExtension , 在類 FrameworkBundle 所繼承的類Syfmony\Component\HttpKernel\Bundle\Bundle中找到
# Syfmony\Component\HttpKernel\Bundle\Bundle public function getContainerExtension() { if (null === $this->extension) { // 獲取extension $extension = $this->createContainerExtension(); if (null !== $extension) { // check naming convention $basename = preg_replace('/Bundle$/', '', $this->getName()); $expectedAlias = Container::underscore($basename); if ($expectedAlias != $extension->getAlias()) { throw new \LogicException(......); } $this->extension = $extension; } else { $this->extension = false; } } if ($this->extension) { return $this->extension; } }
以下是獲取extension的程式碼邏輯
protected function createContainerExtension() { // 這裡例項化一個類並返回 if (class_exists($class = $this->getContainerExtensionClass())) { return new $class(); } } protected function getContainerExtensionClass() { // 類名的規則 // 名稱空間\DependencyInjection\(將類名的Bundle替換成Extension) $basename = preg_replace('/Bundle$/', '', $this->getName()); return $this->getNamespace().'\\DependencyInjection\\'.$basename.'Extension'; } # 以下兩個方法主要還是當 $this->namespace $this->name 不存在的時候 # 通過方法 $this->parseClassName() 來解析出來 public function getNamespace() { if (null === $this->namespace) { $this->parseClassName(); } return $this->namespace; } final public function getName() { if (null === $this->name) { $this->parseClassName(); } return $this->name; } private function parseClassName() { # 名稱空間 型別的擷取 $pos = strrpos(static::class, '\\'); // 擷取類的名稱空間 $this->namespace = false === $pos ? '' : substr(static::class, 0, $pos); if (null === $this->name) { // 擷取類的名稱 $this->name = false === $pos ? static::class : substr(static::class, $pos + 1); } }
上程式碼解析出來的就是 Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension。回到程式碼中
if ($extension = $bundle->getContainerExtension()) { $container->registerExtension($extension); $extensions[] = $extension->getAlias(); }
要執行容器的registerExtension方法
# Symfony\Component\DependencyInjection\ContainerBuilder public function registerExtension(ExtensionInterface $extension) { // $extension->getAlias() 這個方法在 Symfony\Component\DependencyInjection\Extension\Extension 中 // 主要的作用是將類名取出 將類名的Excention字尾去掉 $this->extensions[$extension->getAlias()] = $extension; if (false !== $extension->getNamespace()) { $this->extensionsByNs[$extension->getNamespace()] = $extension; } }
2. 解析容器配置檔案
解析容器配置檔案的程式碼
if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) { $container->merge($cont); }
主要是執行了方法 registerContainerConfiguration 這個方法在 AppKernel 類中
# AppKernel public function registerContainerConfiguration(LoaderInterface $loader) { // 這裡的配置檔案是 app/config/config_[ENV].yml 是Yml格式的檔案 // 這裡的loader 應該是類 `YamlFileLoader` $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml'); } # 所以這裡的程式碼應該是執行 # SymfonyComponent\DependencyInjection\Loader\YamlFileLoader::load public function load($resource, $type = null) { // 載入配置檔案,將yml格式的配置內容解析成php陣列 $content = $this->loadFile($path); $this->container->fileExists($path); // ...... // 處理配置檔案中的 key `imports` 對import 宣告的檔案進行load處理 $this->parseImports($content, $path); // 處理配置檔案中的引數部分 if (isset($content['parameters'])) { if (!is_array($content['parameters'])) { throw new InvalidArgumentException(sprintf('The "parameters" key should contain an array in %s. Check your YAML syntax.', $resource)); } foreach ($content['parameters'] as $key => $value) { $this->container->setParameter($key, $this->resolveServices($value, $resource, true)); } } // extensions $this->loadFromExtensions($content); // services $this->anonymousServicesCount = 0; $this->setCurrentDir(dirname($path)); try { // 這裡將services中的宣告封裝到Definition 將Definition 儲存到Container中 $this->parseDefinitions($content, $resource); } finally { $this->instanceof = array(); } } private function parseDefinitions(array $content, $file) { // 如果配置檔案中不存在key services 這裡直接返回 if (!isset($content['services'])) { return; } // ...... // 這裡處理 配置檔案中 services 下的 _default 配置資訊,處理後會刪除這個key $defaults = $this->parseDefaults($content, $file); //對配置檔案中定義的services進行逐個分析 foreach ($content['services'] as $id => $service) { $this->parseDefinition($id, $service, $file, $defaults); } } private function parseDefinition($id, $service, $file, array $defaults) { //例項化 definition if ($this->isLoadingInstanceof) { $definition = new ChildDefinition(''); } elseif (isset($service['parent'])) { // ...... $definition = new ChildDefinition($service['parent']); } else { $definition = new Definition(); // ...... } // 這裡的大段邏輯是處理一些services相關的配置資訊 if (isset($service['class'])) { $definition->setClass($service['class']); } // ...... if (array_key_exists('resource', $service)) { if (!is_string($service['resource'])) { throw new InvalidArgumentException(sprintf('A "resource" attribute must be of type string for service "%s" in %s. Check your YAML syntax.', $id, $file)); } $exclude = isset($service['exclude']) ? $service['exclude'] : null; // 如果service的配置中存在key resource 進行類的註冊 $this->registerClasses($definition, $id, $service['resource'], $exclude); } else { $this->setDefinition($id, $definition); } } # Symfony\Component\DependencyInjection\Loader\FileLoader public function registerClasses(Definition $prototype, $namespace, $resource, $exclude = null) { // 一些判斷 // ...... // 這裡主要是處理 存在 resource 這個key的services註冊 // symfony的容器會將指定目錄下都所有類都會註冊到container // 這裡的 findClasses 就是查詢類的過程 $classes = $this->findClasses($namespace, $resource, $exclude); // prepare for deep cloning $prototype = serialize($prototype); foreach ($classes as $class) { // 這裡呼叫 setDefinition 對resource下的類全部設定到 container中 $this->setDefinition($class, unserialize($prototype)); } } protected function setDefinition($id, Definition $definition) { // ...... //將definition新增到 container 裡面 $this->container->setDefinition($id, $definition instanceof ChildDefinition ? $definition : $definition->setInstanceofConditionals($this->instanceof)); }
這裡著重分析下 registerClasses方法中的 $this->findClasses
# Symfony\Component\DependencyInjection\Loader\FileLoader private function findClasses($namespace, $pattern, $excludePattern) { // ...... // 這裡是對檔案的處理 foreach ($this->glob($pattern, true, $resource) as $path => $info) { // ...... if (!$r->isInterface() && !$r->isTrait() && !$r->isAbstract()) { $classes[] = $class; } } // ...... return $classes; } # Symfony\Component\Config\Loader\FileLoader protected function glob($pattern, $recursive, &$resource = null, $ignoreErrors = false) { try { $prefix = $this->locator->locate($prefix, $this->currentDir, true); } catch (FileLocatorFileNotFoundException $e) { // ...... } // 這裡返回的是 yield $resource = new GlobResource($prefix, $pattern, $recursive); // 所以這裡需要foreach來觸發 foreach ($resource as $path => $info) { yield $path => $info; } } # Symfony\Component\Config\Resource\GlobResource public function getIterator() { if (false === strpos($this->pattern, '/**/') && (defined('GLOB_BRACE') || false === strpos($this->pattern, '{'))) { foreach (glob($this->prefix.$this->pattern, defined('GLOB_BRACE') ? GLOB_BRACE : 0) as $path) { if ($this->recursive && is_dir($path)) { // 這裡是讀取檔案的主要邏輯 // 使用的是 php 中提供的處理檔案的迭代器 $files = iterator_to_array(new \RecursiveIteratorIterator( new \RecursiveCallbackFilterIterator( new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), function (\SplFileInfo $file) { return '.' !== $file->getBasename()[0]; } ), \RecursiveIteratorIterator::LEAVES_ONLY )); } elseif (is_file($path)) { yield $path => new \SplFileInfo($path); } // ...... } return; } $finder = new Finder(); // ...... foreach ($finder->followLinks()->sortByName()->in($this->prefix) as $path => $info) { if (preg_match($regex, substr($path, $prefixLen)) && $info->isFile()) { yield $path => $info; } } }
3. 編譯生成容器程式碼
這段程式碼主要是執行 Pass程式碼 這裡主要跟蹤下 MergeExtensionConfigurationPass 這個Pass,他執行了 FrameworkExtension::load ,這個方法中載入了框架需要的各種類以及依賴關係
# Symfony\Component\DependencyInjection\Complier\Complier public function compile(ContainerBuilder $container) { try { // 這裡觸發了所有Passs的 process方法 foreach ($this->passConfig->getPasses() as $pass) { $pass->process($container); } } catch (\Exception $e) { // ...... } } # Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass public function process(ContainerBuilder $container) { // ...... parent::process($container); } # Symfony\Component\DependencyInjection\Compiler\MergeExtensionConfigurationPass public function process(ContainerBuilder $container) { foreach ($container->getExtensions() as $name => $extension) { // ...... // 執行了 extension 的 load 方法 $extension->load($config, $tmpContainer); // ...... } $container->addDefinitions($definitions); $container->addAliases($aliases); } # Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension public function load(array $configs, ContainerBuilder $container) { /* 這個方法中的程式碼很多,主要內容是載入了很多的 xml 配置檔案 這些配置檔案的作用是宣告框架所需要的類之間的關係 */ }