In Part II we configured doctrine to connect to our database, created some managed entities out of plain old PHP objects using annotations and then used those annotations to let doctrine create our database tables for us. In this part, we will set up some routing, use controllers and learn about templating with Twig. We are going to be setting up some static pages to show how routing, controllers and templates work at a high level. In the next part we will start getting to some dynamic content and database interaction.

Before we go further lets talk about testing. It just so happens that symfony2 has some fantastic testing support, so lets go ahead and try to be good, responsible programmers and take a test-driven development (TDD) approach to our blog application. To write tests for symfony2 you need to have PHPUnit 3.5.11 or greater installed on your system. Go ahead and install that if you haven’t already. I won’t go step-by-step through the installation here, but it is easy enough and you can find instructions here.

Now that we have PHPUnit installed we can start writing some simple tests. For now we will only be testing two pages. In this part of the tutorial we are going to add a “home” page and an “about” page. So lets create some new test classes to accomplish this. We need to create two new classes which we will use to test our controllers. First we need to add a Controller directory in the src/Company/BlogBundle/Tests directory. In the Controller directory we just created, add a file called BlogControllerTest.php. Here is the code for the class:

  1. namespace Company\BlogBundle\Tests\Controller;  
  3. use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;  
  5. class BlogControllerTest extends WebTestCase  
  6. {  
  7.     public function testIndex()  
  8.     {  
  9.         $client = $this->createClient();  
  11.         $crawler = $client->request('GET''/');  
  13.         $this->assertTrue($client->getResponse()->getStatusCode() == '200' );  
  15.         $this->assertTrue($crawler->filter('title:contains("Home")')->count() > 0);  
  17.         $this->assertTrue($crawler->filter('h2:contains("Welcome to the Blog")')->count() > 0);  
  18.     }  
  19. }

Lets go through this class. As usual we declare our namespace which matches our directory structure. Then we declare that we are going to use the WebTestCase class. This class is provided by symfony2 to be the base class for your test classes. The method name testIndex indicates that we want to test the index action of our BlogController class, which is what we will map to the home page in the routing shortly. We will be digging deeper into testing in future parts of this tutorial, so I will not get deeply into everything that is being done here. Just know that in this method we are sending a GET request to the route matching the ‘/’ pattern. Then we are taking the response generated and testing it to verify that it was a success, that the title element contains the string “Home” and that there is an h2 element on the page that contains the string “Welcome to the Blog”.
我们过一遍这个类。通常我们声明的命名空间是与我们目录结构相匹配的。然后我们声明我们打算使用WebTestCase类。这个类由symfony2提供,是您测试类的基类。TestIndex方法是我们想要测试BlogController类的index动作,我们将会在路由里将它映射到home页。因为我们打算在这个教程的后续部分对测试做深入讨论,所以我对这里所做的就不一一深入介绍了。您只需要知道在该方法中我们发送了一个GET请求,以便让路由去匹配‘/’条目。然后我们将生成响应并且测试它是否成功,title元素包含“Home”字符串,并且页面上的h2元素也包含了“Welcome to The Blog”字符串。

That is it for the “home” page test. Now lets create a test for the “about” page. I personally prefer to create a controller that handles all of my static pages. This way I just need one route to handle all of them. We will see how this works very shortly. For now lets create another test class file in the src/Company/BlogBundle/Tests/Controller directory named PagesControllerTest.php. This class should look familiar.
这是“home”页面的测试。现在我们再创建一个“about”页面的测试。我个人比较喜欢创建一个控制器去处理所有的静态页面。这样我只需要一个路由就可以处理所有的东西。很快我们就可以知道它是如何实现的。现在让我们在src/Company/BlogBundle/Tests/Controller 目录中创建一个名叫PagesControllerTest.php的文件。该类看起来应该很熟悉。

  1. namespace Company\BlogBundle\Tests\Controller; 
  3. use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; 
  5. class PagesControllerTest extends WebTestCase 
  6.     public function testShow() 
  7.     { 
  8.         $client = $this->createClient(); 
  10.         $crawler = $client->request('GET''/about'); 
  12.         $this->assertTrue($client->getResponse()->getStatusCode() == '200' ); 
  14.         $this->assertTrue($crawler->filter('title:contains("About")')->count() > 0); 
  16.         $this->assertTrue($crawler->filter('h2:contains("About")')->count() > 0); 
  17.     } 

