Symfony架构分析

Symfony是可复用的,是组件化的。

Symfony MVC分层架构图

M:Model 模型,负责数据库操作。

V:View 视图,负责调用Model调取数据,再调用模板,展示出最终效果。

C:Controller 控制器,程序的入口,决定改调用哪个View,并告诉View该做什么。

如此说来,程序的执行顺序是C-V-M 或 C-M ,和MVC的名字正好相反。

Request-Response模型

Request-Response模型

从本质上来说,HTTP协议实际上描述了一个Request-Response模型。与之相关的PHP代码实际上都在做着解释请求、生成响应的工作,Symfony则更进一步的将Request和Response对象化了。

Request-Response模型是整个Symfony的基础模型,可以毫不夸张的说,整个Symfony都构筑在这个基础模型之上(参见Front Controller部分)。

Request对象

Request类很简单,封装了原生PHP的各大超全局输入变量:
use Symfony\Component\HttpFoundation\Request


$request = Request::createFromGlobals();

$request->getPathInfo();    //the URI being requested (e.g. /about) minus any query parameters
$request->query->get('foo');    //$_GET 
$request->request->get('bar', 'default value if bar does not exist');    /$_POST
$request->server->get('HTTP_HOST');    //$_SERVER
$request->files->get('foo');     //retrieves an instance of UploadedFile identified by foo
$request->cookies->get('PHPSESSID');   //$_COOKIE 
$request->headers->get('host');
$request->headers->get('content_type');
$request->getMethod();    //GET, POST, PUT, DELETE, HEAD
$request->getLanguages(); // an array of languages the client acceptsxxxxxxxxxxx`z`

Response对象

Response类也非常简单,用来代替原生PHP的echo(),header():

Symfony\Component\HttpFoundation\Response

$response = new Response();

$response->setContent('<html><body><h1>Hello world!</h1></body></html>');
$response->setStatusCode(Response::HTTP_OK);
$response->headers->set('Content-Type', 'text/html');
// prints the HTTP headers followed by the content

$response->send();

Front Controller

一般情况下,代码会以各个独立的模块分层存在。为了根据请求的不同调用合适的功能代码,一些如CodeIgniter的框架都有一个统一入口文件(index.php)负责这项工作。 在Symfony等一些框架(其他如PHPWind9.x以后的版本)中,单独抽象出了Front Controller的概念。和CodeIgniter中的index.php一样,如上图Symfony分层架构图所示,FrontController是一个统一入口,一切发到我们Application的请求都会由其处理,然后根据接收到的Request不同,按照配置的Route规则加载对应的Controller的Action。 处理请求之后,生成响应对象并send()到客户端。

根据环境的不同,Symfony自带有两个Front Controller:

  • web/app.php #生产环境

  • web/app_dev.php #开发环境

之所以没有测试环境对应的前端控制器,是因为测试环境可以通常只在单元测试时使用。

当然console工具也提供了能在任意环境下运行的Front Controller。

Symfony中的Front Controller非常简单,遵循的逻辑可以概括为”处理请求,发送响应“,这也是整个Symfony框架对Request-Response模型的实现:

// web/app.php

require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../app/AppKernel.php';
use Symfony\Component\HttpFoundation\Request;

//初始化一个prod环境、非debug模式运行的AppKernel
$kernel = new AppKernel('prod', false);

$kernel->handle(Request::createFromGlobals())    //处理请求
        ->send();    //发送响应

至此,针对Request-Response模型的处理流程已经总体规划完毕了。

对于一个Symfony项目myproj,为了方便起见,假设整个文件夹都位于/var/www/下,当我们在浏览器中访问:

localhost/myproj/web/some_route

实际上是在调用Front Controller来执行与some_route对应的代码。事实上,上面这个URL在默认情况下等效于:

localhost/myproj/web/app.php/some_route

当然,在开发模式下,可以访问:

localhost/myproj/web/app_dev.php/some_route

