Symfony Book 2.0
Symfony Book 2.0
• Attribution: You must attribute the work in the manner specified by the author or licensor (but
not in any way that suggests that they endorse you or your use of the work).
• Share Alike: If you alter, transform, or build upon this work, you may distribute the resulting work
only under the same, similar or a compatible license. For any reuse or distribution, you must make
clear to others the license terms of this work.
The information in this book is distributed on an “as is” basis, without warranty. Although every precaution
has been taken in the preparation of this work, neither the author(s) nor SensioLabs shall have any liability to
any person or entity with respect to any loss or damage caused or alleged to be caused directly or indirectly by
the information contained in this work.
If you find typos or errors, feel free to report them by creating a ticket on the Symfony ticketing system
(http://github.com/symfony/symfony-docs/issues). Based on tickets and users feedback, this book is
continuously updated.
Contents at a Glance
Congratulations! By learning about Symfony2, you're well on your way towards being a more productive,
well-rounded and popular web developer (actually, you're on your own for the last part). Symfony2 is
built to get back to basics: to develop tools that let you develop faster and build more robust applications,
while staying out of your way. Symfony is built on the best ideas from many technologies: the tools and
concepts you're about to learn represent the efforts of thousands of people, over many years. In other
words, you're not just learning "Symfony", you're learning the fundamentals of the web, development
best practices, and how to use many amazing new PHP libraries, inside or independently of Symfony2.
So, get ready.
True to the Symfony2 philosophy, this chapter begins by explaining the fundamental concept common
to web development: HTTP. Regardless of your background or preferred programming language, this
chapter is a must-read for everyone.
HTTP is Simple
HTTP (Hypertext Transfer Protocol to the geeks) is a text language that allows two machines to
communicate with each other. That's it! For example, when checking for the latest xkcd1 comic, the
following (approximate) conversation takes place:
1. http://xkcd.com/
In HTTP-speak, this HTTP request would actually look something like this:
1 GET
Listing / HTTP/1.1 Listing
1-1 1-2
2 Host: xkcd.com
3 Accept: text/html
4 User-Agent: Mozilla/5.0 (Macintosh)
This simple message communicates everything necessary about exactly which resource the client is
requesting. The first line of an HTTP request is the most important and contains two things: the URI and
the HTTP method.
With this in mind, you can imagine what an HTTP request might look like to delete a specific blog entry,
for example:
Listing 1
Listing DELETE /blog/15 HTTP/1.1
1-3 1-4
There are actually nine HTTP methods defined by the HTTP specification, but many of them are
not widely used or supported. In reality, many modern browsers don't support the PUT and DELETE
methods.
In addition to the first line, an HTTP request invariably contains other lines of information called request
headers. The headers can supply a wide range of information such as the requested Host, the response
formats the client accepts (Accept) and the application the client is using to make the request (User-
Agent). Many other headers exist and can be found on Wikipedia's List of HTTP header fields2 article.
Translated into HTTP, the response sent back to the browser will look something like this:
2. http://en.wikipedia.org/wiki/List_of_HTTP_header_fields
The HTTP response contains the requested resource (the HTML content in this case), as well as other
information about the response. The first line is especially important and contains the HTTP response
status code (200 in this case). The status code communicates the overall outcome of the request back
to the client. Was the request successful? Was there an error? Different status codes exist that indicate
success, an error, or that the client needs to do something (e.g. redirect to another page). A full list can
be found on Wikipedia's List of HTTP status codes3 article.
Like the request, an HTTP response contains additional pieces of information known as HTTP headers.
For example, one important HTTP response header is Content-Type. The body of the same resource
could be returned in multiple different formats like HTML, XML, or JSON and the Content-Type header
uses Internet Media Types like text/html to tell the client which format is being returned. A list of
common media types can be found on Wikipedia's List of common media types4 article.
Many other headers exist, some of which are very powerful. For example, certain headers can be used to
create a powerful caching system.
To learn more about the HTTP specification, read the original HTTP 1.1 RFC5 or the HTTP Bis6,
which is an active effort to clarify the original specification. A great tool to check both the request
and response headers while browsing is the Live HTTP Headers7 extension for Firefox.
Listing Listing
1-7 1-8
3. http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
4. http://en.wikipedia.org/wiki/Internet_media_type#List_of_common_media_types
5. http://www.w3.org/Protocols/rfc2616/rfc2616.html
6. http://datatracker.ietf.org/wg/httpbis/
7. https://addons.mozilla.org/en-US/firefox/addon/live-http-headers/
As strange as it sounds, this small application is in fact taking information from the HTTP request and
using it to create an HTTP response. Instead of parsing the raw HTTP request message, PHP prepares
superglobal variables such as $_SERVER and $_GET that contain all the information from the request.
Similarly, instead of returning the HTTP-formatted text response, you can use the header() function to
create response headers and simply print out the actual content that will be the content portion of the
response message. PHP will create a true HTTP response and return it to the client:
Listing 1
Listing HTTP/1.1 200 OK
1-9 1-10
2 Date: Sat, 03 Apr 2011 02:14:33 GMT
3 Server: Apache/2.2.17 (Unix)
4 Content-Type: text/html
5
6 The URI requested is: /testing?foo=symfony
7 The value of the "foo" parameter is: symfony
1
Listing Listing use Symfony\Component\HttpFoundation\Request;
1-11 1-12
2
3 $request = Request::createFromGlobals();
4
5 // the URI being requested (e.g. /about) minus any query parameters
6 $request->getPathInfo();
7
8 // retrieve GET and POST variables respectively
9 $request->query->get('foo');
10 $request->request->get('bar', 'default value if bar does not exist');
11
12 // retrieve SERVER variables
13 $request->server->get('HTTP_HOST');
14
15 // retrieves an instance of UploadedFile identified by foo
16 $request->files->get('foo');
17
18 // retrieve a COOKIE value
19 $request->cookies->get('PHPSESSID');
8. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Request.html
As a bonus, the Request class does a lot of work in the background that you'll never need to worry about.
For example, the isSecure() method checks the three different values in PHP that can indicate whether
or not the user is connecting via a secured connection (i.e. https).
Symfony also provides a Response class: a simple PHP representation of an HTTP response message.
This allows your application to use an object-oriented interface to construct the response that needs to
be returned to the client:
1 use
Listing Symfony\Component\HttpFoundation\Response; Listing
1-13 1-14
2 $response = new Response();
3
4 $response->setContent('<html><body><h1>Hello world!</h1></body></html>');
5 $response->setStatusCode(200);
6 $response->headers->set('Content-Type', 'text/html');
7
8 // prints the HTTP headers followed by the content
9 $response->send();
If Symfony offered nothing else, you would already have a toolkit for easily accessing request information
and an object-oriented interface for creating the response. Even as you learn the many powerful features
in Symfony, keep in mind that the goal of your application is always to interpret a request and create the
appropriate response based on your application logic.
The Request and Response classes are part of a standalone component included with Symfony
called HttpFoundation. This component can be used entirely independently of Symfony and also
provides classes for handling sessions and file uploads.
9. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/ParameterBag.html
10. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/ParameterBag.html#get()
11. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/ParameterBag.html#has()
12. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/ParameterBag.html#all()
Listing 1
index.php
Listing
1-15 1-16
2 contact.php
3 blog.php
There are several problems with this approach, including the inflexibility of the URLs (what if you
wanted to change blog.php to news.php without breaking all of your links?) and the fact that each file
must manually include some set of core files so that security, database connections and the "look" of the
site can remain consistent.
A much better solution is to use a front controller: a single PHP file that handles every request coming
into your application. For example:
Using Apache's mod_rewrite (or equivalent with other web servers), the URLs can easily be
cleaned up to be just /, /contact and /blog.
Now, every request is handled exactly the same way. Instead of individual URLs executing different PHP
files, the front controller is always executed, and the routing of different URLs to different parts of your
application is done internally. This solves both problems with the original approach. Almost all modern
web apps do this - including apps like WordPress.
Stay Organized
But inside your front controller, how do you know which page should be rendered and how can you
render each in a sane way? One way or another, you'll need to check the incoming URI and execute
different parts of your code depending on that value. This can get ugly quickly:
1
Listing Listing // index.php
1-17 1-18
2 $request = Request::createFromGlobals();
3 $path = $request->getPathInfo(); // the URI path being requested
Solving this problem can be difficult. Fortunately it's exactly what Symfony is designed to do.
Incoming requests are interpreted by the routing and passed to controller functions that return Response
objects.
Each "page" of your site is defined in a routing configuration file that maps different URLs to different
PHP functions. The job of each PHP function, called a controller, is to use information from the request -
along with many other tools Symfony makes available - to create and return a Response object. In other
words, the controller is where your code goes: it's where you interpret the request and create a response.
It's that easy! Let's review:
This example uses YAML to define the routing configuration. Routing configuration can also be
written in other formats such as XML or PHP.
When someone visits the /contact page, this route is matched, and the specified controller is executed.
As you'll learn in the routing chapter, the AcmeDemoBundle:Main:contact string is a short syntax that
points to a specific PHP method contactAction inside a class called MainController:
Listing 1
class MainController
Listing
1-21 1-22
2 {
3 public function contactAction()
4 {
5 return new Response('<h1>Contact us!</h1>');
6 }
7 }
In this very simple example, the controller simply creates a Response object with the HTML
"<h1>Contact us!</h1>". In the controller chapter, you'll learn how a controller can render templates,
allowing your "presentation" code (i.e. anything that actually writes out HTML) to live in a separate
template file. This frees up the controller to worry only about the hard stuff: interacting with the
database, handling submitted data, or sending email messages.
• HttpFoundation - Contains the Request and Response classes, as well as other classes for
handling sessions and file uploads;
• Routing - Powerful and fast routing system that allows you to map a specific URI (e.g.
/contact) to some information about how that request should be handled (e.g. execute the
contactAction() method);
Each and every one of these components is decoupled and can be used in any PHP project, regardless of
whether or not you use the Symfony2 framework. Every part is made to be used if needed and replaced
when necessary.
13. https://github.com/symfony/Form
14. https://github.com/symfony/Validator
15. https://github.com/symfony/Security
16. https://github.com/symfony/Translation
Why is Symfony2 better than just opening up a file and writing flat PHP?
If you've never used a PHP framework, aren't familiar with the MVC philosophy, or just wonder what all
the hype is around Symfony2, this chapter is for you. Instead of telling you that Symfony2 allows you to
develop faster and better software than with flat PHP, you'll see for yourself.
In this chapter, you'll write a simple application in flat PHP, and then refactor it to be more organized.
You'll travel through time, seeing the decisions behind why web development has evolved over the past
several years to where it is now.
By the end, you'll see how Symfony2 can rescue you from mundane tasks and let you take back control
of your code.
1
Listing Listing <?php
2-1 2-2
2 // index.php
3 $link = mysql_connect('localhost', 'myuser', 'mypassword');
4 mysql_select_db('blog_db', $link);
5
6 $result = mysql_query('SELECT id, title FROM post', $link);
7 ?>
8
9 <!doctype html>
10 <html>
11 <head>
12 <title>List of Posts</title>
13 </head>
14 <body>
15 <h1>List of Posts</h1>
16 <ul>
That's quick to write, fast to execute, and, as your app grows, impossible to maintain. There are several
problems that need to be addressed:
Another problem not mentioned here is the fact that the database is tied to MySQL. Though not
covered here, Symfony2 fully integrates Doctrine1, a library dedicated to database abstraction and
mapping.
1 <?php
Listing Listing
2-3 2-4
2 // index.php
3 $link = mysql_connect('localhost', 'myuser', 'mypassword');
4 mysql_select_db('blog_db', $link);
5
6 $result = mysql_query('SELECT id, title FROM post', $link);
7
8 $posts = array();
9 while ($row = mysql_fetch_assoc($result)) {
10 $posts[] = $row;
11 }
12
13 mysql_close($link);
14
1. http://www.doctrine-project.org
The HTML code is now stored in a separate file (templates/list.php), which is primarily an HTML file
that uses a template-like PHP syntax:
1
<!doctype html>
Listing Listing
2-5 2-6
2 <html>
3 <head>
4 <title>List of Posts</title>
5 </head>
6 <body>
7 <h1>List of Posts</h1>
8 <ul>
9 <?php foreach ($posts as $post): ?>
10 <li>
11 <a href="/read?id=<?php echo $post['id'] ?>">
12 <?php echo $post['title'] ?>
13 </a>
14 </li>
15 <?php endforeach; ?>
16 </ul>
17 </body>
18 </html>
By convention, the file that contains all of the application logic - index.php - is known as a "controller".
The term controller is a word you'll hear a lot, regardless of the language or framework you use. It refers
simply to the area of your code that processes user input and prepares the response.
In this case, our controller prepares data from the database and then includes a template to present that
data. With the controller isolated, you could easily change just the template file if you needed to render
the blog entries in some other format (e.g. list.json.php for JSON format).
1
Listing Listing <?php
2-7 2-8
2 // model.php
3 function open_database_connection()
4 {
5 $link = mysql_connect('localhost', 'myuser', 'mypassword');
6 mysql_select_db('blog_db', $link);
7
8 return $link;
9 }
10
11 function close_database_connection($link)
12 {
13 mysql_close($link);
The filename model.php is used because the logic and data access of an application is traditionally
known as the "model" layer. In a well-organized application, the majority of the code representing
your "business logic" should live in the model (as opposed to living in a controller). And unlike in
this example, only a portion (or none) of the model is actually concerned with accessing a database.
1 <?php
Listing Listing
2-9 2-10
2 require_once 'model.php';
3
4 $posts = get_all_posts();
5
6 require 'templates/list.php';
Now, the sole task of the controller is to get data from the model layer of the application (the model) and
to call a template to render that data. This is a very simple example of the model-view-controller pattern.
1 <!--
Listing templates/layout.php --> Listing
2-11 2-12
2 <html>
3 <head>
4 <title><?php echo $title ?></title>
5 </head>
6 <body>
7 <?php echo $content ?>
8 </body>
9 </html>
1
<?php $title = 'List of Posts' ?>
Listing Listing
2-13 2-14
2
3 <?php ob_start() ?>
4 <h1>List of Posts</h1>
5 <ul>
6 <?php foreach ($posts as $post): ?>
7 <li>
8 <a href="/read?id=<?php echo $post['id'] ?>">
9 <?php echo $post['title'] ?>
10 </a>
11 </li>
12 <?php endforeach; ?>
13 </ul>
14 <?php $content = ob_get_clean() ?>
15
16 <?php include 'layout.php' ?>
You've now introduced a methodology that allows for the reuse of the layout. Unfortunately, to
accomplish this, you're forced to use a few ugly PHP functions (ob_start(), ob_get_clean()) in the
template. Symfony2 uses a Templating component that allows this to be accomplished cleanly and
easily. You'll see it in action shortly.
// model.php
1
Listing Listing
2-15 2-16
2 function get_post_by_id($id)
3 {
4 $link = open_database_connection();
5
6 $id = intval($id);
7 $query = 'SELECT date, title, body FROM post WHERE id = '.$id;
8 $result = mysql_query($query);
9 $row = mysql_fetch_assoc($result);
10
11 close_database_connection($link);
12
13 return $row;
14 }
Next, create a new file called show.php - the controller for this new page:
Listing 1
<?php
Listing
2-17 2-18
2 require_once 'model.php';
Finally, create the new template file - templates/show.php - to render the individual blog post:
1 <?php
Listing $title = $post['title'] ?> Listing
2-19 2-20
2
3 <?php ob_start() ?>
4 <h1><?php echo $post['title'] ?></h1>
5
6 <div class="date"><?php echo $post['date'] ?></div>
7 <div class="body">
8 <?php echo $post['body'] ?>
9 </div>
10 <?php $content = ob_get_clean() ?>
11
12 <?php include 'layout.php' ?>
Creating the second page is now very easy and no code is duplicated. Still, this page introduces even
more lingering problems that a framework can solve for you. For example, a missing or invalid id query
parameter will cause the page to crash. It would be better if this caused a 404 page to be rendered, but
this can't really be done easily yet. Worse, had you forgotten to clean the id parameter via the intval()
function, your entire database would be at risk for an SQL injection attack.
Another major problem is that each individual controller file must include the model.php file. What if
each controller file suddenly needed to include an additional file or perform some other global task (e.g.
enforce security)? As it stands now, that code would need to be added to every controller file. If you forget
to include something in one file, hopefully it doesn't relate to security...
1 Without
Listing a front controller Listing
2-21 2-22
2 /index.php => Blog post list page (index.php executed)
3 /show.php => Blog post show page (show.php executed)
4
5 With index.php as the front controller
6 /index.php => Blog post list page (index.php executed)
7 /index.php/show => Blog post show page (index.php executed)
The index.php portion of the URI can be removed if using Apache rewrite rules (or equivalent). In
that case, the resulting URI of the blog show page would be simply /show.
1
Listing Listing <?php
2-23 2-24
2 // index.php
3
4 // load and initialize any global libraries
5 require_once 'model.php';
6 require_once 'controllers.php';
7
8 // route the request internally
9 $uri = $_SERVER['REQUEST_URI'];
10 if ($uri == '/index.php') {
11 list_action();
12 } elseif ($uri == '/index.php/show' && isset($_GET['id'])) {
13 show_action($_GET['id']);
14 } else {
15 header('Status: 404 Not Found');
16 echo '<html><body><h1>Page Not Found</h1></body></html>';
17 }
For organization, both controllers (formerly index.php and show.php) are now PHP functions and each
has been moved into a separate file, controllers.php:
1
Listing Listing function list_action()
2-25 2-26
2 {
3 $posts = get_all_posts();
4 require 'templates/list.php';
5 }
6
7 function show_action($id)
8 {
9 $post = get_post_by_id($id);
10 require 'templates/show.php';
11 }
As a front controller, index.php has taken on an entirely new role, one that includes loading the
core libraries and routing the application so that one of the two controllers (the list_action() and
show_action() functions) is called. In reality, the front controller is beginning to look and act a lot like
Symfony2's mechanism for handling and routing requests.
By now, the application has evolved from a single PHP file into a structure that is organized and allows
for code reuse. You should be happier, but far from satisfied. For example, the "routing" system is
fickle, and wouldn't recognize that the list page (/index.php) should be accessible also via / (if Apache
rewrite rules were added). Also, instead of developing the blog, a lot of time is being spent working on
the "architecture" of the code (e.g. routing, calling controllers, templates, etc.). More time will need to
be spent to handle form submissions, input validation, logging and security. Why should you have to
reinvent solutions to all these routine problems?
1 <?php
Listing Listing
2-27 2-28
2 // bootstrap.php
3 require_once 'model.php';
4 require_once 'controllers.php';
5 require_once 'vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php';
6
7 $loader = new Symfony\Component\ClassLoader\UniversalClassLoader();
8 $loader->registerNamespaces(array(
9 'Symfony' => __DIR__.'/../vendor/symfony/src',
10 ));
11
12 $loader->register();
This tells the autoloader where the Symfony classes are. With this, you can start using Symfony classes
without using the require statement for the files that contain them.
Core to Symfony's philosophy is the idea that an application's main job is to interpret each request and
return a response. To this end, Symfony2 provides both a Request3 and a Response4 class. These classes
are object-oriented representations of the raw HTTP request being processed and the HTTP response
being returned. Use them to improve the blog:
1 <?php
Listing Listing
2-29 2-30
2 // index.php
3 require_once 'app/bootstrap.php';
4
5 use Symfony\Component\HttpFoundation\Request;
6 use Symfony\Component\HttpFoundation\Response;
2. http://symfony.com/download
3. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Request.html
4. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html
The controllers are now responsible for returning a Response object. To make this easier, you can add
a new render_template() function, which, incidentally, acts quite a bit like the Symfony2 templating
engine:
1
Listing Listing // controllers.php
2-31 2-32
2 use Symfony\Component\HttpFoundation\Response;
3
4 function list_action()
5 {
6 $posts = get_all_posts();
7 $html = render_template('templates/list.php', array('posts' => $posts));
8
9 return new Response($html);
10 }
11
12 function show_action($id)
13 {
14 $post = get_post_by_id($id);
15 $html = render_template('templates/show.php', array('post' => $post));
16
17 return new Response($html);
18 }
19
20 // helper function to render templates
21 function render_template($path, array $args)
22 {
23 extract($args);
24 ob_start();
25 require $path;
26 $html = ob_get_clean();
27
28 return $html;
29 }
By bringing in a small part of Symfony2, the application is more flexible and reliable. The Request
provides a dependable way to access information about the HTTP request. Specifically, the
getPathInfo() method returns a cleaned URI (always returning /show and never /index.php/show).
So, even if the user goes to /index.php/show, the application is intelligent enough to route the request
through show_action().
1 <?php
Listing Listing
2-33 2-34
2 // src/Acme/BlogBundle/Controller/BlogController.php
3 namespace Acme\BlogBundle\Controller;
4
5 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
6
7 class BlogController extends Controller
8 {
9 public function listAction()
10 {
11 $posts = $this->get('doctrine')->getEntityManager()
12 ->createQuery('SELECT p FROM AcmeBlogBundle:Post p')
13 ->execute();
14
15 return $this->render('AcmeBlogBundle:Blog:list.html.php', array('posts' =>
16 $posts));
17 }
18
19 public function showAction($id)
20 {
21 $post = $this->get('doctrine')
22 ->getEntityManager()
23 ->getRepository('AcmeBlogBundle:Post')
24 ->find($id);
25
26 if (!$post) {
27 // cause the 404 page not found to be displayed
28 throw $this->createNotFoundException();
29 }
30
31 return $this->render('AcmeBlogBundle:Blog:show.html.php', array('post' => $post));
32 }
}
The two controllers are still lightweight. Each uses the Doctrine ORM library to retrieve objects from the
database and the Templating component to render a template and return a Response object. The list
template is now quite a bit simpler:
5. https://github.com/symfony/Routing
6. https://github.com/symfony/Templating
We'll leave the show template as an exercise, as it should be trivial to create based on the list
template.
When Symfony2's engine (called the Kernel) boots up, it needs a map so that it knows which controllers
to execute based on the request information. A routing configuration map provides this information in a
readable format:
Listing # app/config/routing.yml
1
Listing
2-39 2-40
2 blog_list:
3 pattern: /blog
4 defaults: { _controller: AcmeBlogBundle:Blog:list }
5
6 blog_show:
7 pattern: /blog/show/{id}
8 defaults: { _controller: AcmeBlogBundle:Blog:show }
Now that Symfony2 is handling all the mundane tasks, the front controller is dead simple. And since it
does so little, you'll never have to touch it once it's created (and if you use a Symfony2 distribution, you
won't even need to create it!):
The front controller's only job is to initialize Symfony2's engine (Kernel) and pass it a Request object to
handle. Symfony2's core then uses the routing map to determine which controller to call. Just like before,
the controller method is responsible for returning the final Response object. There's really not much else
to it.
For a visual representation of how Symfony2 handles each request, see the request flow diagram.
• Your application now has clear and consistently organized code (though Symfony doesn't
force you into this). This promotes reusability and allows for new developers to be productive
in your project more quickly.
• 100% of the code you write is for your application. You don't need to develop or maintain
low-level utilities such as autoloading, routing, or rendering controllers.
• Symfony2 gives you access to open source tools such as Doctrine and the Templating,
Security, Form, Validation and Translation components (to name a few).
• The application now enjoys fully-flexible URLs thanks to the Routing component.
• Symfony2's HTTP-centric architecture gives you access to powerful tools such as HTTP
caching powered by Symfony2's internal HTTP cache or more powerful tools such as
Varnish7. This is covered in a later chapter all about caching.
And perhaps best of all, by using Symfony2, you now have access to a whole set of high-quality open
source tools developed by the Symfony2 community! A good selection of Symfony2 community tools
can be found on KnpBundles.com8.
Better templates
If you choose to use it, Symfony2 comes standard with a templating engine called Twig9 that makes
templates faster to write and easier to read. It means that the sample application could contain even less
code! Take, for example, the list template written in Twig:
1 {# src/Acme/BlogBundle/Resources/views/Blog/list.html.twig #}
Listing Listing
2-43 2-44
2 {% extends "::layout.html.twig" %}
3 {% block title %}List of Posts{% endblock %}
7. http://www.varnish-cache.org
8. http://knpbundles.com/
9. http://twig.sensiolabs.org
{# app/Resources/views/layout.html.twig #}
1
Listing Listing
2-45 2-46
2 <!doctype html>
3 <html>
4 <head>
5 <title>{% block title %}Default title{% endblock %}</title>
6 </head>
7 <body>
8 {% block body %}{% endblock %}
9 </body>
10 </html>
Twig is well-supported in Symfony2. And while PHP templates will always be supported in Symfony2,
we'll continue to discuss the many advantages of Twig. For more information, see the templating chapter.
The goal of this chapter is to get you up and running with a working application built on top of Symfony.
Fortunately, Symfony offers "distributions", which are functional Symfony "starter" projects that you can
download and begin developing in immediately.
If you're looking for instructions on how best to create a new project and store it via source control,
see Using Source Control.
First, check that you have installed and configured a Web server (such as Apache) with PHP
5.3.2 or higher. For more information on Symfony2 requirements, see the requirements reference.
For information on configuring your specific web server document root, see the following
documentation: Apache1 | Nginx2 .
Symfony2 packages "distributions", which are fully-functional applications that include the Symfony2
core libraries, a selection of useful bundles, a sensible directory structure and some default configuration.
When you download a Symfony2 distribution, you're downloading a functional application skeleton that
can be used immediately to begin developing your application.
Start by visiting the Symfony2 download page at http://symfony.com/download3. On this page, you'll see
the Symfony Standard Edition, which is the main Symfony2 distribution. Here, you'll need to make two
choices:
• Download either a .tgz or .zip archive - both are equivalent, download whatever you're more
comfortable using;
1. http://httpd.apache.org/docs/current/mod/core.html#documentroot
2. http://wiki.nginx.org/Symfony
3. http://symfony.com/download
Download one of the archives somewhere under your local web server's root directory and unpack it.
From a UNIX command line, this can be done with one of the following commands (replacing ### with
your actual filename):
When you're finished, you should have a Symfony/ directory that looks something like this:
1
Listing Listing www/ <- your web root directory
3-3 3-4
2 Symfony/ <- the unpacked archive
3 app/
4 cache/
5 config/
6 logs/
7 src/
8 ...
9 vendor/
10 ...
11 web/
12 app.php
13 ...
Updating Vendors
Finally, if you downloaded the archive "without vendors", install the vendors by running the following
command from the command line:
Listing 1
Listing $ php bin/vendors install
3-5 3-6
This command downloads all of the necessary vendor libraries - including Symfony itself - into the
vendor/ directory. For more information on how third-party vendor libraries are managed inside
Symfony2, see "Managing Vendor Libraries with bin/vendors and deps".
4. http://git-scm.com/
If there are any issues, correct them now before moving on.
Setting up Permissions
One common issue is that the app/cache and app/logs directories must be writable both by the
web server and the command line user. On a UNIX system, if your web server user is different from
your command line user, you can run the following commands just once in your project to ensure
that permissions will be setup properly. Change www-data to your web server user:
1. Using ACL on a system that supports chmod +a
Many systems allow you to use the chmod +a command. Try this first, and if you get an error - try
the next method:
1 $Listing
rm -rf app/cache/* Listing
2 $ 3-9rm -rf app/logs/* 3-10
3
4 $ sudo chmod +a "www-data allow delete,write,append,file_inherit,directory_inherit"
5 app/cache app/logs
$ sudo chmod +a "`whoami` allow delete,write,append,file_inherit,directory_inherit"
app/cache app/logs
1 $Listing
sudo setfacl -R -m u:www-data:rwx -m u:`whoami`:rwx app/cache app/logs Listing
2 $3-11
sudo setfacl -dR -m u:www-data:rwx -m u:`whoami`:rwx app/cache app/logs 3-12
Note that not all web servers run as the user www-data. You have to check which user the web
server is being run as and put it in for www-data. This can be done by checking your process list to
see which user is running your web server processes.
3. Without using ACL
If you don't have access to changing the ACL of the directories, you will need to change the umask
so that the cache and log directories will be group-writable or world-writable (depending if the
web server user and the command line user are in the same group or not). To achieve this, put the
following line at the beginning of the app/console, web/app.php and web/app_dev.php files:
1 umask(0002);
Listing // This will let the permissions be 0775 Listing
2 3-13 3-14
3 // or
4
5 umask(0000); // This will let the permissions be 0777
Note that using the ACL is recommended when you have access to them on your server because
changing the umask is not thread-safe.
Listing 1
Listing http://localhost/Symfony/web/app_dev.php/
3-15 3-16
Symfony2 should welcome and congratulate you for your hard work so far!
Beginning Development
Now that you have a fully-functional Symfony2 application, you can begin development! Your
distribution may contain some sample code - check the README.rst file included with the distribution
(open it as a text file) to learn about what sample code was included with your distribution and how you
can remove it later.
If you're new to Symfony, join us in the "Creating Pages in Symfony2", where you'll learn how to create
pages, change configuration, and do everything else you'll need in your new application.
5. https://help.ubuntu.com/community/FilePermissionsACLs
1 vendor/
Listing Listing
3-17 3-18
Now, the vendor directory won't be committed to source control. This is fine (actually, it's great!) because
when someone else clones or checks out the project, he/she can simply run the php bin/vendors
install script to download all the necessary vendor libraries.
• Create a route: A route defines the URL (e.g. /about) to your page and specifies a controller
(which is a PHP function) that Symfony2 should execute when the URL of an incoming request
matches the route pattern;
• Create a controller: A controller is a PHP function that takes the incoming request and
transforms it into the Symfony2 Response object that's returned to the user.
This simple approach is beautiful because it matches the way that the Web works. Every interaction on
the Web is initiated by an HTTP request. The job of your application is simply to interpret the request
and return the appropriate HTTP response.
Symfony2 follows this philosophy and provides you with tools and conventions to keep your application
organized as it grows in users and complexity.
Sounds simple enough? Let's dive in!
Listing 1
Listing http://localhost/app_dev.php/hello/Symfony
4-1 4-2
Actually, you'll be able to replace Symfony with any other name to be greeted. To create the page, follow
the simple two-step process.
The tutorial assumes that you've already downloaded Symfony2 and configured your webserver.
The above URL assumes that localhost points to the web directory of your new Symfony2 project.
For detailed information on this process, see the documentation on the web server you are using.
Here's the relevant documentation page for some web server you might be using:
1 $Listing
php app/console generate:bundle --namespace=Acme/HelloBundle --format=yml Listing
4-3 4-4
Behind the scenes, a directory is created for the bundle at src/Acme/HelloBundle. A line is also
automatically added to the app/AppKernel.php file so that the bundle is registered with the kernel:
1 // app/AppKernel.php
Listing Listing
4-5 4-6
2 public function registerBundles()
3 {
4 $bundles = array(
5 // ...
6 new Acme\HelloBundle\AcmeHelloBundle(),
7 );
8 // ...
9
10 return $bundles;
11 }
Now that you have a bundle setup, you can begin building your application inside the bundle.
1 #Listing
app/config/routing.yml Listing
4-7 4-8
2 AcmeHelloBundle:
3 resource: "@AcmeHelloBundle/Resources/config/routing.yml"
4 prefix: /
1. http://httpd.apache.org/docs/2.0/mod/mod_dir.html
2. http://wiki.nginx.org/HttpCoreModule#location
Listing # src/Acme/HelloBundle/Resources/config/routing.yml
1
Listing
4-9 4-10
2 hello:
3 pattern: /hello/{name}
4 defaults: { _controller: AcmeHelloBundle:Hello:index }
The routing consists of two basic pieces: the pattern, which is the URL that this route will match, and
a defaults array, which specifies the controller that should be executed. The placeholder syntax in the
pattern ({name}) is a wildcard. It means that /hello/Ryan, /hello/Fabien or any other similar URL will
match this route. The {name} placeholder parameter will also be passed to the controller so that you can
use its value to personally greet the user.
The routing system has many more great features for creating flexible and powerful URL structures
in your application. For more details, see the chapter all about Routing.
Listing 1
Listing // src/Acme/HelloBundle/Controller/HelloController.php
4-11 4-12
2 namespace Acme\HelloBundle\Controller;
3
4 class HelloController
5 {
6 }
In reality, the controller is nothing more than a PHP method that you create and Symfony executes. This
is where your code uses information from the request to build and prepare the resource being requested.
Except in some advanced cases, the end product of a controller is always the same: a Symfony2 Response
object.
Create the indexAction method that Symfony will execute when the hello route is matched:
1
Listing Listing // src/Acme/HelloBundle/Controller/HelloController.php
4-13 4-14
2 namespace Acme\HelloBundle\Controller;
3
4 use Symfony\Component\HttpFoundation\Response;
The controller is simple: it creates a new Response object, whose first argument is the content that should
be used in the response (a small HTML page in this example).
Congratulations! After creating only a route and a controller, you already have a fully-functional page! If
you've setup everything correctly, your application should greet you:
1 http://localhost/app_dev.php/hello/Ryan
Listing Listing
4-15 4-16
You can also view your app in the "prod" environment by visiting:
1 http://localhost/app.php/hello/Ryan
Listing Listing
4-17 4-18
If you get an error, it's likely because you need to clear your cache by running:
1 $Listing
php app/console cache:clear --env=prod --no-debug Listing
4-19 4-20
Controllers are the main entry point for your code and a key ingredient when creating pages. Much
more information can be found in the Controller Chapter.
1 // src/Acme/HelloBundle/Controller/HelloController.php
Listing Listing
4-21 4-22
2 namespace Acme\HelloBundle\Controller;
3
4 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
5
6 class HelloController extends Controller
7 {
In order to use the render() method, your controller must extend the
Symfony\Bundle\FrameworkBundle\Controller\Controller class (API docs: Controller3),
which adds shortcuts for tasks that are common inside controllers. This is done in the above
example by adding the use statement on line 4 and then extending Controller on line 6.
The render() method creates a Response object filled with the content of the given, rendered template.
Like any other controller, you will ultimately return that Response object.
Notice that there are two different examples for rendering the template. By default, Symfony2 supports
two different templating languages: classic PHP templates and the succinct but powerful Twig4 templates.
Don't be alarmed - you're free to choose either or even both in the same project.
The controller renders the AcmeHelloBundle:Hello:index.html.twig template, which uses the
following naming convention:
BundleName:ControllerName:TemplateName
This is the logical name of the template, which is mapped to a physical location using the following
convention.
/path/to/BundleName/Resources/views/ControllerName/TemplateName
In this case, AcmeHelloBundle is the bundle name, Hello is the controller, and index.html.twig the
template:
Listing 1
Listing {# src/Acme/HelloBundle/Resources/views/Hello/index.html.twig #}
4-23 4-24
2 {% extends '::base.html.twig' %}
3
4 {% block body %}
5 Hello {{ name }}!
6 {% endblock %}
• line 2: The extends token defines a parent template. The template explicitly defines a layout
file inside of which it will be placed.
• line 4: The block token says that everything inside should be placed inside a block called body.
As you'll see, it's the responsibility of the parent template (base.html.twig) to ultimately
render the block called body.
3. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Controller/Controller.html
4. http://twig.sensiolabs.org
1 {# app/Resources/views/base.html.twig #}
Listing Listing
4-25 4-26
2 <!DOCTYPE html>
3 <html>
4 <head>
5 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
6 <title>{% block title %}Welcome!{% endblock %}</title>
7 {% block stylesheets %}{% endblock %}
8 <link rel="shortcut icon" href="{{ asset('favicon.ico') }}" />
9 </head>
10 <body>
11 {% block body %}{% endblock %}
12 {% block javascripts %}{% endblock %}
13 </body>
14 </html>
The base template file defines the HTML layout and renders the body block that you defined in the
index.html.twig template. It also renders a title block, which you could choose to define in the
index.html.twig template. Since you did not define the title block in the child template, it defaults to
"Welcome!".
Templates are a powerful way to render and organize the content for your page. A template can render
anything, from HTML markup, to CSS code, or anything else that the controller may need to return.
In the lifecycle of handling a request, the templating engine is simply an optional tool. Recall that the
goal of each controller is to return a Response object. Templates are a powerful, but optional, tool for
creating the content for that Response object.
1 // web/app.php
Listing Listing
4-27 4-28
2 require_once __DIR__.'/../app/bootstrap.php.cache';
3 require_once __DIR__.'/../app/AppKernel.php';
The front controller file (app.php in this example) is the actual PHP file that's executed when using a
Symfony2 application and its job is to use a Kernel class, AppKernel, to bootstrap the application.
Having a front controller means different and more flexible URLs than are used in a typical flat
PHP application. When using a front controller, URLs are formatted in the following way:
Listing 1
Listing http://localhost/app.php/hello/Ryan
4-29 4-30
The front controller, app.php, is executed and the "internal:" URL /hello/Ryan is routed
internally using the routing configuration. By using Apache mod_rewrite rules, you can force the
app.php file to be executed without needing to specify it in the URL:
Listing 1
Listing http://localhost/hello/Ryan
4-31 4-32
Though front controllers are essential in handling every request, you'll rarely need to modify or even think
about them. We'll mention them again briefly in the Environments section.
• registerBundles(): Returns an array of all bundles needed to run the application (see The
Bundle System);
• registerContainerConfiguration(): Loads the main application configuration resource file
(see the Application Configuration section).
In day-to-day development, you'll mostly use the app/ directory to modify configuration and routing
files in the app/config/ directory (see Application Configuration). It also contains the application cache
directory (app/cache), a log directory (app/logs) and a directory for application-level resource files, such
as templates (app/Resources). You'll learn more about each of these directories in later chapters.
1 Class
Listing Name: Listing
2 4-33 Acme\HelloBundle\Controller\HelloController 4-34
3 Path:
4 src/Acme/HelloBundle/Controller/HelloController.php
Typically, the only time you'll need to worry about the app/autoload.php file is when you're
including a new third-party library in the vendor/ directory. For more information on autoloading,
see How to autoload Classes.
While you'll learn the basics here, an entire cookbook entry is devoted to the organization and best
practices of bundles.
A bundle is simply a structured set of files within a directory that implement a single feature. You might
create a BlogBundle, a ForumBundle or a bundle for user management (many of these exist already as
open source bundles). Each directory contains everything related to that feature, including PHP files,
templates, stylesheets, JavaScripts, tests and anything else. Every aspect of a feature exists in a bundle
and every feature lives in a bundle.
An application is made up of bundles as defined in the registerBundles() method of the AppKernel
class:
5. http://knpbundles.com
With the registerBundles() method, you have total control over which bundles are used by your
application (including the core Symfony bundles).
A bundle can live anywhere as long as it can be autoloaded (via the autoloader configured at app/
autoload.php).
Creating a Bundle
The Symfony Standard Edition comes with a handy task that creates a fully-functional bundle for you.
Of course, creating a bundle by hand is pretty easy as well.
To show you how simple the bundle system is, create a new bundle called AcmeTestBundle and enable
it.
The Acme portion is just a dummy name that should be replaced by some "vendor" name that
represents you or your organization (e.g. ABCTestBundle for some company named ABC).
Start by creating a src/Acme/TestBundle/ directory and adding a new file called AcmeTestBundle.php:
Listing // src/Acme/TestBundle/AcmeTestBundle.php
1
Listing
4-37 4-38
2 namespace Acme\TestBundle;
3
4 use Symfony\Component\HttpKernel\Bundle\Bundle;
5
6 class AcmeTestBundle extends Bundle
The name AcmeTestBundle follows the standard Bundle naming conventions. You could also
choose to shorten the name of the bundle to simply TestBundle by naming this class TestBundle
(and naming the file TestBundle.php).
This empty class is the only piece you need to create the new bundle. Though commonly empty, this
class is powerful and can be used to customize the behavior of the bundle.
Now that you've created the bundle, enable it via the AppKernel class:
1 // app/AppKernel.php
Listing Listing
4-39 4-40
2 public function registerBundles()
3 {
4 $bundles = array(
5 // ...
6
7 // register your bundles
8 new Acme\TestBundle\AcmeTestBundle(),
9 );
10 // ...
11
12 return $bundles;
13 }
1 $Listing
php app/console generate:bundle --namespace=Acme/TestBundle Listing
4-41 4-42
The bundle skeleton generates with a basic controller, template and routing resource that can be
customized. You'll learn more about Symfony2's command-line tools later.
Whenever creating a new bundle or using a third-party bundle, always make sure the bundle has
been enabled in registerBundles(). When using the generate:bundle command, this is done
for you.
A bundle can be as small or large as the feature it implements. It contains only the files you need and
nothing else.
As you move through the book, you'll learn how to persist objects to a database, create and validate
forms, create translations for your application, write tests and much more. Each of these has their own
place and role within the bundle.
Application Configuration
An application consists of a collection of bundles representing all of the features and capabilities of your
application. Each bundle can be customized via configuration files written in YAML, XML or PHP. By
default, the main configuration file lives in the app/config/ directory and is called either config.yml,
config.xml or config.php depending on which format you prefer:
1
Listing Listing # app/config/config.yml
4-43 4-44
2 imports:
3 - { resource: parameters.ini }
4 - { resource: security.yml }
5
6 framework:
7 secret: "%secret%"
8 charset: UTF-8
9 router: { resource: "%kernel.root_dir%/config/routing.yml" }
10 form: true
11 csrf_protection: true
12 validation: { enable_annotations: true }
13 templating: { engines: ['twig'] } #assets_version: SomeVersionScheme
14 session:
15 default_locale: "%locale%"
16 auto_start: true
17
18 # Twig Configuration
19 twig:
20 debug: "%kernel.debug%"
21 strict_variables: "%kernel.debug%"
22
23 # ...
You'll learn exactly how to load each file/format in the next section Environments.
Each top-level entry like framework or twig defines the configuration for a particular bundle. For
example, the framework key defines the configuration for the core Symfony FrameworkBundle and
includes configuration for the routing, templating, and other core systems.
Configuration Formats
Throughout the chapters, all configuration examples will be shown in all three formats (YAML,
XML and PHP). Each has its own advantages and disadvantages. The choice of which to use is up
to you:
Environments
An application can run in various environments. The different environments share the same PHP code
(apart from the front controller), but use different configuration. For instance, a dev environment will
log warnings and errors, while a prod environment will only log errors. Some files are rebuilt on each
request in the dev environment (for the developer's convenience), but cached in the prod environment.
All environments live together on the same machine and execute the same application.
A Symfony2 project generally begins with three environments (dev, test and prod), though creating new
environments is easy. You can view your application in different environments simply by changing the
front controller in your browser. To see the application in the dev environment, access the application
via the development front controller:
1 http://localhost/app_dev.php/hello/Ryan
Listing Listing
4-45 4-46
If you'd like to see how your application will behave in the production environment, call the prod front
controller instead:
1 http://localhost/app.php/hello/Ryan
Listing Listing
4-47 4-48
Since the prod environment is optimized for speed; the configuration, routing and Twig templates are
compiled into flat PHP classes and cached. When viewing changes in the prod environment, you'll need
to clear these cached files and allow them to rebuild:
1 php
Listing app/console cache:clear --env=prod --no-debug Listing
4-49 4-50
If you open the web/app.php file, you'll find that it's configured explicitly to use the prod
environment:
You can create a new front controller for a new environment by copying this file and changing prod
to some other value.
The test environment is used when running automated tests and cannot be accessed directly
through the browser. See the testing chapter for more details.
Environment Configuration
The AppKernel class is responsible for actually loading the configuration file of your choice:
Listing // app/AppKernel.php
1
Listing
4-53 4-54
2 public function registerContainerConfiguration(LoaderInterface $loader)
3 {
4 $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
5 }
You already know that the .yml extension can be changed to .xml or .php if you prefer to use either
XML or PHP to write your configuration. Notice also that each environment loads its own configuration
file. Consider the configuration file for the dev environment.
Listing # app/config/config_dev.yml
1
Listing
4-55 4-56
2 imports:
3 - { resource: config.yml }
4
5 framework:
6 router: { resource: "%kernel.root_dir%/config/routing_dev.yml" }
7 profiler: { only_exceptions: false }
8
9 # ...
The imports key is similar to a PHP include statement and guarantees that the main configuration file
(config.yml) is loaded first. The rest of the file tweaks the default configuration for increased logging
and other settings conducive to a development environment.
Both the prod and test environments follow the same model: each environment imports the base
configuration file and then modifies its configuration values to fit the needs of the specific environment.
This is just a convention, but one that allows you to reuse most of your configuration and customize just
pieces of it between environments.
From here, each chapter will introduce you to more and more powerful tools and advanced concepts.
The more you know about Symfony2, the more you'll appreciate the flexibility of its architecture and the
power it gives you to rapidly develop applications.
A controller is a PHP function you create that takes information from the HTTP request and constructs
and returns an HTTP response (as a Symfony2 Response object). The response could be an HTML page,
an XML document, a serialized JSON array, an image, a redirect, a 404 error or anything else you can
dream up. The controller contains whatever arbitrary logic your application needs to render the content
of a page.
To see how simple this is, let's look at a Symfony2 controller in action. The following controller would
render a page that simply prints Hello world!:
Listing 1
use Symfony\Component\HttpFoundation\Response;
Listing
5-1 5-2
2
3 public function helloAction()
4 {
5 return new Response('Hello world!');
6 }
The goal of a controller is always the same: create and return a Response object. Along the way, it might
read information from the request, load a database resource, send an email, or set information on the
user's session. But in all cases, the controller will eventually return the Response object that will be
delivered back to the client.
There's no magic and no other requirements to worry about! Here are a few common examples:
• Controller A prepares a Response object representing the content for the homepage of the site.
• Controller B reads the slug parameter from the request to load a blog entry from the database
and create a Response object displaying that blog. If the slug can't be found in the database,
it creates and returns a Response object with a 404 status code.
• Controller C handles the form submission of a contact form. It reads the form information
from the request, saves the contact information to the database and emails the contact
information to the webmaster. Finally, it creates a Response object that redirects the client's
browser to the contact form "thank you" page.
Though similarly named, a "front controller" is different from the "controllers" we'll talk about in
this chapter. A front controller is a short PHP file that lives in your web directory and through
which all requests are directed. A typical application will have a production front controller (e.g.
app.php) and a development front controller (e.g. app_dev.php). You'll likely never need to edit,
view or worry about the front controllers in your application.
A Simple Controller
While a controller can be any PHP callable (a function, method on an object, or a Closure), in Symfony2,
a controller is usually a single method inside a controller object. Controllers are also called actions.
1 // src/Acme/HelloBundle/Controller/HelloController.php
Listing Listing
5-3 5-4
2 namespace Acme\HelloBundle\Controller;
3
4 use Symfony\Component\HttpFoundation\Response;
5
6 class HelloController
7 {
8 public function indexAction($name)
9 {
10 return new Response('<html><body>Hello '.$name.'!</body></html>');
11 }
12 }
Note that the controller is the indexAction method, which lives inside a controller class
(HelloController). Don't be confused by the naming: a controller class is simply a convenient
way to group several controllers/actions together. Typically, the controller class will house several
controllers/actions (e.g. updateAction, deleteAction, etc).
• line 3: Symfony2 takes advantage of PHP 5.3 namespace functionality to namespace the entire
controller class. The use keyword imports the Response class, which our controller must
return.
Listing # app/config/routing.yml
1
Listing
5-5 5-6
2 hello:
3 pattern: /hello/{name}
4 defaults: { _controller: AcmeHelloBundle:Hello:index }
This example places the routing configuration directly in the app/config/ directory. A better way
to organize your routes is to place each route in the bundle it belongs to. For more information on
this, see Including External Routing Resources.
You can learn much more about the routing system in the Routing chapter.
1
Listing Listing <?php
5-7 5-8
2 // src/Acme/HelloBundle/Controller/HelloController.php
3 namespace Acme\HelloBundle\Controller;
The controller has a single argument, $name, which corresponds to the {name} parameter from the
matched route (ryan in our example). In fact, when executing your controller, Symfony2 matches each
argument of the controller with a parameter from the matched route. Take the following example:
1 #Listing
app/config/routing.yml Listing
5-9 5-10
2 hello:
3 pattern: /hello/{first_name}/{last_name}
4 defaults: { _controller: AcmeHelloBundle:Hello:index, color: green }
1 public
Listing function indexAction($first_name, $last_name, $color) Listing
5-11 5-12
2 {
3 // ...
4 }
Notice that both placeholder variables ({first_name}, {last_name}) as well as the default color
variable are available as arguments in the controller. When a route is matched, the placeholder variables
are merged with the defaults to make one array that's available to your controller.
Mapping route parameters to controller arguments is easy and flexible. Keep the following guidelines in
mind while you develop.
Symfony is able to match the parameter names from the route to the variable
names in the controller method's signature. In other words, it realizes that the
{last_name} parameter matches up with the $last_name argument. The arguments
of the controller could be totally reordered and still work perfectly:
1 public
Listing function indexAction($last_name, $color, $first_name) Listing
5-13 5-14
2 {
3 // ...
4 }
Making the argument optional, however, is perfectly ok. The following example
would not throw an exception:
Listing 1
public function indexAction($first_name, $last_name, $color, $foo =
Listing
5-17 5-18
2 'bar')
3 {
4 // ...
}
If, for example, the last_name weren't important for your controller, you could omit
it entirely:
Listing 1
public function indexAction($first_name, $color)
Listing
5-19 5-20
2 {
3 // ...
4 }
Every route also has a special _route parameter, which is equal to the name of the route that was
matched (e.g. hello). Though not usually useful, this is equally available as a controller argument.
Listing 1
use Symfony\Component\HttpFoundation\Request;
Listing
5-21 5-22
2
3 public function updateAction(Request $request)
4 {
5 $form = $this->createForm(...);
6
7 $form->bindRequest($request);
8 // ...
9 }
1 // src/Acme/HelloBundle/Controller/HelloController.php
Listing Listing
5-23 5-24
2 namespace Acme\HelloBundle\Controller;
3
4 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
5 use Symfony\Component\HttpFoundation\Response;
6
7 class HelloController extends Controller
8 {
9 public function indexAction($name)
10 {
11 return new Response('<html><body>Hello '.$name.'!</body></html>');
12 }
13 }
This doesn't actually change anything about how your controller works. In the next section, you'll
learn about the helper methods that the base controller class makes available. These methods are just
shortcuts to using core Symfony2 functionality that's available to you with or without the use of the base
Controller class. A great way to see the core functionality in action is to look in the Controller1 class
itself.
Extending the base class is optional in Symfony; it contains useful shortcuts but nothing
mandatory. You can also extend Symfony\Component\DependencyInjection\ContainerAware.
The service container object will then be accessible via the container property.
Redirecting
If you want to redirect the user to another page, use the redirect() method:
1. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Controller/Controller.html
The generateUrl() method is just a helper function that generates the URL for a given route. For more
information, see the Routing chapter.
By default, the redirect() method performs a 302 (temporary) redirect. To perform a 301 (permanent)
redirect, modify the second argument:
Listing 1
public function indexAction()
Listing
5-27 5-28
2 {
3 return $this->redirect($this->generateUrl('homepage'), 301);
4 }
The redirect() method is simply a shortcut that creates a Response object that specializes in
redirecting the user. It's equivalent to:
Listing 1
Listinguse Symfony\Component\HttpFoundation\RedirectResponse;
5-29 5-30
2
3 return new RedirectResponse($this->generateUrl('homepage'));
Forwarding
You can also easily forward to another controller internally with the forward() method. Instead of
redirecting the user's browser, it makes an internal sub-request, and calls the specified controller. The
forward() method returns the Response object that's returned from that controller:
1
public function indexAction($name)
Listing Listing
5-31 5-32
2 {
3 $response = $this->forward('AcmeHelloBundle:Hello:fancy', array(
4 'name' => $name,
5 'color' => 'green'
6 ));
7
8 // ... further modify the response or return it directly
9
10 return $response;
11 }
Notice that the forward() method uses the same string representation of the controller used in the
routing configuration. In this case, the target controller class will be HelloController inside some
AcmeHelloBundle. The array passed to the method becomes the arguments on the resulting controller.
This same interface is used when embedding controllers into templates (see Embedding Controllers). The
target controller method should look something like the following:
And just like when creating a controller for a route, the order of the arguments to fancyAction doesn't
matter. Symfony2 matches the index key names (e.g. name) with the method argument names (e.g.
$name). If you change the order of the arguments, Symfony2 will still pass the correct value to each
variable.
Like other base Controller methods, the forward method is just a shortcut for core Symfony2
functionality. A forward can be accomplished directly via the http_kernel service. A forward
returns a Response object:
1 $httpKernel
Listing = $this->container->get('http_kernel'); Listing
5-35 5-36
2 $response = $httpKernel->forward('AcmeHelloBundle:Hello:fancy', array(
3 'name' => $name,
4 'color' => 'green',
5 ));
Rendering Templates
Though not a requirement, most controllers will ultimately render a template that's responsible for
generating the HTML (or other format) for the controller. The renderView() method renders a template
and returns its content. The content from the template can be used to create a Response object:
1 $content
Listing = $this->renderView('AcmeHelloBundle:Hello:index.html.twig', array('name' => Listing
5-37 5-38
2 $name));
3
return new Response($content);
This can even be done in just one step with the render() method, which returns a Response object
containing the content from the template:
1 return
Listing $this->render('AcmeHelloBundle:Hello:index.html.twig', array('name' => $name)); Listing
5-39 5-40
The renderView method is a shortcut to direct use of the templating service. The templating
service can also be used directly:
It is possible to render templates in deeper subdirectories as well, however be careful to avoid the
pitfall of making your directory structure unduly elaborate:
Listing 1
Listing$templating->render('AcmeHelloBundle:Hello/Greetings:index.html.twig', array('name'
5-43 5-44
2 => $name));
// index.html.twig found in Resources/views/Hello/Greetings is rendered.
Listing 1
$request = $this->getRequest();
Listing
5-45 5-46
2
3 $templating = $this->get('templating');
4
5 $router = $this->get('router');
6
7 $mailer = $this->get('mailer');
There are countless other services available and you are encouraged to define your own. To list all
available services, use the container:debug console command:
Listing 1
Listing $ php app/console container:debug
5-47 5-48
1
Listing Listing public function indexAction()
5-49 5-50
2 {
3 // retrieve the object from database
1 throw
Listing new \Exception('Something went wrong!'); Listing
5-51 5-52
In every case, a styled error page is shown to the end user and a full debug error page is shown to the
developer (when viewing the page in debug mode). Both of these error pages can be customized. For
details, read the "How to customize Error Pages" cookbook recipe.
1 $session
Listing = $this->getRequest()->getSession(); Listing
5-53 5-54
2
3 // store an attribute for reuse during a later user request
4 $session->set('foo', 'bar');
5
6 // in another controller for another request
7 $foo = $session->get('foo');
8
9 // set the user locale
10 $session->setLocale('fr');
These attributes will remain on the user for the remainder of that user's session.
Flash Messages
You can also store small messages that will be stored on the user's session for exactly one additional
request. This is useful when processing a form: you want to redirect and have a special message shown
on the next request. These types of messages are called "flash" messages.
For example, imagine you're processing a form submit:
After processing the request, the controller sets a notice flash message and then redirects. The name
(notice) isn't significant - it's just what you're using to identify the type of the message.
In the template of the next action, the following code could be used to render the notice message:
Listing 1
{% if app.session.hasFlash('notice') %}
Listing
5-57 5-58
2 <div class="flash-notice">
3 {{ app.session.flash('notice') }}
4 </div>
5 {% endif %}
By design, flash messages are meant to live for exactly one request (they're "gone in a flash"). They're
designed to be used across redirects exactly as you've done in this example.
Listing 1
Listing // create a simple Response with a 200 status code (the default)
5-59 5-60
2 $response = new Response('Hello '.$name, 200);
3
4 // create a JSON-response with a 200 status code
5 $response = new Response(json_encode(array('name' => $name)));
6 $response->headers->set('Content-Type', 'application/json');
The headers property is a HeaderBag3 object with several useful methods for reading and mutating
the Response headers. The header names are normalized so that using Content-Type is equivalent
to content-type or even content_type.
2. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html
3. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/HeaderBag.html
1 $request
Listing = $this->getRequest(); Listing
5-61 5-62
2
3 $request->isXmlHttpRequest(); // is it an Ajax request?
4
5 $request->getPreferredLanguage(array('en', 'fr'));
6
7 $request->query->get('page'); // get a $_GET parameter
8
9 $request->request->get('page'); // get a $_POST parameter
Like the Response object, the request headers are stored in a HeaderBag object and are easily accessible.
Final Thoughts
Whenever you create a page, you'll ultimately need to write some code that contains the logic for that
page. In Symfony, this is called a controller, and it's a PHP function that can do anything it needs in order
to return the final Response object that will be returned to the user.
To make life easier, you can choose to extend a base Controller class, which contains shortcut methods
for many common controller tasks. For example, since you don't want to put HTML code in your
controller, you can use the render() method to render and return the content from a template.
In other chapters, you'll see how the controller can be used to persist and fetch objects from a database,
process form submissions, handle caching and more.
Beautiful URLs are an absolute must for any serious web application. This means leaving behind ugly
URLs like index.php?article_id=57 in favor of something like /read/intro-to-symfony.
Having flexibility is even more important. What if you need to change the URL of a page from /blog to
/news? How many links should you need to hunt down and update to make the change? If you're using
Symfony's router, the change is simple.
The Symfony2 router lets you define creative URLs that you map to different areas of your application.
By the end of this chapter, you'll be able to:
Routing in Action
A route is a map from a URL pattern to a controller. For example, suppose you want to match any URL
like /blog/my-post or /blog/all-about-symfony and send it to a controller that can look up and render
that blog entry. The route is simple:
Listing # app/config/routing.yml
1
Listing
6-1 6-2
2 blog_show:
3 pattern: /blog/{slug}
4 defaults: { _controller: AcmeBlogBundle:Blog:show }
The pattern defined by the blog_show route acts like /blog/* where the wildcard is given the name slug.
For the URL /blog/my-blog-post, the slug variable gets a value of my-blog-post, which is available
for you to use in your controller (keep reading).
The _controller parameter is a special key that tells Symfony which controller should be executed when
a URL matches this route. The _controller string is called the logical name. It follows a pattern that
points to a specific PHP class and method:
Congratulations! You've just created your first route and connected it to a controller. Now, when you
visit /blog/my-post, the showAction controller will be executed and the $slug variable will be equal to
my-post.
This is the goal of the Symfony2 router: to map the URL of a request to a controller. Along the way, you'll
learn all sorts of tricks that make mapping even the most complex URLs easy.
1 GET
Listing /blog/my-blog-post Listing
6-5 6-6
The goal of the Symfony2 routing system is to parse this URL and determine which controller should be
executed. The whole process looks like this:
1. The request is handled by the Symfony2 front controller (e.g. app.php);
2. The Symfony2 core (i.e. Kernel) asks the router to inspect the request;
3. The router matches the incoming URL to a specific route and returns information about the
route, including the controller that should be executed;
4. The Symfony2 Kernel executes the controller, which ultimately returns a Response object.
Creating Routes
Symfony loads all the routes for your application from a single routing configuration file. The file is
usually app/config/routing.yml, but can be configured to be anything (including an XML or PHP file)
via the application configuration file:
Listing # app/config/config.yml
1
Listing
6-7 6-8
2 framework:
3 # ...
4 router: { resource: "%kernel.root_dir%/config/routing.yml" }
Even though all routes are loaded from a single file, it's common practice to include additional
routing resources from inside the file. See the Including External Routing Resources section for more
information.
Listing 1
Listing _welcome:
6-9 6-10
2 pattern: /
3 defaults: { _controller: AcmeDemoBundle:Main:homepage }
This route matches the homepage (/) and maps it to the AcmeDemoBundle:Main:homepage controller.
The _controller string is translated by Symfony2 into an actual PHP function and executed. That
process will be explained shortly in the Controller Naming Pattern section.
The pattern will match anything that looks like /blog/*. Even better, the value matching the {slug}
placeholder will be available inside your controller. In other words, if the URL is /blog/hello-world,
a $slug variable, with a value of hello-world, will be available in the controller. This can be used, for
example, to load the blog post matching that string.
The pattern will not, however, match simply /blog. That's because, by default, all placeholders are
required. This can be changed by adding a placeholder value to the defaults array.
1 blog:
Listing Listing
6-13 6-14
2 pattern: /blog
3 defaults: { _controller: AcmeBlogBundle:Blog:index }
So far, this route is as simple as possible - it contains no placeholders and will only match the exact URL
/blog. But what if you need this route to support pagination, where /blog/2 displays the second page of
blog entries? Update the route to have a new {page} placeholder:
1 blog:
Listing Listing
6-15 6-16
2 pattern: /blog/{page}
3 defaults: { _controller: AcmeBlogBundle:Blog:index }
Like the {slug} placeholder before, the value matching {page} will be available inside your controller.
Its value can be used to determine which set of blog posts to display for the given page.
But hold on! Since placeholders are required by default, this route will no longer match on simply /blog.
Instead, to see page 1 of the blog, you'd need to use the URL /blog/1! Since that's no way for a rich web
app to behave, modify the route to make the {page} parameter optional. This is done by including it in
the defaults collection:
1 blog:
Listing Listing
6-17 6-18
2 pattern: /blog/{page}
3 defaults: { _controller: AcmeBlogBundle:Blog:index, page: 1 }
By adding page to the defaults key, the {page} placeholder is no longer required. The URL /blog will
match this route and the value of the page parameter will be set to 1. The URL /blog/2 will also match,
giving the page parameter a value of 2. Perfect.
/blog {page} = 1
/blog/1 {page} = 1
Adding Requirements
Take a quick look at the routes that have been created so far:
Listing 1
blog:
Listing
6-19 6-20
2 pattern: /blog/{page}
3 defaults: { _controller: AcmeBlogBundle:Blog:index, page: 1 }
4
5 blog_show:
6 pattern: /blog/{slug}
7 defaults: { _controller: AcmeBlogBundle:Blog:show }
Can you spot the problem? Notice that both routes have patterns that match URL's that look like
/blog/*. The Symfony router will always choose the first matching route it finds. In other words, the
blog_show route will never be matched. Instead, a URL like /blog/my-blog-post will match the first
route (blog) and return a nonsense value of my-blog-post to the {page} parameter.
The answer to the problem is to add route requirements. The routes in this example would work perfectly
if the /blog/{page} pattern only matched URLs where the {page} portion is an integer. Fortunately,
regular expression requirements can easily be added for each parameter. For example:
Listing 1
Listing blog:
6-21 6-22
2 pattern: /blog/{page}
3 defaults: { _controller: AcmeBlogBundle:Blog:index, page: 1 }
4 requirements:
5 page: \d+
The \d+ requirement is a regular expression that says that the value of the {page} parameter must be a
digit (i.e. a number). The blog route will still match on a URL like /blog/2 (because 2 is a number), but
it will no longer match a URL like /blog/my-blog-post (because my-blog-post is not a number).
As a result, a URL like /blog/my-blog-post will now properly match the blog_show route.
Since the parameter requirements are regular expressions, the complexity and flexibility of each
requirement is entirely up to you. Suppose the homepage of your application is available in two different
languages, based on the URL:
1 homepage:
Listing Listing
6-23 6-24
2 pattern: /{culture}
3 defaults: { _controller: AcmeDemoBundle:Main:homepage, culture: en }
4 requirements:
5 culture: en|fr
For incoming requests, the {culture} portion of the URL is matched against the regular expression
(en|fr).
/ {culture} = en
/en {culture} = en
/fr {culture} = fr
/es won't match this route
1 contact:
Listing Listing
6-25 6-26
2 pattern: /contact
3 defaults: { _controller: AcmeDemoBundle:Main:contact }
4 requirements:
5 _method: GET
6
7 contact_process:
8 pattern: /contact
9 defaults: { _controller: AcmeDemoBundle:Main:contactProcess }
10 requirements:
11 _method: POST
Despite the fact that these two routes have identical patterns (/contact), the first route will match only
GET requests and the second route will match only POST requests. This means that you can display the
form and submit the form via the same URL, while using distinct controllers for the two actions.
Like the other requirements, the _method requirement is parsed as a regular expression. To match GET or
POST requests, you can use GET|POST.
Listing 1
Listing article_show:
6-27 6-28
2 pattern: /articles/{culture}/{year}/{title}.{_format}
3 defaults: { _controller: AcmeDemoBundle:Article:show, _format: html }
4 requirements:
5 culture: en|fr
6 _format: html|rss
7 year: \d+
As you've seen, this route will only match if the {culture} portion of the URL is either en or fr and if
the {year} is a number. This route also shows how you can use a period between placeholders instead
of a slash. URLs matching this route might look like:
• /articles/en/2010/my-post
• /articles/fr/2010/my-post.rss
• _controller: As you've seen, this parameter is used to determine which controller is executed
when the route is matched;
• _format: Used to set the request format (read more);
• _locale: Used to set the locale on the session (read more);
bundle:controller:action
1 // src/Acme/BlogBundle/Controller/BlogController.php
Listing Listing
6-29 6-30
2 namespace Acme\BlogBundle\Controller;
3
4 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
5
6 class BlogController extends Controller
7 {
8 public function showAction($slug)
9 {
10 // ...
11 }
12 }
Notice that Symfony adds the string Controller to the class name (Blog => BlogController) and
Action to the method name (show => showAction).
You could also refer to this controller using its fully-qualified class name and method:
Acme\BlogBundle\Controller\BlogController::showAction. But if you follow some simple
conventions, the logical name is more concise and allows more flexibility.
In addition to using the logical name or the fully-qualified class name, Symfony supports a
third way of referring to a controller. This method uses just one colon separator (e.g.
service_name:indexAction) and refers to the controller as a service (see How to define Controllers
as Services).
1 public
Listing function showAction($slug) Listing
6-31 6-32
2 {
In reality, the entire defaults collection is merged with the parameter values to form a single array. Each
key of that array is available as an argument on the controller.
In other words, for each argument of your controller method, Symfony looks for a route parameter of
that name and assigns its value to that argument. In the advanced example above, any combination (in
any order) of the following variables could be used as arguments to the showAction() method:
• $culture
• $year
• $title
• $_format
• $_controller
Since the placeholders and defaults collection are merged together, even the $_controller variable is
available. For a more detailed discussion, see Route Parameters as Controller Arguments.
You can also use a special $_route variable, which is set to the name of the route that was matched.
Listing # app/config/routing.yml
1
Listing
6-33 6-34
2 acme_hello:
3 resource: "@AcmeHelloBundle/Resources/config/routing.yml"
When importing resources from YAML, the key (e.g. acme_hello) is meaningless. Just be sure that
it's unique so no other lines override it.
The resource key loads the given routing resource. In this example the resource is the full path to a
file, where the @AcmeHelloBundle shortcut syntax resolves to the path of that bundle. The imported file
might look like this:
Listing 1
Listing# src/Acme/HelloBundle/Resources/config/routing.yml
6-35 6-36
2 acme_hello:
3 pattern: /hello/{name}
4 defaults: { _controller: AcmeHelloBundle:Hello:index }
The routes from this file are parsed and loaded in the same way as the main routing file.
1 #Listing
app/config/routing.yml Listing
6-37 6-38
2 acme_hello:
3 resource: "@AcmeHelloBundle/Resources/config/routing.yml"
4 prefix: /admin
The string /admin will now be prepended to the pattern of each route loaded from the new routing
resource.
1 $Listing
php app/console router:debug Listing
6-39 6-40
The command will print a helpful list of all the configured routes in your application:
1 homepage
Listing ANY / Listing
6-41 6-42
2 contact GET /contact
3 contact_process POST /contact
4 article_show ANY /articles/{culture}/{year}/{title}.{_format}
5 blog ANY /blog/{page}
6 blog_show ANY /blog/{slug}
You can also get very specific information on a single route by including the route name after the
command:
1 $Listing
php app/console router:debug article_show Listing
6-43 6-44
Generating URLs
The routing system should also be used to generate URLs. In reality, routing is a bi-directional system:
mapping the URL to a controller+parameters and a route+parameters back to a URL. The match()1 and
generate()2 methods form this bi-directional system. Take the blog_show example route from earlier:
1. http://api.symfony.com/2.0/Symfony/Component/Routing/Router.html#match()
2. http://api.symfony.com/2.0/Symfony/Component/Routing/Router.html#generate()
To generate a URL, you need to specify the name of the route (e.g. blog_show) and any wildcards (e.g.
slug = my-blog-post) used in the pattern for that route. With this information, any URL can easily be
generated:
Listing 1
class MainController extends Controller
Listing
6-47 6-48
2 {
3 public function showAction($slug)
4 {
5 // ...
6
7 $url = $this->get('router')->generate('blog_show', array('slug' => 'my-blog-post'));
8 }
9 }
In an upcoming section, you'll learn how to generate URLs from inside templates.
If the frontend of your application uses AJAX requests, you might want to be able to generate URLs
in JavaScript based on your routing configuration. By using the FOSJsRoutingBundle3, you can do
exactly that:
Listing 1
Listing var url = Routing.generate('blog_show', { "slug": 'my-blog-post'});
6-49 6-50
Listing 1
$router->generate('blog_show', array('slug' => 'my-blog-post'), true);
Listing
6-51 6-52
2 // http://www.example.com/blog/my-blog-post
The host that's used when generating an absolute URL is the host of the current Request object.
This is detected automatically based on server information supplied by PHP. When generating
absolute URLs for scripts run from the command line, you'll need to manually set the desired host
on the RequestContext object:
3. https://github.com/FriendsOfSymfony/FOSJsRoutingBundle
1 $router->generate('blog',
Listing array('page' => 2, 'category' => 'Symfony')); Listing
6-55 6-56
2 // /blog/2?category=Symfony
Summary
Routing is a system for mapping the URL of incoming requests to the controller function that should be
called to process the request. It both allows you to specify beautiful URLs and keeps the functionality of
your application decoupled from those URLs. Routing is a two-way mechanism, meaning that it should
also be used to generate URLs.
As you know, the controller is responsible for handling each request that comes into a Symfony2
application. In reality, the controller delegates the most of the heavy work to other places so that code
can be tested and reused. When a controller needs to generate HTML, CSS or any other content, it hands
the work off to the templating engine. In this chapter, you'll learn how to write powerful templates that
can be used to return content to the user, populate email bodies, and more. You'll learn shortcuts, clever
ways to extend templates and how to reuse template code.
Templates
A template is simply a text file that can generate any text-based format (HTML, XML, CSV, LaTeX ...).
The most familiar type of template is a PHP template - a text file parsed by PHP that contains a mix of
text and PHP code:
1
<!DOCTYPE html>
Listing Listing
7-1 7-2
2 <html>
3 <head>
4 <title>Welcome to Symfony!</title>
5 </head>
6 <body>
7 <h1><?php echo $page_title ?></h1>
8
9 <ul id="navigation">
10 <?php foreach ($navigation as $item): ?>
11 <li>
12 <a href="<?php echo $item->getHref() ?>">
13 <?php echo $item->getCaption() ?>
14 </a>
15 </li>
16 <?php endforeach; ?>
17 </ul>
18 </body>
19 </html>
1 <!DOCTYPE
Listing html> Listing
7-3 7-4
2 <html>
3 <head>
4 <title>Welcome to Symfony!</title>
5 </head>
6 <body>
7 <h1>{{ page_title }}</h1>
8
9 <ul id="navigation">
10 {% for item in navigation %}
11 <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
12 {% endfor %}
13 </ul>
14 </body>
15 </html>
• {{ ... }}: "Says something": prints a variable or the result of an expression to the template;
• {% ... %}: "Does something": a tag that controls the logic of the template; it is used to execute
statements such as for-loops for example.
There is a third syntax used for creating comments: {# this is a comment #}. This syntax can
be used across multiple lines like the PHP-equivalent /* comment */ syntax.
Twig also contains filters, which modify content before being rendered. The following makes the title
variable all uppercase before rendering it:
1 {{ title|upper }}
Listing Listing
7-5 7-6
Twig comes with a long list of tags2 and filters3 that are available by default. You can even add your own
extensions4 to Twig as needed.
Registering a Twig extension is as easy as creating a new service and tagging it with
twig.extension tag.
As you'll see throughout the documentation, Twig also supports functions and new functions can be
easily added. For example, the following uses a standard for tag and the cycle function to print ten div
tags, with alternating odd, even classes:
1. http://twig.sensiolabs.org
2. http://twig.sensiolabs.org/doc/tags/index.html
3. http://twig.sensiolabs.org/doc/filters/index.html
4. http://twig.sensiolabs.org/doc/advanced.html#creating-an-extension
Throughout this chapter, template examples will be shown in both Twig and PHP.
If you do choose to not use Twig and you disable it, you'll need to implement your own exception
handler via the kernel.exception event.
Why Twig?
Twig templates are meant to be simple and won't process PHP tags. This is by design: the Twig
template system is meant to express presentation, not program logic. The more you use Twig, the
more you'll appreciate and benefit from this distinction. And of course, you'll be loved by web
designers everywhere.
Twig can also do things that PHP can't, such as whitespace control, sandboxing, and the inclusion
of custom functions and filters that only affect templates. Twig contains little features that make
writing templates easier and more concise. Take the following example, which combines a loop
with a logical if statement:
Listing 1
Listing<ul>
7-9 7-10
2 {% for user in users %}
3 <li>{{ user.username }}</li>
4 {% else %}
5 <li>No users found</li>
6 {% endfor %}
7 </ul>
1 {# app/Resources/views/base.html.twig #}
Listing Listing
7-11 7-12
2 <!DOCTYPE html>
3 <html>
4 <head>
5 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
6 <title>{% block title %}Test Application{% endblock %}</title>
7 </head>
8 <body>
9 <div id="sidebar">
10 {% block sidebar %}
11 <ul>
12 <li><a href="/">Home</a></li>
13 <li><a href="/blog">Blog</a></li>
14 </ul>
15 {% endblock %}
16 </div>
17
18 <div id="content">
19 {% block body %}{% endblock %}
20 </div>
21 </body>
22 </html>
Though the discussion about template inheritance will be in terms of Twig, the philosophy is the
same between Twig and PHP templates.
This template defines the base HTML skeleton document of a simple two-column page. In this example,
three {% block %} areas are defined (title, sidebar and body). Each block may be overridden by a child
template or left with its default implementation. This template could also be rendered directly. In that
case the title, sidebar and body blocks would simply retain the default values used in this template.
A child template might look like this:
1 {# src/Acme/BlogBundle/Resources/views/Blog/index.html.twig #}
Listing Listing
7-13 7-14
2 {% extends '::base.html.twig' %}
3
4 {% block title %}My cool blog posts{% endblock %}
5
6 {% block body %}
7 {% for entry in blog_entries %}
8 <h2>{{ entry.title }}</h2>
9 <p>{{ entry.body }}</p>
The parent template is identified by a special string syntax (::base.html.twig) that indicates that
the template lives in the app/Resources/views directory of the project. This naming convention
is explained fully in Template Naming and Locations.
The key to template inheritance is the {% extends %} tag. This tells the templating engine to first
evaluate the base template, which sets up the layout and defines several blocks. The child template is
then rendered, at which point the title and body blocks of the parent are replaced by those from the
child. Depending on the value of blog_entries, the output might look like this:
1
<!DOCTYPE html>
Listing Listing
7-15 7-16
2 <html>
3 <head>
4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5 <title>My cool blog posts</title>
6 </head>
7 <body>
8 <div id="sidebar">
9 <ul>
10 <li><a href="/">Home</a></li>
11 <li><a href="/blog">Blog</a></li>
12 </ul>
13 </div>
14
15 <div id="content">
16 <h2>My first post</h2>
17 <p>The body of the first post.</p>
18
19 <h2>Another post</h2>
20 <p>The body of the second post.</p>
21 </div>
22 </body>
23 </html>
Notice that since the child template didn't define a sidebar block, the value from the parent template is
used instead. Content within a {% block %} tag in a parent template is always used by default.
You can use as many levels of inheritance as you want. In the next section, a common three-level
inheritance model will be explained along with how templates are organized inside a Symfony2 project.
When working with template inheritance, here are some tips to keep in mind:
• If you use {% extends %} in a template, it must be the first tag in that template.
• The more {% block %} tags you have in your base templates, the better. Remember, child
templates don't have to define all parent blocks, so create as many blocks in your base
templates as you want and give each a sensible default. The more blocks your base templates
have, the more flexible your layout will be.
• If you find yourself duplicating content in a number of templates, it probably means you
should move that content to a {% block %} in a parent template. In some cases, a better
solution may be to move the content to a new template and include it (see Including other
Templates).
1 {% block sidebar %}
Listing Listing
7-17 7-18
2 <h3>Table of Contents</h3>
3 ...
4 {{ parent() }}
5 {% endblock %}
Symfony2 uses a bundle:controller:template string syntax for templates. This allows for several
different types of templates, each which lives in a specific location:
Assuming that the AcmeBlogBundle lives at src/Acme/BlogBundle, the final path to the layout
would be src/Acme/BlogBundle/Resources/views/Blog/index.html.twig.
• AcmeBlogBundle::layout.html.twig: This syntax refers to a base template that's specific
to the AcmeBlogBundle. Since the middle, "controller", portion is missing (e.g. Blog), the
template lives at Resources/views/layout.html.twig inside AcmeBlogBundle.
• ::base.html.twig: This syntax refers to an application-wide base template or layout. Notice
that the string begins with two colons (::), meaning that both the bundle and controller
portions are missing. This means that the template is not located in any bundle, but instead in
the root app/Resources/views/ directory.
In the Overriding Bundle Templates section, you'll find out how each template living inside the
AcmeBlogBundle, for example, can be overridden by placing a template of the same name in the app/
Resources/AcmeBlogBundle/views/ directory. This gives the power to override templates from any
vendor bundle.
Hopefully the template naming syntax looks familiar - it's the same naming convention used to
refer to Controller Naming Pattern.
By default, any Symfony2 template can be written in either Twig or PHP, and the last part of the
extension (e.g. .twig or .php) specifies which of these two engines should be used. The first part of the
extension, (e.g. .html, .css, etc) is the final format that the template will generate. Unlike the engine,
which determines how Symfony2 parses the template, this is simply an organizational tactic used in case
the same resource needs to be rendered as HTML (index.html.twig), XML (index.xml.twig), or any
other format. For more information, read the Debugging section.
The available "engines" can be configured and even new engines added. See Templating
Configuration for more details.
Listing 1
Listing {# src/Acme/ArticleBundle/Resources/views/Article/articleDetails.html.twig #}
7-19 7-20
2 <h2>{{ article.title }}</h2>
3 <h3 class="byline">by {{ article.authorName }}</h3>
4
5 <p>
6 {{ article.body }}
7 </p>
The template is included using the {% include %} tag. Notice that the template name follows the same
typical convention. The articleDetails.html.twig template uses an article variable. This is passed
in by the list.html.twig template using the with command.
The {'article': article} syntax is the standard Twig syntax for hash maps (i.e. an array with
named keys). If we needed to pass in multiple elements, it would look like this: {'foo': foo,
'bar': bar}.
Embedding Controllers
In some cases, you need to do more than include a simple template. Suppose you have a sidebar in your
layout that contains the three most recent articles. Retrieving the three articles may include querying the
database or performing other heavy logic that can't be done from within a template.
The solution is to simply embed the result of an entire controller from your template. First, create a
controller that renders a certain number of recent articles:
1 // src/Acme/ArticleBundle/Controller/ArticleController.php
Listing Listing
7-23 7-24
2
3 class ArticleController extends Controller
4 {
5 public function recentArticlesAction($max = 3)
6 {
7 // make a database call or other logic to get the "$max" most recent articles
8 $articles = ...;
9
10 return $this->render('AcmeArticleBundle:Article:recentList.html.twig',
11 array('articles' => $articles));
12 }
}
1 {# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
Listing Listing
7-25 7-26
2 {% for article in articles %}
3 <a href="/article/{{ article.slug }}">
4 {{ article.title }}
5 </a>
6 {% endfor %}
To include the controller, you'll need to refer to it using the standard string syntax for controllers (i.e.
bundle:controller:action):
Listing {# app/Resources/views/base.html.twig #}
1
Listing
7-27 7-28
2
3 {# ... #}
4
5 <div id="sidebar">
6 {% render "AcmeArticleBundle:Article:recentArticles" with {'max': 3} %}
7 </div>
Whenever you find that you need a variable or a piece of information that you don't have access to
in a template, consider rendering a controller. Controllers are fast to execute and promote good code
organization and reuse.
Linking to Pages
Creating links to other pages in your application is one of the most common jobs for a template. Instead
of hardcoding URLs in templates, use the path Twig function (or the router helper in PHP) to generate
URLs based on the routing configuration. Later, if you want to modify the URL of a particular page, all
you'll need to do is change the routing configuration; the templates will automatically generate the new
URL.
First, link to the "_welcome" page, which is accessible via the following routing configuration:
Listing 1
Listing _welcome:
7-29 7-30
2 pattern: /
3 defaults: { _controller: AcmeDemoBundle:Welcome:index }
To link to the page, just use the path Twig function and refer to the route:
Listing 1
Listing <a href="{{ path('_welcome') }}">Home</a>
7-31 7-32
As expected, this will generate the URL /. Let's see how this works with a more complicated route:
Listing 1
Listing article_show:
7-33 7-34
2 pattern: /article/{slug}
3 defaults: { _controller: AcmeArticleBundle:Article:show }
In this case, you need to specify both the route name (article_show) and a value for the {slug}
parameter. Using this route, let's revisit the recentList template from the previous section and link to
the articles correctly:
You can also generate an absolute URL by using the url Twig function:
The same can be done in PHP templates by passing a third argument to the generate() method:
Linking to Assets
Templates also commonly refer to images, Javascript, stylesheets and other assets. Of course you could
hard-code the path to these assets (e.g. /images/logo.png), but Symfony2 provides a more dynamic
option via the asset Twig function:
1 <img
Listing src="{{ asset('images/logo.png') }}" alt="Symfony!" /> Listing
7-41 7-42
2
3 <link href="{{ asset('css/blog.css') }}" rel="stylesheet" type="text/css" />
The asset function's main purpose is to make your application more portable. If your application lives
at the root of your host (e.g. http://example.com5), then the rendered paths should be /images/logo.png.
But if your application lives in a subdirectory (e.g. http://example.com/my_app6), each asset path should
render with the subdirectory (e.g. /my_app/images/logo.png). The asset function takes care of this by
determining how your application is being used and generating the correct paths accordingly.
Additionally, if you use the asset function, Symfony can automatically append a query string to your
asset, in order to guarantee that updated static assets won't be cached when deployed. For example,
/images/logo.png might look like /images/logo.png?v2. For more information, see the assets_version
configuration option.
5. http://example.com
6. http://example.com/my_app
Start by adding two blocks to your base template that will hold your assets: one called stylesheets
inside the head tag and another called javascripts just above the closing body tag. These blocks will
contain all of the stylesheets and Javascripts that you'll need throughout your site:
{# 'app/Resources/views/base.html.twig' #}
1
Listing Listing
7-43 7-44
2 <html>
3 <head>
4 {# ... #}
5
6 {% block stylesheets %}
7 <link href="{{ asset('/css/main.css') }}" type="text/css" rel="stylesheet" />
8 {% endblock %}
9 </head>
10 <body>
11 {# ... #}
12
13 {% block javascripts %}
14 <script src="{{ asset('/js/main.js') }}" type="text/javascript"></script>
15 {% endblock %}
16 </body>
17 </html>
That's easy enough! But what if you need to include an extra stylesheet or Javascript from a child
template? For example, suppose you have a contact page and you need to include a contact.css
stylesheet just on that page. From inside that contact page's template, do the following:
1
Listing Listing {# src/Acme/DemoBundle/Resources/views/Contact/contact.html.twig #}
7-45 7-46
2 {% extends '::base.html.twig' %}
3
4 {% block stylesheets %}
5 {{ parent() }}
6
7 <link href="{{ asset('/css/contact.css') }}" type="text/css" rel="stylesheet" />
8 {% endblock %}
9
10 {# ... #}
In the child template, you simply override the stylesheets block and put your new stylesheet tag inside
of that block. Of course, since you want to add to the parent block's content (and not actually replace
it), you should use the parent() Twig function to include everything from the stylesheets block of the
base template.
You can also include assets located in your bundles' Resources/public folder. You will need to run the
php app/console assets:install target [--symlink] command, which moves (or symlinks) files
into the correct location. (target is by default "web").
The end result is a page that includes both the main.css and contact.css stylesheets.
1 <p>Username:
Listing {{ app.user.username }}</p> Listing
7-49 7-50
2 {% if app.debug %}
3 <p>Request method: {{ app.request.method }}</p>
4 <p>Application Environment: {{ app.environment }}</p>
5 {% endif %}
You can add your own global template variables. See the cookbook example on Global Variables.
1 return
Listing $this->render('AcmeArticleBundle:Article:index.html.twig'); Listing
7-51 7-52
is equivalent to:
7. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.html
Listing # app/config/config.yml
1
Listing
7-53 7-54
2 framework:
3 # ...
4 templating: { engines: ['twig'] }
Several configuration options are available and are covered in the Configuration Appendix.
The twig engine is mandatory to use the webprofiler (as well as many third-party bundles).
Listing 1
public function indexAction()
Listing
7-55 7-56
2 {
3 // some logic to retrieve the blogs
4 $blogs = ...;
5
6 $this->render('AcmeBlogBundle:Blog:index.html.twig', array('blogs' => $blogs));
7 }
If you add a template in a new location, you may need to clear your cache (php app/console
cache:clear), even if you are in debug mode.
8. http://knpbundles.com
You can also override templates from within a bundle by using bundle inheritance. For more
information, see How to use Bundle Inheritance to Override parts of a Bundle.
Three-level Inheritance
One common way to use inheritance is to use a three-level approach. This method works perfectly with
the three different types of templates we've just covered:
• Create a app/Resources/views/base.html.twig file that contains the main layout for your
application (like in the previous example). Internally, this template is called
::base.html.twig;
• Create a template for each "section" of your site. For example, an AcmeBlogBundle, would
have a template called AcmeBlogBundle::layout.html.twig that contains only blog section-
specific elements;
1 {# src/Acme/BlogBundle/Resources/views/layout.html.twig #}
Listing Listing
7-57 7-58
2 {% extends '::base.html.twig' %}
3
4 {% block body %}
5 <h1>Blog Application</h1>
6
7 {% block content %}{% endblock %}
8 {% endblock %}
• Create individual templates for each page and make each extend the appropriate section
template. For example, the "index" page would be called something close to
AcmeBlogBundle:Blog:index.html.twig and list the actual blog posts.
Notice that this template extends the section template -(AcmeBlogBundle::layout.html.twig) which
in-turn extends the base application layout (::base.html.twig). This is the common three-level
inheritance model.
When building your application, you may choose to follow this method or simply make each page
template extend the base application template directly (e.g. {% extends '::base.html.twig' %}). The
three-template model is a best-practice method used by vendor bundles so that the base template for a
bundle can be easily overridden to properly extend your application's base layout.
Output Escaping
When generating HTML from a template, there is always a risk that a template variable may output
unintended HTML or dangerous client-side code. The result is that dynamic content could break the
HTML of the resulting page or allow a malicious user to perform a Cross Site Scripting9 (XSS) attack.
Consider this classic example:
Listing 1
Listing Hello {{ name }}
7-61 7-62
Imagine that the user enters the following code as his/her name:
Listing 1
Listing <script>alert('hello!')</script>
7-63 7-64
Without any output escaping, the resulting template will cause a JavaScript alert box to pop up:
Listing 1
Listing Hello <script>alert('hello!')</script>
7-65 7-66
And while this seems harmless, if a user can get this far, that same user should also be able to write
JavaScript that performs malicious actions inside the secure area of an unknowing, legitimate user.
The answer to the problem is output escaping. With output escaping on, the same template will render
harmlessly, and literally print the script tag to the screen:
Listing 1
Listing Hello <script>alert('helloe')</script>
7-67 7-68
9. http://en.wikipedia.org/wiki/Cross-site_scripting
1 Hello
Listing <?php echo $view->escape($name) ?> Listing
7-69 7-70
By default, the escape() method assumes that the variable is being rendered within an HTML context
(and thus the variable is escaped to be safe for HTML). The second argument lets you change the context.
For example, to output something in a JavaScript string, use the js context:
1 var
Listing myMsg = 'Hello <?php echo $view->escape($name, 'js') ?>'; Listing
7-71 7-72
Debugging
New in version 2.0.9: This feature is available as of Twig 1.5.x, which was first shipped with
Symfony 2.0.9.
When using PHP, you can use var_dump() if you need to quickly find the value of a variable passed. This
is useful, for example, inside your controller. The same can be achieved when using Twig by using the
debug extension. This needs to be enabled in the config:
1 #Listing
app/config/config.yml Listing
7-73 7-74
2 services:
3 acme_hello.twig.extension.debug:
4 class: Twig_Extension_Debug
10. http://twig.sensiolabs.org/doc/api.html#escaper-extension
Listing {# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
1
Listing
7-75 7-76
2 {{ dump(articles) }}
3
4 {% for article in articles %}
5 <a href="/article/{{ article.slug }}">
6 {{ article.title }}
7 </a>
8 {% endfor %}
The variables will only be dumped if Twig's debug setting (in config.yml) is true. By default this means
that the variables will be dumped in the dev environment but not the prod environment.
Template Formats
Templates are a generic way to render content in any format. And while in most cases you'll use templates
to render HTML content, a template can just as easily generate JavaScript, CSS, XML or any other format
you can dream of.
For example, the same "resource" is often rendered in several different formats. To render an article index
page in XML, simply include the format in the template name:
In reality, this is nothing more than a naming convention and the template isn't actually rendered
differently based on its format.
In many cases, you may want to allow a single controller to render multiple different formats based on
the "request format". For that reason, a common pattern is to do the following:
Listing 1
public function indexAction()
Listing
7-77 7-78
2 {
3 $format = $this->getRequest()->getRequestFormat();
4
5 return $this->render('AcmeBlogBundle:Blog:index.'.$format.'.twig');
6 }
The getRequestFormat on the Request object defaults to html, but can return any other format based
on the format requested by the user. The request format is most often managed by the routing, where a
route can be configured so that /contact sets the request format to html while /contact.xml sets the
format to xml. For more information, see the Advanced Example in the Routing chapter.
To create links that include the format parameter, include a _format key in the parameter hash:
Final Thoughts
The templating engine in Symfony is a powerful tool that can be used each time you need to generate
presentational content in HTML, XML or any other format. And though templates are a common way to
generate content in a controller, their use is not mandatory. The Response object returned by a controller
can be created with or without the use of a template:
Symfony's templating engine is very flexible and two different template renderers are available by default:
the traditional PHP templates and the sleek and powerful Twig templates. Both support a template
hierarchy and come packaged with a rich set of helper functions capable of performing the most common
tasks.
Overall, the topic of templating should be thought of as a powerful tool that's at your disposal. In some
cases, you may not need to render a template, and in Symfony2, that's absolutely fine.
Let's face it, one of the most common and challenging tasks for any application involves persisting and
reading information to and from a database. Fortunately, Symfony comes integrated with Doctrine1, a
library whose sole goal is to give you powerful tools to make this easy. In this chapter, you'll learn the
basic philosophy behind Doctrine and see how easy working with a database can be.
Doctrine is totally decoupled from Symfony and using it is optional. This chapter is all about
the Doctrine ORM, which aims to let you map objects to a relational database (such as MySQL,
PostgreSQL or Microsoft SQL). If you prefer to use raw database queries, this is easy, and explained
in the "How to use Doctrine's DBAL Layer" cookbook entry.
You can also persist data to MongoDB2 using Doctrine ODM library. For more information, read
the "DoctrineMongoDBBundle" documentation.
Listing 1
Listing $ php app/console generate:bundle --namespace=Acme/StoreBundle
8-1 8-2
1. http://www.doctrine-project.org/
2. http://www.mongodb.org/
1 ;Listing
app/config/parameters.ini Listing
8-3 8-4
2 [parameters]
3 database_driver = pdo_mysql
4 database_host = localhost
5 database_name = test_project
6 database_user = root
7 database_password = password
Defining the configuration via parameters.ini is just a convention. The parameters defined in
that file are referenced by the main configuration file when setting up Doctrine:
# app/config/config.yml Listing
8-5
doctrine:
dbal:
driver: %database_driver%
host: %database_host%
dbname: %database_name%
user: %database_user%
password: %database_password%
By separating the database information into a separate file, you can easily keep different versions
of the file on each server. You can also easily store database configuration (or any sensitive
information) outside of your project, like inside your Apache configuration, for example. For more
information, see How to Set External Parameters in the Service Container.
Listing 1
Listing$ app/console doctrine:database:drop --force
8-6 8-7
2 $ app/console doctrine:database:create
There's no way to configure these defaults inside Doctrine, as it tries to be as agnostic as possible
in terms of environment configuration. One way to solve this problem is to configure server-level
defaults.
Setting UTF8 defaults for MySQL is as simple as adding a few lines to your configuration file
(typically my.cnf):
Listing 1
Listing[mysqld]
8-8 8-9
2 collation-server = utf8_general_ci
3 character-set-server = utf8
Now that Doctrine knows about your database, you can have it create the database for you:
Listing 1
Listing $ php app/console doctrine:database:create
8-10 8-11
1
Listing Listing // src/Acme/StoreBundle/Entity/Product.php
8-12 8-13
2 namespace Acme\StoreBundle\Entity;
3
4 class Product
5 {
6 protected $name;
7
8 protected $price;
9
10 protected $description;
11 }
The class - often called an "entity", meaning a basic class that holds data - is simple and helps fulfill the
business requirement of needing products in your application. This class can't be persisted to a database
yet - it's just a simple PHP class.
1 $Listing
php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" Listing
8-14 8-15
--fields="name:string(255) price:float description:text"
For Doctrine to be able to do this, you just have to create "metadata", or configuration that tells Doctrine
exactly how the Product class and its properties should be mapped to the database. This metadata can
be specified in a number of different formats including YAML, XML or directly inside the Product class
via annotations:
A bundle can accept only one metadata definition format. For example, it's not possible to mix
YAML metadata definitions with annotated PHP entity class definitions.
1 // src/Acme/StoreBundle/Entity/Product.php
Listing Listing
8-16 8-17
2 namespace Acme\StoreBundle\Entity;
3
4 use Doctrine\ORM\Mapping as ORM;
5
6 /**
7 * @ORM\Entity
8 * @ORM\Table(name="product")
9 */
10 class Product
11 {
12 /**
13 * @ORM\Id
14 * @ORM\Column(type="integer")
15 * @ORM\GeneratedValue(strategy="AUTO")
16 */
The table name is optional and if omitted, will be determined automatically based on the name of
the entity class.
Doctrine allows you to choose from a wide variety of different field types, each with their own options.
For information on the available field types, see the Doctrine Field Types Reference section.
You can also check out Doctrine's Basic Mapping Documentation3 for all details about mapping information.
If you use annotations, you'll need to prepend all annotations with ORM\ (e.g. ORM\Column(..)), which is not
shown in Doctrine's documentation. You'll also need to include the use Doctrine\ORM\Mapping as ORM;
statement, which imports the ORM annotations prefix.
Be careful that your class name and properties aren't mapped to a protected SQL keyword (such
as group or user). For example, if your entity class name is Group, then, by default, your table
name will be group, which will cause an SQL error in some engines. See Doctrine's Reserved
SQL keywords documentation4 on how to properly escape these names. Alternatively, if you're
free to choose your database schema, simply map to a different table name or column name. See
Doctrine's Persistent classes5 and Property Mapping6 documentation.
When using another library or program (ie. Doxygen) that uses annotations, you should place the
@IgnoreAnnotation annotation on the class to indicate which annotations Symfony should ignore.
For example, to prevent the @fn annotation from throwing an exception, add the following:
Listing 1
Listing /**
8-18 8-19
2 * @IgnoreAnnotation("fn")
3. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html
4. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html#quoting-reserved-words
5. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html#persistent-classes
6. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html#property-mapping
1 $Listing
php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product Listing
8-20 8-21
This command makes sure that all of the getters and setters are generated for the Product class. This is
a safe command - you can run it over and over again: it only generates getters and setters that don't exist
(i.e. it doesn't replace your existing methods).
You can also generate all known entities (i.e. any PHP class with Doctrine mapping information) of a
bundle or an entire namespace:
1 $Listing
php app/console doctrine:generate:entities AcmeStoreBundle Listing
8-22 8-23
2 $ php app/console doctrine:generate:entities Acme
Doctrine doesn't care whether your properties are protected or private, or whether or not you
have a getter or setter function for a property. The getters and setters are generated here only
because you'll need them to interact with your PHP object.
Listing 1
Listing $ php app/console doctrine:schema:update --force
8-24 8-25
Actually, this command is incredibly powerful. It compares what your database should look like
(based on the mapping information of your entities) with how it actually looks, and generates the
SQL statements needed to update the database to where it should be. In other words, if you add a
new property with mapping metadata to Product and run this task again, it will generate the "alter
table" statement needed to add that new column to the existing product table.
An even better way to take advantage of this functionality is via migrations, which allow you to
generate these SQL statements and store them in migration classes that can be run systematically
on your production server in order to track and migrate your database schema safely and reliably.
Your database now has a fully-functional product table with columns that match the metadata you've
specified.
1
Listing Listing // src/Acme/StoreBundle/Controller/DefaultController.php
8-26 8-27
2
3 // ...
4 use Acme\StoreBundle\Entity\Product;
5 use Symfony\Component\HttpFoundation\Response;
6
7 public function createAction()
8 {
9 $product = new Product();
10 $product->setName('A Foo Bar');
11 $product->setPrice('19.99');
12 $product->setDescription('Lorem ipsum dolor');
13
14 $em = $this->getDoctrine()->getEntityManager();
15 $em->persist($product);
16 $em->flush();
17
18 return new Response('Created product id '.$product->getId());
19 }
If you're following along with this example, you'll need to create a route that points to this action
to see it work.
• lines 8-11 In this section, you instantiate and work with the $product object like any other,
normal PHP object;
• line 13 This line fetches Doctrine's entity manager object, which is responsible for handling
the process of persisting and fetching objects to and from the database;
• line 14 The persist() method tells Doctrine to "manage" the $product object. This does not
actually cause a query to be made to the database (yet).
• line 15 When the flush() method is called, Doctrine looks through all of the objects that it's
managing to see if they need to be persisted to the database. In this example, the $product
object has not been persisted yet, so the entity manager executes an INSERT query and a row is
created in the product table.
In fact, since Doctrine is aware of all your managed entities, when you call the flush() method,
it calculates an overall changeset and executes the most efficient query/queries possible. For
example, if you persist a total of 100 Product objects and then subsequently call flush(), Doctrine
will create a single prepared statement and re-use it for each insert. This pattern is called Unit of
Work, and it's used because it's fast and efficient.
When creating or updating objects, the workflow is always the same. In the next section, you'll see
how Doctrine is smart enough to automatically issue an UPDATE query if the record already exists in the
database.
Doctrine provides a library that allows you to programmatically load testing data into your project
(i.e. "fixture data"). For information, see DoctrineFixturesBundle.
1 public
Listing function showAction($id) Listing
8-28 8-29
2 {
3 $product = $this->getDoctrine()
4 ->getRepository('AcmeStoreBundle:Product')
5 ->find($id);
6
7 if (!$product) {
8 throw $this->createNotFoundException('No product found for id '.$id);
9 }
10
11 // ... do something, like pass the $product object into a template
12 }
When you query for a particular type of object, you always use what's known as its "repository". You can
think of a repository as a PHP class whose only job is to help you fetch entities of a certain class. You can
access the repository object for an entity class via:
Listing Listing
8-30 8-31
The AcmeStoreBundle:Product string is a shortcut you can use anywhere in Doctrine instead of
the full class name of the entity (i.e. Acme\StoreBundle\Entity\Product). As long as your entity
lives under the Entity namespace of your bundle, this will work.
Once you have your repository, you have access to all sorts of helpful methods:
1
Listing Listing // query by the primary key (usually "id")
8-32 8-33
2 $product = $repository->find($id);
3
4 // dynamic method names to find based on a column value
5 $product = $repository->findOneById($id);
6 $product = $repository->findOneByName('foo');
7
8 // find *all* products
9 $products = $repository->findAll();
10
11 // find a group of products based on an arbitrary column value
12 $products = $repository->findByPrice(19.99);
Of course, you can also issue complex queries, which you'll learn more about in the Querying for
Objects section.
You can also take advantage of the useful findBy and findOneBy methods to easily fetch objects based
on multiple conditions:
Listing 1
Listing // query for one product matching be name and price
8-34 8-35
2 $product = $repository->findOneBy(array('name' => 'foo', 'price' => 19.99));
3
4 // query for all products matching the name, ordered by price
5 $product = $repository->findBy(
6 array('name' => 'foo'),
7 array('price' => 'ASC')
8 );
When you render any page, you can see how many queries were made in the bottom right corner
of the web debug toolbar.
Updating an Object
Once you've fetched an object from Doctrine, updating it is easy. Suppose you have a route that maps a
product id to an update action in a controller:
1 public
Listing function updateAction($id) Listing
8-36 8-37
2 {
3 $em = $this->getDoctrine()->getEntityManager();
4 $product = $em->getRepository('AcmeStoreBundle:Product')->find($id);
5
6 if (!$product) {
7 throw $this->createNotFoundException('No product found for id '.$id);
8 }
9
10 $product->setName('New product name!');
11 $em->flush();
12
13 return $this->redirect($this->generateUrl('homepage'));
14 }
Deleting an Object
Deleting an object is very similar, but requires a call to the remove() method of the entity manager:
As you might expect, the remove() method notifies Doctrine that you'd like to remove the given entity
from the database. The actual DELETE query, however, isn't actually executed until the flush() method
is called.
Listing 1
$repository->find($id);
Listing
8-40 8-41
2
3 $repository->findOneByName('Foo');
Of course, Doctrine also allows you to write more complex queries using the Doctrine Query Language
(DQL). DQL is similar to SQL except that you should imagine that you're querying for one or more
objects of an entity class (e.g. Product) instead of querying for rows on a table (e.g. product).
When querying in Doctrine, you have two options: writing pure Doctrine queries or using Doctrine's
Query Builder.
Listing 1
$em = $this->getDoctrine()->getEntityManager();
Listing
8-42 8-43
2 $query = $em->createQuery(
3 'SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC'
4 )->setParameter('price', '19.99');
5
6 $products = $query->getResult();
If you're comfortable with SQL, then DQL should feel very natural. The biggest difference is that you
need to think in terms of "objects" instead of rows in a database. For this reason, you select from
AcmeStoreBundle:Product and then alias it as p.
The getResult() method returns an array of results. If you're querying for just one object, you can use
the getSingleResult() method instead:
Listing 1
Listing $product = $query->getSingleResult();
8-44 8-45
1 $query
Listing = $em->createQuery('SELECT ...') Listing
8-46 8-47
2 ->setMaxResults(1);
3
4 try {
5 $product = $query->getSingleResult();
6 } catch (\Doctrine\Orm\NoResultException $e) {
7 $product = null;
8 }
9 // ...
The DQL syntax is incredibly powerful, allowing you to easily join between entities (the topic of
relations will be covered later), group, etc. For more information, see the official Doctrine Doctrine Query
Language7 documentation.
Setting Parameters
Take note of the setParameter() method. When working with Doctrine, it's always a good idea
to set any external values as "placeholders", which was done in the above query:
1 ...
Listing WHERE p.price > :price ... Listing
8-48 8-49
You can then set the value of the price placeholder by calling the setParameter() method:
1 ->setParameter('price',
Listing '19.99') Listing
8-50 8-51
Using parameters instead of placing values directly in the query string is done to prevent SQL
injection attacks and should always be done. If you're using multiple parameters, you can set their
values at once using the setParameters() method:
1 ->setParameters(array(
Listing Listing
2 8-52 'price' => '19.99', 8-53
7. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/dql-doctrine-query-language.html
The QueryBuilder object contains every method necessary to build your query. By calling the
getQuery() method, the query builder returns a normal Query object, which is the same object you built
directly in the previous section.
For more information on Doctrine's Query Builder, consult Doctrine's Query Builder8 documentation.
1
Listing Listing // src/Acme/StoreBundle/Entity/Product.php
8-56 8-57
2 namespace Acme\StoreBundle\Entity;
3
4 use Doctrine\ORM\Mapping as ORM;
5
6 /**
7 * @ORM\Entity(repositoryClass="Acme\StoreBundle\Repository\ProductRepository")
8 */
9 class Product
10 {
11 //...
12 }
Doctrine can generate the repository class for you by running the same command used earlier to generate
the missing getter and setter methods:
Listing 1
Listing $ php app/console doctrine:generate:entities Acme
8-58 8-59
Next, add a new method - findAllOrderedByName() - to the newly generated repository class. This
method will query for all of the Product entities, ordered alphabetically.
1
Listing Listing // src/Acme/StoreBundle/Repository/ProductRepository.php
8-60 8-61
2 namespace Acme\StoreBundle\Repository;
8. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/query-builder.html
The entity manager can be accessed via $this->getEntityManager() from inside the repository.
You can use this new method just like the default finder methods of the repository:
1 $em
Listing = $this->getDoctrine()->getEntityManager(); Listing
8-62 8-63
2 $products = $em->getRepository('AcmeStoreBundle:Product')
3 ->findAllOrderedByName();
When using a custom repository class, you still have access to the default finder methods such as
find() and findAll().
Entity Relationships/Associations
Suppose that the products in your application all belong to exactly one "category". In this case, you'll
need a Category object and a way to relate a Product object to a Category object. Start by creating the
Category entity. Since you know that you'll eventually need to persist the class through Doctrine, you
can let Doctrine create the class for you.
1 $Listing
php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Category" Listing
8-64 8-65
--fields="name:string(255)"
This task generates the Category entity for you, with an id field, a name field and the associated getter
and setter functions.
First, since a Category object will relate to many Product objects, a products array property is added
to hold those Product objects. Again, this isn't done because Doctrine needs it, but instead because it
makes sense in the application for each Category to hold an array of Product objects.
The code in the __construct() method is important because Doctrine requires the $products
property to be an ArrayCollection object. This object looks and acts almost exactly like an array,
but has some added flexibility. If this makes you uncomfortable, don't worry. Just imagine that it's
an array and you'll be in good shape.
The targetEntity value in the decorator used above can reference any entity with a valid namespace,
not just entities defined in the same class. To relate to an entity defined in a different class or
bundle, enter a full namespace as the targetEntity.
Next, since each Product class can relate to exactly one Category object, you'll want to add a $category
property to the Product class:
1
Listing Listing // src/Acme/StoreBundle/Entity/Product.php
8-68 8-69
2
3 // ...
4
5 class Product
6 {
7 // ...
8
9 /**
10 * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
11 * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
12 */
13 protected $category;
14 }
1 $Listing
php app/console doctrine:generate:entities Acme Listing
8-70 8-71
Ignore the Doctrine metadata for a moment. You now have two classes - Category and Product with a
natural one-to-many relationship. The Category class holds an array of Product objects and the Product
object can hold one Category object. In other words - you've built your classes in a way that makes sense
for your needs. The fact that the data needs to be persisted to a database is always secondary.
Now, look at the metadata above the $category property on the Product class. The information here
tells doctrine that the related class is Category and that it should store the id of the category record on
a category_id field that lives on the product table. In other words, the related Category object will be
stored on the $category property, but behind the scenes, Doctrine will persist this relationship by storing
the category's id value on a category_id column of the product table.
The metadata above the $products property of the Category object is less important, and simply tells
Doctrine to look at the Product.category property to figure out how the relationship is mapped.
Listing 1
Listing $ php app/console doctrine:schema:update --force
8-72 8-73
This task should only be really used during development. For a more robust method of
systematically updating your production database, read about Doctrine migrations.
1
Listing Listing // ...
8-74 8-75
2
3 use Acme\StoreBundle\Entity\Category;
4 use Acme\StoreBundle\Entity\Product;
5 use Symfony\Component\HttpFoundation\Response;
6
7 class DefaultController extends Controller
8 {
9 public function createProductAction()
10 {
11 $category = new Category();
12 $category->setName('Main Products');
13
14 $product = new Product();
15 $product->setName('Foo');
16 $product->setPrice(19.99);
17 // relate this product to the category
18 $product->setCategory($category);
19
20 $em = $this->getDoctrine()->getEntityManager();
21 $em->persist($category);
22 $em->persist($product);
23 $em->flush();
24
25 return new Response(
26 'Created product id: '.$product->getId().' and category id:
27 '.$category->getId()
28 );
29 }
}
Now, a single row is added to both the category and product tables. The product.category_id column
for the new product is set to whatever the id is of the new category. Doctrine manages the persistence of
this relationship for you.
In this example, you first query for a Product object based on the product's id. This issues a query for
just the product data and hydrates the $product object with that data. Later, when you call $product-
>getCategory()->getName(), Doctrine silently makes a second query to find the Category that's related
to this Product. It prepares the $category object and returns it to you.
What's important is the fact that you have easy access to the product's related category, but the category
data isn't actually retrieved until you ask for the category (i.e. it's "lazily loaded").
You can also query in the other direction:
1 public
Listing function showProductAction($id) Listing
8-78 8-79
2 {
3 $category = $this->getDoctrine()
4 ->getRepository('AcmeStoreBundle:Category')
5 ->find($id);
6
7 $products = $category->getProducts();
8
9 // ...
10 }
In this case, the same things occurs: you first query out for a single Category object, and then Doctrine
makes a second query to retrieve the related Product objects, but only once/if you ask for them (i.e. when
Listing 1
Listing$product = $this->getDoctrine()
8-80 8-81
2 ->getRepository('AcmeStoreBundle:Product')
3 ->find($id);
4
5 $category = $product->getCategory();
6
7 // prints "Proxies\AcmeStoreBundleEntityCategoryProxy"
8 echo get_class($category);
This proxy object extends the true Category object, and looks and acts exactly like it. The
difference is that, by using a proxy object, Doctrine can delay querying for the real Category data
until you actually need that data (e.g. until you call $category->getName()).
The proxy classes are generated by Doctrine and stored in the cache directory. And though you'll
probably never even notice that your $category object is actually a proxy object, it's important to
keep in mind.
In the next section, when you retrieve the product and category data all at once (via a join),
Doctrine will return the true Category object, since nothing needs to be lazily loaded.
Remember that you can see all of the queries made during a request via the web debug toolbar.
Of course, if you know up front that you'll need to access both objects, you can avoid the second query
by issuing a join in the original query. Add the following method to the ProductRepository class:
// src/Acme/StoreBundle/Repository/ProductRepository.php
1
Listing Listing
8-82 8-83
2 public function findOneByIdJoinedToCategory($id)
3 {
4 $query = $this->getEntityManager()
5 ->createQuery('
6 SELECT p, c FROM AcmeStoreBundle:Product p
7 JOIN p.category c
8 WHERE p.id = :id'
9 )->setParameter('id', $id);
10
11 try {
12 return $query->getSingleResult();
13 } catch (\Doctrine\ORM\NoResultException $e) {
Now, you can use this method in your controller to query for a Product object and its related Category
with just one query:
1 public
Listing function showAction($id) Listing
8-84 8-85
2 {
3 $product = $this->getDoctrine()
4 ->getRepository('AcmeStoreBundle:Product')
5 ->findOneByIdJoinedToCategory($id);
6
7 $category = $product->getCategory();
8
9 // ...
10 }
If you're using annotations, you'll need to prepend all annotations with ORM\ (e.g. ORM\OneToMany),
which is not reflected in Doctrine's documentation. You'll also need to include the use
Doctrine\ORM\Mapping as ORM; statement, which imports the ORM annotations prefix.
Configuration
Doctrine is highly configurable, though you probably won't ever need to worry about most of its options.
To find out more about configuring Doctrine, see the Doctrine section of the reference manual.
Lifecycle Callbacks
Sometimes, you need to perform an action right before or after an entity is inserted, updated, or deleted.
These types of actions are known as "lifecycle" callbacks, as they're callback methods that you need to
execute during different stages of the lifecycle of an entity (e.g. the entity is inserted, updated, deleted,
etc).
If you're using annotations for your metadata, start by enabling the lifecycle callbacks. This is not
necessary if you're using YAML or XML for your mapping:
1 /**
Listing Listing
8-86 8-87
2 * @ORM\Entity()
9. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/association-mapping.html
Now, you can tell Doctrine to execute a method on any of the available lifecycle events. For example,
suppose you want to set a created date column to the current date, only when the entity is first persisted
(i.e. inserted):
Listing /**
1
Listing
8-88 8-89
2 * @ORM\PrePersist
3 */
4 public function setCreatedValue()
5 {
6 $this->created = new \DateTime();
7 }
The above example assumes that you've created and mapped a created property (not shown here).
Now, right before the entity is first persisted, Doctrine will automatically call this method and the
created field will be set to the current date.
This can be repeated for any of the other lifecycle events, which include:
• preRemove
• postRemove
• prePersist
• postPersist
• preUpdate
• postUpdate
• postLoad
• loadClassMetadata
For more information on what these lifecycle events mean and lifecycle callbacks in general, see
Doctrine's Lifecycle Events documentation10
10. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/events.html#lifecycle-events
• Strings
• Numbers
• integer
• smallint
• bigint
• decimal
• float
• Dates and Times (use a DateTime11 object for these fields in PHP)
• date
• time
• datetime
• Other Types
• boolean
• object (serialized and stored in a CLOB field)
• array (serialized and stored in a CLOB field)
Field Options
Each field can have a set of options applied to it. The available options include type (defaults to string),
name, length, unique and nullable. Take a few examples:
1 /**
Listing Listing
8-90 8-91
2 * A string field with length 255 that cannot be null
3 * (reflecting the default values for the "type", "length" and *nullable* options)
4 *
5 * @ORM\Column()
6 */
11. http://php.net/manual/en/class.datetime.php
12. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html#doctrine-mapping-types
There are a few more options not listed here. For more details, see Doctrine's Property Mapping
documentation13
Console Commands
The Doctrine2 ORM integration offers several console commands under the doctrine namespace. To
view the command list you can run the console without any arguments:
Listing 1
Listing $ php app/console
8-92 8-93
A list of available command will print out, many of which start with the doctrine: prefix. You can find
out more information about any of these commands (or any Symfony command) by running the help
command. For example, to get details about the doctrine:database:create task, run:
Listing 1
Listing $ php app/console help doctrine:database:create
8-94 8-95
Listing 1
Listing $ php app/console doctrine:ensure-production-settings --env=prod
8-96 8-97
13. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html#property-mapping
Summary
With Doctrine, you can focus on your objects and how they're useful in your application and worry about
database persistence second. This is because Doctrine allows you to use any PHP object to hold your data
and relies on mapping metadata information to map an object's data to a particular database table.
And even though Doctrine revolves around a simple concept, it's incredibly powerful, allowing you to
create complex queries and subscribe to events that allow you to take different actions as objects go
through their persistence lifecycle.
For more information about Doctrine, see the Doctrine section of the cookbook, which includes the
following articles:
• DoctrineFixturesBundle
• How to use Doctrine Extensions: Timestampable, Sluggable, Translatable, etc.
Let's face it, one of the most common and challenging tasks for any application involves persisting and
reading information to and from a database. Symfony2 does not come integrated with any ORMs but the
Propel integration is easy. To get started, read Working With Symfony21.
Listing 1
Listing $ php app/console generate:bundle --namespace=Acme/StoreBundle
9-1 9-2
Listing ; app/config/parameters.ini
1
Listing
9-3 9-4
2 [parameters]
3 database_driver = mysql
4 database_host = localhost
5 database_name = test_project
1. http://www.propelorm.org/cookbook/symfony2/working-with-symfony2.html#installation
Defining the configuration via parameters.ini is just a convention. The parameters defined in
that file are referenced by the main configuration file when setting up Propel:
propel: Listing
9-5
dbal:
driver: %database_driver%
user: %database_user%
password: %database_password%
dsn:
%database_driver%:host=%database_host%;dbname=%database_name%;charset=%database_charset%
Now that Propel knows about your database, Symfony2 can create the database for you:
1 $Listing
php app/console propel:database:create Listing
9-6 9-7
In this example, you have one configured connection, named default. If you want to configure
more than one connection, read the PropelBundle configuration section.
For people who use Symfony2 with Doctrine2, models are equivalent to entities.
Suppose you're building an application where products need to be displayed. First, create a schema.xml
file inside the Resources/config directory of your AcmeStoreBundle:
1 <?xml
Listing version="1.0" encoding="UTF-8"?> Listing
9-8 9-9
2 <database name="default" namespace="Acme\StoreBundle\Model" defaultIdMethod="native">
3 <table name="product">
4 <column name="id" type="integer" required="true" primaryKey="true"
5 autoIncrement="true" />
6 <column name="name" type="varchar" primaryString="true" size="100" />
7 <column name="price" type="decimal" />
8 <column name="description" type="longvarchar" />
9 </table>
</database>
Listing 1
Listing $ php app/console propel:model:build
9-10 9-11
This generates each model class to quickly develop your application in the Model/ directory the
AcmeStoreBundle bundle.
Listing 1
$ php app/console propel:sql:build
Listing
9-12 9-13
2 $ php app/console propel:sql:insert --force
Your database now has a fully-functional product table with columns that match the schema you've
specified.
You can run the last three commands combined by using the following command: php app/
console propel:build --insert-sql.
1
Listing Listing // src/Acme/StoreBundle/Controller/DefaultController.php
9-14 9-15
2
3 // ...
4 use Acme\StoreBundle\Model\Product;
5 use Symfony\Component\HttpFoundation\Response;
6
7 public function createAction()
8 {
9 $product = new Product();
10 $product->setName('A Foo Bar');
11 $product->setPrice(19.99);
12 $product->setDescription('Lorem ipsum dolor');
13
14 $product->save();
15
16 return new Response('Created product id '.$product->getId());
17 }
If you're following along with this example, you'll need to create a route that points to this action
to see it in action.
1 // ...
Listing Listing
9-16 9-17
2 use Acme\StoreBundle\Model\ProductQuery;
3
4 public function showAction($id)
5 {
6 $product = ProductQuery::create()
7 ->findPk($id);
8
9 if (!$product) {
10 throw $this->createNotFoundException('No product found for id '.$id);
11 }
12
13 // ... do something, like pass the $product object into a template
14 }
Updating an Object
Once you've fetched an object from Propel, updating it is easy. Suppose you have a route that maps a
product id to an update action in a controller:
1 // ...
Listing Listing
9-18 9-19
2 use Acme\StoreBundle\Model\ProductQuery;
3
4 public function updateAction($id)
5 {
6 $product = ProductQuery::create()
7 ->findPk($id);
8
9 if (!$product) {
10 throw $this->createNotFoundException('No product found for id '.$id);
11 }
12
13 $product->setName('New product name!');
14 $product->save();
15
16 return $this->redirect($this->generateUrl('homepage'));
17 }
Deleting an Object
Deleting an object is very similar, but requires a call to the delete() method on the object:
Listing 1
Listing $product->delete();
9-20 9-21
Listing 1
\Acme\StoreBundle\Model\ProductQuery::create()->findPk($id);
Listing
9-22 9-23
2
3 \Acme\StoreBundle\Model\ProductQuery::create()
4 ->filterByName('Foo')
5 ->findOne();
Imagine that you want to query for products which cost more than 19.99, ordered from cheapest to most
expensive. From inside a controller, do the following:
Listing 1
Listing $products = \Acme\StoreBundle\Model\ProductQuery::create()
9-24 9-25
2 ->filterByPrice(array('min' => 19.99))
3 ->orderByPrice()
4 ->find();
In one line, you get your products in a powerful oriented object way. No need to waste your time
with SQL or whatever, Symfony2 offers fully object oriented programming and Propel respects the same
philosophy by providing an awesome abstraction layer.
If you want to reuse some queries, you can add your own methods to the ProductQuery class:
Listing // src/Acme/StoreBundle/Model/ProductQuery.php
1
Listing
9-26 9-27
2 class ProductQuery extends BaseProductQuery
3 {
4 public function filterByExpensivePrice()
5 {
6 return $this
7 ->filterByPrice(array('min' => 1000))
8 }
9 }
But note that Propel generates a lot of methods for you and a simple findAllOrderedByName() can be
written without any effort:
Relationships/Associations
Suppose that the products in your application all belong to exactly one "category". In this case, you'll
need a Category object and a way to relate a Product object to a Category object.
Start by adding the category definition in your schema.xml:
1 <database
Listing name="default" namespace="Acme\StoreBundle\Model" defaultIdMethod="native"> Listing
9-30 9-31
2 <table name="product">
3 <column name="id" type="integer" required="true" primaryKey="true"
4 autoIncrement="true" />
5 <column name="name" type="varchar" primaryString="true" size="100" />
6 <column name="price" type="decimal" />
7 <column name="description" type="longvarchar" />
8
9 <column name="category_id" type="integer" />
10 <foreign-key foreignTable="category">
11 <reference local="category_id" foreign="id" />
12 </foreign-key>
13 </table>
14
15 <table name="category">
16 <column name="id" type="integer" required="true" primaryKey="true"
17 autoIncrement="true" />
18 <column name="name" type="varchar" primaryString="true" size="100" />
</table>
</database>
1 $Listing
php app/console propel:model:build Listing
9-32 9-33
Assuming you have products in your database, you don't want lose them. Thanks to migrations, Propel
will be able to update your database without losing existing data.
1 $Listing
php app/console propel:migration:generate-diff Listing
9-34 9-35
2 $ php app/console propel:migration:migrate
Your database has been updated, you can continue to write your application.
Now, a single row is added to both the category and product tables. The product.category_id column
for the new product is set to whatever the id is of the new category. Propel manages the persistence of
this relationship for you.
1
Listing Listing // ...
9-38 9-39
2 use Acme\StoreBundle\Model\ProductQuery;
3
4 public function showAction($id)
5 {
6 $product = ProductQuery::create()
7 ->joinWithCategory()
8 ->findPk($id);
9
10 $categoryName = $product->getCategory()->getName();
11
12 // ...
13 }
Lifecycle Callbacks
Sometimes, you need to perform an action right before or after an object is inserted, updated, or deleted.
These types of actions are known as "lifecycle" callbacks or "hooks", as they're callback methods that you
need to execute during different stages of the lifecycle of an object (e.g. the object is inserted, updated,
deleted, etc).
To add a hook, just add a new method to the object class:
1 // src/Acme/StoreBundle/Model/Product.php
Listing Listing
9-40 9-41
2
3 // ...
4
5 class Product extends BaseProduct
6 {
7 public function preInsert(\PropelPDO $con = null)
8 {
9 // do something before the object is inserted
10 }
11 }
Behaviors
All bundled behaviors in Propel are working with Symfony2. To get more information about how to use
Propel behaviors, look at the Behaviors reference section.
Commands
You should read the dedicated section for Propel commands in Symfony23.
2. http://www.propelorm.org/documentation/04-relationships.html
3. http://www.propelorm.org/cookbook/symfony2/working-with-symfony2#the_commands
Whenever you write a new line of code, you also potentially add new bugs. To build better and more
reliable applications, you should test your code using both functional and unit tests.
Each test - whether it's a unit test or a functional test - is a PHP class that should live in the Tests/
subdirectory of your bundles. If you follow this rule, then you can run all of your application's tests with
the following command:
The -c option tells PHPUnit to look in the app/ directory for a configuration file. If you're curious about
the PHPUnit options, check out the app/phpunit.xml.dist file.
1. http://www.phpunit.de/manual/3.5/en/
1 // src/Acme/DemoBundle/Utility/Calculator.php
Listing Listing
10-3 10-4
2 namespace Acme\DemoBundle\Utility;
3
4 class Calculator
5 {
6 public function add($a, $b)
7 {
8 return $a + $b;
9 }
10 }
To test this, create a CalculatorTest file in the Tests/Utility directory of your bundle:
1 // src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
Listing Listing
10-5 10-6
2 namespace Acme\DemoBundle\Tests\Utility;
3
4 use Acme\DemoBundle\Utility\Calculator;
5
6 class CalculatorTest extends \PHPUnit_Framework_TestCase
7 {
8 public function testAdd()
9 {
10 $calc = new Calculator();
11 $result = $calc->add(30, 12);
12
13 // assert that our calculator added the numbers correctly!
14 $this->assertEquals(42, $result);
15 }
16 }
By convention, the Tests/ sub-directory should replicate the directory of your bundle. So, if you're
testing a class in your bundle's Utility/ directory, put the test in the Tests/Utility/ directory.
Just like in your real application - autoloading is automatically enabled via the bootstrap.php.cache file
(as configured by default in the phpunit.xml.dist file).
Running tests for a given file or directory is also very easy:
1 #Listing
run all tests in the Utility directory Listing
10-7 10-8
2 $ phpunit -c app src/Acme/DemoBundle/Tests/Utility/
3
Functional Tests
Functional tests check the integration of the different layers of an application (from the routing to the
views). They are no different from unit tests as far as PHPUnit is concerned, but they have a very specific
workflow:
• Make a request;
• Test the response;
• Click on a link or submit a form;
• Test the response;
• Rinse and repeat.
1
Listing Listing // src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
10-9 10-10
2 namespace Acme\DemoBundle\Tests\Controller;
3
4 use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
5
6 class DemoControllerTest extends WebTestCase
7 {
8 public function testIndex()
9 {
10 $client = static::createClient();
11
12 $crawler = $client->request('GET', '/demo/hello/Fabien');
13
14 $this->assertGreaterThan(0, $crawler->filter('html:contains("Hello
15 Fabien")')->count());
16 }
}
To run your functional tests, the WebTestCase class bootstraps the kernel of your application. In
most cases, this happens automatically. However, if your kernel is in a non-standard directory,
you'll need to modify your phpunit.xml.dist file to set the KERNEL_DIR environment variable to
the directory of your kernel:
2. https://github.com/symfony/symfony-standard/blob/master/src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
3 <php>
4 <server name="KERNEL_DIR" value="/path/to/your/app/" />
5 </php>
6 <!-- ... -->
7 </phpunit>
The createClient() method returns a client, which is like a browser that you'll use to crawl your site:
1 $crawler
Listing = $client->request('GET', '/demo/hello/Fabien'); Listing
10-13 10-14
The request() method (see more about the request method) returns a Crawler3 object which can be used
to select elements in the Response, click on links, and submit forms.
The Crawler only works when the response is an XML or an HTML document. To get the raw
content response, call $client->getResponse()->getContent().
Click on a link by first selecting it with the Crawler using either an XPath expression or a CSS selector,
then use the Client to click on it. For example, the following code finds all links with the text Greet, then
selects the second one, and ultimately clicks on it:
1 $link
Listing = $crawler->filter('a:contains("Greet")')->eq(1)->link(); Listing
10-15 10-16
2
3 $crawler = $client->click($link);
Submitting a form is very similar; select a form button, optionally override some form values, and submit
the corresponding form:
1 $form
Listing = $crawler->selectButton('submit')->form(); Listing
10-17 10-18
2
3 // set some values
4 $form['name'] = 'Lucas';
5 $form['form_name[subject]'] = 'Hey there!';
6
7 // submit the form
8 $crawler = $client->submit($form);
The form can also handle uploads and contains methods to fill in different types of form fields (e.g.
select() and tick()). For details, see the Forms section below.
3. http://api.symfony.com/2.0/Symfony/Component/DomCrawler/Crawler.html
Or, test against the Response content directly if you just want to assert that the content contains some
text, or if the Response is not an XML/HTML document:
Listing 1
Listing $this->assertRegExp('/Hello Fabien/', $client->getResponse()->getContent());
10-2110-22
Listing 1
Listingrequest(
10-2310-24
2 $method,
3 $uri,
4 array $parameters = array(),
5 array $files = array(),
6 array $server = array(),
7 $content = null,
8 $changeHistory = true
9 )
The server array is the raw values that you'd expect to normally find in the PHP $_SERVER4
superglobal. For example, to set the Content-Type and Referer HTTP headers, you'd pass the
following:
1
$client->request(
ListingListing
10-25 10-26
2 'GET',
3 '/demo/hello/Fabien',
4 array(),
5 array(),
6 array(
7 'CONTENT_TYPE' => 'application/json',
8 'HTTP_REFERER' => '/foo/bar',
9 )
10 );
4. http://php.net/manual/en/reserved.variables.server.php
1 // Assert that there is more than one h2 tag with the class "subtitle"
Listing Listing
10-27 10-28
2 $this->assertGreaterThan(0, $crawler->filter('h2.subtitle')->count());
3
4 // Assert that there are exactly 4 h2 tags on the page
5 $this->assertCount(4, $crawler->filter('h2'));
6
7 // Assert that the "Content-Type" header is "application/json"
8 $this->assertTrue($client->getResponse()->headers->contains('Content-Type',
9 'application/json'));
10
11 // Assert that the response content matches a regexp.
12 $this->assertRegExp('/foo/', $client->getResponse()->getContent());
13
14 // Assert that the response status code is 2xx
15 $this->assertTrue($client->getResponse()->isSuccessful());
16 // Assert that the response status code is 404
17 $this->assertTrue($client->getResponse()->isNotFound());
18 // Assert a specific 200 status code
19 $this->assertEquals(200, $client->getResponse()->getStatusCode());
20
21 // Assert that the response is a redirect to /demo/contact
22 $this->assertTrue($client->getResponse()->isRedirect('/demo/contact'));
23 // or simply check that the response is a redirect to any URL
$this->assertTrue($client->getResponse()->isRedirect());
1 $crawler
Listing = $client->request('GET', '/hello/Fabien'); Listing
10-29 10-30
The request() method takes the HTTP method and a URL as arguments and returns a Crawler
instance.
Use the Crawler to find DOM elements in the Response. These elements can then be used to click on
links and submit forms:
1 $link
Listing = $crawler->selectLink('Go elsewhere...')->link(); Listing
10-31 10-32
2 $crawler = $client->click($link);
3
4 $form = $crawler->selectButton('validate')->form();
5 $crawler = $client->submit($form, array('name' => 'Fabien'));
You will learn more about the Link and Form objects in the Crawler section below.
The request method can also be used to simulate form submissions directly or perform more complex
requests:
1
Listing Listing // Directly submit a form (but using the Crawler is easier!)
10-33 10-34
2 $client->request('POST', '/submit', array('name' => 'Fabien'));
3
4 // Form submission with a file upload
5 use Symfony\Component\HttpFoundation\File\UploadedFile;
6
7 $photo = new UploadedFile(
8 '/path/to/photo.jpg',
9 'photo.jpg',
10 'image/jpeg',
11 123
12 );
13 // or
14 $photo = array(
15 'tmp_name' => '/path/to/photo.jpg',
16 'name' => 'photo.jpg',
17 'type' => 'image/jpeg',
18 'size' => 123,
19 'error' => UPLOAD_ERR_OK
20 );
21 $client->request(
22 'POST',
23 '/submit',
24 array('name' => 'Fabien'),
25 array('photo' => $photo)
26 );
27
28 // Perform a DELETE requests, and pass HTTP headers
29 $client->request(
30 'DELETE',
31 '/post/12',
32 array(),
33 array(),
34 array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word')
35 );
Last but not least, you can force each request to be executed in its own PHP process to avoid any side-
effects when working with several clients in the same script:
Listing 1
Listing $client->insulate();
10-3510-36
1 $client->back();
Listing Listing
10-37 10-38
2 $client->forward();
3 $client->reload();
4
5 // Clears all cookies and the history
6 $client->restart();
1 $history
Listing = $client->getHistory(); Listing
10-39 10-40
2 $cookieJar = $client->getCookieJar();
You can also get the objects related to the latest request:
1 $request
Listing = $client->getRequest(); Listing
10-41 10-42
2 $response = $client->getResponse();
3 $crawler = $client->getCrawler();
If your requests are not insulated, you can also access the Container and the Kernel:
1 $container
Listing = $client->getContainer(); Listing
10-43 10-44
2 $kernel = $client->getKernel();
1 $container
Listing = $client->getContainer(); Listing
10-45 10-46
Be warned that this does not work if you insulate the client or if you use an HTTP layer. For a list of
services available in your application, use the container:debug console task.
If the information you need to check is available from the profiler, use it instead.
Listing 1
Listing $profile = $client->getProfile();
10-4710-48
For specific details on using the profiler inside a test, see the How to use the Profiler in a Functional Test
cookbook entry.
Redirecting
When a request returns a redirect response, the client does not follow it automatically. You can examine
the response and force a redirection afterwards with the followRedirect() method:
Listing 1
Listing $crawler = $client->followRedirect();
10-4910-50
If you want the client to automatically follow all redirects, you can force him with the
followRedirects() method:
Listing 1
Listing $client->followRedirects();
10-5110-52
The Crawler
A Crawler instance is returned each time you make a request with the Client. It allows you to traverse
HTML documents, select nodes, find links and forms.
Traversing
Like jQuery, the Crawler has methods to traverse the DOM of an HTML/XML document. For example,
the following finds all input[type=submit] elements, selects the last one on the page, and then selects
its immediate parent element:
Listing 1
$newCrawler = $crawler->filter('input[type=submit]')
Listing
10-5310-54
2 ->last()
3 ->parents()
4 ->first()
5 ;
Since each of these methods returns a new Crawler instance, you can narrow down your node selection
by chaining the method calls:
1 $crawler
Listing Listing
10-55 10-56
2 ->filter('h1')
3 ->reduce(function ($node, $i)
4 {
5 if (!$node->getAttribute('class')) {
6 return false;
7 }
8 })
9 ->first();
Use the count() function to get the number of nodes stored in a Crawler: count($crawler)
Extracting Information
The Crawler can extract information from the nodes:
Links
To select links, you can use the traversing methods above or the convenient selectLink() shortcut:
Listing 1
Listing $crawler->selectLink('Click here');
10-5910-60
This selects all links that contain the given text, or clickable images for which the alt attribute contains
the given text. Like the other filtering methods, this returns another Crawler object.
Once you've selected a link, you have access to a special Link object, which has helpful methods specific
to links (such as getMethod() and getUri()). To click on the link, use the Client's click() method and
pass it a Link object:
Listing 1
$link = $crawler->selectLink('Click here')->link();
Listing
10-6110-62
2
3 $client->click($link);
Forms
Just like links, you select forms with the selectButton() method:
Listing 1
Listing $buttonCrawlerNode = $crawler->selectButton('submit');
10-6310-64
Notice that we select form buttons and not forms as a form can have several buttons; if you use the
traversing API, keep in mind that you must look for a button.
The selectButton() method can select button tags and submit input tags. It uses several different parts
of the buttons to find them:
Once you have a Crawler representing a button, call the form() method to get a Form instance for the
form wrapping the button node:
Listing 1
Listing $form = $buttonCrawlerNode->form();
10-6510-66
1 $form
Listing = $buttonCrawlerNode->form(array( Listing
10-67 10-68
2 'name' => 'Fabien',
3 'my_form[subject]' => 'Symfony rocks!',
4 ));
And if you want to simulate a specific HTTP method for the form, pass it as a second argument:
1 $form
Listing = $buttonCrawlerNode->form(array(), 'DELETE'); Listing
10-69 10-70
1 $client->submit($form);
Listing Listing
10-71 10-72
The field values can also be passed as a second argument of the submit() method:
1 $client->submit($form,
Listing array( Listing
10-73 10-74
2 'name' => 'Fabien',
3 'my_form[subject]' => 'Symfony rocks!',
4 ));
For more complex situations, use the Form instance as an array to set the value of each field individually:
There is also a nice API to manipulate the values of the fields according to their type:
You can get the values that will be submitted by calling the getValues() method on the Form
object. The uploaded files are available in a separate array returned by getFiles(). The
getPhpValues() and getPhpFiles() methods also return the submitted values, but in the PHP
Testing Configuration
The Client used by functional tests creates a Kernel that runs in a special test environment. Since
Symfony loads the app/config/config_test.yml in the test environment, you can tweak any of your
application's settings specifically for testing.
For example, by default, the swiftmailer is configured to not actually deliver emails in the test
environment. You can see this under the swiftmailer configuration option:
Listing # app/config/config_test.yml
1
Listing
10-7910-80
2
3 # ...
4
5 swiftmailer:
6 disable_delivery: true
You can also use a different environment entirely, or override the default debug mode (true) by passing
each as options to the createClient() method:
Listing 1
$client = static::createClient(array(
Listing
10-8110-82
2 'environment' => 'my_test_env',
3 'debug' => false,
4 ));
If your application behaves according to some HTTP headers, pass them as the second argument of
createClient():
Listing 1
$client = static::createClient(array(), array(
Listing
10-8310-84
2 'HTTP_HOST' => 'en.example.com',
3 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
4 ));
Listing 1
$client->request('GET', '/', array(), array(), array(
Listing
10-8510-86
2 'HTTP_HOST' => 'en.example.com',
3 'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
4 ));
PHPUnit Configuration
Each application has its own PHPUnit configuration, stored in the phpunit.xml.dist file. You can edit
this file to change the defaults or create a phpunit.xml file to tweak the configuration for your local
machine.
Store the phpunit.xml.dist file in your code repository, and ignore the phpunit.xml file.
By default, only the tests stored in "standard" bundles are run by the phpunit command (standard being
tests in the src/*/Bundle/Tests or src/*/Bundle/*Bundle/Tests directories) But you can easily add
more directories. For instance, the following configuration adds the tests from the installed third-party
bundles:
1 <!--
Listing hello/phpunit.xml.dist --> Listing
10-87 10-88
2 <testsuites>
3 <testsuite name="Project Test Suite">
4 <directory>../src/*/*Bundle/Tests</directory>
5 <directory>../src/Acme/Bundle/*Bundle/Tests</directory>
6 </testsuite>
7 </testsuites>
To include other directories in the code coverage, also edit the <filter> section:
1 <filter>
Listing Listing
10-89 10-90
2 <whitelist>
3 <directory>../src</directory>
4 <exclude>
5 <directory>../src/*/*Bundle/Resources</directory>
6 <directory>../src/*/*Bundle/Tests</directory>
7 <directory>../src/Acme/Bundle/*Bundle/Resources</directory>
8 <directory>../src/Acme/Bundle/*Bundle/Tests</directory>
9 </exclude>
10 </whitelist>
11 </filter>
Validation is a very common task in web applications. Data entered in forms needs to be validated. Data
also needs to be validated before it is written into a database or passed to a web service.
Symfony2 ships with a Validator1 component that makes this task easy and transparent. This component
is based on the JSR303 Bean Validation specification2. What? A Java specification in PHP? You heard
right, but it's not as bad as it sounds. Let's look at how it can be used in PHP.
Listing 1
Listing // src/Acme/BlogBundle/Entity/Author.php
11-1 11-2
2 namespace Acme\BlogBundle\Entity;
3
4 class Author
5 {
6 public $name;
7 }
So far, this is just an ordinary class that serves some purpose inside your application. The goal of
validation is to tell you whether or not the data of an object is valid. For this to work, you'll configure a list
of rules (called constraints) that the object must follow in order to be valid. These rules can be specified
via a number of different formats (YAML, XML, annotations, or PHP).
For example, to guarantee that the $name property is not empty, add the following:
1. https://github.com/symfony/Validator
2. http://jcp.org/en/jsr/detail?id=303
Protected and private properties can also be validated, as well as "getter" methods (see validator-
constraint-targets).
1 // ...
Listing Listing
11-5 11-6
2 use Symfony\Component\HttpFoundation\Response;
3 use Acme\BlogBundle\Entity\Author;
4
5 public function indexAction()
6 {
7 $author = new Author();
8 // ... do something to the $author object
9
10 $validator = $this->get('validator');
11 $errors = $validator->validate($author);
12
13 if (count($errors) > 0) {
14 return new Response(print_r($errors, true));
15 } else {
16 return new Response('The author is valid! Yes!');
17 }
18 }
If the $name property is empty, you will see the following error message:
1 Acme\BlogBundle\Author.name:
Listing Listing
11-7 11-8
2 This value should not be blank
If you insert a value into the name property, the happy success message will appear.
Most of the time, you won't interact directly with the validator service or need to worry about
printing out the errors. Most of the time, you'll use validation indirectly when handling submitted
form data. For more information, see the Validation and Forms.
3. http://api.symfony.com/2.0/Symfony/Component/Validator/Validator.html
Listing 1
if (count($errors) > 0) {
Listing
11-911-10
2 return $this->render('AcmeBlogBundle:Author:validate.html.twig', array(
3 'errors' => $errors,
4 ));
5 } else {
6 // ...
7 }
Inside the template, you can output the list of errors exactly as needed:
Listing 1
Listing {# src/Acme/BlogBundle/Resources/views/Author/validate.html.twig #}
11-1111-12
2 <h3>The author has the following errors</h3>
3 <ul>
4 {% for error in errors %}
5 <li>{{ error.message }}</li>
6 {% endfor %}
7 </ul>
1
Listing Listing // ...
11-13 11-14
2 use Acme\BlogBundle\Entity\Author;
3 use Acme\BlogBundle\Form\AuthorType;
4 use Symfony\Component\HttpFoundation\Request;
5
6 public function updateAction(Request $request)
7 {
8 $author = new Author();
9 $form = $this->createForm(new AuthorType(), $author);
10
11 if ($request->getMethod() == 'POST') {
12 $form->bindRequest($request);
13
14 if ($form->isValid()) {
15 // the validation passed, do something with the $author object
4. http://api.symfony.com/2.0/Symfony/Component/Validator/ConstraintViolation.html
This example uses an AuthorType form class, which is not shown here.
Configuration
The Symfony2 validator is enabled by default, but you must explicitly enable annotations if you're using
the annotation method to specify your constraints:
1 #Listing
app/config/config.yml Listing
11-15 11-16
2 framework:
3 validation: { enable_annotations: true }
Constraints
The validator is designed to validate objects against constraints (i.e. rules). In order to validate an
object, simply map one or more constraints to its class and then pass it to the validator service.
Behind the scenes, a constraint is simply a PHP object that makes an assertive statement. In real life,
a constraint could be: "The cake must not be burned". In Symfony2, constraints are similar: they are
assertions that a condition is true. Given a value, a constraint will tell you whether or not that value
adheres to the rules of the constraint.
Supported Constraints
Symfony2 packages a large number of the most commonly-needed constraints:
Basic Constraints
These are the basic constraints: use them to assert very basic things about the value of properties or the
return value of methods on your object.
• NotBlank
• Blank
• NotNull
• Null
• True
String Constraints
• Email
• MinLength
• MaxLength
• Url
• Regex
• Ip
Number Constraints
• Max
• Min
Date Constraints
• Date
• DateTime
• Time
Collection Constraints
• Choice
• Collection
• UniqueEntity
• Language
• Locale
• Country
File Constraints
• File
• Image
Other Constraints
• Callback
• All
• Valid
You can also create your own custom constraints. This topic is covered in the "How to create a Custom
Validation Constraint" article of the cookbook.
Constraint Configuration
Some constraints, like NotBlank, are simple whereas others, like the Choice constraint, have several
configuration options available. Suppose that the Author class has another property, gender that can be
set to either "male" or "female":
The options of a constraint can always be passed in as an array. Some constraints, however, also allow
you to pass the value of one, "default", option in place of the array. In the case of the Choice constraint,
the choices options can be specified in this way.
1 #Listing
src/Acme/BlogBundle/Resources/config/validation.yml Listing
11-19 11-20
2 Acme\BlogBundle\Entity\Author:
3 properties:
4 gender:
5 - Choice: [male, female]
This is purely meant to make the configuration of the most common option of a constraint shorter and
quicker.
If you're ever unsure of how to specify an option, either check the API documentation for the constraint
or play it safe by always passing in an array of options (the first method shown above).
Constraint Targets
Constraints can be applied to a class property (e.g. name) or a public getter method (e.g. getFullName).
The first is the most common and easy to use, but the second allows you to specify more complex
validation rules.
Properties
Validating class properties is the most basic validation technique. Symfony2 allows you to validate
private, protected or public properties. The next listing shows you how to configure the $firstName
property of an Author class to have at least 3 characters.
1 #Listing
src/Acme/BlogBundle/Resources/config/validation.yml Listing
11-21 11-22
2 Acme\BlogBundle\Entity\Author:
3 properties:
4 firstName:
5 - NotBlank: ~
6 - MinLength: 3
Listing # src/Acme/BlogBundle/Resources/config/validation.yml
1
Listing
11-2311-24
2 Acme\BlogBundle\Entity\Author:
3 getters:
4 passwordLegal:
5 - "True": { message: "The password cannot match your first name" }
Now, create the isPasswordLegal() method, and include the logic you need:
Listing 1
public function isPasswordLegal()
Listing
11-2511-26
2 {
3 return ($this->firstName != $this->password);
4 }
The keen-eyed among you will have noticed that the prefix of the getter ("get" or "is") is omitted
in the mapping. This allows you to move the constraint to a property with the same name later (or
vice versa) without changing your validation logic.
Classes
Some constraints apply to the entire class being validated. For example, the Callback constraint is a
generic constraint that's applied to the class itself. When that class is validated, methods specified by that
constraint are simply executed so that each can provide more custom validation.
Validation Groups
So far, you've been able to add constraints to a class and ask whether or not that class passes all of
the defined constraints. In some cases, however, you'll need to validate an object against only some of
the constraints on that class. To do this, you can organize each constraint into one or more "validation
groups", and then apply validation against just one group of constraints.
For example, suppose you have a User class, which is used both when a user registers and when a user
updates his/her contact information later:
1
Listing Listing # src/Acme/BlogBundle/Resources/config/validation.yml
11-27 11-28
2 Acme\BlogBundle\Entity\User:
3 properties:
4 email:
To tell the validator to use a specific group, pass one or more group names as the second argument to the
validate() method:
1 $errors
Listing = $validator->validate($author, array('registration')); Listing
11-29 11-30
Of course, you'll usually work with validation indirectly through the form library. For information on
how to use validation groups inside forms, see Validation Groups.
Final Thoughts
The Symfony2 validator is a powerful tool that can be leveraged to guarantee that the data of any
object is "valid". The power behind validation lies in "constraints", which are rules that you can apply
to properties or getter methods of your object. And while you'll most commonly use the validation
framework indirectly when using forms, remember that it can be used anywhere to validate any object.
5. http://api.symfony.com/2.0/Symfony/Component/Validator/ConstraintViolationList.html
6. http://api.symfony.com/2.0/Symfony/Component/Validator/ConstraintViolation.html
Dealing with HTML forms is one of the most common - and challenging - tasks for a web developer.
Symfony2 integrates a Form component that makes dealing with forms easy. In this chapter, you'll build
a complex form from the ground-up, learning the most important features of the form library along the
way.
The Symfony form component is a standalone library that can be used outside of Symfony2
projects. For more information, see the Symfony2 Form Component1 on Github.
1 // src/Acme/TaskBundle/Entity/Task.php
Listing Listing
12-1 12-2
2 namespace Acme\TaskBundle\Entity;
3
4 class Task
5 {
6 protected $task;
7
8 protected $dueDate;
9
10 public function getTask()
11 {
12 return $this->task;
13 }
14 public function setTask($task)
1. https://github.com/symfony/Form
If you're coding along with this example, create the AcmeTaskBundle first by running the following
command (and accepting all of the default options):
Listing 1
Listing $ php app/console generate:bundle --namespace=Acme/TaskBundle
12-3 12-4
This class is a "plain-old-PHP-object" because, so far, it has nothing to do with Symfony or any other
library. It's quite simply a normal PHP object that directly solves a problem inside your application (i.e.
the need to represent a task in your application). Of course, by the end of this chapter, you'll be able to
submit data to a Task instance (via an HTML form), validate its data, and persist it to the database.
1
Listing Listing // src/Acme/TaskBundle/Controller/DefaultController.php
12-5 12-6
2 namespace Acme\TaskBundle\Controller;
3
4 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
5 use Acme\TaskBundle\Entity\Task;
6 use Symfony\Component\HttpFoundation\Request;
7
8 class DefaultController extends Controller
9 {
10 public function newAction(Request $request)
11 {
12 // create a task and give it some dummy data for this example
13 $task = new Task();
14 $task->setTask('Write a blog post');
15 $task->setDueDate(new \DateTime('tomorrow'));
16
17 $form = $this->createFormBuilder($task)
18 ->add('task', 'text')
19 ->add('dueDate', 'date')
20 ->getForm();
This example shows you how to build your form directly in the controller. Later, in the "Creating
Form Classes" section, you'll learn how to build your form in a standalone class, which is
recommended as your form becomes reusable.
Creating a form requires relatively little code because Symfony2 form objects are built with a "form
builder". The form builder's purpose is to allow you to write simple form "recipes", and have it do all the
heavy-lifting of actually building the form.
In this example, you've added two fields to your form - task and dueDate - corresponding to the task
and dueDate properties of the Task class. You've also assigned each a "type" (e.g. text, date), which,
among other things, determines which HTML form tag(s) is rendered for that field.
Symfony2 comes with many built-in types that will be discussed shortly (see Built-in Field Types).
1 {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
Listing Listing
12-7 12-8
2 <form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
3 {{ form_widget(form) }}
4
5 <input type="submit" />
6 </form>
This example assumes that you've created a route called task_new that points to the
AcmeTaskBundle:Default:new controller that was created earlier.
That's it! By printing form_widget(form), each field in the form is rendered, along with a label and error
message (if there is one). As easy as this is, it's not very flexible (yet). Usually, you'll want to render
The form system is smart enough to access the value of the protected task property via the
getTask() and setTask() methods on the Task class. Unless a property is public, it must have a
"getter" and "setter" method so that the form component can get and put data onto the property.
For a Boolean property, you can use an "isser" method (e.g. isPublished()) instead of a getter
(e.g. getPublished()).
// ...
1
Listing Listing
12-9 12-10
2
3 public function newAction(Request $request)
4 {
5 // just setup a fresh $task object (remove the dummy data)
6 $task = new Task();
7
8 $form = $this->createFormBuilder($task)
9 ->add('task', 'text')
10 ->add('dueDate', 'date')
11 ->getForm();
12
13 if ($request->getMethod() == 'POST') {
14 $form->bindRequest($request);
15
16 if ($form->isValid()) {
17 // perform some action, such as saving the task to the database
18
19 return $this->redirect($this->generateUrl('task_success'));
20 }
21 }
22
23 // ...
24 }
Now, when submitting the form, the controller binds the submitted data to the form, which translates
that data back to the task and dueDate properties of the $task object. This all happens via the
bindRequest() method.
As soon as bindRequest() is called, the submitted data is transferred to the underlying object
immediately. This happens regardless of whether or not the underlying data is actually valid.
This controller follows a common pattern for handling forms, and has three possible paths:
Redirecting a user after a successful form submission prevents the user from being able to hit
"refresh" and re-post the data.
Form Validation
In the previous section, you learned how a form can be submitted with valid or invalid data. In Symfony2,
validation is applied to the underlying object (e.g. Task). In other words, the question isn't whether the
"form" is valid, but whether or not the $task object is valid after the form has applied the submitted data
to it. Calling $form->isValid() is a shortcut that asks the $task object whether or not it has valid data.
Validation is done by adding a set of rules (called constraints) to a class. To see this in action, add
validation constraints so that the task field cannot be empty and the dueDate field cannot be empty and
must be a valid DateTime object.
1 #Listing
Acme/TaskBundle/Resources/config/validation.yml Listing
12-11 12-12
2 Acme\TaskBundle\Entity\Task:
3 properties:
4 task:
5 - NotBlank: ~
6 dueDate:
7 - NotBlank: ~
8 - Type: \DateTime
That's it! If you re-submit the form with invalid data, you'll see the corresponding errors printed out with
the form.
HTML5 Validation
As of HTML5, many browsers can natively enforce certain validation constraints on the client
side. The most common validation is activated by rendering a required attribute on fields that
are required. For browsers that support HTML5, this will result in a native browser message being
displayed if the user tries to submit the form with that field blank.
Generated forms take full advantage of this new feature by adding sensible HTML attributes
that trigger the validation. The client-side validation, however, can be disabled by adding the
novalidate attribute to the form tag or formnovalidate to the submit tag. This is especially useful
when you want to test your server-side validation constraints, but are being prevented by your
browser from, for example, submitting blank fields.
Validation is a very powerful feature of Symfony2 and has its own dedicated chapter.
If you're not using validation groups, then you can skip this section.
If your object takes advantage of validation groups, you'll need to specify which validation group(s) your
form should use:
Listing 1
$form = $this->createFormBuilder($users, array(
Listing
12-1312-14
2 'validation_groups' => array('registration'),
3 ))->add(...);
If you're creating form classes (a good practice), then you'll need to add the following to the
getDefaultOptions() method:
Listing 1
public function getDefaultOptions(array $options)
Listing
12-1512-16
2 {
3 return array(
4 'validation_groups' => array('registration')
5 );
6 }
In both of these cases, only the registration validation group will be used to validate the underlying
object.
Text Fields
• text
• textarea
• email
• integer
• money
• number
• password
• percent
• search
• url
Choice Fields
• choice
• entity
• country
Other Fields
• checkbox
• file
• radio
Field Groups
• collection
• repeated
Hidden Fields
• hidden
• csrf
Base Fields
• field
• form
You can also create your own custom field types. This topic is covered in the "How to Create a Custom
Form Field Type" article of the cookbook.
1 ->add('dueDate',
Listing 'date', array('widget' => 'single_text')) Listing
12-17 12-18
Each field type has a number of different options that can be passed to it. Many of these are specific to
the field type and details can be found in the documentation for each type.
Listing 1
Listing->add('dueDate', 'date', array(
12-1912-20
2 'widget' => 'single_text',
3 'label' => 'Due Date',
4 ))
The label for a field can also be set in the template rendering the form, see below.
Listing 1
public function newAction()
Listing
12-2112-22
2 {
3 $task = new Task();
4
5 $form = $this->createFormBuilder($task)
6 ->add('task')
7 ->add('dueDate', null, array('widget' => 'single_text'))
8 ->getForm();
9 }
The "guessing" is activated when you omit the second argument to the add() method (or if you pass null
to it). If you pass an options array as the third argument (done for dueDate above), these options are
applied to the guessed field.
When these options are set, the field will be rendered with special HTML attributes that provide
for HTML5 client-side validation. However, it doesn't generate the equivalent server-side
constraints (e.g. Assert\MaxLength). And though you'll need to manually add your server-side
validation, these field type options can then be guessed from that information.
• required: The required option can be guessed based on the validation rules (i.e. is the field
NotBlank or NotNull) or the Doctrine metadata (i.e. is the field nullable). This is very useful,
as your client-side validation will automatically match your validation rules.
• max_length: If the field is some sort of text field, then the max_length option can be guessed
from the validation constraints (if MaxLength or Max is used) or from the Doctrine metadata
(via the field's length).
These field options are only guessed if you're using Symfony to guess the field type (i.e. omit or
pass null as the second argument to add()).
If you'd like to change one of the guessed values, you can override it by passing the option in the options
field array:
1 ->add('task',
Listing null, array('max_length' => 4)) Listing
12-23 12-24
1 {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
Listing Listing
12-25 12-26
2 <form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
3 {{ form_errors(form) }}
4
5 {{ form_row(form.task) }}
6 {{ form_row(form.dueDate) }}
7
8 {{ form_rest(form) }}
9
10 <input type="submit" />
11 </form>
• form_enctype(form) - If at least one field is a file upload field, this renders the obligatory
enctype="multipart/form-data";
• form_errors(form) - Renders any errors global to the whole form (field-specific errors are
displayed next to each field);
• form_row(form.dueDate) - Renders the label, any errors, and the HTML form widget for the
given field (e.g. dueDate) inside, by default, a div element;
• form_rest(form) - Renders any fields that have not yet been rendered. It's usually a good idea
to place a call to this helper at the bottom of each form (in case you forgot to output a field
or don't want to bother manually rendering hidden fields). This helper is also useful for taking
advantage of the automatic CSRF Protection.
The majority of the work is done by the form_row helper, which renders the label, errors and HTML
form widget of each field inside a div tag by default. In the Form Theming section, you'll learn how the
form_row output can be customized on many different levels.
You can access the current data of your form via form.vars.value:
Listing 1
Listing {{ form.vars.value.task }}
12-2712-28
1
Listing Listing {{ form_errors(form) }}
12-29 12-30
2
3 <div>
4 {{ form_label(form.task) }}
5 {{ form_errors(form.task) }}
6 {{ form_widget(form.task) }}
7 </div>
8
9 <div>
10 {{ form_label(form.dueDate) }}
11 {{ form_errors(form.dueDate) }}
12 {{ form_widget(form.dueDate) }}
13 </div>
14
15 {{ form_rest(form) }}
If the auto-generated label for a field isn't quite right, you can explicitly specify it:
Listing 1
Listing {{ form_label(form.task, 'Task Description') }}
12-3112-32
If you need to render form fields "by hand" then you can access individual values for fields such as the
id, name and label. For example to get the id:
1 {{ form.task.vars.id }}
Listing Listing
12-35 12-36
To get the value used for the form field's name attribute you need to use the full_name value:
1 {{ form.task.vars.full_name }}
Listing Listing
12-37 12-38
1 // src/Acme/TaskBundle/Form/Type/TaskType.php
Listing Listing
12-39 12-40
2 namespace Acme\TaskBundle\Form\Type;
3
4 use Symfony\Component\Form\AbstractType;
5 use Symfony\Component\Form\FormBuilder;
6
7 class TaskType extends AbstractType
8 {
9 public function buildForm(FormBuilder $builder, array $options)
10 {
11 $builder->add('task');
12 $builder->add('dueDate', null, array('widget' => 'single_text'));
13 }
14
15 public function getName()
16 {
17 return 'task';
18 }
19 }
1
Listing Listing // src/Acme/TaskBundle/Controller/DefaultController.php
12-41 12-42
2
3 // add this new use statement at the top of the class
4 use Acme\TaskBundle\Form\Type\TaskType;
5
6 public function newAction()
7 {
8 $task = ...;
9 $form = $this->createForm(new TaskType(), $task);
10
11 // ...
12 }
Placing the form logic into its own class means that the form can be easily reused elsewhere in your
project. This is the best way to create forms, but the choice is ultimately up to you.
Listing 1
Listingpublic function getDefaultOptions(array $options)
12-4312-44
2 {
3 return array(
4 'data_class' => 'Acme\TaskBundle\Entity\Task',
5 );
6 }
When mapping forms to objects, all fields are mapped. Any fields on the form that do not exist on
the mapped object will cause an exception to be thrown.
In cases where you need extra fields in the form (for example: a "do you agree with these terms"
checkbox) that will not be mapped to the underlying object, you need to set the property_path
option to false:
Listing 1
Listingpublic function buildForm(FormBuilder $builder, array $options)
12-4512-46
2 {
3 $builder->add('task');
4 $builder->add('dueDate', null, array('property_path' => false));
5 }
1 $form->get('dueDate')->getData();
Listing Listing
12-47 12-48
1 if ($form->isValid()) {
Listing Listing
12-49 12-50
2 $em = $this->getDoctrine()->getEntityManager();
3 $em->persist($task);
4 $em->flush();
5
6 return $this->redirect($this->generateUrl('task_success'));
7 }
If, for some reason, you don't have access to your original $task object, you can fetch it from the form:
1 $task
Listing = $form->getData(); Listing
12-51 12-52
Embedded Forms
Often, you'll want to build a form that will include fields from many different objects. For example,
a registration form may contain data belonging to a User object as well as many Address objects.
Fortunately, this is easy and natural with the form component.
// ...
1
Listing Listing
12-55 12-56
2
3 class Task
4 {
5 // ...
6
7 /**
8 * @Assert\Type(type="Acme\TaskBundle\Entity\Category")
9 */
10 protected $category;
11
12 // ...
13
14 public function getCategory()
15 {
16 return $this->category;
17 }
18
19 public function setCategory(Category $category = null)
20 {
21 $this->category = $category;
22 }
23 }
Now that your application has been updated to reflect the new requirements, create a form class so that
a Category object can be modified by the user:
1
Listing Listing // src/Acme/TaskBundle/Form/Type/CategoryType.php
12-57 12-58
2 namespace Acme\TaskBundle\Form\Type;
3
4 use Symfony\Component\Form\AbstractType;
5 use Symfony\Component\Form\FormBuilder;
6
7 class CategoryType extends AbstractType
8 {
9 public function buildForm(FormBuilder $builder, array $options)
10 {
11 $builder->add('name');
The end goal is to allow the Category of a Task to be modified right inside the task form itself. To
accomplish this, add a category field to the TaskType object whose type is an instance of the new
CategoryType class:
1 public
Listing function buildForm(FormBuilder $builder, array $options) Listing
12-59 12-60
2 {
3 // ...
4
5 $builder->add('category', new CategoryType());
6 }
The fields from CategoryType can now be rendered alongside those from the TaskType class. Render the
Category fields in the same way as the original Task fields:
1 {# ... #}
Listing Listing
12-61 12-62
2
3 <h3>Category</h3>
4 <div class="category">
5 {{ form_row(form.category.name) }}
6 </div>
7
8 {{ form_rest(form) }}
9 {# ... #}
When the user submits the form, the submitted data for the Category fields are used to construct an
instance of Category, which is then set on the category field of the Task instance.
The Category instance is accessible naturally via $task->getCategory() and can be persisted to the
database or used however you need.
1
Listing Listing {# src/Acme/TaskBundle/Resources/views/Form/fields.html.twig #}
12-63 12-64
2 {% block field_row %}
3 {% spaceless %}
4 <div class="form_row">
5 {{ form_label(form) }}
6 {{ form_errors(form) }}
7 {{ form_widget(form) }}
8 </div>
9 {% endspaceless %}
10 {% endblock field_row %}
The field_row form fragment is used when rendering most fields via the form_row function. To tell the
form component to use your new field_row fragment defined above, add the following to the top of the
template that renders the form:
Listing {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
12-65
{% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' %}
<form ...>
The form_theme tag (in Twig) "imports" the fragments defined in the given template and uses them when
rendering the form. In other words, when the form_row function is called later in this template, it will use
the field_row block from your custom theme (instead of the default field_row block that ships with
Symfony).
Your custom theme does not have to override all the blocks. When rendering a block which is not
overridden in your custom theme, the theming engine will fall back to the global theme (defined at the
bundle level).
If several custom themes are provided they will be searched in the listed order before falling back to the
global theme.
To customize any portion of a form, you just need to override the appropriate fragment. Knowing exactly
which block or file to override is the subject of the next section.
For a more extensive discussion, see How to customize Form Rendering.
Each fragment follows the same basic pattern: type_part. The type portion corresponds to the field type
being rendered (e.g. textarea, checkbox, date, etc) whereas the part portion corresponds to what is
being rendered (e.g. label, widget, errors, etc). By default, there are 4 possible parts of a form that can
be rendered:
There are actually 3 other parts - rows, rest, and enctype - but you should rarely if ever need to
worry about overriding them.
By knowing the field type (e.g. textarea) and which part you want to customize (e.g. widget), you can
construct the fragment name that needs to be overridden (e.g. textarea_widget).
2. https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
3. https://github.com/symfony/symfony/tree/master/src/Symfony/Bridge/Twig
4. https://github.com/symfony/symfony/tree/master/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form
Twig
To automatically include the customized blocks from the fields.html.twig template created earlier in
all templates, modify your application configuration file:
Listing # app/config/config.yml
1
Listing
12-6612-67
2 twig:
3 form:
4 resources:
5 - 'AcmeTaskBundle:Form:fields.html.twig'
6 # ...
Any blocks inside the fields.html.twig template are now used globally to define form output.
1 {% extends '::base.html.twig' %}
Listing Listing
12-68 12-69
2
3 {# import "_self" as the form theme #}
4 {% form_theme form _self %}
5
6 {# make the form fragment customization #}
7 {% block field_row %}
8 {# custom field row output #}
9 {% endblock field_row %}
10
11 {% block content %}
12 {# ... #}
13
14 {{ form_row(form.task) }}
15 {% endblock %}
The {% form_theme form _self %} tag allows form blocks to be customized directly inside
the template that will use those customizations. Use this method to quickly make form output
customizations that will only ever be needed in a single template.
This {% form_theme form _self %} functionality will only work if your template extends
another. If your template does not, you must point form_theme to a separate template.
PHP
To automatically include the customized templates from the Acme/TaskBundle/Resources/views/Form
directory created earlier in all templates, modify your application configuration file:
1 #Listing
app/config/config.yml Listing
12-70 12-71
2 framework:
3 templating:
4 form:
5 resources:
6 - 'AcmeTaskBundle:Form'
7 # ...
Any fragments inside the Acme/TaskBundle/Resources/views/Form directory are now used globally to
define form output.
1
class TaskType extends AbstractType
Listing Listing
12-72 12-73
2 {
3 // ...
4
5 public function getDefaultOptions(array $options)
6 {
7 return array(
8 'data_class' => 'Acme\TaskBundle\Entity\Task',
9 'csrf_protection' => true,
10 'csrf_field_name' => '_token',
11 // a unique key to help generate the secret token
12 'intention' => 'task_item',
13 );
14 }
15
16 // ...
17 }
To disable CSRF protection, set the csrf_protection option to false. Customizations can also be made
globally in your project. For more information, see the form configuration reference section.
The intention option is optional but greatly enhances the security of the generated token by
making it different for each form.
5. http://en.wikipedia.org/wiki/Cross-site_request_forgery
By default, a form actually assumes that you want to work with arrays of data, instead of an object. There
are exactly two ways that you can change this behavior and tie the form to an object instead:
1. Pass an object when creating the form (as the first argument to createFormBuilder or the
second argument to createForm);
2. Declare the data_class option on your form.
If you don't do either of these, then the form will return the data as an array. In this example, since
$defaultData is not an object (and no data_class option is set), $form->getData() ultimately returns
an array.
You can also access POST values (in this case "name") directly through the request object, like so:
1 $this->get('request')->request->get('name');
Listing Listing
12-76 12-77
Be advised, however, that in most cases using the getData() method is a better choice, since it
returns the data (usually an object) after it's been transformed by the form framework.
Adding Validation
The only missing piece is validation. Usually, when you call $form->isValid(), the object is validated
by reading the constraints that you applied to that class. But without a class, how can you add constraints
to the data of your form?
The answer is to setup the constraints yourself, and pass them into your form. The overall approach is
covered a bit more in the validation chapter, but here's a short example:
Now, when you call $form->bindRequest($request), the constraints setup here are run against your form's
data. If you're using a form class, override the getDefaultOptions method to specify the option:
1
Listing Listing namespace Acme\TaskBundle\Form\Type;
12-80 12-81
2
3 use Symfony\Component\Form\AbstractType;
4 use Symfony\Component\Form\FormBuilder;
5 use Symfony\Component\Validator\Constraints\Email;
6 use Symfony\Component\Validator\Constraints\MinLength;
7 use Symfony\Component\Validator\Constraints\Collection;
8
9 class ContactType extends AbstractType
10 {
11 // ...
12
13 public function getDefaultOptions(array $options)
14 {
15 $collectionConstraint = new Collection(array(
16 'name' => new MinLength(5),
17 'email' => new Email(array('message' => 'Invalid email address')),
18 ));
19
20 return array('validation_constraint' => $collectionConstraint);
21 }
22 }
Now, you have the flexibility to create forms - with validation - that return an array of data, instead of
an object. In most cases, it's better - and certainly more robust - to bind your form to an object. But for
simple forms, this is a great approach.
Final Thoughts
You now know all of the building blocks necessary to build complex and functional forms for your
application. When building forms, keep in mind that the first goal of a form is to translate data from an
object (Task) to an HTML form so that the user can modify that data. The second goal of a form is to
take the data submitted by the user and to re-apply it to the object.
There's still much more to learn about the powerful world of forms, such as how to handle file uploads
with Doctrine or how to create a form where a dynamic number of sub-forms can be added (e.g. a todo
Security is a two-step process whose goal is to prevent a user from accessing a resource that he/she should
not have access to.
In the first step of the process, the security system identifies who the user is by requiring the user to
submit some sort of identification. This is called authentication, and it means that the system is trying
to find out who you are.
Once the system knows who you are, the next step is to determine if you should have access to a given
resource. This part of the process is called authorization, and it means that the system is checking to see
if you have privileges to perform a certain action.
Since the best way to learn is to see an example, let's dive right in.
Symfony's security component1 is available as a standalone PHP library for use inside any PHP
project.
1 #Listing
app/config/security.yml Listing
13-1 13-2
2 security:
3 firewalls:
4 secured_area:
5 pattern: ^/
6 anonymous: ~
7 http_basic:
8 realm: "Secured Demo Area"
9
10 access_control:
11 - { path: ^/admin, roles: ROLE_ADMIN }
12
13 providers:
14 in_memory:
15 users:
16 ryan: { password: ryanpass, roles: 'ROLE_USER' }
17 admin: { password: kitten, roles: 'ROLE_ADMIN' }
18
19 encoders:
20 Symfony\Component\Security\Core\User\User: plaintext
A standard Symfony distribution separates the security configuration into a separate file (e.g. app/
config/security.yml). If you don't have a separate security file, you can put the configuration
directly into your main config file (e.g. app/config/config.yml).
The end result of this configuration is a fully-functional security system that looks like the following:
Let's look briefly at how security works and how each part of the configuration comes into play.
1. https://github.com/symfony/Security
This works first because the firewall allows anonymous users via the anonymous configuration parameter.
In other words, the firewall doesn't require the user to fully authenticate immediately. And because no
special role is needed to access /foo (under the access_control section), the request can be fulfilled
without ever asking the user to authenticate.
If you remove the anonymous key, the firewall will always make a user fully authenticate immediately.
When Symfony denies the user access, the user sees an error screen and receives a 403 HTTP status
code (Forbidden). You can customize the access denied error screen by following the directions in
the Error Pages cookbook entry to customize the 403 error page.
Finally, if the admin user requests /admin/foo, a similar process takes place, except now, after being
authenticated, the access control layer will let the request pass through:
The exact process actually depends a little bit on which authentication mechanism you're using.
For example, when using form login, the user submits its credentials to one URL that processes the
form (e.g. /login_check) and then is redirected back to the originally requested URL (e.g. /admin/
foo). But with HTTP authentication, the user submits its credentials directly to the original URL
(e.g. /admin/foo) and then the page is returned to the user in that same request (i.e. no redirect).
These types of idiosyncrasies shouldn't cause you any problems, but they're good to keep in mind.
You'll also learn later how anything can be secured in Symfony2, including specific controllers,
objects, or even PHP methods.
In this section, you'll learn how to create a basic login form that continues to use the hard-coded
users that are defined in the security.yml file.
So far, you've seen how to blanket your application beneath a firewall and then protect access to certain
areas with roles. By using HTTP Authentication, you can effortlessly tap into the native username/
password box offered by all browsers. However, Symfony supports many authentication mechanisms out
of the box. For details on all of them, see the Security Configuration Reference.
In this section, you'll enhance this process by allowing the user to authenticate via a traditional HTML
login form.
First, enable form login under your firewall:
Listing # app/config/security.yml
1
Listing
13-3 13-4
2 security:
3 firewalls:
4 secured_area:
5 pattern: ^/
6 anonymous: ~
7 form_login:
8 login_path: /login
9 check_path: /login_check
If you don't need to customize your login_path or check_path values (the values used here are
the default values), you can shorten your configuration:
Listing 1
Listing form_login: ~
13-5 13-6
Now, when the security system initiates the authentication process, it will redirect the user to the login
form (/login by default). Implementing this login form visually is your job. First, create two routes: one
that will display the login form (i.e. /login) and one that will handle the login form submission (i.e.
/login_check):
Listing # app/config/routing.yml
1
Listing
13-7 13-8
2 login:
3 pattern: /login
4 defaults: { _controller: AcmeSecurityBundle:Security:login }
5 login_check:
6 pattern: /login_check
You will not need to implement a controller for the /login_check URL as the firewall will
automatically catch and process any form submitted to this URL. It's optional, but helpful, to
create a route so that you can use it to generate the form submission URL in the login template
below.
1 // src/Acme/SecurityBundle/Controller/SecurityController.php;
Listing Listing
13-9 13-10
2 namespace Acme\SecurityBundle\Controller;
3
4 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
5 use Symfony\Component\Security\Core\SecurityContext;
6
7 class SecurityController extends Controller
8 {
9 public function loginAction()
10 {
11 $request = $this->getRequest();
12 $session = $request->getSession();
13
14 // get the login error if there is one
15 if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
16 $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
17 } else {
18 $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
19 $session->remove(SecurityContext::AUTHENTICATION_ERROR);
20 }
21
22 return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
23 // last username entered by the user
24 'last_username' => $session->get(SecurityContext::LAST_USERNAME),
25 'error' => $error,
26 ));
27 }
28 }
Don't let this controller confuse you. As you'll see in a moment, when the user submits the form, the
security system automatically handles the form submission for you. If the user had submitted an invalid
username or password, this controller reads the form submission error from the security system so that it
can be displayed back to the user.
In other words, your job is to display the login form and any login errors that may have occurred, but the
security system itself takes care of checking the submitted username and password and authenticating
the user.
Finally, create the corresponding template:
1 {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
Listing Listing
13-11 13-12
2 {% if error %}
3 <div>{{ error.message }}</div>
4 {% endif %}
5
6 <form action="{{ path('login_check') }}" method="post">
7 <label for="username">Username:</label>
8 <input type="text" id="username" name="_username" value="{{ last_username }}" />
9
10 <label for="password">Password:</label>
11 <input type="password" id="password" name="_password" />
The error variable passed into the template is an instance of AuthenticationException2. It may
contain more information - or even sensitive information - about the authentication failure, so use
it wisely!
The form has very few requirements. First, by submitting the form to /login_check (via the
login_check route), the security system will intercept the form submission and process the form for
you automatically. Second, the security system expects the submitted fields to be called _username and
_password (these field names can be configured).
And that's it! When you submit the form, the security system will automatically check the user's
credentials and either authenticate the user or send the user back to the login form where the error can
be displayed.
Let's review the whole process:
1. The user tries to access a resource that is protected;
2. The firewall initiates the authentication process by redirecting the user to the login form
(/login);
3. The /login page renders login form via the route and controller created in this example;
4. The user submits the login form to /login_check;
5. The security system intercepts the request, checks the user's submitted credentials,
authenticates the user if they are correct, and sends the user back to the login form if they are
not.
By default, if the submitted credentials are correct, the user will be redirected to the original page that
was requested (e.g. /admin/foo). If the user originally went straight to the login page, he'll be redirected
to the homepage. This can be highly customized, allowing you to, for example, redirect the user to a
specific URL.
For more details on this and how to customize the form login process in general, see How to customize
your Form Login.
2. http://api.symfony.com/2.0/Symfony/Component/Security/Core/Exception/AuthenticationException.html
1 access_control:
Listing Listing
2 13-13 - { path: ^/, roles: ROLE_ADMIN } 13-14
Removing the access control on the /login URL fixes the problem:
1 access_control:
Listing Listing
2 13-15 - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } 13-16
Also, if your firewall does not allow for anonymous users, you'll need to create a special firewall
that allows anonymous users for the login page:
1 firewalls:
Listing Listing
2 13-17 login_firewall: 13-18
3 pattern: ^/login$
4 anonymous: ~
5 secured_area:
6 pattern: ^/
7 form_login: ~
Listing # app/config/security.yml
1
Listing
13-1913-20
2 security:
3 # ...
4 access_control:
5 - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
6 - { path: ^/admin, roles: ROLE_ADMIN }
Prepending the path with ^ ensures that only URLs beginning with the pattern are matched. For
example, a path of simply /admin (without the ^) would correctly match /admin/foo but would
also match URLs like /foo/admin.
For each incoming request, Symfony2 tries to find a matching access control rule (the first one wins).
If the user isn't authenticated yet, the authentication process is initiated (i.e. the user is given a chance
to login). However, if the user is authenticated but doesn't have the required role, an
AccessDeniedException3 exception is thrown, which you can handle and turn into a nice "access
denied" error page for the user. See How to customize Error Pages for more information.
Since Symfony uses the first access control rule it matches, a URL like /admin/users/new will match the
first rule and require only the ROLE_SUPER_ADMIN role. Any URL like /admin/blog will match the second
rule and require ROLE_ADMIN.
Securing by IP
Certain situations may arise when you may need to restrict access to a given route based on IP. This is
particularly relevant in the case of Edge Side Includes (ESI), for example, which utilize a route named
"_internal". When ESI is used, the _internal route is required by the gateway cache to enable different
caching options for subsections within a given page. This route comes with the ^/_internal prefix by
default in the standard edition (assuming you've uncommented those lines from the routing file).
3. http://api.symfony.com/2.0/Symfony/Component/Security/Core/Exception/AccessDeniedException.html
1 #Listing
app/config/security.yml Listing
13-21 13-22
2 security:
3 # ...
4 access_control:
5 - { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
Securing by Channel
Much like securing based on IP, requiring the use of SSL is as simple as adding a new access_control
entry:
1 #Listing
app/config/security.yml Listing
13-23 13-24
2 security:
3 # ...
4 access_control:
5 - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel:
https }
Securing a Controller
Protecting your application based on URL patterns is easy, but may not be fine-grained enough in certain
cases. When necessary, you can easily force authorization from inside a controller:
1 // ...
Listing Listing
13-25 13-26
2 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
3
4 public function helloAction($name)
5 {
6 if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
7 throw new AccessDeniedException();
8 }
9
10 // ...
11 }
You can also choose to install and use the optional JMSSecurityExtraBundle, which can secure your
controller using annotations:
1 // ...
Listing Listing
13-27 13-28
2 use JMS\SecurityExtraBundle\Annotation\Secure;
3
4 /**
5 * @Secure(roles="ROLE_ADMIN")
6 */
7 public function helloAction($name)
8 {
For more information, see the JMSSecurityExtraBundle4 documentation. If you're using Symfony's
Standard Distribution, this bundle is available by default. If not, you can easily download and install it.
Users
In the previous sections, you learned how you can protect different resources by requiring a set of roles
for a resource. In this section we'll explore the other side of authorization: users.
4. https://github.com/schmittjoh/JMSSecurityExtraBundle
This user provider is called the "in-memory" user provider, since the users aren't stored anywhere in a
database. The actual user object is provided by Symfony (User5).
Any user provider can load users directly from configuration by specifying the users configuration
parameter and listing the users beneath it.
If your username is completely numeric (e.g. 77) or contains a dash (e.g. user-name), you should
use that alternative syntax when specifying users in YAML:
1 users:
Listing Listing
2 13-31 - { name: 77, password: pass, roles: 'ROLE_USER' } 13-32
For smaller sites, this method is quick and easy to setup. For more complex systems, you'll want to load
your users from the database.
1 // src/Acme/UserBundle/Entity/User.php
Listing Listing
13-33 13-34
2 namespace Acme\UserBundle\Entity;
3
4 use Symfony\Component\Security\Core\User\UserInterface;
5 use Doctrine\ORM\Mapping as ORM;
6
7 /**
8 * @ORM\Entity
9 */
10 class User implements UserInterface
11 {
12 /**
13 * @ORM\Column(type="string", length=255)
14 */
15 protected $username;
5. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/User.html
As far as the security system is concerned, the only requirement for your custom user class is that it
implements the UserInterface6 interface. This means that your concept of a "user" can be anything, as
long as it implements this interface.
The user object will be serialized and saved in the session during requests, therefore it is
recommended that you implement the Serializable interface7 in your user object. This is especially
important if your User class has a parent class with private properties.
Next, configure an entity user provider, and point it to your User class:
Listing # app/config/security.yml
1
Listing
13-3513-36
2 security:
3 providers:
4 main:
5 entity: { class: Acme\UserBundle\Entity\User, property: username }
With the introduction of this new provider, the authentication system will attempt to load a User object
from the database by using the username field of that class.
This example is just meant to show you the basic idea behind the entity provider. For a full
working example, see How to load Security Users from the Database (the Entity Provider).
For more information on creating your own custom provider (e.g. if you needed to load users via a web
service), see How to create a custom User Provider.
1
Listing Listing # app/config/security.yml
13-37 13-38
2 security:
3 # ...
4 providers:
5 in_memory:
6 users:
7 ryan: { password: bb87a29949f3a1ee0559f8a57357487151281386, roles:
8 'ROLE_USER' }
6. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserInterface.html
7. http://php.net/manual/en/class.serializable.php
By setting the iterations to 1 and the encode_as_base64 to false, the password is simply run through
the sha1 algorithm one time and without any extra encoding. You can now calculate the hashed
password either programmatically (e.g. hash('sha1', 'ryanpass')) or via some online tool like
functions-online.com8
If you're creating your users dynamically (and storing them in a database), you can use even tougher
hashing algorithms and then rely on an actual password encoder object to help you encode passwords.
For example, suppose your User object is Acme\UserBundle\Entity\User (like in the above example).
First, configure the encoder for that user:
1 #Listing
app/config/security.yml Listing
13-39 13-40
2 security:
3 # ...
4
5 encoders:
6 Acme\UserBundle\Entity\User: sha512
In this case, you're using the stronger sha512 algorithm. Also, since you've simply specified the algorithm
(sha512) as a string, the system will default to hashing your password 5000 times in a row and then
encoding it as base64. In other words, the password has been greatly obfuscated so that the hashed
password can't be decoded (i.e. you can't determine the password from the hashed password).
If you have some sort of registration form for users, you'll need to be able to determine the hashed
password so that you can set it on your user. No matter what algorithm you configure for your user
object, the hashed password can always be determined in the following way from a controller:
1 $factory
Listing = $this->get('security.encoder_factory'); Listing
13-41 13-42
2 $user = new Acme\UserBundle\Entity\User();
3
4 $encoder = $factory->getEncoder($user);
5 $password = $encoder->encodePassword('ryanpass', $user->getSalt());
6 $user->setPassword($password);
8. http://www.functions-online.com/sha1.html
Anonymous users are technically authenticated, meaning that the isAuthenticated() method of
an anonymous user object will return true. To check if your user is actually authenticated, check
for the IS_AUTHENTICATED_FULLY role.
In a Twig Template this object can be accessed via the app.user key, which calls the
GlobalVariables::getUser()9 method:
Listing 1
Listing <p>Username: {{ app.user.username }}</p>
13-4513-46
# app/config/security.yml
1
Listing Listing
13-47 13-48
2 security:
3 providers:
4 chain_provider:
5 providers: [in_memory, user_db]
6 in_memory:
7 users:
8 foo: { password: test }
9 user_db:
10 entity: { class: Acme\UserBundle\Entity\User, property: username }
Now, all authentication mechanisms will use the chain_provider, since it's the first specified. The
chain_provider will, in turn, try to load the user from both the in_memory and user_db providers.
If you have no reasons to separate your in_memory users from your user_db users, you can
accomplish this even more easily by combining the two sources into a single provider:
Listing 1
Listing# app/config/security.yml
13-4913-50
2 security:
3 providers:
4 main_provider:
5 users:
9. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.html#getUser()
You can also configure the firewall or individual authentication mechanisms to use a specific provider.
Again, unless a provider is specified explicitly, the first provider is always used:
1 #Listing
app/config/security.yml Listing
13-51 13-52
2 security:
3 firewalls:
4 secured_area:
5 # ...
6 provider: user_db
7 http_basic:
8 realm: "Secured Demo Area"
9 provider: in_memory
10 form_login: ~
In this example, if a user tries to login via HTTP authentication, the authentication system will use the
in_memory user provider. But if the user tries to login via the form login, the user_db provider will be
used (since it's the default for the firewall as a whole).
For more information about user provider and firewall configuration, see the Security Configuration
Reference.
Roles
The idea of a "role" is key to the authorization process. Each user is assigned a set of roles and then each
resource requires one or more roles. If the user has the required roles, access is granted. Otherwise access
is denied.
Roles are pretty simple, and are basically strings that you can invent and use as needed (though roles
are objects internally). For example, if you need to start limiting access to the blog admin section of
your website, you could protect that section using a ROLE_BLOG_ADMIN role. This role doesn't need to be
defined anywhere - you can just start using it.
All roles must begin with the ROLE_ prefix to be managed by Symfony2. If you define your own
roles with a dedicated Role class (more advanced), don't use the ROLE_ prefix.
Hierarchical Roles
Instead of associating many roles to users, you can define role inheritance rules by creating a role
hierarchy:
1 #Listing
app/config/security.yml Listing
13-53 13-54
2 security:
In the above configuration, users with ROLE_ADMIN role will also have the ROLE_USER role. The
ROLE_SUPER_ADMIN role has ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH and ROLE_USER (inherited from
ROLE_ADMIN).
Logging Out
Usually, you'll also want your users to be able to log out. Fortunately, the firewall can handle this
automatically for you when you activate the logout config parameter:
Listing # app/config/security.yml
1
Listing
13-5513-56
2 security:
3 firewalls:
4 secured_area:
5 # ...
6 logout:
7 path: /logout
8 target: /
9 # ...
Once this is configured under your firewall, sending a user to /logout (or whatever you configure the
path to be), will un-authenticate the current user. The user will then be sent to the homepage (the
value defined by the target parameter). Both the path and target config parameters default to what's
specified here. In other words, unless you need to customize them, you can omit them entirely and
shorten your configuration:
Listing 1
Listing logout: ~
13-5713-58
Note that you will not need to implement a controller for the /logout URL as the firewall takes care of
everything. You may, however, want to create a route so that you can use it to generate the URL:
Listing # app/config/routing.yml
1
Listing
13-5913-60
2 logout:
3 pattern: /logout
Once the user has been logged out, he will be redirected to whatever path is defined by the target
parameter above (e.g. the homepage). For more information on configuring the logout, see the Security
Configuration Reference.
If you use this function and are not at a URL where there is a firewall active, an exception will be
thrown. Again, it's almost always a good idea to have a main firewall that covers all URLs (as has
been shown in this chapter).
1 public
Listing function indexAction() Listing
13-63 13-64
2 {
3 // show different content to admin users
4 if ($this->get('security.context')->isGranted('ROLE_ADMIN')) {
5 // Load admin content here
6 }
7 // load other regular content here
8 }
A firewall must be active or an exception will be thrown when the isGranted method is called. See
the note above about templates for more details.
Impersonating a User
Sometimes, it's useful to be able to switch from one user to another without having to logout and login
again (for instance when you are debugging or trying to understand a bug a user sees that you can't
reproduce). This can be easily done by activating the switch_user firewall listener:
1 #Listing
app/config/security.yml Listing
13-65 13-66
2 security:
3 firewalls:
4 main:
5 # ...
6 switch_user: true
To switch to another user, just add a query string with the _switch_user parameter and the username as
the value to the current URL:
http://example.com/somewhere?_switch_user=thomas10
10. http://example.com/somewhere?_switch_user=thomas
http://example.com/somewhere?_switch_user=_exit11
Of course, this feature needs to be made available to a small group of users. By default, access is restricted
to users having the ROLE_ALLOWED_TO_SWITCH role. The name of this role can be modified via the role
setting. For extra security, you can also change the query parameter name via the parameter setting:
Listing # app/config/security.yml
1
Listing
13-6713-68
2 security:
3 firewalls:
4 main:
5 // ...
6 switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user }
Stateless Authentication
By default, Symfony2 relies on a cookie (the Session) to persist the security context of the user. But if you
use certificates or HTTP authentication for instance, persistence is not needed as credentials are available
for each request. In that case, and if you don't need to store anything else between requests, you can
activate the stateless authentication (which means that no cookie will be ever created by Symfony2):
Listing # app/config/security.yml
1
Listing
13-6913-70
2 security:
3 firewalls:
4 main:
5 http_basic: ~
6 stateless: true
If you use a form login, Symfony2 will create a cookie even if you set stateless to true.
Final Words
Security can be a deep and complex issue to solve correctly in your application. Fortunately, Symfony's
security component follows a well-proven security model based around authentication and authorization.
Authentication, which always happens first, is handled by a firewall whose job is to determine the
identity of the user through several different methods (e.g. HTTP authentication, login form, etc). In
the cookbook, you'll find examples of other methods for handling authentication, including how to
implement a "remember me" cookie functionality.
Once a user is authenticated, the authorization layer can determine whether or not the user should have
access to a specific resource. Most commonly, roles are applied to URLs, classes or methods and if the
current user doesn't have that role, access is denied. The authorization layer, however, is much deeper,
and follows a system of "voting" so that multiple parties can determine if the current user should have
access to a given resource. Find out more about this and other topics in the cookbook.
11. http://example.com/somewhere?_switch_user=_exit
The nature of rich web applications means that they're dynamic. No matter how efficient your
application, each request will always contain more overhead than serving a static file.
And for most Web applications, that's fine. Symfony2 is lightning fast, and unless you're doing some
serious heavy-lifting, each request will come back quickly without putting too much stress on your server.
But as your site grows, that overhead can become a problem. The processing that's normally performed
on every request should be done only once. This is exactly what caching aims to accomplish.
• Step 1: A gateway cache, or reverse proxy, is an independent layer that sits in front of your
application. The reverse proxy caches responses as they're returned from your application and
answers requests with cached responses before they hit your application. Symfony2 provides
its own reverse proxy, but any reverse proxy can be used.
• Step 2: HTTP cache headers are used to communicate with the gateway cache and any other
caches between your application and the client. Symfony2 provides sensible defaults and a
powerful interface for interacting with the cache headers.
• Step 3: HTTP expiration and validation are the two models used for determining whether
cached content is fresh (can be reused from the cache) or stale (should be regenerated by the
application).
Since caching with HTTP isn't unique to Symfony, many articles already exist on the topic. If you're new
to HTTP caching, we highly recommend Ryan Tomayko's article Things Caches Do1. Another in-depth
resource is Mark Nottingham's Cache Tutorial2.
Types of Caches
But a gateway cache isn't the only type of cache. In fact, the HTTP cache headers sent by your application
are consumed and interpreted by up to three different types of caches:
• Browser caches: Every browser comes with its own local cache that is mainly useful for when
you hit "back" or for images and other assets. The browser cache is a private cache as cached
resources aren't shared with anyone else.
• Proxy caches: A proxy is a shared cache as many people can be behind a single one. It's usually
installed by large corporations and ISPs to reduce latency and network traffic.
• Gateway caches: Like a proxy, it's also a shared cache but on the server side. Installed by
network administrators, it makes websites more scalable, reliable and performant.
Gateway caches are sometimes referred to as reverse proxy caches, surrogate caches, or even HTTP
accelerators.
The significance of private versus shared caches will become more obvious as we talk about caching
responses containing content that is specific to exactly one user (e.g. account information).
Each response from your application will likely go through one or both of the first two cache types. These
caches are outside of your control but follow the HTTP cache directions set in the response.
1. http://tomayko.com/writings/things-caches-do
2. http://www.mnot.net/cache_docs/
3. http://www.varnish-cache.org/
4. http://wiki.squid-cache.org/SquidFaq/ReverseProxy
1
Listing Listing // web/app.php
14-1 14-2
2 require_once __DIR__.'/../app/bootstrap.php.cache';
3 require_once __DIR__.'/../app/AppKernel.php';
4 require_once __DIR__.'/../app/AppCache.php';
5
6 use Symfony\Component\HttpFoundation\Request;
7
8 $kernel = new AppKernel('prod', false);
9 $kernel->loadClassCache();
10 // wrap the default AppKernel with the AppCache one
11 $kernel = new AppCache($kernel);
12 $kernel->handle(Request::createFromGlobals())->send();
The caching kernel will immediately act as a reverse proxy - caching responses from your application and
returning them to the client.
The cache kernel has a special getLog() method that returns a string representation of what
happened in the cache layer. In the development environment, use it to debug and validate your
cache strategy:
Listing 1
Listing error_log($kernel->getLog());
14-3 14-4
The AppCache object has a sensible default configuration, but it can be finely tuned via a set of options
you can set by overriding the getOptions() method:
// app/AppCache.php
1
Listing Listing
14-5 14-6
2 use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
3
4 class AppCache extends HttpCache
5 {
6 protected function getOptions()
7 {
8 return array(
9 'debug' => false,
10 'default_ttl' => 0,
11 'private_headers' => array('Authorization', 'Cookie'),
12 'allow_reload' => false,
13 'allow_revalidate' => false,
14 'stale_while_revalidate' => 2,
15 'stale_if_error' => 60,
16 );
Unless overridden in getOptions(), the debug option will be set to automatically be the debug
value of the wrapped AppKernel.
• default_ttl: The number of seconds that a cache entry should be considered fresh when no
explicit freshness information is provided in a response. Explicit Cache-Control or Expires
headers override this value (default: 0);
• private_headers: Set of request headers that trigger "private" Cache-Control behavior on
responses that don't explicitly state whether the response is public or private via a Cache-
Control directive. (default: Authorization and Cookie);
• allow_reload: Specifies whether the client can force a cache reload by including a Cache-
Control "no-cache" directive in the request. Set it to true for compliance with RFC 2616
(default: false);
• allow_revalidate: Specifies whether the client can force a cache revalidate by including a
Cache-Control "max-age=0" directive in the request. Set it to true for compliance with RFC
2616 (default: false);
• stale_while_revalidate: Specifies the default number of seconds (the granularity is the
second as the Response TTL precision is a second) during which the cache can immediately
return a stale response while it revalidates it in the background (default: 2); this setting
is overridden by the stale-while-revalidate HTTP Cache-Control extension (see RFC
5861);
• stale_if_error: Specifies the default number of seconds (the granularity is the second)
during which the cache can serve a stale response when an error is encountered (default: 60).
This setting is overridden by the stale-if-error HTTP Cache-Control extension (see RFC
5861).
If debug is true, Symfony2 automatically adds a X-Symfony-Cache header to the response containing
useful information about cache hits and misses.
The performance of the Symfony2 reverse proxy is independent of the complexity of the
application. That's because the application kernel is only booted when the request needs to be
forwarded to it.
Keep in mind that "HTTP" is nothing more than the language (a simple text language) that web
clients (e.g. browsers) and web servers use to communicate with each other. When we talk about
HTTP caching, we're talking about the part of that language that allows clients and servers to
exchange information related to caching.
HTTP specifies four response cache headers that we're concerned with:
• Cache-Control
• Expires
• ETag
• Last-Modified
The most important and versatile header is the Cache-Control header, which is actually a collection of
various cache information.
Each of the headers will be explained in full detail in the HTTP Expiration and Validation section.
Symfony provides an abstraction around the Cache-Control header to make its creation more
manageable:
1
Listing Listing $response = new Response();
14-7 14-8
2
3 // mark the response as either public or private
4 $response->setPublic();
5 $response->setPrivate();
6
7 // set the private or shared max age
8 $response->setMaxAge(600);
9 $response->setSharedMaxAge(600);
10
11 // set a custom Cache-Control directive
12 $response->headers->addCacheControlDirective('must-revalidate', true);
• public: Indicates that the response may be cached by both private and shared caches;
• private: Indicates that all or part of the response message is intended for a single user and must
not be cached by a shared cache.
Symfony conservatively defaults each response to be private. To take advantage of shared caches (like the
Symfony2 reverse proxy), the response will need to be explicitly set as public.
Safe Methods
HTTP caching only works for "safe" HTTP methods (like GET and HEAD). Being safe means that
you never change the application's state on the server when serving the request (you can of course log
information, cache data, etc). This has two very reasonable consequences:
• You should never change the state of your application when responding to a GET or HEAD
request. Even if you don't use a gateway cache, the presence of proxy caches mean that any
GET or HEAD request may or may not actually hit your server.
• Don't expect PUT, POST or DELETE methods to cache. These methods are meant to be used
when mutating the state of your application (e.g. deleting a blog post). Caching them would
prevent certain requests from hitting and mutating your application.
• With the expiration model5, you simply specify how long a response should be considered
"fresh" by including a Cache-Control and/or an Expires header. Caches that understand
expiration will not make the same request until the cached version reaches its expiration time
and becomes "stale".
5. http://tools.ietf.org/html/rfc2616#section-13.2
The goal of both models is to never generate the same response twice by relying on a cache to store and
return "fresh" responses.
Expiration
The expiration model is the more efficient and straightforward of the two caching models and should be
used whenever possible. When a response is cached with an expiration, the cache will store the response
and return it directly without hitting the application until it expires.
The expiration model can be accomplished using one of two, nearly identical, HTTP headers: Expires
or Cache-Control.
Listing 1
$date = new DateTime();
Listing
14-914-10
2 $date->modify('+600 seconds');
3
4 $response->setExpires($date);
Listing 1
Listing Expires: Thu, 01 Mar 2011 16:00:00 GMT
14-1114-12
6. http://tools.ietf.org/html/rfc2616#section-13.3
7. http://tools.ietf.org/html/rfc2616
8. http://tools.ietf.org/wg/httpbis/
9. http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-12
Note that in HTTP versions before 1.1 the origin server wasn't required to send the Date header.
Consequently the cache (e.g. the browser) might need to rely onto his local clock to evaluate the Expires
header making the lifetime calculation vulnerable to clock skew. Another limitation of the Expires
header is that the specification states that "HTTP/1.1 servers should not send Expires dates more than
one year in the future."
The Cache-Control header would take on the following format (it may have additional directives):
1 Cache-Control:
Listing max-age=600, s-maxage=600 Listing
14-15 14-16
Validation
When a resource needs to be updated as soon as a change is made to the underlying data, the expiration
model falls short. With the expiration model, the application won't be asked to return the updated
response until the cache finally becomes stale.
The validation model addresses this issue. Under this model, the cache continues to store responses. The
difference is that, for each request, the cache asks the application whether or not the cached response is
still valid. If the cache is still valid, your application should return a 304 status code and no content. This
tells the cache that it's ok to return the cached response.
Under this model, you mainly save bandwidth as the representation is not sent twice to the same client
(a 304 response is sent instead). But if you design your application carefully, you might be able to get the
bare minimum data needed to send a 304 response and save CPU also (see below for an implementation
example).
The 304 status code means "Not Modified". It's important because with this status code do not
contain the actual content being requested. Instead, the response is simply a light-weight set of
directions that tell cache that it should use its stored version.
Like with expiration, there are two different HTTP headers that can be used to implement the validation
model: ETag and Last-Modified.
Listing 1
public function indexAction()
Listing
14-1714-18
2 {
3 $response = $this->render('MyBundle:Main:index.html.twig');
4 $response->setETag(md5($response->getContent()));
5 $response->isNotModified($this->getRequest());
6
7 return $response;
8 }
The Response::isNotModified() method compares the ETag sent with the Request with the one set on
the Response. If the two match, the method automatically sets the Response status code to 304.
This algorithm is simple enough and very generic, but you need to create the whole Response before
being able to compute the ETag, which is sub-optimal. In other words, it saves on bandwidth, but not
CPU cycles.
In the Optimizing your Code with Validation section, we'll show how validation can be used more
intelligently to determine the validity of a cache without doing so much work.
Symfony2 also supports weak ETags by passing true as the second argument to the setETag()10
method.
1
Listing Listing public function showAction($articleSlug)
14-19 14-20
2 {
3 // ...
4
5 $articleDate = new \DateTime($article->getUpdatedAt());
6 $authorDate = new \DateTime($author->getUpdatedAt());
7
8 $date = $authorDate > $articleDate ? $authorDate : $articleDate;
9
10. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html#setETag()
The Response::isNotModified() method compares the If-Modified-Since header sent by the request
with the Last-Modified header set on the response. If they are equivalent, the Response will be set to a
304 status code.
The If-Modified-Since request header equals the Last-Modified header of the last response sent
to the client for the particular resource. This is how the client and server communicate with each
other and decide whether or not the resource has been updated since it was cached.
1 public
Listing function showAction($articleSlug) Listing
14-21 14-22
2 {
3 // Get the minimum information to compute
4 // the ETag or the Last-Modified value
5 // (based on the Request, data is retrieved from
6 // a database or a key-value store for instance)
7 $article = ...;
8
9 // create a Response with a ETag and/or a Last-Modified header
10 $response = new Response();
11 $response->setETag($article->computeETag());
12 $response->setLastModified($article->getPublishedAt());
13
14 // Check that the Response is not modified for the given Request
15 if ($response->isNotModified($this->getRequest())) {
16 // return the 304 Response immediately
17 return $response;
18 } else {
19 // do more work here - like retrieving more data
20 $comments = ...;
21
22 // or render a template with the $response you've already started
23 return $this->render(
24 'MyBundle:MyController:article.html.twig',
25 array('article' => $article, 'comments' => $comments),
26 $response
27 );
28 }
29 }
When the Response is not modified, the isNotModified() automatically sets the response status code
to 304, removes the content, and removes some headers that must not be present for 304 responses (see
setNotModified()11).
Listing 1
Listing Vary: Accept-Encoding, User-Agent
14-2314-24
This particular Vary header would cache different versions of each resource based on the URI and
the value of the Accept-Encoding and User-Agent request header.
The Response object offers a clean interface for managing the Vary header:
The setVary() method takes a header name or an array of header names for which the response varies.
11. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html#setNotModified()
1 <!doctype
Listing html> Listing
14-31 14-32
2 <html>
3 <body>
4 ... some content
5
6 <!-- Embed the content of another page here -->
7 <esi:include src="http://..." />
8
9 ... more content
10 </body>
11 </html>
Notice from the example that each ESI tag has a fully-qualified URL. An ESI tag represents a page
fragment that can be fetched via the given URL.
When a request is handled, the gateway cache fetches the entire page from its cache or requests it from
the backend application. If the response contains one or more ESI tags, these are processed in the same
way. In other words, the gateway cache either retrieves the included page fragment from its cache or
requests the page fragment from the backend application again. When all the ESI tags have been resolved,
the gateway cache merges each into the main page and sends the final content to the client.
All of this happens transparently at the gateway cache level (i.e. outside of your application). As you'll
see, if you choose to take advantage of ESI tags, Symfony2 makes the process of including them almost
effortless.
12. http://www.w3.org/TR/esi-lang
Listing # app/config/config.yml
1
Listing
14-3314-34
2 framework:
3 # ...
4 esi: { enabled: true }
Now, suppose we have a page that is relatively static, except for a news ticker at the bottom of the
content. With ESI, we can cache the news ticker independent of the rest of the page.
Listing 1
public function indexAction()
Listing
14-3514-36
2 {
3 $response = $this->render('MyBundle:MyController:index.html.twig');
4 $response->setSharedMaxAge(600);
5
6 return $response;
7 }
In this example, we've given the full-page cache a lifetime of ten minutes. Next, let's include the news
ticker in the template by embedding an action. This is done via the render helper (See Embedding
Controllers for more details).
As the embedded content comes from another page (or controller for that matter), Symfony2 uses the
standard render helper to configure ESI tags:
Listing 1
Listing {% render '...:news' with {}, {'standalone': true} %}
14-3714-38
By setting standalone to true, you tell Symfony2 that the action should be rendered as an ESI tag. You
might be wondering why you would want to use a helper instead of just writing the ESI tag yourself.
That's because using a helper makes your application work even if there is no gateway cache installed.
Let's see how it works.
When standalone is false (the default), Symfony2 merges the included page content within the main
one before sending the response to the client. But when standalone is true, and if Symfony2 detects that
it's talking to a gateway cache that supports ESI, it generates an ESI include tag. But if there is no gateway
cache or if it does not support ESI, Symfony2 will just merge the included page content within the main
one as it would have done were standalone set to false.
Symfony2 detects if a gateway cache supports ESI via another Akamaï specification that is
supported out of the box by the Symfony2 reverse proxy.
The embedded action can now specify its own caching rules, entirely independent of the master page.
Listing 1
public function newsAction()
Listing
14-3914-40
2 {
3 // ...
With ESI, the full page cache will be valid for 600 seconds, but the news component cache will only last
for 60 seconds.
A requirement of ESI, however, is that the embedded action be accessible via a URL so the gateway cache
can fetch it independently of the rest of the page. Of course, an action can't be accessed via a URL unless
it has a route that points to it. Symfony2 takes care of this via a generic route and controller. For the ESI
include tag to work properly, you must define the _internal route:
1 #Listing
app/config/routing.yml Listing
14-41 14-42
2 _internal:
3 resource: "@FrameworkBundle/Resources/config/routing/internal.xml"
4 prefix: /_internal
Since this route allows all actions to be accessed via a URL, you might want to protect it by using
the Symfony2 firewall feature (by allowing access to your reverse proxy's IP range). See the Securing
by IP section of the Security Chapter for more information on how to do this.
One great advantage of this caching strategy is that you can make your application as dynamic as needed
and at the same time, hit the application as little as possible.
Once you start using ESI, remember to always use the s-maxage directive instead of max-age. As
the browser only ever receives the aggregated resource, it is not aware of the sub-components, and
so it will obey the max-age directive and cache the entire page. And you don't want that.
• alt: used as the alt attribute on the ESI tag, which allows you to specify an alternative URL
to be used if the src cannot be found;
• ignore_errors: if set to true, an onerror attribute will be added to the ESI with a value of
continue indicating that, in the event of a failure, the gateway cache will simply remove the
ESI tag silently.
Cache Invalidation
"There are only two hard things in Computer Science: cache invalidation and naming things."
--Phil Karlton
You should never need to invalidate cached data because invalidation is already taken into account
natively in the HTTP cache models. If you use validation, you never need to invalidate anything by
definition; and if you use expiration and need to invalidate a resource, it means that you set the expires
date too far away in the future.
Actually, all reverse proxies provide ways to purge cached data, but you should avoid them as much as
possible. The most standard way is to purge the cache for a given URL by requesting it with the special
PURGE HTTP method.
Here is how you can configure the Symfony2 reverse proxy to support the PURGE HTTP method:
1
Listing Listing // app/AppCache.php
14-43 14-44
2
3 use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
4
5 class AppCache extends HttpCache
6 {
7 protected function invalidate(Request $request)
8 {
9 if ('PURGE' !== $request->getMethod()) {
10 return parent::invalidate($request);
11 }
12
13 $response = new Response();
14 if (!$this->getStore()->purge($request->getUri())) {
15 $response->setStatusCode(404, 'Not purged');
16 } else {
17 $response->setStatusCode(200, 'Purged');
18 }
19
20 return $response;
21 }
22 }
You must protect the PURGE HTTP method somehow to avoid random people purging your cached
data.
Summary
Symfony2 was designed to follow the proven rules of the road: HTTP. Caching is no exception.
Mastering the Symfony2 cache system means becoming familiar with the HTTP cache models and
using them effectively. This means that, instead of relying only on Symfony2 documentation and code
examples, you have access to a world of knowledge related to HTTP caching and gateway caches such as
Varnish.
The term "internationalization" (often abbreviated i18n1) refers to the process of abstracting strings and
other locale-specific pieces out of your application and into a layer where they can be translated and
converted based on the user's locale (i.e. language and country). For text, this means wrapping each with
a function capable of translating the text (or "message") into the language of the user:
The term locale refers roughly to the user's language and country. It can be any string that
your application uses to manage translations and other format differences (e.g. currency format).
We recommended the ISO639-12 language code, an underscore (_), then the ISO3166 Alpha-23
country code (e.g. fr_FR for French/France).
In this chapter, we'll learn how to prepare an application to support multiple locales and then how to
create translations for multiple locales. Overall, the process has several common steps:
1. Enable and configure Symfony's Translation component;
2. Abstract strings (i.e. "messages") by wrapping them in calls to the Translator;
3. Create translation resources for each supported locale that translate each message in the
application;
4. Determine, set and manage the user's locale in the session.
1. http://en.wikipedia.org/wiki/Internationalization_and_localization
2. http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
3. http://en.wikipedia.org/wiki/ISO_3166-1#Current_codes
Listing # app/config/config.yml
1
Listing
15-3 15-4
2 framework:
3 translator: { fallback: en }
The fallback option defines the fallback locale when a translation does not exist in the user's locale.
When a translation does not exist for a locale, the translator first tries to find the translation for the
language (fr if the locale is fr_FR for instance). If this also fails, it looks for a translation using the
fallback locale.
The locale used in translations is the one stored in the user session.
Basic Translation
Translation of text is done through the translator service (Translator4). To translate a block of text
(called a message), use the trans()5 method. Suppose, for example, that we're translating a simple
message from inside a controller:
Listing 1
public function indexAction()
Listing
15-5 15-6
2 {
3 $t = $this->get('translator')->trans('Symfony2 is great');
4
5 return new Response($t);
6 }
When this code is executed, Symfony2 will attempt to translate the message "Symfony2 is great" based
on the locale of the user. For this to work, we need to tell Symfony2 how to translate the message via a
"translation resource", which is a collection of message translations for a given locale. This "dictionary"
of translations can be created in several different formats, XLIFF being the recommended format:
4. http://api.symfony.com/2.0/Symfony/Component/Translation/Translator.html
5. http://api.symfony.com/2.0/Symfony/Component/Translation/Translator.html#trans()
Now, if the language of the user's locale is French (e.g. fr_FR or fr_BE), the message will be translated
into J'aime Symfony2.
• The locale of the current user, which is stored in the session, is determined;
• A catalog of translated messages is loaded from translation resources defined for the locale
(e.g. fr_FR). Messages from the fallback locale are also loaded and added to the catalog if
they don't already exist. The end result is a large "dictionary" of translations. See Message
Catalogues for more details;
• If the message is located in the catalog, the translation is returned. If not, the translator returns
the original message.
When using the trans() method, Symfony2 looks for the exact string inside the appropriate message
catalog and returns it (if it exists).
Message Placeholders
Sometimes, a message containing a variable needs to be translated:
1 public
Listing function indexAction($name) Listing
15-9 15-10
2 {
3 $t = $this->get('translator')->trans('Hello '.$name);
4
5 return new Response($t);
6 }
However, creating a translation for this string is impossible since the translator will try to look up
the exact message, including the variable portions (e.g. "Hello Ryan" or "Hello Fabien"). Instead of
writing a translation for every possible iteration of the $name variable, we can replace the variable with a
"placeholder":
1 public
Listing function indexAction($name) Listing
15-11 15-12
2 {
3 $t = $this->get('translator')->trans('Hello %name%', array('%name%' => $name));
4
5 new Response($t);
6 }
Symfony2 will now look for a translation of the raw message (Hello %name%) and then replace the
placeholders with their values. Creating a translation is done just as before:
Listing Listing
15-13 15-14
The placeholders can take on any form as the full message is reconstructed using the PHP strtr
function6. However, the %var% notation is required when translating in Twig templates, and is
overall a sensible convention to follow.
Message Catalogues
When a message is translated, Symfony2 compiles a message catalogue for the user's locale and looks in
it for a translation of the message. A message catalogue is like a dictionary of translations for a specific
locale. For example, the catalogue for the fr_FR locale might contain the following translation:
It's the responsibility of the developer (or translator) of an internationalized application to create these
translations. Translations are stored on the filesystem and discovered by Symfony, thanks to some
conventions.
Each time you create a new translation resource (or install a bundle that includes a translation
resource), be sure to clear your cache so that Symfony can discover the new translation resource:
Listing 1
Listing $ php app/console cache:clear
15-1515-16
• For messages found in a bundle, the corresponding message files should live in the Resources/
translations/ directory of the bundle;
6. http://www.php.net/manual/en/function.strtr.php
The filename of the translations is also important as Symfony2 uses a convention to determine details
about the translations. Each message file must be named according to the following pattern:
domain.locale.loader:
• domain: An optional way to organize messages into groups (e.g. admin, navigation or the
default messages) - see Using Message Domains;
• locale: The locale that the translations are for (e.g. en_GB, en, etc);
• loader: How Symfony2 should load and parse the file (e.g. xliff, php or yml).
The loader can be the name of any registered loader. By default, Symfony provides the following loaders:
The choice of which loader to use is entirely up to you and is a matter of taste.
You can also store translations in a database, or any other storage by providing a custom class
implementing the LoaderInterface7 interface.
Creating Translations
The act of creating translation files is an important part of "localization" (often abbreviated L10n8).
Translation files consist of a series of id-translation pairs for the given domain and locale. The source is
the identifier for the individual translation, and can be the message in the main locale (e.g. "Symfony is
great") of your application or a unique identifier (e.g. "symfony2.great" - see the sidebar below):
1 <!--
Listing src/Acme/DemoBundle/Resources/translations/messages.fr.xliff --> Listing
15-17 15-18
2 <?xml version="1.0"?>
3 <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
4 <file source-language="en" datatype="plaintext" original="file.ext">
5 <body>
6 <trans-unit id="1">
7 <source>Symfony2 is great</source>
8 <target>J'aime Symfony2</target>
9 </trans-unit>
10 <trans-unit id="2">
11 <source>symfony2.great</source>
12 <target>J'aime Symfony2</target>
13 </trans-unit>
14 </body>
15 </file>
16 </xliff>
Symfony2 will discover these files and use them when translating either "Symfony2 is great" or
"symfony2.great" into a French language locale (e.g. fr_FR or fr_BE).
7. http://api.symfony.com/2.0/Symfony/Component/Translation/Loader/LoaderInterface.html
8. http://en.wikipedia.org/wiki/Internationalization_and_localization
Listing 1
Listing$t = $translator->trans('Symfony2 is great');
15-1915-20
2
3 $t = $translator->trans('symfony2.great');
In the first method, messages are written in the language of the default locale (English in this case).
That message is then used as the "id" when creating translations.
In the second method, messages are actually "keywords" that convey the idea of the message. The
keyword message is then used as the "id" for any translations. In this case, translations must be
made for the default locale (i.e. to translate symfony2.great to Symfony2 is great).
The second method is handy because the message key won't need to be changed in every
translation file if we decide that the message should actually read "Symfony2 is really great" in the
default locale.
The choice of which method to use is entirely up to you, but the "keyword" format is often
recommended.
Additionally, the php and yaml file formats support nested ids to avoid repeating yourself if you
use keywords instead of real text for your ids:
Listing 1
Listingsymfony2:
15-2115-22
2 is:
3 great: Symfony2 is great
4 amazing: Symfony2 is amazing
5 has:
6 bundles: Symfony2 has bundles
7 user:
8 login: Login
The multiple levels are flattened into single id/translation pairs by adding a dot (.) between every
level, therefore the above examples are equivalent to the following:
Listing 1
Listingsymfony2.is.great: Symfony2 is great
15-2315-24
2 symfony2.is.amazing: Symfony2 is amazing
3 symfony2.has.bundles: Symfony2 has bundles
4 user.login: Login
• messages.fr.xliff
When translating strings that are not in the default domain (messages), you must specify the domain as
the third argument of trans():
1 $this->get('translator')->trans('Symfony2
Listing is great', array(), 'admin'); Listing
15-25 15-26
Symfony2 will now look for the message in the admin domain of the user's locale.
1 $locale
Listing = $this->get('session')->getLocale(); Listing
15-27 15-28
2
3 $this->get('session')->setLocale('en_US');
1 #Listing
app/config/config.yml Listing
15-29 15-30
2 framework:
3 session: { default_locale: en }
1 contact:
Listing Listing
15-31 15-32
2 pattern: /{_locale}/contact
3 defaults: { _controller: AcmeDemoBundle:Contact:index, _locale: en }
4 requirements:
5 _locale: en|fr|de
Pluralization
Message pluralization is a tough topic as the rules can be quite complex. For instance, here is the
mathematic representation of the Russian pluralization rules:
Listing 1
Listing (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number %
15-3315-34
10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
As you can see, in Russian, you can have three different plural forms, each given an index of 0, 1 or 2.
For each form, the plural is different, and so the translation is also different.
When a translation has different forms due to pluralization, you can provide all the forms as a string
separated by a pipe (|):
Listing 1
Listing 'There is one apple|There are %count% apples'
15-3515-36
Listing 1
$t = $this->get('translator')->transChoice(
Listing
15-3715-38
2 'There is one apple|There are %count% apples',
3 10,
4 array('%count%' => 10)
5 );
The second argument (10 in this example), is the number of objects being described and is used to
determine which translation to use and also to populate the %count% placeholder.
Based on the given number, the translator chooses the right plural form. In English, most words have a
singular form when there is exactly one object and a plural form for all other numbers (0, 2, 3...). So, if
count is 1, the translator will use the first string (There is one apple) as the translation. Otherwise it
will use There are %count% apples.
Here is the French translation:
Listing 1
Listing 'Il y a %count% pomme|Il y a %count% pommes'
15-3915-40
Even if the string looks similar (it is made of two sub-strings separated by a pipe), the French rules are
different: the first form (no plural) is used when count is 0 or 1. So, the translator will automatically use
the first string (Il y a %count% pomme) when count is 0 or 1.
Each locale has its own set of rules, with some having as many as six different plural forms with complex
rules behind which numbers map to which plural form. The rules are quite simple for English and
9. http://api.symfony.com/2.0/Symfony/Component/Translation/Translator.html#transChoice()
1 'one:
Listing There is one apple|some: There are %count% apples' Listing
15-41 15-42
2
3 'none_or_one: Il y a %count% pomme|some: Il y a %count% pommes'
The tags are really only hints for translators and don't affect the logic used to determine which plural
form to use. The tags can be any descriptive string that ends with a colon (:). The tags also do not need
to be the same in the original message as in the translated one.
1 '{0}
Listing There are no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf] Listing
15-43 15-44
There are many apples'
The intervals follow the ISO 31-1110 notation. The above string specifies four different intervals: exactly
0, exactly 1, 2-19, and 20 and higher.
You can also mix explicit math rules and standard rules. In this case, if the count is not matched by a
specific interval, the standard rules take effect after removing the explicit rules:
1 '{0}
Listing There are no apples|[20,Inf] There are many apples|There is one apple|a_few: There Listing
15-45 15-46
are %count% apples'
For example, for 1 apple, the standard rule There is one apple will be used. For 2-19 apples, the
second standard rule There are %count% apples will be selected.
An Interval11 can represent a finite set of numbers:
1 {1,2,3,4}
Listing Listing
15-47 15-48
1 [1,
Listing +Inf[ Listing
15-49 15-50
2 ]-1,2[
The left delimiter can be [ (inclusive) or ] (exclusive). The right delimiter can be [ (exclusive) or ]
(inclusive). Beside numbers, you can use -Inf and +Inf for the infinite.
10. http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation
11. http://api.symfony.com/2.0/Symfony/Component/Translation/Interval.html
Twig Templates
Symfony2 provides specialized Twig tags (trans and transchoice) to help with message translation of
static blocks of text:
Listing 1
{% trans %}Hello %name%{% endtrans %}
Listing
15-5115-52
2
3 {% transchoice count %}
4 {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
5 {% endtranschoice %}
The transchoice tag automatically gets the %count% variable from the current context and passes it to
the translator. This mechanism only works when you use a placeholder following the %var% pattern.
If you need to use the percent character (%) in a string, escape it by doubling it: {% trans
%}Percent: %percent%%%{% endtrans %}
You can also specify the message domain and pass some additional variables:
Listing 1
{% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %}
Listing
15-5315-54
2
3 {% trans with {'%name%': 'Fabien'} from "app" into "fr" %}Hello %name%{% endtrans %}
4
5 {% transchoice count with {'%name%': 'Fabien'} from "app" %}
6 {0} There is no apples|{1} There is one apple|]1,Inf] There are %count% apples
7 {% endtranschoice %}
The trans and transchoice filters can be used to translate variable texts and complex expressions:
Listing 1
{{ message|trans }}
Listing
15-5515-56
2
3 {{ message|transchoice(5) }}
4
5 {{ message|trans({'%name%': 'Fabien'}, "app") }}
6
7 {{ message|transchoice(5, {'%name%': 'Fabien'}, 'app') }}
Using the translation tags or filters have the same effect, but with one subtle difference: automatic
output escaping is only applied to variables translated using a filter. In other words, if you need to
be sure that your translated variable is not output escaped, you must apply the raw filter after the
translation filter:
PHP Templates
The translator service is accessible in PHP templates through the translator helper:
1 <?php
Listing echo $view['translator']->trans('Symfony2 is great') ?> Listing
15-59 15-60
2
3 <?php echo $view['translator']->transChoice(
4 '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
5 10,
6 array('%count%' => 10)
7 ) ?>
1 $this->get('translator')->trans(
Listing Listing
15-61 15-62
2 'Symfony2 is great',
3 array(),
4 'messages',
5 'fr_FR',
6 );
7
8 $this->get('translator')->transChoice(
9 '{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
10 10,
11 array('%count%' => 10),
12 'messages',
13 'fr_FR',
14 );
Listing 1
Listing // src/Acme/BlogBundle/Entity/Author.php
15-6315-64
2 namespace Acme\BlogBundle\Entity;
3
4 class Author
5 {
6 public $name;
7 }
Add constraints though any of the supported methods. Set the message option to the translation source
text. For example, to guarantee that the $name property is not empty, add the following:
Listing # src/Acme/BlogBundle/Resources/config/validation.yml
1
Listing
15-6515-66
2 Acme\BlogBundle\Entity\Author:
3 properties:
4 name:
5 - NotBlank: { message: "author.name.not_blank" }
Create a translation file under the validators catalog for the constraint messages, typically in the
Resources/translations/ directory of the bundle. See Message Catalogues for more details.
12. https://github.com/l3pp4rd/DoctrineExtensions
13. http://api.symfony.com/2.0/Symfony/Component/Translation/Translator.html#trans()
14. http://api.symfony.com/2.0/Symfony/Component/Translation/Translator.html#transChoice()
A modern PHP application is full of objects. One object may facilitate the delivery of email messages
while another may allow you to persist information into a database. In your application, you may create
an object that manages your product inventory, or another object that processes data from a third-party
API. The point is that a modern application does many things and is organized into many objects that
handle each task.
In this chapter, we'll talk about a special PHP object in Symfony2 that helps you instantiate, organize and
retrieve the many objects of your application. This object, called a service container, will allow you to
standardize and centralize the way objects are constructed in your application. The container makes your
life easier, is super fast, and emphasizes an architecture that promotes reusable and decoupled code. And
since all core Symfony2 classes use the container, you'll learn how to extend, configure and use any object
in Symfony2. In large part, the service container is the biggest contributor to the speed and extensibility
of Symfony2.
Finally, configuring and using the service container is easy. By the end of this chapter, you'll be
comfortable creating your own objects via the container and customizing objects from any third-party
bundle. You'll begin writing code that is more reusable, testable and decoupled, simply because the
service container makes writing good code so easy.
What is a Service?
Put simply, a Service is any PHP object that performs some sort of "global" task. It's a purposefully-generic
name used in computer science to describe an object that's created for a specific purpose (e.g. delivering
emails). Each service is used throughout your application whenever you need the specific functionality it
provides. You don't have to do anything special to make a service: simply write a PHP class with some
code that accomplishes a specific task. Congratulations, you've just created a service!
As a rule, a PHP object is a service if it is used globally in your application. A single Mailer service
is used globally to send email messages whereas the many Message objects that it delivers are not
services. Similarly, a Product object is not a service, but an object that persists Product objects to
a database is a service.
1 use
Listing Acme\HelloBundle\Mailer; Listing
16-1 16-2
2
3 $mailer = new Mailer('sendmail');
4 $mailer->send('[email protected]', ...);
This is easy enough. The imaginary Mailer class allows us to configure the method used to deliver the
email messages (e.g. sendmail, smtp, etc). But what if we wanted to use the mailer service somewhere
else? We certainly don't want to repeat the mailer configuration every time we need to use the Mailer
object. What if we needed to change the transport from sendmail to smtp everywhere in the
application? We'd need to hunt down every place we create a Mailer service and change it.
1 #Listing
app/config/config.yml Listing
16-3 16-4
2 services:
3 my_mailer:
4 class: Acme\HelloBundle\Mailer
5 arguments: [sendmail]
When Symfony2 initializes, it builds the service container using the application configuration
(app/config/config.yml by default). The exact file that's loaded is dictated by the
AppKernel::registerContainerConfiguration() method, which loads an environment-specific
configuration file (e.g. config_dev.yml for the dev environment or config_prod.yml for prod).
An instance of the Acme\HelloBundle\Mailer object is now available via the service container. The
container is available in any traditional Symfony2 controller where you can access the services of the
container via the get() shortcut method:
1. http://wikipedia.org/wiki/Service-oriented_architecture
When we ask for the my_mailer service from the container, the container constructs the object and
returns it. This is another major advantage of using the service container. Namely, a service is never
constructed until it's needed. If you define a service and never use it on a request, the service is never
created. This saves memory and increases the speed of your application. This also means that there's very
little or no performance hit for defining lots of services. Services that are never used are never constructed.
As an added bonus, the Mailer service is only created once and the same instance is returned each time
you ask for the service. This is almost always the behavior you'll need (it's more flexible and powerful),
but we'll learn later how you can configure a service that has multiple instances.
Service Parameters
The creation of new services (i.e. objects) via the container is pretty straightforward. Parameters make
defining services more organized and flexible:
Listing # app/config/config.yml
16-7
parameters:
my_mailer.class: Acme\HelloBundle\Mailer
my_mailer.transport: sendmail
services:
my_mailer:
class: %my_mailer.class%
arguments: [%my_mailer.transport%]
The end result is exactly the same as before - the difference is only in how we defined the service. By
surrounding the my_mailer.class and my_mailer.transport strings in percent (%) signs, the container
knows to look for parameters with those names. When the container is built, it looks up the value of each
parameter and uses it in the service definition.
The percent sign inside a parameter or argument, as part of the string, must be escaped with
another percent sign:
The purpose of parameters is to feed information into services. Of course there was nothing wrong with
defining the service without using any parameters. Parameters, however, have several advantages:
• separation and organization of all service "options" under a single parameters key;
• parameter values can be used in multiple service definitions;
The choice of using or not using parameters is up to you. High-quality third-party bundles will always
use parameters as they make the service stored in the container more configurable. For the services in
your application, however, you may not need the flexibility of parameters.
Array Parameters
Parameters do not need to be flat strings, they can also be arrays. For the XML format, you need to use
the type="collection" attribute for all parameters that are arrays.
1 #Listing
app/config/config.yml Listing
16-9 16-10
2 parameters:
3 my_mailer.gateways:
4 - mail1
5 - mail2
6 - mail3
7 my_multilang.language_fallback:
8 en:
9 - en
10 - fr
11 fr:
12 - fr
13 - en
In this section, we'll refer to service configuration files as resources. This is to highlight that fact
that, while most configuration resources will be files (e.g. YAML, XML, PHP), Symfony2 is so
flexible that configuration could be loaded from anywhere (e.g. a database or even via an external
web service).
The service container is built using a single configuration resource (app/config/config.yml by default).
All other service configuration (including the core Symfony2 and third-party bundle configuration) must
be imported from inside this file in one way or another. This gives you absolute flexibility over the
services in your application.
External service configuration can be imported in two different ways. First, we'll talk about the method
that you'll use most commonly in your application: the imports directive. In the following section,
we'll introduce the second method, which is the flexible and preferred method for importing service
configuration from third-party bundles.
services:
my_mailer:
class: %my_mailer.class%
arguments: [%my_mailer.transport%]
The definition itself hasn't changed, only its location. Of course the service container doesn't know about
the new resource file. Fortunately, we can easily import the resource file using the imports key in the
application configuration.
Listing # app/config/config.yml
16-12
imports:
- { resource: @AcmeHelloBundle/Resources/config/services.yml }
The imports directive allows your application to include service container configuration resources from
any other location (most commonly from bundles). The resource location, for files, is the absolute path
to the resource file. The special @AcmeHello syntax resolves the directory path of the AcmeHelloBundle
bundle. This helps you specify the path to the resource without worrying later if you move the
AcmeHelloBundle to a different directory.
• import all service container resources needed to configure the services for the bundle;
• provide semantic, straightforward configuration so that the bundle can be configured without
interacting with the flat parameters of the bundle's service container configuration.
In other words, a service container extension configures the services for a bundle on your behalf. And as
we'll see in a moment, the extension provides a sensible, high-level interface for configuring the bundle.
Take the FrameworkBundle - the core Symfony2 framework bundle - as an example. The presence of
the following code in your application configuration invokes the service container extension inside the
FrameworkBundle:
Listing # app/config/config.yml
1
Listing
16-1316-14
2 framework:
3 secret: xxxxxxxxxx
4 charset: UTF-8
5 form: true
6 csrf_protection: true
7 router: { resource: "%kernel.root_dir%/config/routing.yml" }
8 # ...
Natively, the service container only recognizes the parameters, services, and imports directives.
Any other directives are handled by a service container extension.
If you want to expose user friendly configuration in your own bundles, read the "How to expose a
Semantic Configuration for a Bundle" cookbook recipe.
1 // src/Acme/HelloBundle/Newsletter/NewsletterManager.php
Listing Listing
16-15 16-16
2 namespace Acme\HelloBundle\Newsletter;
3
4 use Acme\HelloBundle\Mailer;
5
6 class NewsletterManager
7 {
8 protected $mailer;
9
10 public function __construct(Mailer $mailer)
11 {
12 $this->mailer = $mailer;
13 }
14
Without using the service container, we can create a new NewsletterManager fairly easily from inside a
controller:
Listing 1
public function sendNewsletterAction()
Listing
16-1716-18
2 {
3 $mailer = $this->get('my_mailer');
4 $newsletter = new Acme\HelloBundle\Newsletter\NewsletterManager($mailer);
5 // ...
6 }
This approach is fine, but what if we decide later that the NewsletterManager class needs a second or
third constructor argument? What if we decide to refactor our code and rename the class? In both cases,
you'd need to find every place where the NewsletterManager is instantiated and modify it. Of course,
the service container gives us a much more appealing option:
Listing # src/Acme/HelloBundle/Resources/config/services.yml
16-19
parameters:
# ...
newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager
services:
my_mailer:
# ...
newsletter_manager:
class: %newsletter_manager.class%
arguments: [@my_mailer]
In YAML, the special @my_mailer syntax tells the container to look for a service named my_mailer and to
pass that object into the constructor of NewsletterManager. In this case, however, the specified service
my_mailer must exist. If it does not, an exception will be thrown. You can mark your dependencies as
optional - this will be discussed in the next section.
Using references is a very powerful tool that allows you to create independent service classes with well-
defined dependencies. In this example, the newsletter_manager service needs the my_mailer service in
order to function. When you define this dependency in the service container, the container takes care of
all the work of instantiating the objects.
1
Listing Listing namespace Acme\HelloBundle\Newsletter;
16-20 16-21
2
3 use Acme\HelloBundle\Mailer;
4
Injecting the dependency by the setter method just needs a change of syntax:
# src/Acme/HelloBundle/Resources/config/services.yml Listing
16-22
parameters:
# ...
newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager
services:
my_mailer:
# ...
newsletter_manager:
class: %newsletter_manager.class%
calls:
- [ setMailer, [ @my_mailer ] ]
The approaches presented in this section are called "constructor injection" and "setter injection".
The Symfony2 service container also supports "property injection".
# src/Acme/HelloBundle/Resources/config/services.yml Listing
16-23
parameters:
# ...
services:
newsletter_manager:
class: %newsletter_manager.class%
arguments: [@?my_mailer]
In YAML, the special @? syntax tells the service container that the dependency is optional. Of course, the
NewsletterManager must also be written to allow for an optional dependency:
Listing Listing
16-24 16-25
Listing 1
public function indexAction($bar)
Listing
16-2616-27
2 {
3 $session = $this->get('session');
4 $session->set('foo', $bar);
5
6 // ...
7 }
In Symfony2, you'll constantly use services provided by the Symfony core or other third-party bundles
to perform tasks such as rendering templates (templating), sending emails (mailer), or accessing
information on the request (request).
We can take this a step further by using these services inside services that you've created for your
application. Let's modify the NewsletterManager to use the real Symfony2 mailer service (instead of the
pretend my_mailer). Let's also pass the templating engine service to the NewsletterManager so that it
can generate the email content via a template:
1
Listing Listing namespace Acme\HelloBundle\Newsletter;
16-28 16-29
2
3 use Symfony\Component\Templating\EngineInterface;
4
5 class NewsletterManager
6 {
7 protected $mailer;
8
9 protected $templating;
10
11 public function __construct(\Swift_Mailer $mailer, EngineInterface $templating)
12 {
13 $this->mailer = $mailer;
14 $this->templating = $templating;
15 }
16
17 // ...
18 }
The newsletter_manager service now has access to the core mailer and templating services. This is a
common way to create services specific to your application that leverage the power of different services
within the framework.
Tags
In the same way that a blog post on the Web might be tagged with things such as "Symfony" or "PHP",
services configured in your container can also be tagged. In the service container, a tag implies that the
service is meant to be used for a specific purpose. Take the following example:
1 services:
Listing Listing
16-31 16-32
2 foo.twig.extension:
3 class: Acme\HelloBundle\Extension\FooExtension
4 tags:
5 - { name: twig.extension }
The twig.extension tag is a special tag that the TwigBundle uses during configuration. By giving
the service this twig.extension tag, the bundle knows that the foo.twig.extension service should
be registered as a Twig extension with Twig. In other words, Twig finds all services tagged with
twig.extension and automatically registers them as extensions.
Tags, then, are a way to tell Symfony2 or other third-party bundles that your service should be registered
or used in some special way by the bundle.
The following is a list of tags available with the core Symfony2 bundles. Each of these has a different
effect on your service and many tags require additional arguments (beyond just the name parameter).
• assetic.filter
• assetic.templating.php
• data_collector
• form.field_factory.guesser
• kernel.cache_warmer
• kernel.event_listener
• monolog.logger
• routing.loader
• security.listener.factory
• security.voter
• templating.helper
• twig.extension
• translation.loader
• validator.constraint_validator
Symfony2 is fast, right out of the box. Of course, if you really need speed, there are many ways that you
can make Symfony even faster. In this chapter, you'll explore many of the most common and powerful
ways to make your Symfony application even faster.
Further Optimizations
Byte code caches usually monitor the source files for changes. This ensures that if the source of a
file changes, the byte code is recompiled automatically. This is really convenient, but obviously adds
overhead.
For this reason, some byte code caches offer an option to disable these checks. Obviously, when disabling
these checks, it will be up to the server admin to ensure that the cache is cleared whenever any source
files change. Otherwise, the updates you've made won't be seen.
For example, to disable these checks in APC, simply add apc.stat=0 to your php.ini configuration.
1. http://en.wikipedia.org/wiki/List_of_PHP_accelerators
2. http://php.net/manual/en/book.apc.php
Listing 1
Listing // app/autoload.php
17-1 17-2
2 require __DIR__.'/../vendor/symfony/src/Symfony/Component/ClassLoader/
3 ApcUniversalClassLoader.php';
4
5 use Symfony\Component\ClassLoader\ApcUniversalClassLoader;
6
7 $loader = new ApcUniversalClassLoader('some caching unique prefix');
// ...
When using the APC autoloader, if you add new classes, they will be found automatically and
everything will work the same as before (i.e. no reason to "clear" the cache). However, if you
change the location of a particular namespace or prefix, you'll need to flush your APC cache.
Otherwise, the autoloader will still be looking at the old location for all classes inside that
namespace.
Listing 1
Listing require_once __DIR__.'/../app/bootstrap.php.cache';
17-3 17-4
Note that there are two disadvantages when using a bootstrap file:
• the file needs to be regenerated whenever any of the original sources change (i.e. when you
update the Symfony2 source or vendor libraries);
3. https://github.com/symfony/symfony-standard/blob/2.0/app/autoload.php
4. https://github.com/sensio/SensioDistributionBundle/blob/2.0/Resources/bin/build_bootstrap.php
If you're using Symfony2 Standard Edition, the bootstrap file is automatically rebuilt after updating the
vendor libraries via the php bin/vendors install command.
Looks like you want to understand how Symfony2 works and how to extend it. That makes me very
happy! This section is an in-depth explanation of the Symfony2 internals.
You need to read this section only if you want to understand how Symfony2 works behind the
scene, or if you want to extend Symfony2.
Overview
The Symfony2 code is made of several independent layers. Each layer is built on top of the previous one.
Autoloading is not managed by the framework directly; it's done independently with the help of
the UniversalClassLoader1 class and the src/autoload.php file. Read the dedicated chapter for
more information.
HttpFoundation Component
The deepest level is the HttpFoundation2 component. HttpFoundation provides the main objects needed
to deal with HTTP. It is an Object-Oriented abstraction of some native PHP functions and variables:
• The Request3 class abstracts the main PHP global variables like $_GET, $_POST, $_COOKIE,
$_FILES, and $_SERVER;
• The Response4 class abstracts some PHP functions like header(), setcookie(), and echo;
1. http://api.symfony.com/2.0/Symfony/Component/ClassLoader/UniversalClassLoader.html
2. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation.html
3. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Request.html
4. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html
HttpKernel Component
On top of HttpFoundation is the HttpKernel7 component. HttpKernel handles the dynamic part of
HTTP; it is a thin wrapper on top of the Request and Response classes to standardize the way requests
are handled. It also provides extension points and tools that makes it the ideal starting point to create a
Web framework without too much overhead.
It also optionally adds configurability and extensibility, thanks to the Dependency Injection component
and a powerful plugin system (bundles).
FrameworkBundle Bundle
The FrameworkBundle8 bundle is the bundle that ties the main components and libraries together
to make a lightweight and fast MVC framework. It comes with a sensible default configuration and
conventions to ease the learning curve.
Kernel
The HttpKernel9 class is the central class of Symfony2 and is responsible for handling client requests. Its
main goal is to "convert" a Request10 object to a Response11 object.
Every Symfony2 Kernel implements HttpKernelInterface12:
1 function
Listing handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) Listing
18-1 18-2
Controllers
To convert a Request to a Response, the Kernel relies on a "Controller". A Controller can be any valid
PHP callable.
The Kernel delegates the selection of what Controller should be executed to an implementation of
ControllerResolverInterface13:
1 public
Listing function getController(Request $request); Listing
18-3 18-4
2
3 public function getArguments(Request $request, $controller);
5. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Session.html
6. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/SessionStorage/SessionStorageInterface.html
7. http://api.symfony.com/2.0/Symfony/Component/HttpKernel.html
8. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle.html
9. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/HttpKernel.html
10. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Request.html
11. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html
12. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/HttpKernelInterface.html
13. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.html
The default implementation uses the RouterListener16 to define the _controller Request
attribute (see kernel.request Event).
The getArguments()17 method returns an array of arguments to pass to the Controller callable. The
default implementation automatically resolves the method arguments, based on the Request attributes.
Listing 1
Listing // Symfony2 will look for an 'id' attribute (mandatory)
18-5 18-6
2 // and an 'admin' one (optional)
3 public function showAction($id, $admin = true)
4 {
5 // ...
6 }
Handling Requests
The handle() method takes a Request and always returns a Response. To convert the Request,
handle() relies on the Resolver and an ordered chain of Event notifications (see the next section for more
information about each Event):
1. Before doing anything else, the kernel.request event is notified -- if one of the listeners returns
a Response, it jumps to step 8 directly;
2. The Resolver is called to determine the Controller to execute;
3. Listeners of the kernel.controller event can now manipulate the Controller callable the way
they want (change it, wrap it, ...);
4. The Kernel checks that the Controller is actually a valid PHP callable;
5. The Resolver is called to determine the arguments to pass to the Controller;
6. The Kernel calls the Controller;
7. If the Controller does not return a Response, listeners of the kernel.view event can convert the
Controller return value to a Response;
8. Listeners of the kernel.response event can manipulate the Response (content and headers);
9. The Response is returned.
If an Exception is thrown during processing, the kernel.exception is notified and listeners are given a
chance to convert the Exception to a Response. If that works, the kernel.response event is notified; if
not, the Exception is re-thrown.
If you don't want Exceptions to be caught (for embedded requests for instance), disable the
kernel.exception event by passing false as the third argument to the handle() method.
14. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.html#getController()
15. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Controller/ControllerResolver.html
16. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/EventListener/RouterListener.html
17. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.html#getArguments()
• HttpKernelInterface::MASTER_REQUEST;
• HttpKernelInterface::SUB_REQUEST.
The type is passed to all events and listeners can act accordingly (some processing must only occur on
the master request).
Events
Each event thrown by the Kernel is a subclass of KernelEvent18. This means that each event has access
to the same basic information:
getRequestType()
The getRequestType() method allows listeners to know the type of the request. For instance, if a listener
must only be active for master requests, add the following code at the beginning of your listener method:
1 use
Listing Symfony\Component\HttpKernel\HttpKernelInterface; Listing
18-7 18-8
2
3 if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
4 // return immediately
5 return;
6 }
If you are not yet familiar with the Symfony2 Event Dispatcher, read the Event Dispatcher
Component Documentation section first.
kernel.request Event
Event Class: GetResponseEvent19
The goal of this event is to either return a Response object immediately or setup variables so that a
Controller can be called after the event. Any listener can return a Response object via the setResponse()
method on the event. In this case, all other listeners won't be called.
This event is used by FrameworkBundle to populate the _controller Request attribute, via the
RouterListener20. RequestListener uses a RouterInterface21 object to match the Request and
determine the Controller name (stored in the _controller Request attribute).
18. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Event/KernelEvent.html
19. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Event/GetResponseEvent.html
20. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/EventListener/RouterListener.html
21. http://api.symfony.com/2.0/Symfony/Component/Routing/RouterInterface.html
1
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
Listing Listing
18-9 18-10
2
3 public function onKernelController(FilterControllerEvent $event)
4 {
5 $controller = $event->getController();
6 // ...
7
8 // the controller can be changed to any PHP callable
9 $event->setController($controller);
10 }
kernel.view Event
Event Class: GetResponseForControllerResultEvent23
This event is not used by FrameworkBundle, but it can be used to implement a view sub-system. This
event is called only if the Controller does not return a Response object. The purpose of the event is to
allow some other return value to be converted into a Response.
The value returned by the Controller is accessible via the getControllerResult method:
1
Listing Listing use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
18-11 18-12
2 use Symfony\Component\HttpFoundation\Response;
3
4 public function onKernelView(GetResponseForControllerResultEvent $event)
5 {
6 $val = $event->getControllerResult();
7 $response = new Response();
8 // some how customize the Response from the return value
9
10 $event->setResponse($response);
11 }
kernel.response Event
Event Class: FilterResponseEvent24
The purpose of this event is to allow other systems to modify or replace the Response object after its
creation:
Listing 1
public function onKernelResponse(FilterResponseEvent $event)
Listing
18-1318-14
2 {
3 $response = $event->getResponse();
22. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Event/FilterControllerEvent.html
23. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Event/GetResponseForControllerResultEvent.html
24. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Event/FilterResponseEvent.html
kernel.exception Event
Event Class: GetResponseForExceptionEvent29
FrameworkBundle registers an ExceptionListener30 that forwards the Request to a given Controller (the
value of the exception_listener.controller parameter -- must be in the class::method notation).
A listener on this event can create and set a Response object, create and set a new Exception object, or
do nothing:
1 use
Listing Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; Listing
18-15 18-16
2 use Symfony\Component\HttpFoundation\Response;
3
4 public function onKernelException(GetResponseForExceptionEvent $event)
5 {
6 $exception = $event->getException();
7 $response = new Response();
8 // setup the Response object based on the caught exception
9 $event->setResponse($response);
10
11 // you can alternatively set a new Exception
12 // $exception = new \Exception('Some special exception');
13 // $event->setException($exception);
14 }
25. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/EventListener/ProfilerListener.html
26. http://api.symfony.com/2.0/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.html
27. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/EventListener/ResponseListener.html
28. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/EventListener/EsiListener.html
29. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.html
30. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/EventListener/ExceptionListener.html
The profiler collects information for all requests (simple requests, redirects, exceptions, Ajax
requests, ESI requests; and for all HTTP methods and all formats). It means that for a single URL,
you can have several associated profiling data (one per external request/response pair).
If the token is not clickable, it means that the profiler routes are not registered (see below for
configuration information).
Listing 1
$profile = $container->get('profiler')->loadProfileFromResponse($response);
Listing
18-1718-18
2
3 $profile = $container->get('profiler')->loadProfile($token);
When the profiler is enabled but not the web debug toolbar, or when you want to get the token for
an Ajax request, use a tool like Firebug to get the value of the X-Debug-Token HTTP header.
If you want to manipulate profiling data on a different machine than the one where the information were
generated, use the export() and import() methods:
Configuration
The default Symfony2 configuration comes with sensible settings for the profiler, the web debug toolbar,
and the web profiler. Here is for instance the configuration for the development environment:
1 #Listing
load the profiler Listing
18-23 18-24
2 framework:
3 profiler: { only_exceptions: false }
4
5 # enable the web profiler
6 web_profiler:
7 toolbar: true
8 intercept_redirects: true
9 verbose: true
When only-exceptions is set to true, the profiler only collects data when an exception is thrown by the
application.
When intercept-redirects is set to true, the web profiler intercepts the redirects and gives you the
opportunity to look at the collected data before following the redirect.
When verbose is set to true, the Web Debug Toolbar displays a lot of information. Setting verbose to
false hides some secondary information to make the toolbar shorter.
If you enable the web profiler, you also need to mount the profiler routes:
_profiler: Listing
18-25
resource: @WebProfilerBundle/Resources/config/routing/profiler.xml
prefix: /_profiler
As the profiler adds some overhead, you might want to enable it only under certain circumstances in the
production environment. The only-exceptions settings limits profiling to 500 pages, but what if you
1
Listing Listing # enables the profiler only for request coming for the 192.168.0.0 network
18-26 18-27
2 framework:
3 profiler:
4 matcher: { ip: 192.168.0.0/24 }
5
6 # enables the profiler only for the /admin URLs
7 framework:
8 profiler:
9 matcher: { path: "^/admin/" }
10
11 # combine rules
12 framework:
13 profiler:
14 matcher: { ip: 192.168.0.0/24, path: "^/admin/" }
15
16 # use a custom matcher instance defined in the "custom_matcher" service
17 framework:
18 profiler:
19 matcher: { service: custom_matcher }
The Symfony2 stable API is a subset of all Symfony2 published public methods (components and core
bundles) that share the following properties:
The implementation itself can change though. The only valid case for a change in the stable API is in
order to fix a security issue.
The stable API is based on a whitelist, tagged with @api. Therefore, everything not tagged explicitly is
not part of the stable API.
Any third party bundle should also publish its own stable API.
• BrowserKit
• ClassLoader
• Console
• CssSelector
• DependencyInjection
• DomCrawler
• EventDispatcher
• Finder
• HttpFoundation
• HttpKernel
• Locale
• Process
• Routing
• Templating
• Translation
PDF brought to you by Chapter 19: The Symfony2 Stable API | 239
generated on August 13, 2012
• Validator
• Yaml
PDF brought to you by Chapter 19: The Symfony2 Stable API | 240
generated on August 13, 2012