Here we are running a similar test to the one we just completed. The testShow method will test the show action of the PagesController. We will create this controller in our bundle shortly. The PagesController will handle all of our requests to pages with static content. In this test we are sending a GET request to the route matching ‘/about’ and verifying that the response was a success, that the title element that contains the string “About” and that it also has a h2 element that contains the string “About.” These tests are very simple, but are fine for now. We will be adding more complex tests, once we start to add more complex functionality to our blog.

Running the tests is very simple as symfony2 has already provided a default PHPUnit xml config file in the app directory. Just open up a terminal and run the following command from your base project directory:

  1. phpunit -c app/

You should see the that we have 2 tests and 6 assertions and they all fail. Now we need to write code to make those tests pass. This is the core concept of TDD. Write tests that fail, then implement the functionality to make them pass. After you have done this you can safely refactor code and implement new features and be confident that your old code has not broken. The initial investment of writing tests up front will pay off the larger your application grows in size.

The first thing we need to do to get our tests passing is to set up the routing. Routing is one of the most importants parts of your application. If your routing is incorrect or missing then the symfony2 framework would never be able to translate a request from a client into a bundle controller action. This is the key functionality that the routing component provides. It parses out a request and matches that against your routing configuration, then it uses the route that it matches and invokes your controller and the corresponding action. Then your action returns a Response object. The symfony2 framework then takes this Response object and outputs it to the client.

Lets get started by opening up the routing.yml file in the app/config directory. This is the main routing configuration file for the application. When you open this file you will see the following:

  1. Blog: 
  2.     resource: "@BlogBundle/Resources/config/routing.yml" 

Here we have imported the routing.yml file located in our BlogBundle. The resource  simply appends all the routes for the BlogBundle to the app routing configuration. This is a simple way to add routes defined in the various bundles you will use to your application without having to rewrite them or copy and paste anything. It is very simple and efficient. So now that we have learned how to import routes from your bundle, lets take a look at our BlogBundle routing.yml. Open up the routing.yml file located in the src/Company/BlogBundle/Resources/config directory. Remove any existing routes that were auto-generated. Our goal is to get the tests to pass that we wrote earlier. We need to add routes to match the patterns ‘/’ and ‘/about’.  Here are those routes:
在这里,我们已经导入了BlogBundle中的routing.yml文件。resource可以很容易地将BlogBundle的所有路由都添加到配置文件里。这是一个简单的方式,可以很方便地添加定义在各个Bundle中的路由,您可以在您的应用程序中使用,而无须去重新编 写或拷贝粘贴它,它非常简单高效。我们已经学会了如何从您的Bundles中导入路由,现在站我们看一下BlogBundle的routing.yml。打开 src/Company/BlogBundle/Resources/config目录中的routing.yml文件。删除所有自动生成的路由。我们的目标是让我们先前写的测试通过。我们需要添加匹配‘/’和‘/about’的路由。以下是这些路由:

  1. show_page: 
  2.     pattern:  /{page} 
  3.     defaults: { _controller: BlogBundle:Pages:show } 
  5. homepage: 
  6.     pattern:  / 
  7.     defaults: { _controller: BlogBundle:Blog:index } 

We have defined two routes. One named show_page and one named homepage. Each route that you define has a pattern that the symfony2 routing component tries to match. A route also has has the defaults parameter. Here you define the special _controller parameter which tells symfony2 what controller and action to map this route to as well as default values for any custom parameters you put in your route. The show_page route has a _controller value of BlogBundle:Pages:show. The symfony2 framework will take this string and determine that it should look in the BlogBundle for the PagesController and call the show action. Not so hard, right?

In the show_page route we have defined a custom parameter named page. Any word wrapped in brackets will be transformed into a variable for your controller to use. In our example, our controller will have access to a variable named $page which holds the value of the string after the ‘/’ character. One important thing to remember is that the order in which your routes are defined matters. The symfony2 routing framework will choose the first route that it finds which matches the requirements. So that means that if we had a route with the pattern ‘/contact’ declared before the route with pattern ‘/{page}’ then the request would map to the ‘/contact’ route. If the ‘/contact’ route was declared after the ‘/{page}’ route, the a request with ‘/contact’ would match the ‘/{page}’ route. Easy enough right? There are other parameters and requirements that you can put on routes, such as the HTTP verb, but we have covered what you need to get started. We will be diving much deeper into the routing system in future parts when we start to work with the database and our entities. Hopefully you can see how the show_page route as we have defined it is very helpful in rendering static pages in a generic way. If you don’t, then don’t worry it will be made very clear in just a moment.