激活debug工具并能自动重建缓存。

正是由于Front Controller已经实现了Request-Response这样的基本流程,在Symfony中为一个基本组件(Bundle)添加页面只需要要遵循两步:

  1. 创建Controller #定义如何根据Response(Request?)生成Response对象

  2. 配置Route #配置URL和Controller的映射关系

当然,为了避免组织混乱、保持结构清晰,实际中,Route、Controller等等都是以Bundle来设计的。

Bundle

Bundle从PHP的角度而言,可以视作一个命名空间。一旦一个PHP命名空间添加了Bundle Class,就成为Bundle。这个Bundle Class的命名必须遵循以下规则:

  • 只使用字母和下划线

  • 使用CamelCased命名风格

  • 使用descriptive和short的名字

  • 以vender名为prefix

  • 以”Bundle”为suffix

Bundle Class的getClass()方法返回这个类名。

Bundle是Symfony的基本组件。Bundle存放了与某个特性相关的一切文件(比如PHP类、配置、甚至是css文件和JavaScript文件)的目录。 事实上,Symfony的Bundle和PHPCMS里的module作用相当,类似于模块、插件。但是相较于PHPCMS之类其他的框架,Symfony的Bundle具有更好的抽象和实现。

一个Bundle,通常位于src/VenderOfBundle/BundleName之下,其中的目录结构多为:

Vender/
    YourBundle/
        VenderYourBundle.php
        Controller/               #控制器
            Spec1Controller.php
            Spec2Controller.php
        DependencyInjection/      #DI
        Resources/
            config/
            views/
        Tests/                    #测试

想要添加一个Bundle,应该先创建以上目录,然后修改app/Kernel.php文件,为registerBundles()方法添加一个该Bundle的实例:

// app/AppKernel.php

public function registerBundles(){

    $bundles=array(
        //...
        new Vender\YourBundle\VenderYourBundle();
    );

    //...

    return $bundles;
}

以上两步可以归纳为:

  1. 创建Bundle

  2. 注册Bundle

当然,添加Bundle的这些步骤可以用一个命令代替:

php app/console generate:bundle --namespace=Vender/YourBundle --format=yml

Route

Route是指从Request(如URL路径,HTTP Method)到控制器(具体到Action)的映射。所以, 一条路由规则有两个要素组成:

  1. URL Path

  2. 与URL Path匹配的Controller

我们还可以为这条路由规则起一个独一无二的名字,这样我们就能用于生成URL了。

路由层的作用就是把输入进来的URL转换为要执行的Controller。

Symfony会从一个单独的路由配置文件中加载所有的路由规则。这个路由配置文件通常是

app/config/routing.yml

,当然,Symfony支持高度定制,我们可以把默认的路由文件配置成其他任意其他文件(包括XML和PHP文件)。如:

# app/config/config.yml
framework
    # ....
    router: { resource: "%kernel.root_dir%/config/routing.yml"}

当然,从URL到控制器动作,参数匹配是必不可少的。Symfony的路由系统支持:

  • URL匹配 #通过@Route()设置

    • 必选参数 #通过占位符来设置

    • 可选参数 #通过占位符和设置defaults来设置

    • 正则匹配 #通过requirements设置

  • HTTP Method匹配 #通过@Method()

/**
 *@Route("/blog/{page}",defaults={"page": 1},requirements={
 *    "page": "\d+"
 *})
 *@Method("GET")
 */
public function indexAction($page){
    //...
}

当然,威力更巨大的是condition属性,支持无限可能的定制。

contact:
    path: /contact
    defaults: { _controller: AcmeDemoBundle:Main:contact}
    condition: "context.getMethod() in ['GET','HEAD'] and request.headers.get('User-Agent') matches '/firefox/i' "

这个配置会被转换为以下的PHP代码:

if(rtrim($pathinfo,"/contact")===''&&
    (
        in_array($context->getMethod(),array(0=>'GET',1=>'HEAD')) &&
        preg_match('/firefox/i',$request->headers->get("User-Agent"))
    )
    
){
    //....
}

