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 配置文件 
	這些配置文件的作用是聲明框架所需要的類之間的關係

	 */
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章