Now that we have our routing setup we are on our way to making our tests go from red to green. Next we need to create our controllers and actions. Navigate to the src/Company/BlogBundle/Controller directory and open the BlogController.php file. Make your file look like this:
现在我们已经有了我们的路由配置,这使得我们的测试道路上的红灯变成绿灯。接下来我们需要创建控制器和动作。打开src/Company/BlogBundle/Controller 目录中的BlogController.php文件。让您的文件看上去如下所示:

  1. // BlogController.php 
  3. namespace Company\BlogBundle\Controller; 
  5. use Symfony\Bundle\FrameworkBundle\Controller\Controller; 
  7. class BlogController extends Controller 
  8.     public function indexAction() 
  9.     { 
  10.         return $this->render('BlogBundle:Blog:index.html.twig'); 
  11.     } 

In our BlogController class we are defining an indexAction method. If you take a look back at the homepage route that we defined we declared that the route should map to the indexAction in the BlogController of the BlogBundle. This is exactly what we have implemented here. The action in the controller should return a Response object and that is exactly what the render method inherited from Symfony\Bundle\FrameworkBundle\Controller\Controller does. The render method takes as a parameter the name of the template to render. Lets translate the string BlogBundle:Blog:index.html.twig. Here we are telling the controller to render the template in the src/Company/BlogBundle/Resources/views/Blog folder named index.html.twig.
在BlogController类中我们定义了一个indexAction方法。如果您回顾一下我们定义的homepage路由,您将会发现我们声明该路由是映射到BlogBundle中BlogController控制器的indexAction方法的。这正是我们在这里实现的。控制器中的动作会返回一个Response对象,那是从Symfony\Bundle\FrameworkBundle\Controller\Controller 继承过来的render方法。render方法将模板名作为参数去渲染。让我们翻译一下BlogBundle:Blog:index.html.twig字符串 。在这里我们告诉控制器去渲染src/Company/BlogBundle/Resources/views/Blog文件夹中名为index.html.twig的模板。