一个Bundle中的Route

要让合适的Controller和Action发生调用,必须建立url与之的映射。

#src/Vender/YourBundle/Resources/config/routing.yml

specController:
    path: /specController/{limit}
    defaults: { _controller: VenderYourBundle:specController:yourAction}

app级Route

尽管所有的路由配置规则是从一个单独的文件中读取的,大家在实际中还是会通过resource导入其他路由规则。比如,使用Annotation格式的路由配置应设置:

app: 
    resource: "@AppBundle/Controller"
    type: annotation  #使用Annotation reader来读取resource变量

如果我们手工添加了一个Bundle,我们可以把它自身包含的Route规则导入app level的配置中,即应该在app/config/routing.yml中添加配置:

# app/config/routing.yml

vender_yourbundlename
    resource: "@VenderYourBundle/Resources/config/routing.yml"
    prefix: /

当然,如果是用php app/console generate:bundle命令生成的bundle,那么这一步已经由Symfony替我们做好了。

双向映射

Route提供了bidirectional System:

  1. match($URL) #返回匹配到的控制器及参数构成的数组

  2. generate($RouteName,$paramsArray) #生成URL

Controller

我们知道,每一个Route规则都有一个_controller对象,我们当然可以用完全限定名的ClassName::ActionName的形式来引用一个Controller,比如:AppBundle\Controller\BlogController::ShowAction。

但实际上这样的表达是有冗余信息的,最起码还要指出BlogController位于的命名空间Controller是没必要的,所以Symfony还支持对Controller的逻辑命名, 一条指定Controller的Action的逻辑命名通常遵循这样的约定:BundleName:ControllerName:ActionName

通常这样的逻辑名称会被映射为:path/to/BundleName/Controller/ControllerName.php文件中的ActionName方法

比如:AcmeDemoBundle:Random:Index这个控制器通常会会映射为:Acme\DemoBundle\Controller\RandomController类中的indexAction方法。

另外值得注意的是,Symfony中Controller的Action 与CodeIgniter之类的框架并完全一样:

  1. CodeIgniter中的控制器直接输出响应,而Symfony中则是必须返回Response对象;

  2. Symfony支持从Route和Request定制Action方法的参数。而且对于Action方法声明,参数顺序并不重要。

use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/hello/{firstName}/{lastName}",name="hello")
 */
public function indexAction($lastName,$firstName,Request $request){

    //$firstName和$lastName等参数顺序并不重要
    //可以直接使用$request
    $page=$request->query->get("page",1);
}

此外,Symfony\Bundle\FrameworkBundle\Controller\Controller提供了一系列helper方法。

  • Redirecting

    • generateUrl($route)

    • redirect($absUrl)

    • redirectToRoute($route) # new RedirectResponse($this->generateUrl($route))

  • Rendering Templates

    • render($pathOrLogicalTemplateName,$array) #render a template and return a Response object

  • Accessing other Services

    • get(‘templating’)

    • get(‘router’)

    • get(‘mailer’)

  • Exception

  • FlashMessage

    • addFlash()

  • Forwarding

Symfony2目录结构分析

了解框架的目录结构是框架快速入门的一个途径,一个成熟的框架,每个功能模块都被划分存放在不同的目录。

Symfony的基本架构十分清晰。与架构相对应,Symfony的目录结构也是非常清晰的。默认的结构组织形式如下:尽管拥有如此清晰的文件结构,Symfony也支持任意定制目录结构。