Next we need to implement our PagesController. Create a new file named PagesController.php in the src/Company/BlogBundle/Controller directory. Here is the source for this file:
接下来我们需要实现我们的PagesController。在src/Company/BlogBundle/Controller 目录中创建一个名为PagesController.php的新文件。下面是该文件的源代码:

  1. // PagesController.php 
  3. namespace Company\BlogBundle\Controller; 
  5. use Symfony\Bundle\FrameworkBundle\Controller\Controller; 
  6. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 
  8. class PagesController extends Controller 
  9.     public function showAction($page
  10.     { 
  11.         $template = sprintf('BlogBundle:Pages:%s.html.twig'$page); 
  13.         if (!$this->get('templating')->exists($template)) { 
  14.             throw new NotFoundHttpException("The specified page could not be found."); 
  15.         } 
  16.         return $this->render($template); 
  17.     } 

This is almost exactly like our BlogController except for two things. Instead of indexAction we have the showAction. Remember that page parameter in our route that we wanted to use in our controller? To use it in our action we simply declare it as a parameter to the action method. The symfony2 framework will transform the string in the route to a variable named $page. This action is set up to render a static page based on the value of the $page parameter. All we do in this action is render the template with the page value inserted into the template name string. Now to create new static pages all we have to do is create a template file with the appropriate name and put it in our src/Company/BlogBundle/Resources/views/Pages folder. That’s it. We don’t even have to modify the routing. Pretty cool, huh? You don’t have to handle your static pages this way, but I find this makes it easy.
除了两点之外,上述代码和我们的BlogController几乎完全一样。我们用showAction替代了indexAction。还记得我们想在控制器中使用的那个路由page参数吗?要在我们的动作中使用它,只需要简单地将它声明为动作方法的参数。设置该动作是为了渲染一个基于$page参数的静态页。我们在该动作中所做的就是将带有page值的模板插入到模板名字符串中。现在要创建新的静态页,我们所有要做的就是创建一个适当 的模板,并将其放在src/Company/BlogBundle/Resources/views/Pages文件夹中。就这样,我们甚至不需要去修改路由。很酷吧?这样您就不必处理去您的静态页,但我发现这样可以很容易。

Now that we have our routing and controllers set up, all that is left is to tackle some templating with Twig. Twig is a new templating language that is packaged with symfony2. You are not required to use Twig. You can use regular old PHP templates if you would like, but I am going to use Twig in this tutorial. Twig is powerful and allows for template inheritance, filters and much more. If you want to read about why Twig is so awesome then check out this page.

Twig supports template inheritance. This allows templates to inherit from one another much like an object inherits functionality from its parent classes. In this tutorial I am going to use a three-level inheritance structure. We will have a base.html.twig. This file will contain the html, head and body elements. It will also contain any javascripts and style sheets that we want every page in our application to include. We will also have a layout.html.twig file where we will keep things like the main header, navigation and footer for the application. We will use Twig blocks to allow our other templates to include their content into these. If you are coming from symfony1 then you can think of a block as a slot. We will see an example of a block shortly.

The base.html.twig file is located in the app/views folder [Update: A change has been made to the framework after I wrote this part. The base.html.twig file should now reside in the app/Resources/views folder]. Open up the base.html.twig file and change your copy to match the following:

  1. <!DOCTYPE html> 
  2. <html> 
  3.     <head> 
  4.         <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
  5.         <title>{% block title %}symfony2 Blog Tutorial{% endblock %}</title> 
  6.         <link rel="shortcut icon" href="/favicon.ico" /> 
  7.         <!--[if lt IE 9]> 
  8.         <script src=""></script> 
  9.         <![endif]--> 
  10.         <link rel="stylesheet" type="text/css" href="{{ asset('css/reset.css') }}" media="screen" /> 
  11.         {% block head %}{% endblock %} 
  12.     </head> 
  13.     <body> 
  14.         {% block body %}{% endblock %} 
  15.     </body> 
  16. </html> 

Lets inspect this file in detail. We are going to use html5 so we have declared an html5 doctype. In our head section we have pretty much the standard stuff. We include an html5 shiv if the browser is Internet Explorer and the version number is less than 9. Now if we take a look at the title element, we can see a block declaration. Here we have a block with the name of title. The content between the block declaration is the content that will be included in the page if no child template fills the block with its own content. We also have blocks named head and body. The head block is there to allow any child of this template to include head content that it may need to include. The body block should not need any explanation.
让我们仔细检查一下这个文件。因为我们打算使用html5,因此我们声明了一个html5的doctype。我们的head部分是非常标准的。如果浏览器是IE并且它的版本小于9的话,我们包含了一个html5的shiv。现在如果我们看一下title元素,我们可以看到一个区块的声明。在这里,我们有一个名为title的区块。如果没有子模板用它自己的内容去装填区块内容的话,那么页面将包含在区块声明之间的内容。我们还有名为head和body的区块。Head区块允许该模板的任意子模板包含他们所需的head内容。Body 区块就不多做解释了。

We also include a reset stylesheet in our head section. You can download the reset stylesheet I am using here. Place this file in the web/css folder. In Twig, to output a value you use the {{ … }} syntax. You can think of this as the PHP echo function. Using the asset function to output your asset urls will make your application more portable. Lets move on to the layout.html.twig file located in the src/Company/BlogBundle/Resources/views folder. Open this file and change yours to look like the following:
我们还在我们的head部分包含了一个reset样式表。您可以在这里下载我所使用的reset样式表。将该文件放到web/css文件夹中。在Twig里您可以使用{{……}}语法来输出值。您可以把它看做PHP的echo函数。使用asset函数输出您的URL,可以使您的应用程序移植性更好。让我们再看看src/Company/BlogBundle/Resources/views 文件夹下的layout.html.twig文件。打开该文件并将您的代码如下所示:

  1. {% extends "::base.html.twig" %} 
  3. {% block head %} 
  4.     <link rel="stylesheet" type="text/css" href="{{ asset('bundles/blog/css/blog.css') }}" media="screen" /> 
  5. {% endblock %} 
  7. {% block body %} 
  8.     <div id="container"> 
  9.         <header class="clearfix"> 
  10.             <h1> 
  11.                 symfony2 Blog Tutorial 
  12.             </h1> 
  13.             <nav> 
  14.                 <ul> 
  15.                     <li> 
  16.                         <a href="{{ path('show_page', { 'page' : 'about' }) }}"> 
  17.                             About 
  18.                         </a> 
  19.                     </li> 
  20.                 </ul> 
  21.             </nav> 
  22.         </header> 
  23.         <div id="content"> 
  24.             {% block content %}No content.{% endblock %} 
  25.         </div> 
  26.         <footer class="clearfix"> 
  27.             <p class="symfony2"> 
  28.                 Powered by <a href="" target="_blank">symfony2</a> 
  29.             </p> 
  30.             <p class="copy"> 
  31.                 &copy; Company 2011 
  32.             </p> 
  33.         </footer> 
  34.     </div> 
  35. {% endblock %} 

As you can see, in the layout.html.twig the first thing we do is extend the base.html.twig file. Also, we have made use of the blocks that we setup in the base.html.twig file. We have included another stylesheet which you can download here, but this time we have included it from the bundle resources instead of the app resources. Many bundles that you use will package resources along with them. Luckily, symfony2 provides us with a console command that will publish these assets into our web folder. After you put the blog.css file in the src/Company/BlogBundle/Resources/public/css folder, open a terminal and navigate to your base project directory. Then run the following command:
正如你所看到的那样,layout.html.twig做的第一件事就是继承base.html.twig文件。另外,我们使用了我们在base.html.twig设置的区块。我们还包含了另一个样式表,您可以从这里下载,但这次我们是从Bundle的Resources而非从app的Resources中包含的。您所用的很多Bundle都是和Resources一起单独打包的。幸运的是,symfony2给我们提供了一个控制台命令行可以将这些资源发布到我们的web文件夹中。在您将blog.css文件放到src/Company/BlogBundle/Resources/public/css 文件夹后,打开终端,并进入您的项目根目录下。然后运行以下命令:

  1. php app/console assets:install web

This will copy all of the assets in the public folder of your bundles to the application’s web directory so you can include them in your templates. Next we define our body content. The only new thing in this section is the Twig path function. This returns a url to the route that you specify. This is similar to the symfony1 url_for function.

Now all we have to do is create our index.html.twig and about.html.twig. The index.html.twig file should be placed in the src/Company/BlogBundle/Resources/views/Blog folder and should look like this:
现在我们要做的是创建我们的index.html.twig和about.html.twig。index.html.twig文件将放在src/Company/BlogBundle/Resources/views/Blog 文件夹中,其代码如下所示:

  1. {% extends "BlogBundle::layout.html.twig" %} 
  3. {% block title %} 
  4.     symfony2 Blog Tutorial | Home 
  5. {% endblock %} 
  7. {% block content %} 
  8.     <h2>Welcome to the Blog!</h2> 
  9. {% endblock %} 

Knowing what you now know about Twig, you should have no problem understanding this template. All that is left is to create the about.html.twig template and put it in the src/Company/BlogBundle/Resources/views/Pages folder. It should look look like this:
在了解了您现在所需知道的有关Twig内容,理解上述模板应该没什么问题。剩下的就是创建about.html.twig文件,并将其放置在src/Company/BlogBundle/Resources/views/Pages 文件夹中,如下所示:

  1. {% extends "BlogBundle::layout.html.twig" %} 
  3. {% block title %} 
  4.     symfony2 Blog Tutorial | About 
  5. {% endblock %} 
  7. {% block content %} 
  8.     <h2>About</h2> 
  9. {% endblock %} 

When you look at these two files remember back to the tests we wrote and that our goal was to get these tests to pass. These templates are obviously very simple, but don’t worry we will be making them look a lot better and more complicated in the upcoming tutorials.

If you have followed along exactly, your home page and about page should look like the following screenshots.

symfony2 Blog Tutorial Home Page

symfony2 Blog Tutorial Home Page (Click for full size)

symfony2 Blog Tutorial - About Page

symfony2 Blog Tutorial About Page (Click for full size)

Pretty boring site so far, huh? Don’t worry we are on our way to a cool symfony2 powered blog! We finally have all the code in place to re-run our tests and now they should be passing. Success!

This has been a much longer post than I had originally intended, but we learned a lot about routing, controllers and templating. We even managed to squeeze some testing in too. But alas, we have only scraped the surface of each of these topics. Next time we will start creating some pages with dynamic content and work with the database and doctrine2. I may also revisit the routing a little and include some of the Controller annotations available in the FrameworkExtraBundle. Doing these things means that we will most definitely be diving further into routing, controllers and templates. These posts take hours to prepare, so I apologize if some time passes before I get the next one posted. I really didn’t know it would be this much work to write all this out! So hang in there with me! Until next time…