|--app //Application级别文件目录,应用配置config(插件的配置文件需要import进来才生效),应用缓存cache(缓存的类和模板),应用命令行console
|--|--AppCache.php
|--|--AppKernel.php //入口文件里面会初始化一个AppKernel类,AppKernel类就是在这个文件里面,Appkernel类的主要功能是初始化整个web应用的Bundle。包括Symfony2框架的核心Bundle、第三方插件的Bundle、我们自己编写的应用的Bundle,Bundle在Symfony2里面就相当于一个具有完成某一功能的完整的包,而且我们要用的Bundle都必须在AppKernel类里面注册。
|--|--autoload.php //该文件负责自动加载注册在里面的类,通常我们不需要手动修改它
|--|--bootstrap.php.cache //Symfony2核心的类的缓存文件,Symfony2框架必须用到的核心的类都会被编写整理到这个文件里面。这样做的目的是减少运行的时候打开文件的个数,提高运行的速度。因为不同的类都被存放在不同的文件里面,如果没有把这些必要的类缓存在一个文件里面,那么我们每次运行都要打开多个文件。如果把这些必要的类整理到一个文件里面,那么我们每次运行这些类就在同一个文件里面了。例如:Request类、Response类、Container类、Kernel类等都会被缓存到这个文件里面。所以,如果我们想在 Request类 里面 echo '在Request里面调试'; 这样的语句,我们就把这语句编写在bootstrap.php.cache文件下的Request类而不是symfony/vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/Request.php里面的Request类。其实symfony/vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/Request.php里面的Request类就被缓存到bootstrap.php.cache里了
|--|--cache //缓存目录,按不同模式(生成模式、调试模式)缓存。主要缓存了模板文件、Container类、路由映射相关数据等
|--|--|--dev
|--|--|--prod
|--|--check.php
|--|--config //存放配置文件的目录,config_dev.yml和config_prod.yml才是被Symfony2框架加载的配置文件。但是为了方便管理,我们会把不同模块的配置编写到不同配置文件中,要使这些配置文件生效,那么我们还需要import它们进config_dev.yml和config_prod.yml。
|--|--|--config_dev.yml //调试模式的配置文件
|--|--|--config_prod.yml //生产模式的配置文件
|--|--|--config_test.yml
|--|--|--config.yml //通用的配置文件,只要import进相应的调试模式下的配置文件,就可以生效
|--|--|--parameters.yml //存放配置文件使用的变量,例如:数据名、数据库密码、数据库host等等
|--|--|--parameters.yml.dist
|--|--|--routing_dev.yml //调试模式下的路由配置文件,我们在src里面编写的路由配置文件需要import到这个文件写才可以生效
|--|--|--routing.yml //通用路由配置文件
|--|--|--security.yml //防火墙配置文件,这里的防火墙是web应用防火墙,不是服务器的防火墙,里面配置有角色权限、ACL等,这个文件需要config_*.php import进去才可以生效
|--|--console
|--|--logs //Symfony2运行的日志,同理,不同模式下有不同的日志
|--|--|--dev.log
|--|--|--prod.log
|--|--phpunit.xml.dist
|--|--Resources
|--|--|--views
|--|--SymfonyRequirements.php
|--bin
|--composer.json
|--composer.lock
|--LICENSE
|--README.md
|--src //项目源码(包含Controller、Model、View、路由配置文件、应用的配置文件等),一般以各个Vender提供的针对网站各个功能的Bundle分类。
|--|--DemoBundle //src目录下存放的就是我们应用层的代码,一个功能就可以组织成一个Bundle,例如简单一点的一个购物车功能、复杂一点的一个博客系统都可以组织成一个Bundle。
|--|--|--AcmeDemoBundle.php //每一个新Bundle文件里面的Bundle类都需要通过手动添加或命令行生成的方法注册到app/AppKernel.php/AppKernel类中,才会被Symfony2框架加载而生效,
|--|--|--Command
|--|--|--Controller //Controller目录,顾名思义,这个目录下存放的就是Controller类,如果不懂什么是Controller,麻烦请先学习MVC
|--|--|--DependencyInjection //该目录存放对AcmeDemoBundle的扩展
|--|--|--EventListener //该目录存放事件监听器的类,Symfony2框架是一个事件驱动的框架,相应事件触发时,这些监听器就会被执行。
|--|--|--Form  //该目录存放着表单类。
|--|--|--Resources //该目录存放着Bundle的配置文件、模板文件等
|--|--|--|--config
|--|--|--|--|--routing.yml //该文件存放着Bundle的路由配置
|--|--|--|--|--services.xml //该文件存放着Bundle的services配置
|--|--|--|--public
|--|--|--|--views //该文件夹存放着Bundle的所有模板文件
|--|--|--|--|--Tests
|--|--|--|--|--Twig
|--UPGRADE-2.2.md
|--UPGRADE-2.3.md
|--UPGRADE-2.4.md
|--UPGRADE.md
|--vendor //由composer管理的第三方依赖模块,包括Symfony2的核心模块(HttpKernel、DependencyInjection等组件)和第三方插件(如最常用的SonataAdmin)
|--web //网站的根目录,全站公开访问的入口,包括入口脚本Front Controller(app.php和app_dev.php)和静态资源性文件。
|--|--app_dev.php         //调试模式下的入口文件(在调试模式下可以额外输出应用的运行信息,包括加载时间、执行的路由、执行sql语句等)
|--|--apple-touch-icon.png
|--|--app.php             //生产环境下的入口文件(相当于TP框架index.php作用)
|--|--bundles
|--|--config.php
|--|--favicon.ico
|--|--robots.txt

Symfony启动顺序&nbsp;


Kernel->initializeBundles() 初始化Bundle
    Kernel->registerBundle() 注册Bundle
    Kernel->bundleMap生成Bundle关系图
    
Kernel->initializeContainer() 初始化容器(缓存)
    Kernel->buildContainer() 建立容器
        Kernel->prepareContainer() 预备容器
           Bundle->getContainerExtension()  获取Bundle扩展
           Bundle->build($container) 编译Bundle
               Symfony\Bundle\FrameworkBundle->build($container) 添加了不少 CompilerPassInterface(路由、事件、模板、资源等)
           ContainerBuilder-> … ->setMergePass(new MergeExtensionConfigurationPass($extensions)) 设置MergePass(CompilerPassInterface),用于合并Bundle扩展配置
       Kernel->registerContainerConfiguration(LoaderInterface $loader) 获取配置、服务
    ContainerBundler->compile() 编译容器
        $compiler->compile($this);  
           开始执行$container->addCompilerPass() 里面的 CompilerPassInterface ,(包括 FrameworkBundle->build($container)里添加的)
           默认从MergeExtensionConfigurationPass 开始
               MergeExtensionConfigurationPass->process($container)
                   process 包含
                       BundleExtension->prepend($container);      // 必须实现 PrependExtensionInterface 接口才执行该方法
                       BundleExtension->load($config, $container)
            
           所有CompilerPassInterface 依次 ->process()
           foreach ($this->passConfig->getPasses() as $pass) {
                   $pass->process($container);
           }
            
foreach(Kernel->getBundles() as $bundle){
   Bundle->boot() 启动
}
$reqponse = Kernel->handle() 处理
    Container->get('http_kernel')->handle($request)
$response->send();
$kernel->terminate($request, $response)

 

后续更新CompilerPassInterface 详细顺序

 

 

symfony调用的流程

symfony框架调用的流程

目前这个是在RegisterController控制器中的一段代码

$user = this>getAuthService()>register(this-&gt;getAuthService()-&gt;register(this>getAuthService()>register(registration);

 

现在我们去找getAuthService()这个方法

** 仅以使用PHPstudy ctrl + F 查找public function getAuthSercive() 类似的这种控制器 **

return $this->getServiceKernel()->createService(‘User.AuthService’);

我们在里面可以看到在Service里面的User目录下的AuthService控制器

** 找到回到上面,看到getAuthService()->register调用的是register方法,我们去AuthService去找register方法

public function register($registration, $type = ‘default’)

参见

  1. Symfony架构分析| itminus

  2. Symfony目录结构分析· symfony开发blog实录· 看云