The all-in-one Book
for Symfony 2.0
generated on October 26, 2012
The all-in-one Book (2.0)
This work is licensed under the “Attribution-Share Alike 3.0 Unported” license (http://creativecommons.org/
licenses/by-sa/3.0/).
You are free to share (to copy, distribute and transmit the work), and to remix (to adapt the work) under the
following conditions:
• 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
The Quick Tour
The Big Picture .................................................................................................................................10
The View ..........................................................................................................................................19
The Controller ..................................................................................................................................24
The Architecture ...............................................................................................................................29
The Book
Symfony2 and HTTP Fundamentals ..................................................................................................36
Symfony2 versus Flat PHP.................................................................................................................46
Installing and Configuring Symfony...................................................................................................58
Creating Pages in Symfony2 ..............................................................................................................63
Controller.........................................................................................................................................76
Routing ............................................................................................................................................87
Creating and using Templates ...........................................................................................................98
Databases and Doctrine .................................................................................................................. 115
Databases and Propel ...................................................................................................................... 138
Testing ........................................................................................................................................... 146
Validation....................................................................................................................................... 160
Forms ............................................................................................................................................. 168
Security .......................................................................................................................................... 189
HTTP Cache................................................................................................................................... 209
Translations.................................................................................................................................... 224
Service Container ............................................................................................................................ 236
Performance ................................................................................................................................... 246
Internals ......................................................................................................................................... 249
The Symfony2 Stable API ................................................................................................................ 258
The Cookbook
How to Create and store a Symfony2 Project in git ........................................................................... 261
How to Create and store a Symfony2 Project in Subversion .............................................................. 265
How to customize Error Pages......................................................................................................... 270
How to define Controllers as Services .............................................................................................. 272
PDF brought to you by
generated on October 26, 2012
Contents at a Glance | iii
How to force routes to always use HTTPS or HTTP......................................................................... 274
How to allow a "/" character in a route parameter ............................................................................ 275
How to configure a redirect to another route without a custom controller......................................... 276
How to use HTTP Methods beyond GET and POST in Routes......................................................... 277
How to Use Assetic for Asset Management ...................................................................................... 279
How to Minify JavaScripts and Stylesheets with YUI Compressor..................................................... 283
How to Use Assetic For Image Optimization with Twig Functions ................................................... 285
How to Apply an Assetic Filter to a Specific File Extension............................................................... 288
How to handle File Uploads with Doctrine ...................................................................................... 290
How to use Doctrine Extensions: Timestampable, Sluggable, Translatable, etc. ................................ 298
How to Register Event Listeners and Subscribers ............................................................................. 299
How to use Doctrine's DBAL Layer ................................................................................................. 301
How to generate Entities from an Existing Database......................................................................... 303
How to work with Multiple Entity Managers and Connections......................................................... 307
How to Register Custom DQL Functions......................................................................................... 310
How to implement a simple Registration Form ................................................................................ 311
How to customize Form Rendering ................................................................................................. 316
How to use Data Transformers........................................................................................................ 327
How to Dynamically Generate Forms Using Form Events ................................................................ 333
How to Embed a Collection of Forms .............................................................................................. 336
How to Create a Custom Form Field Type....................................................................................... 348
How to Create a Form Type Extension ............................................................................................ 353
How to use the Virtual Form Field Option....................................................................................... 358
How to create a Custom Validation Constraint ................................................................................ 361
How to Master and Create new Environments ................................................................................. 365
How to override Symfony's Default Directory Structure.................................................................... 370
How to Set External Parameters in the Service Container ................................................................. 373
How to use PdoSessionStorage to store Sessions in the Database ...................................................... 376
How to use the Apache Router ........................................................................................................ 378
How to create an Event Listener ...................................................................................................... 380
How to work with Scopes ............................................................................................................... 382
How to work with Compiler Passes in Bundles ................................................................................ 385
How to use Best Practices for Structuring Bundles............................................................................ 386
How to use Bundle Inheritance to Override parts of a Bundle ........................................................... 391
How to Override any Part of a Bundle ............................................................................................. 394
How to expose a Semantic Configuration for a Bundle ..................................................................... 397
How to send an Email ..................................................................................................................... 405
How to use Gmail to send Emails .................................................................................................... 407
How to Work with Emails During Development .............................................................................. 408
How to Spool Email ........................................................................................................................ 410
How to simulate HTTP Authentication in a Functional Test ............................................................ 412
How to test the Interaction of several Clients ................................................................................... 413
How to use the Profiler in a Functional Test..................................................................................... 414
How to test Doctrine Repositories ................................................................................................... 416
How to customize the Bootstrap Process before running Tests.......................................................... 418
How to load Security Users from the Database (the Entity Provider) ................................................. 420
How to add "Remember Me" Login Functionality ............................................................................ 430
iv | Contents at a Glance
Contents at a Glance | 4
How to implement your own Voter to blacklist IP Addresses............................................................ 433
How to use Access Control Lists (ACLs).......................................................................................... 436
How to use Advanced ACL Concepts .............................................................................................. 440
How to force HTTPS or HTTP for Different URLs ........................................................................... 444
How to customize your Form Login ................................................................................................ 445
How to secure any Service or Method in your Application................................................................ 449
How to create a custom User Provider ............................................................................................. 453
How to create a custom Authentication Provider ............................................................................. 458
How to use Varnish to speed up my Website ................................................................................... 467
How to Inject Variables into all Templates (i.e. Global Variables) ..................................................... 469
How to use PHP instead of Twig for Templates ............................................................................... 471
How to write a custom Twig Extension ........................................................................................... 476
How to use Monolog to write Logs.................................................................................................. 479
How to Configure Monolog to Email Errors .................................................................................... 483
How to create a Console Command ................................................................................................ 485
How to use the Console .................................................................................................................. 488
How to generate URLs with a custom Host in Console Commands .................................................. 490
How to optimize your development Environment for debugging ...................................................... 492
How to setup before and after Filters ............................................................................................... 494
How to extend a Class without using Inheritance............................................................................. 498
How to customize a Method Behavior without using Inheritance...................................................... 501
How to register a new Request Format and Mime Type.................................................................... 503
How to create a custom Data Collector............................................................................................ 505
How to Create a SOAP Web Service in a Symfony2 Controller ......................................................... 508
How Symfony2 differs from symfony1 ............................................................................................. 512
The Components
The ClassLoader Component .......................................................................................................... 518
The Config Component .................................................................................................................. 521
Loading resources ........................................................................................................................... 522
Caching based on resources............................................................................................................. 524
Define and process configuration values .......................................................................................... 526
The Console Component ................................................................................................................ 534
Using Console Commands, Shortcuts and Built-in Commands......................................................... 542
How to build an Application that is a single Command .................................................................... 545
The CssSelector Component ........................................................................................................... 547
The DomCrawler Component ......................................................................................................... 549
The Dependency Injection Component............................................................................................ 556
Types of Injection ........................................................................................................................... 561
Working with Container Parameters and Definitions ....................................................................... 564
Compiling the Container................................................................................................................. 567
Working with Tagged Services ........................................................................................................ 575
Using a Factory to Create Services ................................................................................................... 579
Managing Common Dependencies with Parent Services ................................................................... 581
Advanced Container Configuration ................................................................................................. 586
Container Building Workflow ......................................................................................................... 588
PDF brought to you by
generated on October 26, 2012
Contents at a Glance | v
The Event Dispatcher Component................................................................................................... 590
The Finder Component................................................................................................................... 598
The HttpFoundation Component.................................................................................................... 603
The Locale Component................................................................................................................... 609
The Process Component ................................................................................................................. 611
The Routing Component ................................................................................................................ 613
The Serializer Component ............................................................................................................... 620
The Templating Component ........................................................................................................... 623
The YAML Component................................................................................................................... 626
Contributing
Reporting a Bug .............................................................................................................................. 635
Submitting a Patch .......................................................................................................................... 636
Reporting a Security Issue ............................................................................................................... 642
Running Symfony2 Tests................................................................................................................. 643
Coding Standards ........................................................................................................................... 645
Conventions ................................................................................................................................... 648
Symfony2 License ........................................................................................................................... 650
Contributing to the Documentation ................................................................................................ 651
Documentation Format................................................................................................................... 654
Translations.................................................................................................................................... 658
Symfony2 Documentation License................................................................................................... 660
The Release Process ........................................................................................................................ 661
IRC Meetings.................................................................................................................................. 664
Other Resources ............................................................................................................................. 666
Reference Documents
FrameworkBundle Configuration ("framework").............................................................................. 668
AsseticBundle Configuration Reference ........................................................................................... 674
Configuration Reference.................................................................................................................. 676
Security Configuration Reference..................................................................................................... 680
SwiftmailerBundle Configuration ("swiftmailer").............................................................................. 684
TwigBundle Configuration Reference .............................................................................................. 688
Configuration Reference.................................................................................................................. 690
WebProfilerBundle Configuration ................................................................................................... 692
Form Types Reference..................................................................................................................... 693
birthday Field Type......................................................................................................................... 695
checkbox Field Type ....................................................................................................................... 699
choice Field Type ............................................................................................................................ 701
collection Field Type....................................................................................................................... 706
country Field Type.......................................................................................................................... 712
csrf Field Type ................................................................................................................................ 715
date Field Type ............................................................................................................................... 717
datetime Field Type ........................................................................................................................ 721
email Field Type ............................................................................................................................. 725
vi | Contents at a Glance
Contents at a Glance | 6
entity Field Type ............................................................................................................................. 727
file Field Type ................................................................................................................................. 732
The Abstract "field" Type ................................................................................................................ 735
form Field Type .............................................................................................................................. 737
hidden Field Type ........................................................................................................................... 738
integer Field Type ........................................................................................................................... 740
language Field Type ........................................................................................................................ 743
locale Field Type............................................................................................................................. 746
money Field Type ........................................................................................................................... 749
number Field Type.......................................................................................................................... 752
password Field Type ....................................................................................................................... 755
percent Field Type .......................................................................................................................... 757
radio Field Type.............................................................................................................................. 760
repeated Field Type......................................................................................................................... 762
search Field Type ............................................................................................................................ 765
text Field Type................................................................................................................................ 767
textarea Field Type ......................................................................................................................... 769
time Field Type............................................................................................................................... 771
timezone Field Type........................................................................................................................ 775
url Field Type ................................................................................................................................. 778
Twig Template Form Function Reference ........................................................................................ 780
Validation Constraints Reference..................................................................................................... 783
NotBlank........................................................................................................................................ 785
Blank.............................................................................................................................................. 786
NotNull.......................................................................................................................................... 787
Null................................................................................................................................................ 788
True ............................................................................................................................................... 790
False............................................................................................................................................... 792
Type............................................................................................................................................... 794
Email.............................................................................................................................................. 796
MinLength...................................................................................................................................... 798
MaxLength ..................................................................................................................................... 800
Url.................................................................................................................................................. 802
Regex ............................................................................................................................................. 804
Ip ................................................................................................................................................... 806
Max................................................................................................................................................ 808
Min ................................................................................................................................................ 810
Date ............................................................................................................................................... 812
DateTime ....................................................................................................................................... 813
Time............................................................................................................................................... 814
Choice............................................................................................................................................ 815
Collection....................................................................................................................................... 819
UniqueEntity .................................................................................................................................. 822
Language ........................................................................................................................................ 824
Locale............................................................................................................................................. 825
Country.......................................................................................................................................... 827
File ................................................................................................................................................. 828
PDF brought to you by
generated on October 26, 2012
Contents at a Glance | vii
Image ............................................................................................................................................. 831
Callback ......................................................................................................................................... 832
Valid .............................................................................................................................................. 835
All .................................................................................................................................................. 837
The Dependency Injection Tags....................................................................................................... 839
Requirements for running Symfony2................................................................................................ 849
viii | Contents at a Glance
Contents at a Glance | 8
Part I
The Quick Tour
Chapter 1
The Big Picture
Start using Symfony2 in 10 minutes! This chapter will walk you through some of the most important
concepts behind Symfony2 and explain how you can get started quickly by showing you a simple project
in action.
If you've used a web framework before, you should feel right at home with Symfony2. If not, welcome to
a whole new way of developing web applications!
Want to learn why and when you need to use a framework? Read the "Symfony in 5 minutes"
document.
Downloading Symfony2
First, check that you have installed and configured a Web server (such as Apache) with PHP 5.3.2 or
higher.
If you have PHP 5.4, you could use the built-in web server. The built-in server should be used only
for development purpose, but it can help you to start your project quickly and easily.
Just use this command to launch the server:
Listing 1-1
1 $ php -S localhost:80 -t /path/to/www
where "/path/to/www" is the path to some directory on your machine that you'll extract Symfony
into so that the eventual URL to your application is "http://localhost/Symfony/app_dev.php1". You
can also extract Symfony first and then start the web server in the Symfony "web" directory. If you
do this, the URL to your application will be "http://localhost/app_dev.php2".
1. http://localhost/Symfony/app_dev.php
2. http://localhost/app_dev.php
PDF brought to you by
generated on October 26, 2012
Chapter 1: The Big Picture | 10
Ready? Start by downloading the "Symfony2 Standard Edition3", a Symfony distribution that is
preconfigured for the most common use cases and also contains some code that demonstrates how to use
Symfony2 (get the archive with the vendors included to get started even faster).
After unpacking the archive under your web server root directory, you should have a Symfony/ directory
that looks like this:
Listing 1-2
1 www/ <- your web root directory
2
Symfony/ <- the unpacked archive
3
app/
4
cache/
5
config/
6
logs/
7
Resources/
8
bin/
9
src/
10
Acme/
11
DemoBundle/
12
Controller/
13
Resources/
14
...
15
vendor/
16
symfony/
17
doctrine/
18
...
19
web/
20
app.php
21
...
If you downloaded the Standard Edition without vendors, simply run the following command to
download all of the vendor libraries:
Listing 1-3
1 $ php bin/vendors install
Checking the Configuration
Symfony2 comes with a visual server configuration tester to help avoid some headaches that come from
Web server or PHP misconfiguration. Use the following URL to see the diagnostics for your machine:
Listing 1-4
1 http://localhost/Symfony/web/config.php
If there are any outstanding issues listed, correct them. You might also tweak your configuration by
following any given recommendations. When everything is fine, click on "Bypass configuration and go to
the Welcome page" to request your first "real" Symfony2 webpage:
Listing 1-5
1 http://localhost/Symfony/web/app_dev.php/
Symfony2 should welcome and congratulate you for your hard work so far!
3. http://symfony.com/download
PDF brought to you by
generated on October 26, 2012
Chapter 1: The Big Picture | 11
Understanding the Fundamentals
One of the main goals of a framework is to ensure Separation of Concerns4. This keeps your code
organized and allows your application to evolve easily over time by avoiding the mixing of database calls,
HTML tags, and business logic in the same script. To achieve this goal with Symfony, you'll first need to
learn a few fundamental concepts and terms.
Want proof that using a framework is better than mixing everything in the same script? Read the
"Symfony2 versus Flat PHP" chapter of the book.
The distribution comes with some sample code that you can use to learn more about the main Symfony2
concepts. Go to the following URL to be greeted by Symfony2 (replace Fabien with your first name):
Listing 1-6
1 http://localhost/Symfony/web/app_dev.php/demo/hello/Fabien
4. http://en.wikipedia.org/wiki/Separation_of_concerns
PDF brought to you by
generated on October 26, 2012
Chapter 1: The Big Picture | 12
What's going on here? Let's dissect the URL:
• app_dev.php: This is a front controller. It is the unique entry point of the application and it
responds to all user requests;
• /demo/hello/Fabien: This is the virtual path to the resource the user wants to access.
Your responsibility as a developer is to write the code that maps the user's request (/demo/hello/Fabien)
to the resource associated with it (the Hello Fabien! HTML page).
Routing
Symfony2 routes the request to the code that handles it by trying to match the requested URL against
some configured patterns. By default, these patterns (called routes) are defined in the app/config/
routing.yml configuration file. When you're in the dev environment - indicated by the app_**dev**.php
front controller - the app/config/routing_dev.yml configuration file is also loaded. In the Standard
Edition, the routes to these "demo" pages are placed in that file:
Listing 1-7
1
2
3
4
5
6
7
8
9
10
11
# app/config/routing_dev.yml
_welcome:
pattern: /
defaults: { _controller: AcmeDemoBundle:Welcome:index }
_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type:
annotation
prefix:
/demo
# ...
The first three lines (after the comment) define the code that is executed when the user requests the "/"
resource (i.e. the welcome page you saw earlier). When requested, the AcmeDemoBundle:Welcome:index
controller will be executed. In the next section, you'll learn exactly what that means.
The Symfony2 Standard Edition uses YAML5 for its configuration files, but Symfony2 also supports
XML, PHP, and annotations natively. The different formats are compatible and may be used
interchangeably within an application. Also, the performance of your application does not depend
on the configuration format you choose as everything is cached on the very first request.
PDF brought to you by
generated on October 26, 2012
Chapter 1: The Big Picture | 13
Controllers
A controller is a fancy name for a PHP function or method that handles incoming requests and returns
responses (often HTML code). Instead of using the PHP global variables and functions (like $_GET
or header()) to manage these HTTP messages, Symfony uses objects: Request6 and Response7. The
simplest possible controller might create the response by hand, based on the request:
Listing 1-8
1 use Symfony\Component\HttpFoundation\Response;
2
3 $name = $request->query->get('name');
4
5 return new Response('Hello '.$name, 200, array('Content-Type' => 'text/plain'));
Symfony2 embraces the HTTP Specification, which are the rules that govern all communication on
the Web. Read the "Symfony2 and HTTP Fundamentals" chapter of the book to learn more about
this and the added power that this brings.
Symfony2 chooses the controller based on the _controller value from the routing configuration:
AcmeDemoBundle:Welcome:index. This string is the controller logical name, and it references the
indexAction method from the Acme\DemoBundle\Controller\WelcomeController class:
Listing 1-9
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/DemoBundle/Controller/WelcomeController.php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class WelcomeController extends Controller
{
public function indexAction()
{
return $this->render('AcmeDemoBundle:Welcome:index.html.twig');
}
}
You
could
have
used
the
full
class
and
method
name
Acme\DemoBundle\Controller\WelcomeController::indexAction - for the _controller value.
But if you follow some simple conventions, the logical name is shorter and allows for more
flexibility.
The WelcomeController class extends the built-in Controller class, which provides useful shortcut
methods,
like
the
render()8
method
that
loads
and
renders
a
template
(AcmeDemoBundle:Welcome:index.html.twig). The returned value is a Response object populated with
the rendered content. So, if the needs arise, the Response can be tweaked before it is sent to the browser:
Listing 1-10
1 public function indexAction()
2 {
3
$response = $this->render('AcmeDemoBundle:Welcome:index.txt.twig');
4
$response->headers->set('Content-Type', 'text/plain');
5. http://www.yaml.org/
6. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Request.html
7. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html
8. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Controller/Controller.html#render()
PDF brought to you by
generated on October 26, 2012
Chapter 1: The Big Picture | 14
5
6
7 }
return $response;
No matter how you do it, the end goal of your controller is always to return the Response object that
should be delivered back to the user. This Response object can be populated with HTML code, represent
a client redirect, or even return the contents of a JPG image with a Content-Type header of image/jpg.
Extending the Controller base class is optional. As a matter of fact, a controller can be a plain
PHP function or even a PHP closure. "The Controller" chapter of the book tells you everything
about Symfony2 controllers.
The template name, AcmeDemoBundle:Welcome:index.html.twig, is the template logical name and it
references the Resources/views/Welcome/index.html.twig file inside the AcmeDemoBundle (located at
src/Acme/DemoBundle). The bundles section below will explain why this is useful.
Now, take a look at the routing configuration again and find the _demo key:
Listing 1-11
1 # app/config/routing_dev.yml
2 _demo:
3
resource: "@AcmeDemoBundle/Controller/DemoController.php"
4
type:
annotation
5
prefix:
/demo
Symfony2 can read/import the routing information from different files written in YAML, XML, PHP,
or even embedded in PHP annotations. Here, the file's logical name is @AcmeDemoBundle/Controller/
DemoController.php and refers to the src/Acme/DemoBundle/Controller/DemoController.php file.
In this file, routes are defined as annotations on action methods:
Listing 1-12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class DemoController extends Controller
{
/**
* @Route("/hello/{name}", name="_demo_hello")
* @Template()
*/
public function helloAction($name)
{
return array('name' => $name);
}
// ...
}
The @Route() annotation defines a new route with a pattern of /hello/{name} that executes the
helloAction method when matched. A string enclosed in curly brackets like {name} is called a
placeholder. As you can see, its value can be retrieved through the $name method argument.
Even if annotations are not natively supported by PHP, you use them extensively in Symfony2 as a
convenient way to configure the framework behavior and keep the configuration next to the code.
PDF brought to you by
generated on October 26, 2012
Chapter 1: The Big Picture | 15
If you take a closer look at the controller code, you can see that instead of rendering a template
and returning a Response object like before, it just returns an array of parameters. The @Template()
annotation tells Symfony to render the template for you, passing in each variable of the array to
the template. The name of the template that's rendered follows the name of the controller. So, in
this example, the AcmeDemoBundle:Demo:hello.html.twig template is rendered (located at src/Acme/
DemoBundle/Resources/views/Demo/hello.html.twig).
The @Route() and @Template() annotations are more powerful than the simple examples shown
in this tutorial. Learn more about "annotations in controllers" in the official documentation.
Templates
The controller renders the src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig template
(or AcmeDemoBundle:Demo:hello.html.twig if you use the logical name):
Listing 1-13
1
2
3
4
5
6
7
8
{# src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig #}
{% extends "AcmeDemoBundle::layout.html.twig" %}
{% block title "Hello " ~ name %}
{% block content %}
<h1>Hello {{ name }}!</h1>
{% endblock %}
By default, Symfony2 uses Twig9 as its template engine but you can also use traditional PHP templates if
you choose. The next chapter will introduce how templates work in Symfony2.
Bundles
You might have wondered why the bundle word is used in many names we have seen so far. All the code
you write for your application is organized in bundles. In Symfony2 speak, a bundle is a structured set
of files (PHP files, stylesheets, JavaScripts, images, ...) that implements a single feature (a blog, a forum,
...) and which can be easily shared with other developers. As of now, we have manipulated one bundle,
AcmeDemoBundle. You will learn more about bundles in the last chapter of this tutorial.
Working with Environments
Now that you have a better understanding of how Symfony2 works, take a closer look at the bottom of
any Symfony2 rendered page. You should notice a small bar with the Symfony2 logo. This is called the
"Web Debug Toolbar" and it is the developer's best friend.
9. http://twig.sensiolabs.org/
PDF brought to you by
generated on October 26, 2012
Chapter 1: The Big Picture | 16
But what you see initially is only the tip of the iceberg; click on the weird hexadecimal number to reveal
yet another very useful Symfony2 debugging tool: the profiler.
Of course, you won't want to show these tools when you deploy your application to production. That's
why you will find another front controller in the web/ directory (app.php), which is optimized for the
production environment:
Listing 1-14
1 http://localhost/Symfony/web/app.php/demo/hello/Fabien
And if you use Apache with mod_rewrite enabled, you can even omit the app.php part of the URL:
Listing 1-15
1 http://localhost/Symfony/web/demo/hello/Fabien
Last but not least, on the production servers, you should point your web root directory to the web/
directory to secure your installation and have an even better looking URL:
Listing 1-16
1 http://localhost/demo/hello/Fabien
PDF brought to you by
generated on October 26, 2012
Chapter 1: The Big Picture | 17
Note that the three URLs above are provided here only as examples of how a URL looks like
when the production front controller is used (with or without mod_rewrite). If you actually try
them in an out of the box installation of Symfony Standard Edition you will get a 404 error
as AcmeDemoBundle is enabled only in dev environment and its routes imported in app/config/
routing_dev.yml.
To make your application respond faster, Symfony2 maintains a cache under the app/cache/ directory.
In the development environment (app_dev.php), this cache is flushed automatically whenever you make
changes to any code or configuration. But that's not the case in the production environment (app.php)
where performance is key. That's why you should always use the development environment when
developing your application.
Different environments of a given application differ only in their configuration. In fact, a configuration
can inherit from another one:
Listing 1-17
1 # app/config/config_dev.yml
2 imports:
3
- { resource: config.yml }
4
5 web_profiler:
6
toolbar: true
7
intercept_redirects: false
The dev environment (which loads the config_dev.yml configuration file) imports the global
config.yml file and then modifies it by, in this example, enabling the web debug toolbar.
Final Thoughts
Congratulations! You've had your first taste of Symfony2 code. That wasn't so hard, was it? There's a lot
more to explore, but you should already see how Symfony2 makes it really easy to implement web sites
better and faster. If you are eager to learn more about Symfony2, dive into the next section: "The View".
PDF brought to you by
generated on October 26, 2012
Chapter 1: The Big Picture | 18
Chapter 2
The View
After reading the first part of this tutorial, you have decided that Symfony2 was worth another 10
minutes. Great choice! In this second part, you will learn more about the Symfony2 template engine,
Twig1. Twig is a flexible, fast, and secure template engine for PHP. It makes your templates more readable
and concise; it also makes them more friendly for web designers.
Instead of Twig, you can also use PHP for your templates. Both template engines are supported by
Symfony2.
Getting familiar with Twig
If you want to learn Twig, we highly recommend you to read its official documentation2. This
section is just a quick overview of the main concepts.
A Twig template is a text file that can generate any type of content (HTML, XML, CSV, LaTeX, ...). Twig
defines two kinds of delimiters:
• {{ ... }}: Prints a variable or the result of an expression;
• {% ... %}: Controls the logic of the template; it is used to execute for loops and if
statements, for example.
Below is a minimal template that illustrates a few basics, using two variables page_title and
navigation, which would be passed into the template:
Listing 2-1
1 <!DOCTYPE html>
2 <html>
3
<head>
1. http://twig.sensiolabs.org/
2. http://twig.sensiolabs.org/documentation
PDF brought to you by
generated on October 26, 2012
Chapter 2: The View | 19
4
<title>My Webpage</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>
Comments can be included inside templates using the {# ... #} delimiter.
To render a template in Symfony, use the render method from within a controller and pass it any
variables needed in the template:
Listing 2-2
1 $this->render('AcmeDemoBundle:Demo:hello.html.twig', array(
2
'name' => $name,
3 ));
Variables passed to a template can be strings, arrays, or even objects. Twig abstracts the difference
between them and lets you access "attributes" of a variable with the dot (.) notation:
Listing 2-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{# array('name' => 'Fabien') #}
{{ name }}
{# array('user' => array('name' => 'Fabien')) #}
{{ user.name }}
{# force array lookup #}
{{ user['name'] }}
{# array('user' => new User('Fabien')) #}
{{ user.name }}
{{ user.getName }}
{# force method name lookup #}
{{ user.name() }}
{{ user.getName() }}
{# pass arguments to a method #}
{{ user.date('Y-m-d') }}
It's important to know that the curly braces are not part of the variable but the print statement. If
you access variables inside tags don't put the braces around.
PDF brought to you by
generated on October 26, 2012
Chapter 2: The View | 20
Decorating Templates
More often than not, templates in a project share common elements, like the well-known header and
footer. In Symfony2, we like to think about this problem differently: a template can be decorated by
another one. This works exactly the same as PHP classes: template inheritance allows you to build a
base "layout" template that contains all the common elements of your site and defines "blocks" that child
templates can override.
The hello.html.twig template inherits from layout.html.twig, thanks to the extends tag:
Listing 2-4
1
2
3
4
5
6
7
8
{# src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig #}
{% extends "AcmeDemoBundle::layout.html.twig" %}
{% block title "Hello " ~ name %}
{% block content %}
<h1>Hello {{ name }}!</h1>
{% endblock %}
The AcmeDemoBundle::layout.html.twig notation sounds familiar, doesn't it? It is the same notation
used to reference a regular template. The :: part simply means that the controller element is empty, so
the corresponding file is directly stored under the Resources/views/ directory.
Now, let's have a look at a simplified layout.html.twig:
Listing 2-5
1 {# src/Acme/DemoBundle/Resources/views/layout.html.twig #}
2 <div class="symfony-content">
3
{% block content %}
4
{% endblock %}
5 </div>
The {% block %} tags define blocks that child templates can fill in. All the block tag does is to tell the
template engine that a child template may override those portions of the template.
In this example, the hello.html.twig template overrides the content block, meaning that the "Hello
Fabien" text is rendered inside the div.symfony-content element.
Using Tags, Filters, and Functions
One of the best feature of Twig is its extensibility via tags, filters, and functions. Symfony2 comes
bundled with many of these built-in to ease the work of the template designer.
Including other Templates
The best way to share a snippet of code between several distinct templates is to create a new template
that can then be included from other templates.
Create an embedded.html.twig template:
Listing 2-6
1 {# src/Acme/DemoBundle/Resources/views/Demo/embedded.html.twig #}
2 Hello {{ name }}
And change the index.html.twig template to include it:
Listing 2-7
PDF brought to you by
generated on October 26, 2012
Chapter 2: The View | 21
1
2
3
4
5
6
7
{# src/Acme/DemoBundle/Resources/views/Demo/hello.html.twig #}
{% extends "AcmeDemoBundle::layout.html.twig" %}
{# override the body block from embedded.html.twig #}
{% block content %}
{% include "AcmeDemoBundle:Demo:embedded.html.twig" %}
{% endblock %}
Embedding other Controllers
And what if you want to embed the result of another controller in a template? That's very useful when
working with Ajax, or when the embedded template needs some variable not available in the main
template.
Suppose you've created a fancy action, and you want to include it inside the index template. To do this,
use the render tag:
Listing 2-8
1 {# src/Acme/DemoBundle/Resources/views/Demo/index.html.twig #}
2 {% render "AcmeDemoBundle:Demo:fancy" with {'name': name, 'color': 'green'} %}
Here, the AcmeDemoBundle:Demo:fancy string refers to the fancy action of the Demo controller. The
arguments (name and color) act like simulated request variables (as if the fancyAction were handling a
whole new request) and are made available to the controller:
Listing 2-9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/DemoBundle/Controller/DemoController.php
class DemoController extends Controller
{
public function fancyAction($name, $color)
{
// create some object, based on the $color variable
$object = ...;
return $this->render('AcmeDemoBundle:Demo:fancy.html.twig', array('name' => $name,
'object' => $object));
}
// ...
}
Creating Links between Pages
Speaking of web applications, creating links between pages is a must. Instead of hardcoding URLs in
templates, the path function knows how to generate URLs based on the routing configuration. That way,
all your URLs can be easily updated by just changing the configuration:
Listing 2-10
1 <a href="{{ path('_demo_hello', { 'name': 'Thomas' }) }}">Greet Thomas!</a>
The path function takes the route name and an array of parameters as arguments. The route name is the
main key under which routes are referenced and the parameters are the values of the placeholders defined
in the route pattern:
Listing 2-11
PDF brought to you by
generated on October 26, 2012
Chapter 2: The View | 22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
// ...
/**
* @Route("/hello/{name}", name="_demo_hello")
* @Template()
*/
public function helloAction($name)
{
return array('name' => $name);
}
The url function generates absolute URLs: {{ url('_demo_hello', { 'name': 'Thomas'}) }}.
Including Assets: images, JavaScripts, and stylesheets
What would the Internet be without images, JavaScripts, and stylesheets? Symfony2 provides the asset
function to deal with them easily:
Listing 2-12
1 <link href="{{ asset('css/blog.css') }}" rel="stylesheet" type="text/css" />
2
3 <img src="{{ asset('images/logo.png') }}" />
The asset function's main purpose is to make your application more portable. Thanks to this function,
you can move the application root directory anywhere under your web root directory without changing
anything in your template's code.
Escaping Variables
Twig is configured to automatically escape all output by default. Read Twig documentation3 to learn more
about output escaping and the Escaper extension.
Final Thoughts
Twig is simple yet powerful. Thanks to layouts, blocks, templates and action inclusions, it is very easy to
organize your templates in a logical and extensible way. However, if you're not comfortable with Twig,
you can always use PHP templates inside Symfony without any issues.
You have only been working with Symfony2 for about 20 minutes, but you can already do pretty amazing
stuff with it. That's the power of Symfony2. Learning the basics is easy, and you will soon learn that this
simplicity is hidden under a very flexible architecture.
But I'm getting ahead of myself. First, you need to learn more about the controller and that's exactly the
topic of the next part of this tutorial. Ready for another 10 minutes with Symfony2?
3. http://twig.sensiolabs.org/documentation
PDF brought to you by
generated on October 26, 2012
Chapter 2: The View | 23
Chapter 3
The Controller
Still with us after the first two parts? You are already becoming a Symfony2 addict! Without further ado,
let's discover what controllers can do for you.
Using Formats
Nowadays, a web application should be able to deliver more than just HTML pages. From XML for RSS
feeds or Web Services, to JSON for Ajax requests, there are plenty of different formats to choose from.
Supporting those formats in Symfony2 is straightforward. Tweak the route by adding a default value of
xml for the _format variable:
Listing 3-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
// ...
/**
* @Route("/hello/{name}", defaults={"_format"="xml"}, name="_demo_hello")
* @Template()
*/
public function helloAction($name)
{
return array('name' => $name);
}
By using the request format (as defined by the _format value), Symfony2 automatically selects the right
template, here hello.xml.twig:
Listing 3-2
1 <!-- src/Acme/DemoBundle/Resources/views/Demo/hello.xml.twig -->
2 <hello>
3
<name>{{ name }}</name>
4 </hello>
PDF brought to you by
generated on October 26, 2012
Chapter 3: The Controller | 24
That's all there is to it. For standard formats, Symfony2 will also automatically choose the best ContentType header for the response. If you want to support different formats for a single action, use the
{_format} placeholder in the route pattern instead:
Listing 3-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/DemoBundle/Controller/DemoController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
// ...
/**
* @Route("/hello/{name}.{_format}", defaults={"_format"="html"},
requirements={"_format"="html|xml|json"}, name="_demo_hello")
* @Template()
*/
public function helloAction($name)
{
return array('name' => $name);
}
The controller will now be called for URLs like /demo/hello/Fabien.xml or /demo/hello/
Fabien.json.
The requirements entry defines regular expressions that placeholders must match. In this example, if
you try to request the /demo/hello/Fabien.js resource, you will get a 404 HTTP error, as it does not
match the _format requirement.
Redirecting and Forwarding
If you want to redirect the user to another page, use the redirect() method:
Listing 3-4
1 return $this->redirect($this->generateUrl('_demo_hello', array('name' => 'Lucas')));
The generateUrl() is the same method as the path() function we used in templates. It takes the route
name and an array of parameters as arguments and returns the associated friendly URL.
You can also easily forward the action to another one with the forward() method. Internally, Symfony
makes a "sub-request", and returns the Response object from that sub-request:
Listing 3-5
1 $response = $this->forward('AcmeDemoBundle:Hello:fancy', array('name' => $name, 'color' =>
2 'green'));
3
// ... do something with the response or return it directly
Getting information from the Request
Besides the values of the routing placeholders, the controller also has access to the Request object:
Listing 3-6
1 $request = $this->getRequest();
2
3 $request->isXmlHttpRequest(); // is it an Ajax request?
4
5 $request->getPreferredLanguage(array('en', 'fr'));
PDF brought to you by
generated on October 26, 2012
Chapter 3: The Controller | 25
6
7 $request->query->get('page'); // get a $_GET parameter
8
9 $request->request->get('page'); // get a $_POST parameter
In a template, you can also access the Request object via the app.request variable:
Listing 3-7
1 {{ app.request.query.get('page') }}
2
3 {{ app.request.parameter('page') }}
Persisting Data in the Session
Even if the HTTP protocol is stateless, Symfony2 provides a nice session object that represents the client
(be it a real person using a browser, a bot, or a web service). Between two requests, Symfony2 stores the
attributes in a cookie by using native PHP sessions.
Storing and retrieving information from the session can be easily achieved from any controller:
Listing 3-8
1
2
3
4
5
6
7
8
9
10
$session = $this->getRequest()->getSession();
// store an attribute for reuse during a later user request
$session->set('foo', 'bar');
// in another controller for another request
$foo = $session->get('foo');
// set the user locale
$session->setLocale('fr');
You can also store small messages that will only be available for the very next request:
Listing 3-9
1
2
3
4
5
// store a message for the very next request (in a controller)
$session->setFlash('notice', 'Congratulations, your action succeeded!');
// display the message back in the next request (in a template)
{{ app.session.flash('notice') }}
This is useful when you need to set a success message before redirecting the user to another page (which
will then show the message).
Securing Resources
The Symfony Standard Edition comes with a simple security configuration that fits most common needs:
Listing 3-10
1 # app/config/security.yml
2 security:
3
encoders:
4
Symfony\Component\Security\Core\User\User: plaintext
5
6
role_hierarchy:
7
ROLE_ADMIN:
ROLE_USER
PDF brought to you by
generated on October 26, 2012
Chapter 3: The Controller | 26
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
in_memory:
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/demo/secured/login$
security: false
secured_area:
pattern:
^/demo/secured/
form_login:
check_path: /demo/secured/login_check
login_path: /demo/secured/login
logout:
path:
/demo/secured/logout
target: /demo/
This configuration requires users to log in for any URL starting with /demo/secured/ and defines two
valid users: user and admin. Moreover, the admin user has a ROLE_ADMIN role, which includes the
ROLE_USER role as well (see the role_hierarchy setting).
For readability, passwords are stored in clear text in this simple configuration, but you can use any
hashing algorithm by tweaking the encoders section.
Going to the http://localhost/Symfony/web/app_dev.php/demo/secured/hello URL
automatically redirect you to the login form because this resource is protected by a firewall.
will
You can also force the action to require a given role by using the @Secure annotation on the controller:
Listing 3-11
1
2
3
4
5
6
7
8
9
10
11
12
13
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use JMS\SecurityExtraBundle\Annotation\Secure;
/**
* @Route("/hello/admin/{name}", name="_demo_secured_hello_admin")
* @Secure(roles="ROLE_ADMIN")
* @Template()
*/
public function helloAdminAction($name)
{
return array('name' => $name);
}
Now, log in as user (who does not have the ROLE_ADMIN role) and from the secured hello page, click on
the "Hello resource secured" link. Symfony2 should return a 403 HTTP status code, indicating that the
user is "forbidden" from accessing that resource.
PDF brought to you by
generated on October 26, 2012
Chapter 3: The Controller | 27
The Symfony2 security layer is very flexible and comes with many different user providers (like
one for the Doctrine ORM) and authentication providers (like HTTP basic, HTTP digest, or X509
certificates). Read the "Security" chapter of the book for more information on how to use and
configure them.
Caching Resources
As soon as your website starts to generate more traffic, you will want to avoid generating the same
resource again and again. Symfony2 uses HTTP cache headers to manage resources cache. For simple
caching strategies, use the convenient @Cache() annotation:
Listing 3-12
1
2
3
4
5
6
7
8
9
10
11
12
13
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
/**
* @Route("/hello/{name}", name="_demo_hello")
* @Template()
* @Cache(maxage="86400")
*/
public function helloAction($name)
{
return array('name' => $name);
}
In this example, the resource will be cached for a day. But you can also use validation instead of
expiration or a combination of both if that fits your needs better.
Resource caching is managed by the Symfony2 built-in reverse proxy. But because caching is managed
using regular HTTP cache headers, you can replace the built-in reverse proxy with Varnish or Squid and
easily scale your application.
But what if you cannot cache whole pages? Symfony2 still has the solution via Edge Side Includes
(ESI), which are supported natively. Learn more by reading the "HTTP Cache" chapter of the book.
Final Thoughts
That's all there is to it, and I'm not even sure we have spent the full 10 minutes. We briefly introduced
bundles in the first part, and all the features we've learned about so far are part of the core framework
bundle. But thanks to bundles, everything in Symfony2 can be extended or replaced. That's the topic of
the next part of this tutorial.
PDF brought to you by
generated on October 26, 2012
Chapter 3: The Controller | 28
Chapter 4
The Architecture
You are my hero! Who would have thought that you would still be here after the first three parts? Your
efforts will be well rewarded soon. The first three parts didn't look too deeply at the architecture of
the framework. Because it makes Symfony2 stand apart from the framework crowd, let's dive into the
architecture now.
Understanding the Directory Structure
The directory structure of a Symfony2 application is rather flexible, but the directory structure of the
Standard Edition distribution reflects the typical and recommended structure of a Symfony2 application:
•
•
•
•
app/: The application configuration;
src/: The project's PHP code;
vendor/: The third-party dependencies;
web/: The web root directory.
The web/ Directory
The web root directory is the home of all public and static files like images, stylesheets, and JavaScript
files. It is also where each front controller lives:
Listing 4-1
1
2
3
4
5
6
7
8
9
// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();
The kernel first requires the bootstrap.php.cache file, which bootstraps the framework and registers
the autoloader (see below).
Like any front controller, app.php uses a Kernel Class, AppKernel, to bootstrap the application.
PDF brought to you by
generated on October 26, 2012
Chapter 4: The Architecture | 29
The app/ Directory
The AppKernel class is the main entry point of the application configuration and as such, it is stored in
the app/ directory.
This class must implement two methods:
• registerBundles() must return an array of all bundles needed to run the application;
• registerContainerConfiguration() loads the application configuration (more on this
later).
PHP autoloading can be configured via app/autoload.php:
Listing 4-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// app/autoload.php
use Symfony\Component\ClassLoader\UniversalClassLoader;
$loader = new UniversalClassLoader();
$loader->registerNamespaces(array(
'Symfony'
=> array(__DIR__.'/../vendor/symfony/src', __DIR__.'/../vendor/
bundles'),
'Sensio'
=> __DIR__.'/../vendor/bundles',
'JMS'
=> __DIR__.'/../vendor/bundles',
'Doctrine\\Common' => __DIR__.'/../vendor/doctrine-common/lib',
'Doctrine\\DBAL'
=> __DIR__.'/../vendor/doctrine-dbal/lib',
'Doctrine'
=> __DIR__.'/../vendor/doctrine/lib',
'Monolog'
=> __DIR__.'/../vendor/monolog/src',
'Assetic'
=> __DIR__.'/../vendor/assetic/src',
'Metadata'
=> __DIR__.'/../vendor/metadata/src',
));
$loader->registerPrefixes(array(
'Twig_Extensions_' => __DIR__.'/../vendor/twig-extensions/lib',
'Twig_'
=> __DIR__.'/../vendor/twig/lib',
));
// ...
$loader->registerNamespaceFallbacks(array(
__DIR__.'/../src',
));
$loader->register();
The UniversalClassLoader1 is used to autoload files that respect either the technical interoperability
standards2 for PHP 5.3 namespaces or the PEAR naming convention3 for classes. As you can see here, all
dependencies are stored under the vendor/ directory, but this is just a convention. You can store them
wherever you want, globally on your server or locally in your projects.
If you want to learn more about the flexibility of the Symfony2 autoloader, read the "The
ClassLoader Component" chapter.
Understanding the Bundle System
This section introduces one of the greatest and most powerful features of Symfony2, the bundle system.
1. http://api.symfony.com/2.0/Symfony/Component/ClassLoader/UniversalClassLoader.html
2. http://symfony.com/PSR0
3. http://pear.php.net/
PDF brought to you by
generated on October 26, 2012
Chapter 4: The Architecture | 30
A bundle is kind of like a plugin in other software. So why is it called a bundle and not a plugin? This
is because everything is a bundle in Symfony2, from the core framework features to the code you write
for your application. Bundles are first-class citizens in Symfony2. This gives you the flexibility to use prebuilt features packaged in third-party bundles or to distribute your own bundles. It makes it easy to pick
and choose which features to enable in your application and optimize them the way you want. And at the
end of the day, your application code is just as important as the core framework itself.
Registering a Bundle
An application is made up of bundles as defined in the registerBundles() method of the AppKernel
class. Each bundle is a directory that contains a single Bundle class that describes it:
Listing 4-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
);
if (in_array($this->getEnvironment(), array('dev', 'test'))) {
$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}
return $bundles;
}
In addition to the AcmeDemoBundle that we have already talked about, notice that the kernel also
enables other bundles such as the FrameworkBundle, DoctrineBundle, SwiftmailerBundle, and
AsseticBundle bundle. They are all part of the core framework.
Configuring a Bundle
Each bundle can be customized via configuration files written in YAML, XML, or PHP. Have a look at
the default configuration:
Listing 4-4
1 # app/config/config.yml
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
PDF brought to you by
generated on October 26, 2012
Chapter 4: The Architecture | 31
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
validation:
{ enable_annotations: true }
templating:
{ engines: ['twig'] } #assets_version: SomeVersionScheme
session:
default_locale: "%locale%"
auto_start:
true
# Twig Configuration
twig:
debug:
"%kernel.debug%"
strict_variables: "%kernel.debug%"
# Assetic Configuration
assetic:
debug:
"%kernel.debug%"
use_controller: false
filters:
cssrewrite: ~
# closure:
#
jar: "%kernel.root_dir%/java/compiler.jar"
# yui_css:
#
jar: "%kernel.root_dir%/java/yuicompressor-2.4.2.jar"
# Doctrine Configuration
doctrine:
dbal:
driver: "%database_driver%"
host:
"%database_host%"
dbname: "%database_name%"
user:
"%database_user%"
password: "%database_password%"
charset: UTF8
orm:
auto_generate_proxy_classes: "%kernel.debug%"
auto_mapping: true
# Swiftmailer Configuration
swiftmailer:
transport: "%mailer_transport%"
host:
"%mailer_host%"
username: "%mailer_user%"
password: "%mailer_password%"
jms_security_extra:
secure_controllers: true
secure_all_services: false
Each entry like framework defines the configuration for a specific bundle. For example, framework
configures the FrameworkBundle while swiftmailer configures the SwiftmailerBundle.
Each environment can override the default configuration by providing a specific configuration file. For
example, the dev environment loads the config_dev.yml file, which loads the main configuration (i.e.
config.yml) and then modifies it to add some debugging tools:
Listing 4-5
1 # app/config/config_dev.yml
2 imports:
3
- { resource: config.yml }
4
PDF brought to you by
generated on October 26, 2012
Chapter 4: The Architecture | 32
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
framework:
router:
{ resource: "%kernel.root_dir%/config/routing_dev.yml" }
profiler: { only_exceptions: false }
web_profiler:
toolbar: true
intercept_redirects: false
monolog:
handlers:
main:
type:
path:
level:
firephp:
type:
level:
stream
"%kernel.logs_dir%/%kernel.environment%.log"
debug
firephp
info
assetic:
use_controller: true
Extending a Bundle
In addition to being a nice way to organize and configure your code, a bundle can extend another bundle.
Bundle inheritance allows you to override any existing bundle in order to customize its controllers,
templates, or any of its files. This is where the logical names (e.g. @AcmeDemoBundle/Controller/
SecuredController.php) come in handy: they abstract where the resource is actually stored.
Logical File Names
When you want to reference a file from a bundle, use this notation: @BUNDLE_NAME/path/to/file;
Symfony2 will resolve @BUNDLE_NAME to the real path to the bundle. For instance, the logical path
@AcmeDemoBundle/Controller/DemoController.php would be converted to src/Acme/DemoBundle/
Controller/DemoController.php, because Symfony knows the location of the AcmeDemoBundle.
Logical Controller Names
For
controllers,
you
need
to
reference
method
names
using
the
format
BUNDLE_NAME:CONTROLLER_NAME:ACTION_NAME. For instance, AcmeDemoBundle:Welcome:index maps to
the indexAction method from the Acme\DemoBundle\Controller\WelcomeController class.
Logical Template Names
For templates, the logical name AcmeDemoBundle:Welcome:index.html.twig is converted to the file
path src/Acme/DemoBundle/Resources/views/Welcome/index.html.twig. Templates become even
more interesting when you realize they don't need to be stored on the filesystem. You can easily store
them in a database table for instance.
Extending Bundles
If you follow these conventions, then you can use bundle inheritance to "override" files, controllers
or templates. For example, you can create a bundle - AcmeNewBundle - and specify that it overrides
AcmeDemoBundle. When Symfony loads the AcmeDemoBundle:Welcome:index controller, it will first
look for the WelcomeController class in AcmeNewBundle and, if it doesn't exist, then look inside
AcmeDemoBundle. This means that one bundle can override almost any part of another bundle!
PDF brought to you by
generated on October 26, 2012
Chapter 4: The Architecture | 33
Do you understand now why Symfony2 is so flexible? Share your bundles between applications, store
them locally or globally, your choice.
Using Vendors
Odds are that your application will depend on third-party libraries. Those should be stored in the
vendor/ directory. This directory already contains the Symfony2 libraries, the SwiftMailer library, the
Doctrine ORM, the Twig templating system, and some other third party libraries and bundles.
Understanding the Cache and Logs
Symfony2 is probably one of the fastest full-stack frameworks around. But how can it be so fast if it
parses and interprets tens of YAML and XML files for each request? The speed is partly due to its cache
system. The application configuration is only parsed for the very first request and then compiled down
to plain PHP code stored in the app/cache/ directory. In the development environment, Symfony2 is
smart enough to flush the cache when you change a file. But in the production environment, it is your
responsibility to clear the cache when you update your code or change its configuration.
When developing a web application, things can go wrong in many ways. The log files in the app/logs/
directory tell you everything about the requests and help you fix the problem quickly.
Using the Command Line Interface
Each application comes with a command line interface tool (app/console) that helps you maintain your
application. It provides commands that boost your productivity by automating tedious and repetitive
tasks.
Run it without any arguments to learn more about its capabilities:
Listing 4-6
1 $ php app/console
The --help option helps you discover the usage of a command:
Listing 4-7
1 $ php app/console router:debug --help
Final Thoughts
Call me crazy, but after reading this part, you should be comfortable with moving things around and
making Symfony2 work for you. Everything in Symfony2 is designed to get out of your way. So, feel free
to rename and move directories around as you see fit.
And that's all for the quick tour. From testing to sending emails, you still need to learn a lot to become a
Symfony2 master. Ready to dig into these topics now? Look no further - go to the official The Book and
pick any topic you want.
PDF brought to you by
generated on October 26, 2012
Chapter 4: The Architecture | 34
Part II
The Book
Chapter 5
Symfony2 and HTTP Fundamentals
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/
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 36
And while the actual language used is a bit more formal, it's still dead-simple. HTTP is the term used to
describe this simple text-based language. And no matter how you develop on the web, the goal of your
server is always to understand simple text requests, and return simple text responses.
Symfony2 is built from the ground-up around that reality. Whether you realize it or not, HTTP is
something you use everyday. With Symfony2, you'll learn how to master it.
Step1: The Client sends a Request
Every conversation on the web starts with a request. The request is a text message created by a client (e.g.
a browser, an iPhone app, etc) in a special format known as HTTP. The client sends that request to a
server, and then waits for the response.
Take a look at the first part of the interaction (the request) between a browser and the xkcd web server:
In HTTP-speak, this HTTP request would actually look something like this:
Listing 5-1
1
2
3
4
GET / HTTP/1.1
Host: xkcd.com
Accept: text/html
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.
The URI (e.g. /, /contact, etc) is the unique address or location that identifies the resource the client
wants. The HTTP method (e.g. GET) defines what you want to do with the resource. The HTTP methods
are the verbs of the request and define the few common ways that you can act upon the resource:
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 37
GET
Retrieve the resource from the server
POST
Create a resource on the server
PUT
Update the resource on the server
DELETE Delete the resource from the server
With this in mind, you can imagine what an HTTP request might look like to delete a specific blog entry,
for example:
Listing 5-2
1 DELETE /blog/15 HTTP/1.1
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 (UserAgent). Many other headers exist and can be found on Wikipedia's List of HTTP header fields2 article.
Step 2: The Server returns a Response
Once a server has received the request, it knows exactly which resource the client needs (via the URI)
and what the client wants to do with that resource (via the method). For example, in the case of a GET
request, the server prepares the resource and returns it in an HTTP response. Consider the response from
the xkcd web server:
Translated into HTTP, the response sent back to the browser will look something like this:
Listing 5-3
1
2
3
4
5
6
HTTP/1.1 200 OK
Date: Sat, 02 Apr 2011 21:05:05 GMT
Server: lighttpd/1.4.19
Content-Type: text/html
<html>
2. http://en.wikipedia.org/wiki/List_of_HTTP_header_fields
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 38
7
<!-- ... HTML for the xkcd comic -->
8 </html>
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.
Requests, Responses and Web Development
This request-response conversation is the fundamental process that drives all communication on the web.
And as important and powerful as this process is, it's inescapably simple.
The most important fact is this: regardless of the language you use, the type of application you build
(web, mobile, JSON API), or the development philosophy you follow, the end goal of an application is
always to understand each request and create and return the appropriate response.
Symfony is architected to match this reality.
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.
Requests and Responses in PHP
So how do you interact with the "request" and create a "response" when using PHP? In reality, PHP
abstracts you a bit from the whole process:
Listing 5-4
1
2
3
4
5
6
7
<?php
$uri = $_SERVER['REQUEST_URI'];
$foo = $_GET['foo'];
header('Content-type: text/html');
echo 'The URI requested is: '.$uri;
echo 'The value of the "foo" parameter is: '.$foo;
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
3. http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
4.
5.
6.
7.
http://en.wikipedia.org/wiki/Internet_media_type#List_of_common_media_types
http://www.w3.org/Protocols/rfc2616/rfc2616.html
http://datatracker.ietf.org/wg/httpbis/
https://addons.mozilla.org/en-US/firefox/addon/live-http-headers/
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 39
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 5-5
1
2
3
4
5
6
7
HTTP/1.1 200 OK
Date: Sat, 03 Apr 2011 02:14:33 GMT
Server: Apache/2.2.17 (Unix)
Content-Type: text/html
The URI requested is: /testing?foo=symfony
The value of the "foo" parameter is: symfony
Requests and Responses in Symfony
Symfony provides an alternative to the raw PHP approach via two classes that allow you to interact
with the HTTP request and response in an easier way. The Request8 class is a simple object-oriented
representation of the HTTP request message. With it, you have all the request information at your
fingertips:
Listing 5-6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
// the URI being requested (e.g. /about) minus any query parameters
$request->getPathInfo();
// retrieve GET and POST variables respectively
$request->query->get('foo');
$request->request->get('bar', 'default value if bar does not exist');
// retrieve SERVER variables
$request->server->get('HTTP_HOST');
// retrieves an instance of UploadedFile identified by foo
$request->files->get('foo');
// retrieve a COOKIE value
$request->cookies->get('PHPSESSID');
// retrieve an HTTP request header, with normalized, lowercase keys
$request->headers->get('host');
$request->headers->get('content_type');
$request->getMethod();
$request->getLanguages();
// GET, POST, PUT, DELETE, HEAD
// an array of languages the client accepts
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).
8. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Request.html
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 40
ParameterBags and Request attributes
As seen above, the $_GET and $_POST variables are accessible via the public query and request
properties respectively. Each of these objects is a ParameterBag9 object, which has methods like
get()10, has()11, all()12 and more. In fact, every public property used in the previous example is
some instance of the ParameterBag.
The Request class also has a public attributes property, which holds special data related to how
the application works internally. For the Symfony2 framework, the attributes holds the values
returned by the matched route, like _controller, id (if you have an {id} wildcard), and even the
name of the matched route (_route). The attributes property exists entirely to be a place where
you can prepare and store context-specific information about the request.
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:
Listing 5-7
1
2
3
4
5
6
7
8
9
use Symfony\Component\HttpFoundation\Response;
$response = new Response();
$response->setContent('<html><body><h1>Hello world!</h1></body></html>');
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/html');
// prints the HTTP headers followed by the content
$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.
The Journey from the Request to the Response
Like HTTP itself, the Request and Response objects are pretty simple. The hard part of building an
application is writing what comes in between. In other words, the real work comes in writing the code
that interprets the request information and creates the response.
Your application probably does many things, like sending emails, handling form submissions, saving
things to a database, rendering HTML pages and protecting content with security. How can you manage
all of this and still keep your code organized and maintainable?
Symfony was created to solve these problems so that you don't have to.
9.
10.
11.
12.
http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/ParameterBag.html
http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/ParameterBag.html#get()
http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/ParameterBag.html#has()
http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/ParameterBag.html#all()
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 41
The Front Controller
Traditionally, applications were built so that each "page" of a site was its own physical file:
Listing 5-8
1 index.php
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:
/index.php
executes index.php
/index.php/contact executes index.php
/index.php/blog
executes index.php
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:
Listing 5-9
1
2
3
4
5
6
7
8
9
10
11
12
// index.php
$request = Request::createFromGlobals();
$path = $request->getPathInfo(); // the URI path being requested
if (in_array($path, array('', '/')) {
$response = new Response('Welcome to the homepage.');
} elseif ($path == '/contact') {
$response = new Response('Contact us');
} else {
$response = new Response('Page not found.', 404);
}
$response->send();
Solving this problem can be difficult. Fortunately it's exactly what Symfony is designed to do.
The Symfony Application Flow
When you let Symfony handle each request, life is much easier. Symfony follows the same simple pattern
for every request:
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 42
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:
• Each request executes a front controller file;
• The routing system determines which PHP function should be executed based on information
from the request and routing configuration you've created;
• The correct PHP function is executed, where your code creates and returns the appropriate
Response object.
A Symfony Request in Action
Without diving into too much detail, let's see this process in action. Suppose you want to add a
/contact page to your Symfony application. First, start by adding an entry for /contact to your routing
configuration file:
Listing 5-10
1 contact:
2
pattern: /contact
3
defaults: { _controller: AcmeDemoBundle:Main:contact }
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 5-11
1 class MainController
2 {
3
public function contactAction()
4
{
5
return new Response('<h1>Contact us!</h1>');
6
}
7 }
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 43
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.
Symfony2: Build your App, not your Tools.
You now know that the goal of any app is to interpret each incoming request and create an appropriate
response. As an application grows, it becomes more difficult to keep your code organized and
maintainable. Invariably, the same complex tasks keep coming up over and over again: persisting things
to the database, rendering and reusing templates, handling form submissions, sending emails, validating
user input and handling security.
The good news is that none of these problems is unique. Symfony provides a framework full of tools that
allow you to build your application, not your tools. With Symfony2, nothing is imposed on you: you're
free to use the full Symfony framework, or just one piece of Symfony all by itself.
Standalone Tools: The Symfony2 Components
So what is Symfony2? First, Symfony2 is a collection of over twenty independent libraries that can be
used inside any PHP project. These libraries, called the Symfony2 Components, contain something useful
for almost any situation, regardless of how your project is developed. To name a few:
• 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);
• Form13 - A full-featured and flexible framework for creating forms and handling form
submissions;
• Validator14 A system for creating rules about data and then validating whether or not usersubmitted data follows those rules;
• ClassLoader An autoloading library that allows PHP classes to be used without needing to
manually require the files containing those classes;
• Templating A toolkit for rendering templates, handling template inheritance (i.e. a template is
decorated with a layout) and performing other common template tasks;
• Security15 - A powerful library for handling all types of security inside an application;
• Translation16 A framework for translating strings in your application.
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.
The Full Solution: The Symfony2 Framework
So then, what is the Symfony2 Framework? The Symfony2 Framework is a PHP library that accomplishes
two distinct tasks:
1. Provides a selection of components (i.e. the Symfony2 Components) and third-party libraries
(e.g. Swiftmailer for sending emails);
13. https://github.com/symfony/Form
14. https://github.com/symfony/Validator
15. https://github.com/symfony/Security
16. https://github.com/symfony/Translation
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 44
2. Provides sensible configuration and a "glue" library that ties all of these pieces together.
The goal of the framework is to integrate many independent tools in order to provide a consistent
experience for the developer. Even the framework itself is a Symfony2 bundle (i.e. a plugin) that can be
configured or replaced entirely.
Symfony2 provides a powerful set of tools for rapidly developing web applications without imposing on
your application. Normal users can quickly start development by using a Symfony2 distribution, which
provides a project skeleton with sensible defaults. For more advanced users, the sky is the limit.
PDF brought to you by
generated on October 26, 2012
Chapter 5: Symfony2 and HTTP Fundamentals | 45
Chapter 6
Symfony2 versus Flat PHP
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.
A simple Blog in flat PHP
In this chapter, you'll build the token blog application using only flat PHP. To begin, create a single page
that displays blog entries that have been persisted to the database. Writing in flat PHP is quick and dirty:
Listing 6-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);
$result = mysql_query('SELECT id, title FROM post', $link);
?>
<!doctype html>
<html>
<head>
<title>List of Posts</title>
</head>
<body>
<h1>List of Posts</h1>
<ul>
<?php while ($row = mysql_fetch_assoc($result)): ?>
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 46
18
19
20
21
22
23
24
25
26
27
28
29
30
<li>
<a href="/show.php?id=<?php echo $row['id'] ?>">
<?php echo $row['title'] ?>
</a>
</li>
<?php endwhile; ?>
</ul>
</body>
</html>
<?php
mysql_close($link);
?>
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:
• No error-checking: What if the connection to the database fails?
• Poor organization: If the application grows, this single file will become increasingly
unmaintainable. Where should you put code to handle a form submission? How can you
validate data? Where should code go for sending emails?
• Difficult to reuse code: Since everything is in one file, there's no way to reuse any part of the
application for other "pages" of the blog.
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.
Let's get to work on solving these problems and more.
Isolating the Presentation
The code can immediately gain from separating the application "logic" from the code that prepares the
HTML "presentation":
Listing 6-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
// index.php
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);
$result = mysql_query('SELECT id, title FROM post', $link);
$posts = array();
while ($row = mysql_fetch_assoc($result)) {
$posts[] = $row;
}
mysql_close($link);
// include the HTML presentation code
require 'templates/list.php';
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. http://www.doctrine-project.org
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 47
Listing 6-3
1 <!doctype html>
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).
Isolating the Application (Domain) Logic
So far the application contains only one page. But what if a second page needed to use the same database
connection, or even the same array of blog posts? Refactor the code so that the core behavior and dataaccess functions of the application are isolated in a new file called model.php:
Listing 6-4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
// model.php
function open_database_connection()
{
$link = mysql_connect('localhost', 'myuser', 'mypassword');
mysql_select_db('blog_db', $link);
return $link;
}
function close_database_connection($link)
{
mysql_close($link);
}
function get_all_posts()
{
$link = open_database_connection();
$result = mysql_query('SELECT id, title FROM post', $link);
$posts = array();
while ($row = mysql_fetch_assoc($result)) {
$posts[] = $row;
}
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 48
25
26
27
28 }
close_database_connection($link);
return $posts;
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.
The controller (index.php) is now very simple:
Listing 6-5
1
2
3
4
5
6
<?php
require_once 'model.php';
$posts = get_all_posts();
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.
Isolating the Layout
At this point, the application has been refactored into three distinct pieces offering various advantages
and the opportunity to reuse almost everything on different pages.
The only part of the code that can't be reused is the page layout. Fix that by creating a new layout.php
file:
Listing 6-6
1 <!-- templates/layout.php -->
2 <html>
3
<head>
4
<title><?php echo $title ?></title>
5
</head>
6
<body>
7
<?php echo $content ?>
8
</body>
9 </html>
The template (templates/list.php) can now be simplified to "extend" the layout:
Listing 6-7
1 <?php $title = 'List of Posts' ?>
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; ?>
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 49
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.
Adding a Blog "show" Page
The blog "list" page has now been refactored so that the code is better-organized and reusable. To prove
it, add a blog "show" page, which displays an individual blog post identified by an id query parameter.
To begin, create a new function in the model.php file that retrieves an individual blog result based on a
given id:
Listing 6-8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// model.php
function get_post_by_id($id)
{
$link = open_database_connection();
$id = intval($id);
$query = 'SELECT date, title, body FROM post WHERE id = '.$id;
$result = mysql_query($query);
$row = mysql_fetch_assoc($result);
close_database_connection($link);
return $row;
}
Next, create a new file called show.php - the controller for this new page:
Listing 6-9
1
2
3
4
5
6
<?php
require_once 'model.php';
$post = get_post_by_id($_GET['id']);
require 'templates/show.php';
Finally, create the new template file - templates/show.php - to render the individual blog post:
Listing 6-10
1 <?php $title = $post['title'] ?>
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() ?>
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 50
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...
A "Front Controller" to the Rescue
The solution is to use a front controller: a single PHP file through which all requests are processed. With
a front controller, the URIs for the application change slightly, but start to become more flexible:
Listing 6-11
1
2
3
4
5
6
7
Without a front controller
/index.php
=> Blog post list page (index.php executed)
/show.php
=> Blog post show page (show.php executed)
With index.php as the front controller
/index.php
=> Blog post list page (index.php executed)
/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.
When using a front controller, a single PHP file (index.php in this case) renders every request. For
the blog post show page, /index.php/show will actually execute the index.php file, which is now
responsible for routing requests internally based on the full URI. As you'll see, a front controller is a very
powerful tool.
Creating the Front Controller
You're about to take a big step with the application. With one file handling all requests, you can
centralize things such as security handling, configuration loading, and routing. In this application,
index.php must now be smart enough to render the blog post list page or the blog post show page based
on the requested URI:
Listing 6-12
1
2
3
4
5
6
7
8
9
<?php
// index.php
// load and initialize any global libraries
require_once 'model.php';
require_once 'controllers.php';
// route the request internally
$uri = $_SERVER['REQUEST_URI'];
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 51
10
11
12
13
14
15
16
17
if ($uri == '/index.php') {
list_action();
} elseif ($uri == '/index.php/show' && isset($_GET['id'])) {
show_action($_GET['id']);
} else {
header('Status: 404 Not Found');
echo '<html><body><h1>Page Not Found</h1></body></html>';
}
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:
Listing 6-13
1
2
3
4
5
6
7
8
9
10
11
function list_action()
{
$posts = get_all_posts();
require 'templates/list.php';
}
function show_action($id)
{
$post = get_post_by_id($id);
require 'templates/show.php';
}
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.
Another advantage of a front controller is flexible URLs. Notice that the URL to the blog post show
page could be changed from /show to /read by changing code in only one location. Before, an
entire file needed to be renamed. In Symfony2, URLs are even more flexible.
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?
Add a Touch of Symfony2
Symfony2 to the rescue. Before actually using Symfony2, you need to make sure PHP knows how to find
the Symfony2 classes. This is accomplished via an autoloader that Symfony provides. An autoloader is a
tool that makes it possible to start using PHP classes without explicitly including the file containing the
class.
First, download symfony2 and place it into a vendor/symfony/ directory. Next, create an app/
bootstrap.php file. Use it to require the two files in the application and to configure the autoloader:
Listing 6-14
2. http://symfony.com/download
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 52
1
2
3
4
5
6
7
8
9
10
11
12
<?php
// bootstrap.php
require_once 'model.php';
require_once 'controllers.php';
require_once 'vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php';
$loader = new Symfony\Component\ClassLoader\UniversalClassLoader();
$loader->registerNamespaces(array(
'Symfony' => __DIR__.'/../vendor/symfony/src',
));
$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:
Listing 6-15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// index.php
require_once 'app/bootstrap.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$uri = $request->getPathInfo();
if ($uri == '/') {
$response = list_action();
} elseif ($uri == '/show' && $request->query->has('id')) {
$response = show_action($request->query->get('id'));
} else {
$html = '<html><body><h1>Page Not Found</h1></body></html>';
$response = new Response($html, 404);
}
// echo the headers and send the response
$response->send();
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:
Listing 6-16
1
2
3
4
5
6
7
// controllers.php
use Symfony\Component\HttpFoundation\Response;
function list_action()
{
$posts = get_all_posts();
$html = render_template('templates/list.php', array('posts' => $posts));
3. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Request.html
4. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 53
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
return new Response($html);
}
function show_action($id)
{
$post = get_post_by_id($id);
$html = render_template('templates/show.php', array('post' => $post));
return new Response($html);
}
// helper function to render templates
function render_template($path, array $args)
{
extract($args);
ob_start();
require $path;
$html = ob_get_clean();
return $html;
}
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().
The Response object gives flexibility when constructing the HTTP response, allowing HTTP headers
and content to be added via an object-oriented interface. And while the responses in this application are
simple, this flexibility will pay dividends as your application grows.
The Sample Application in Symfony2
The blog has come a long way, but it still contains a lot of code for such a simple application. Along the
way, we've also invented a simple routing system and a method using ob_start() and ob_get_clean()
to render templates. If, for some reason, you needed to continue building this "framework" from scratch,
you could at least use Symfony's standalone Routing5 and Templating6 components, which already solve
these problems.
Instead of re-solving common problems, you can let Symfony2 take care of them for you. Here's the same
sample application, now built in Symfony2:
Listing 6-17
1
2
3
4
5
6
7
8
9
10
<?php
// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
public function listAction()
{
5. https://github.com/symfony/Routing
6. https://github.com/symfony/Templating
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 54
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:
Listing 6-18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- src/Acme/BlogBundle/Resources/views/Blog/list.html.php -->
<?php $view->extend('::layout.html.php') ?>
<?php $view['slots']->set('title', 'List of Posts') ?>
<h1>List of Posts</h1>
<ul>
<?php foreach ($posts as $post): ?>
<li>
<a href="<?php echo $view['router']->generate('blog_show', array('id' =>
$post->getId())) ?>">
<?php echo $post->getTitle() ?>
</a>
</li>
<?php endforeach; ?>
</ul>
The layout is nearly identical:
Listing 6-19
1
2
3
4
5
6
7
8
9
10
<!-- app/Resources/views/layout.html.php -->
<!doctype html>
<html>
<head>
<title><?php echo $view['slots']->output('title', 'Default title') ?></title>
</head>
<body>
<?php echo $view['slots']->output('_content') ?>
</body>
</html>
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 55
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 6-20
1 # app/config/routing.yml
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!):
Listing 6-21
1
2
3
4
5
6
7
8
9
<?php
// web/app.php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../app/AppKernel.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('prod', false);
$kernel->handle(Request::createFromGlobals())->send();
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.
Where Symfony2 Delivers
In the upcoming chapters, you'll learn more about how each piece of Symfony works and the
recommended organization of a project. For now, let's see how migrating the blog from flat PHP to
Symfony2 has improved life:
• 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.
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 56
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:
Listing 6-22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{# src/Acme/BlogBundle/Resources/views/Blog/list.html.twig #}
{% extends "::layout.html.twig" %}
{% block title %}List of Posts{% endblock %}
{% block body %}
<h1>List of Posts</h1>
<ul>
{% for post in posts %}
<li>
<a href="{{ path('blog_show', {'id': post.id}) }}">
{{ post.title }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %}
The corresponding layout.html.twig template is also easier to write:
Listing 6-23
1
2
3
4
5
6
7
8
9
10
{# app/Resources/views/layout.html.twig #}
<!doctype html>
<html>
<head>
<title>{% block title %}Default title{% endblock %}</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</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.
Learn more from the Cookbook
• How to use PHP instead of Twig for Templates
• How to define Controllers as Services
7. http://www.varnish-cache.org
8. http://knpbundles.com/
9. http://twig.sensiolabs.org
PDF brought to you by
generated on October 26, 2012
Chapter 6: Symfony2 versus Flat PHP | 57
Chapter 7
Installing and Configuring Symfony
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.
Downloading a Symfony2 Distribution
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
PDF brought to you by
generated on October 26, 2012
Chapter 7: Installing and Configuring Symfony | 58
• Download the distribution with or without vendors. If you have Git4 installed on your
computer, you should download Symfony2 "without vendors", as it adds a bit more flexibility
when including third-party/vendor libraries.
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):
Listing 7-1
1
2
3
4
5
# for .tgz file
$ tar zxvf Symfony_Standard_Vendors_2.0.###.tgz
# for a .zip file
$ unzip Symfony_Standard_Vendors_2.0.###.zip
When you're finished, you should have a Symfony/ directory that looks something like this:
Listing 7-2
1 www/ <- your web root directory
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
...
You can easily override the default directory structure. See How to override Symfony's Default
Directory Structure for more information.
Updating Vendors
Finally, if you downloaded the archive "without vendors", install the vendors by running the following
command from the command line:
Listing 7-3
1 $ php bin/vendors install
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".
Configuration and Setup
At this point, all of the needed third-party libraries now live in the vendor/ directory. You also have a
default application setup in app/ and some sample code inside the src/ directory.
Symfony2 comes with a visual server configuration tester to help make sure your Web server and PHP
are configured to use Symfony. Use the following URL to check your configuration:
4. http://git-scm.com/
PDF brought to you by
generated on October 26, 2012
Chapter 7: Installing and Configuring Symfony | 59
Listing 7-4
1 http://localhost/Symfony/web/config.php
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:
Listing 7-5
1
2
3
4
5
$ rm -rf app/cache/*
$ rm -rf app/logs/*
$ sudo chmod +a "www-data allow delete,write,append,file_inherit,directory_inherit"
app/cache app/logs
$ sudo chmod +a "`whoami` allow delete,write,append,file_inherit,directory_inherit"
app/cache app/logs
2. Using Acl on a system that does not support chmod +a
Some systems don't support chmod +a, but do support another utility called setfacl. You may
need to enable ACL support5 on your partition and install setfacl before using it (as is the case with
Ubuntu), like so:
Listing 7-6
1 $ sudo setfacl -R -m u:www-data:rwx -m u:`whoami`:rwx app/cache app/logs
2 $ sudo setfacl -dR -m u:www-data:rwx -m u:`whoami`:rwx app/cache app/logs
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:
Listing 7-7
1 umask(0002); // This will let the permissions be 0775
2
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.
When everything is fine, click on "Go to the Welcome page" to request your first "real" Symfony2
webpage:
Listing 7-8
5. https://help.ubuntu.com/community/FilePermissionsACLs
PDF brought to you by
generated on October 26, 2012
Chapter 7: Installing and Configuring Symfony | 60
1 http://localhost/Symfony/web/app_dev.php/
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.md 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.
Using Source Control
If you're using a version control system like Git or Subversion, you can setup your version control
system and begin committing your project to it as normal. The Symfony Standard edition is the starting
point for your new project.
For specific instructions on how best to setup your project to be stored in git, see How to Create and store
a Symfony2 Project in git.
Ignoring the vendor/ Directory
If you've downloaded the archive without vendors, you can safely ignore the entire vendor/ directory
and not commit it to source control. With Git, this is done by creating and adding the following to a
.gitignore file:
Listing 7-9
1 vendor/
PDF brought to you by
generated on October 26, 2012
Chapter 7: Installing and Configuring Symfony | 61
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.
PDF brought to you by
generated on October 26, 2012
Chapter 7: Installing and Configuring Symfony | 62
Chapter 8
Creating Pages in Symfony2
Creating a new page in Symfony2 is a simple two-step process:
• 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!
The "Hello Symfony!" Page
Let's start with a spin off of the classic "Hello World!" application. When you're finished, the user will be
able to get a personal greeting (e.g. "Hello Symfony") by going to the following URL:
Listing 8-1
1 http://localhost/app_dev.php/hello/Symfony
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:
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 63
• For Apache HTTP Server, refer to Apache's DirectoryIndex documentation1.
• For Nginx, refer to Nginx HttpCoreModule location documentation2.
Before you begin: Create the Bundle
Before you begin, you'll need to create a bundle. In Symfony2, a bundle is like a plugin, except that all of
the code in your application will live inside a bundle.
A bundle is nothing more than a directory that houses everything related to a specific feature, including
PHP classes, configuration, and even stylesheets and Javascript files (see The Bundle System).
To create a bundle called AcmeHelloBundle (a play bundle that you'll build in this chapter), run the
following command and follow the on-screen instructions (use all of the default options):
Listing 8-2
1 $ php app/console generate:bundle --namespace=Acme/HelloBundle --format=yml
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:
Listing 8-3
1
2
3
4
5
6
7
8
9
10
11
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
new Acme\HelloBundle\AcmeHelloBundle(),
);
// ...
return $bundles;
}
Now that you have a bundle setup, you can begin building your application inside the bundle.
Step 1: Create the Route
By default, the routing configuration file in a Symfony2 application is located at app/config/
routing.yml. Like all configuration in Symfony2, you can also choose to use XML or PHP out of the box
to configure routes.
If you look at the main routing file, you'll see that Symfony already added an entry when you generated
the AcmeHelloBundle:
Listing 8-4
1 # app/config/routing.yml
2 AcmeHelloBundle:
3
resource: "@AcmeHelloBundle/Resources/config/routing.yml"
4
prefix:
/
This entry is pretty basic: it tells Symfony to load routing configuration from the Resources/config/
routing.yml file that lives inside the AcmeHelloBundle. This means that you place routing configuration
directly in app/config/routing.yml or organize your routes throughout your application, and import
them from here.
1. http://httpd.apache.org/docs/2.0/mod/mod_dir.html
2. http://wiki.nginx.org/HttpCoreModule#location
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 64
Now that the routing.yml file from the bundle is being imported, add the new route that defines the
URL of the page that you're about to create:
Listing 8-5
1 # src/Acme/HelloBundle/Resources/config/routing.yml
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.
Step 2: Create the Controller
When a URL such as /hello/Ryan is handled by the application, the hello route is matched and the
AcmeHelloBundle:Hello:index controller is executed by the framework. The second step of the pagecreation process is to create that controller.
The controller - AcmeHelloBundle:Hello:index is the logical name of the controller, and it maps to the
indexAction method of a PHP class called Acme\HelloBundle\Controller\HelloController. Start by
creating this file inside your AcmeHelloBundle:
Listing 8-6
1
2
3
4
5
6
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
class HelloController
{
}
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:
Listing 8-7
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 65
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:
Listing 8-8
1 http://localhost/app_dev.php/hello/Ryan
You can also view your app in the "prod" environment by visiting:
Listing 8-9
1 http://localhost/app.php/hello/Ryan
If you get an error, it's likely because you need to clear your cache by running:
Listing 8-10
1 $ php app/console cache:clear --env=prod --no-debug
An optional, but common, third step in the process is to create a template.
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.
Optional Step 3: Create the Template
Templates allow you to move all of the presentation (e.g. HTML code) into a separate file and reuse
different portions of the page layout. Instead of writing the HTML inside the controller, render a template
instead:
Listing 8-11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class HelloController extends Controller
{
public function indexAction($name)
{
return $this->render('AcmeHelloBundle:Hello:index.html.twig', array('name' =>
$name));
// render a PHP template instead
// return $this->render('AcmeHelloBundle:Hello:index.html.php', array('name' =>
$name));
}
}
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.
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 66
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 8-12
{# src/Acme/HelloBundle/Resources/views/Hello/index.html.twig #}
{% extends '::base.html.twig' %}
1
2
3
4
5
6
{% block body %}
Hello {{ name }}!
{% endblock %}
Let's step through the Twig template line-by-line:
• 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.
The parent template, ::base.html.twig, is missing both the BundleName and ControllerName
portions of its name (hence the double colon (::) at the beginning). This means that the template lives
outside of the bundles and in the app directory:
Listing 8-13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{# app/Resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="shortcut icon" href="{{ asset('favicon.ico') }}" />
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</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
3. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Controller/Controller.html
4. http://twig.sensiolabs.org
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 67
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.
The Directory Structure
After just a few short sections, you already understand the philosophy behind creating and rendering
pages in Symfony2. You've also already begun to see how Symfony2 projects are structured and
organized. By the end of this section, you'll know where to find and put different types of files and why.
Though entirely flexible, by default, each Symfony application has the same basic and recommended
directory structure:
•
•
•
•
app/: This directory contains the application configuration;
src/: All the project PHP code is stored under this directory;
vendor/: Any vendor libraries are placed here by convention;
web/: This is the web root directory and contains any publicly accessible files;
The Web Directory
The web root directory is the home of all public and static files including images, stylesheets, and
JavaScript files. It is also where each front controller lives:
Listing 8-14
1
2
3
4
5
6
7
8
9
// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();
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 8-15
1 http://localhost/app.php/hello/Ryan
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 8-16
1 http://localhost/hello/Ryan
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 68
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.
The Application (app) Directory
As you saw in the front controller, the AppKernel class is the main entry point of the application and is
responsible for all configuration. As such, it is stored in the app/ directory.
This class must implement two methods that define everything that Symfony needs to know about your
application. You don't even need to worry about these methods when starting - Symfony fills them in for
you with sensible defaults.
• 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.
Autoloading
When Symfony is loading, a special file - app/autoload.php - is included. This file is responsible
for configuring the autoloader, which will autoload your application files from the src/ directory
and third-party libraries from the vendor/ directory.
Because of the autoloader, you never need to worry about using include or require statements.
Instead, Symfony2 uses the namespace of a class to determine its location and automatically
includes the file on your behalf the instant you need a class.
The autoloader is already configured to look in the src/ directory for any of your PHP classes. For
autoloading to work, the class name and path to the file have to follow the same pattern:
Listing 8-17
1 Class Name:
2
Acme\HelloBundle\Controller\HelloController
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.
The Source (src) Directory
Put simply, the src/ directory contains all of the actual code (PHP code, templates, configuration files,
stylesheets, etc) that drives your application. When developing, the vast majority of your work will be
done inside one or more bundles that you create in this directory.
But what exactly is a bundle?
The Bundle System
A bundle is similar to a plugin in other software, but even better. The key difference is that everything
is a bundle in Symfony2, including both the core framework functionality and the code written for your
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 69
application. Bundles are first-class citizens in Symfony2. This gives you the flexibility to use pre-built
features packaged in third-party bundles5 or to distribute your own bundles. It makes it easy to pick and
choose which features to enable in your application and to optimize them the way you want.
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:
Listing 8-18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new JMS\SecurityExtraBundle\JMSSecurityExtraBundle(),
);
if (in_array($this->getEnvironment(), array('dev', 'test'))) {
$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}
return $bundles;
}
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.
5. http://knpbundles.com
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 70
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 8-19
1
2
3
4
5
6
7
8
// src/Acme/TestBundle/AcmeTestBundle.php
namespace Acme\TestBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
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:
Listing 8-20
1
2
3
4
5
6
7
8
9
10
11
12
13
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
// register your bundles
new Acme\TestBundle\AcmeTestBundle(),
);
// ...
return $bundles;
}
And while it doesn't do anything yet, AcmeTestBundle is now ready to be used.
And as easy as this is, Symfony also provides a command-line interface for generating a basic bundle
skeleton:
Listing 8-21
1 $ php app/console generate:bundle --namespace=Acme/TestBundle
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.
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 71
Bundle Directory Structure
The directory structure of a bundle is simple and flexible. By default, the bundle system follows a
set of conventions that help to keep code consistent between all Symfony2 bundles. Take a look at
AcmeHelloBundle, as it contains some of the most common elements of a bundle:
• Controller/ contains the controllers of the bundle (e.g. HelloController.php);
• DependencyInjection/ holds certain dependency injection extension classes, which may
import service configuration, register compiler passes or more (this directory is not necessary);
• Resources/config/ houses configuration, including routing configuration (e.g.
routing.yml);
• Resources/views/ holds templates organized by controller name (e.g. Hello/
index.html.twig);
• Resources/public/ contains web assets (images, stylesheets, etc) and is copied or
symbolically linked into the project web/ directory via the assets:install console command;
• Tests/ holds all tests for the bundle.
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:
Listing 8-22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# app/config/config.yml
imports:
- { resource: parameters.ini }
- { resource: security.yml }
framework:
secret:
"%secret%"
charset:
UTF-8
router:
{ resource: "%kernel.root_dir%/config/routing.yml" }
form:
true
csrf_protection: true
validation:
{ enable_annotations: true }
templating:
{ engines: ['twig'] } #assets_version: SomeVersionScheme
session:
default_locale: "%locale%"
auto_start:
true
# Twig Configuration
twig:
debug:
"%kernel.debug%"
strict_variables: "%kernel.debug%"
# ...
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 72
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.
For now, don't worry about the specific configuration options in each section. The configuration file
ships with sensible defaults. As you read more and explore each part of Symfony2, you'll learn about the
specific configuration options of each feature.
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:
• YAML: Simple, clean and readable;
• XML: More powerful than YAML at times and supports IDE autocompletion;
• PHP: Very powerful but less readable than standard configuration formats.
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:
Listing 8-23
1 http://localhost/app_dev.php/hello/Ryan
If you'd like to see how your application will behave in the production environment, call the prod front
controller instead:
Listing 8-24
1 http://localhost/app.php/hello/Ryan
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:
Listing 8-25
1 php app/console cache:clear --env=prod --no-debug
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 73
If you open the web/app.php file, you'll find that it's configured explicitly to use the prod
environment:
Listing 8-26
1 $kernel = new AppKernel('prod', false);
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 8-27
1
2
3
4
5
// app/AppKernel.php
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
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 8-28
1
2
3
4
5
6
7
8
9
# app/config/config_dev.yml
imports:
- { resource: config.yml }
framework:
router:
{ resource: "%kernel.root_dir%/config/routing_dev.yml" }
profiler: { only_exceptions: false }
# ...
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.
Summary
Congratulations! You've now seen every fundamental aspect of Symfony2 and have hopefully discovered
how easy and flexible it can be. And while there are a lot of features still to come, be sure to keep the
following basic points in mind:
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 74
• creating a page is a three-step process involving a route, a controller and (optionally) a
template.
• each project contains just a few main directories: web/ (web assets and the front controllers),
app/ (configuration), src/ (your bundles), and vendor/ (third-party code) (there's also a bin/
directory that's used to help updated vendor libraries);
• each feature in Symfony2 (including the Symfony2 framework core) is organized into a bundle,
which is a structured set of files for that feature;
• the configuration for each bundle lives in the Resources/config directory of the bundle and
can be specified in YAML, XML or PHP;
• the global application configuration lives in the app/config directory;
• each environment is accessible via a different front controller (e.g. app.php and app_dev.php)
and loads a different configuration file.
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.
PDF brought to you by
generated on October 26, 2012
Chapter 8: Creating Pages in Symfony2 | 75
Chapter 9
Controller
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 9-1
1
2
3
4
5
6
use Symfony\Component\HttpFoundation\Response;
public function helloAction()
{
return new Response('Hello world!');
}
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.
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 76
Requests, Controller, Response Lifecycle
Every request handled by a Symfony2 project goes through the same simple lifecycle. The framework
takes care of the repetitive tasks and ultimately executes a controller, which houses your custom
application code:
1. Each request is handled by a single front controller file (e.g. app.php or app_dev.php) that
bootstraps the application;
2. The Router reads information from the request (e.g. the URI), finds a route that matches that
information, and reads the _controller parameter from the route;
3. The controller from the matched route is executed and the code inside the controller creates
and returns a Response object;
4. The HTTP headers and content of the Response object are sent back to the client.
Creating a page is as easy as creating a controller (#3) and making a route that maps a URL to that
controller (#2).
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.
Listing 9-2
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
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).
This controller is pretty straightforward, but let's walk through it:
• line 4: 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.
• line 6: The class name is the concatenation of a name for the controller class (i.e. Hello)
and the word Controller. This is a convention that provides consistency to controllers and
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 77
allows them to be referenced only by the first part of the name (i.e. Hello) in the routing
configuration.
• line 8: Each action in a controller class is suffixed with Action and is referenced in the routing
configuration by the action's name (index). In the next section, you'll create a route that maps
a URI to this action. You'll learn how the route's placeholders ({name}) become arguments to
the action method ($name).
• line 10: The controller creates and returns a Response object.
Mapping a URL to a Controller
The new controller returns a simple HTML page. To actually view this page in your browser, you need
to create a route, which maps a specific URL pattern to the controller:
Listing 9-3
1 # app/config/routing.yml
2 hello:
3
pattern:
/hello/{name}
4
defaults:
{ _controller: AcmeHelloBundle:Hello:index }
Going to /hello/ryan now executes the HelloController::indexAction() controller and passes in
ryan for the $name variable. Creating a "page" means simply creating a controller method and associated
route.
Notice the syntax used to refer to the controller: AcmeHelloBundle:Hello:index. Symfony2 uses a
flexible string notation to refer to different controllers. This is the most common syntax and tells
Symfony2 to look for a controller class called HelloController inside a bundle named
AcmeHelloBundle. The method indexAction() is then executed.
For more details on the string format used to reference different controllers, see Controller Naming
Pattern.
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.
Route Parameters as Controller Arguments
You already know that the _controller parameter AcmeHelloBundle:Hello:index refers to a
HelloController::indexAction() method that lives inside the AcmeHelloBundle bundle. What's more
interesting is the arguments that are passed to that method:
Listing 9-4
1
2
3
4
5
6
7
8
<?php
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class HelloController extends Controller
{
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 78
9
10
11
12
13 }
public function indexAction($name)
{
// ...
}
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:
Listing 9-5
1 # app/config/routing.yml
2 hello:
3
pattern:
/hello/{first_name}/{last_name}
4
defaults:
{ _controller: AcmeHelloBundle:Hello:index, color: green }
The controller for this can take several arguments:
Listing 9-6
1 public function indexAction($first_name, $last_name, $color)
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.
• The order of the controller arguments does not matter
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:
Listing 9-7
1 public function indexAction($last_name, $color, $first_name)
2 {
3
// ...
4 }
• Each required controller argument must match up with a routing parameter
The following would throw a RuntimeException because there is no foo parameter
defined in the route:
Listing 9-8
1 public function indexAction($first_name, $last_name, $color, $foo)
2 {
3
// ...
4 }
Making the argument optional, however, is perfectly ok. The following example
would not throw an exception:
Listing 9-9
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 79
1 public function indexAction($first_name, $last_name, $color, $foo =
2 'bar')
3 {
4
// ...
}
• Not all routing parameters need to be arguments on your controller
If, for example, the last_name weren't important for your controller, you could omit
it entirely:
Listing 9-10
1 public function indexAction($first_name, $color)
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.
The Request as a Controller Argument
For convenience, you can also have Symfony pass you the Request object as an argument to your
controller. This is especially convenient when you're working with forms, for example:
Listing 9-11
1
2
3
4
5
6
7
8
9
use Symfony\Component\HttpFoundation\Request;
public function updateAction(Request $request)
{
$form = $this->createForm(...);
$form->bindRequest($request);
// ...
}
The Base Controller Class
For convenience, Symfony2 comes with a base Controller class that assists with some of the most
common controller tasks and gives your controller class access to any resource it might need. By
extending this Controller class, you can take advantage of several helper methods.
Add the use statement atop the Controller class and then modify the HelloController to extend it:
Listing 9-12
1
2
3
4
5
6
7
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController extends Controller
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 80
8 {
9
10
11
12
13 }
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
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.
You can also define your Controllers as Services.
Common Controller Tasks
Though a controller can do virtually anything, most controllers will perform the same basic tasks over
and over again. These tasks, such as redirecting, forwarding, rendering templates and accessing core
services, are very easy to manage in Symfony2.
Redirecting
If you want to redirect the user to another page, use the redirect() method:
Listing 9-13
1 public function indexAction()
2 {
3
return $this->redirect($this->generateUrl('homepage'));
4 }
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 9-14
1 public function indexAction()
2 {
3
return $this->redirect($this->generateUrl('homepage'), 301);
4 }
1. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Controller/Controller.html
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 81
The redirect() method is simply a shortcut that creates a Response object that specializes in
redirecting the user. It's equivalent to:
Listing 9-15
1 use Symfony\Component\HttpFoundation\RedirectResponse;
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:
Listing 9-16
1 public function indexAction($name)
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:
Listing 9-17
1 public function fancyAction($name, $color)
2 {
3
// ... create and return a Response object
4 }
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:
Listing 9-18
1 $httpKernel = $this->container->get('http_kernel');
2 $response = $httpKernel->forward('AcmeHelloBundle:Hello:fancy', array(
3
'name' => $name,
4
'color' => 'green',
5 ));
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 82
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:
Listing 9-19
1 $content = $this->renderView('AcmeHelloBundle:Hello:index.html.twig', array('name' =>
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:
Listing 9-20
1 return $this->render('AcmeHelloBundle:Hello:index.html.twig', array('name' => $name));
In both cases, the Resources/views/Hello/index.html.twig template inside the AcmeHelloBundle
will be rendered.
The Symfony templating engine is explained in great detail in the Templating chapter.
The renderView method is a shortcut to direct use of the templating service. The templating
service can also be used directly:
Listing 9-21
1 $templating = $this->get('templating');
2 $content = $templating->render('AcmeHelloBundle:Hello:index.html.twig', array('name'
=> $name));
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 9-22
1 $templating->render('AcmeHelloBundle:Hello/Greetings:index.html.twig', array('name'
2 => $name));
// index.html.twig found in Resources/views/Hello/Greetings is rendered.
Accessing other Services
When extending the base controller class, you can access any Symfony2 service via the get() method.
Here are several common services you might need:
Listing 9-23
1
2
3
4
5
6
7
$request = $this->getRequest();
$templating = $this->get('templating');
$router = $this->get('router');
$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 9-24
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 83
1 $ php app/console container:debug
For more information, see the Service Container chapter.
Managing Errors and 404 Pages
When things are not found, you should play well with the HTTP protocol and return a 404 response.
To do this, you'll throw a special type of exception. If you're extending the base controller class, do the
following:
Listing 9-25
1 public function indexAction()
2 {
3
// retrieve the object from database
4
$product = ...;
5
if (!$product) {
6
throw $this->createNotFoundException('The product does not exist');
7
}
8
9
return $this->render(...);
10 }
The createNotFoundException() method creates a special NotFoundHttpException object, which
ultimately triggers a 404 HTTP response inside Symfony.
Of course, you're free to throw any Exception class in your controller - Symfony2 will automatically
return a 500 HTTP response code.
Listing 9-26
1 throw new \Exception('Something went wrong!');
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.
Managing the Session
Symfony2 provides a nice session object that you can use to store information about the user (be it a
real person using a browser, a bot, or a web service) between requests. By default, Symfony2 stores the
attributes in a cookie by using the native PHP sessions.
Storing and retrieving information from the session can be easily achieved from any controller:
Listing 9-27
1
2
3
4
5
6
7
8
9
10
$session = $this->getRequest()->getSession();
// store an attribute for reuse during a later user request
$session->set('foo', 'bar');
// in another controller for another request
$foo = $session->get('foo');
// set the user locale
$session->setLocale('fr');
These attributes will remain on the user for the remainder of that user's session.
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 84
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:
Listing 9-28
1 public function updateAction()
2 {
3
$form = $this->createForm(...);
4
5
$form->bindRequest($this->getRequest());
6
if ($form->isValid()) {
7
// do some sort of processing
8
9
$this->get('session')->setFlash('notice', 'Your changes were saved!');
10
11
return $this->redirect($this->generateUrl(...));
12
}
13
14
return $this->render(...);
15 }
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 9-29
1 {% if app.session.hasFlash('notice') %}
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.
The Response Object
The only requirement for a controller is to return a Response object. The Response2 class is a PHP
abstraction around the HTTP response - the text-based message filled with HTTP headers and content
that's sent back to the client:
Listing 9-30
1
2
3
4
5
6
// create a simple Response with a 200 status code (the default)
$response = new Response('Hello '.$name, 200);
// create a JSON-response with a 200 status code
$response = new Response(json_encode(array('name' => $name)));
$response->headers->set('Content-Type', 'application/json');
2. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 85
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.
The Request Object
Besides the values of the routing placeholders, the controller also has access to the Request object when
extending the base Controller class:
Listing 9-31
1
2
3
4
5
6
7
8
9
$request = $this->getRequest();
$request->isXmlHttpRequest(); // is it an Ajax request?
$request->getPreferredLanguage(array('en', 'fr'));
$request->query->get('page'); // get a $_GET parameter
$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.
Learn more from the Cookbook
• How to customize Error Pages
• How to define Controllers as Services
3. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/HeaderBag.html
PDF brought to you by
generated on October 26, 2012
Chapter 9: Controller | 86
Chapter 10
Routing
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:
•
•
•
•
Create complex routes that map to controllers
Generate URLs inside templates and controllers
Load routing resources from bundles (or anywhere else)
Debug your routes
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 10-1
1 # app/config/routing.yml
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:
Listing 10-2
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 87
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
public function showAction($slug)
{
// use the $slug variable to query the database
$blog = ...;
return $this->render('AcmeBlogBundle:Blog:show.html.twig', array(
'blog' => $blog,
));
}
}
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.
Routing: Under the Hood
When a request is made to your application, it contains an address to the exact "resource" that the
client is requesting. This address is called the URL, (or URI), and could be /contact, /blog/read-me, or
anything else. Take the following HTTP request for example:
Listing 10-3
1 GET /blog/my-blog-post
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.
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 88
The routing layer is a tool that translates the incoming URL into a specific controller to execute.
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 10-4
1 # app/config/config.yml
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. To do so, just point out in the main routing configuration file which external
files should be included. See the Including External Routing Resources section for more
information.
Basic Route Configuration
Defining a route is easy, and a typical application will have lots of routes. A basic route consists of just
two parts: the pattern to match and a defaults array:
Listing 10-5
1 _welcome:
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.
Routing with Placeholders
Of course the routing system supports much more interesting routes. Many routes will contain one or
more named "wildcard" placeholders:
Listing 10-6
1 blog_show:
2
pattern:
3
defaults:
/blog/{slug}
{ _controller: AcmeBlogBundle:Blog:show }
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.
Required and Optional Placeholders
To make things more exciting, add a new route that displays a list of all the available blog posts for this
imaginary blog application:
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 89
Listing 10-7
1 blog:
2
pattern:
3
defaults:
/blog
{ _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:
Listing 10-8
1 blog:
2
pattern:
3
defaults:
/blog/{page}
{ _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:
Listing 10-9
1 blog:
2
pattern:
3
defaults:
/blog/{page}
{ _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
/blog/2
{page} = 2
Adding Requirements
Take a quick look at the routes that have been created so far:
Listing 10-10
1 blog:
2
pattern:
3
defaults:
4
5 blog_show:
6
pattern:
7
defaults:
/blog/{page}
{ _controller: AcmeBlogBundle:Blog:index, page: 1 }
/blog/{slug}
{ _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.
URL
route parameters
/blog/2
blog
PDF brought to you by
generated on October 26, 2012
{page} = 2
Chapter 10: Routing | 90
URL
route parameters
/blog/my-blog-post
blog
{page} = my-blog-post
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 10-11
1 blog:
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.
URL
route
parameters
/blog/2
blog
{page} = 2
/blog/my-blog-post
blog_show {slug} = my-blog-post
Earlier Routes always Win
What this all means is that the order of the routes is very important. If the blog_show route were
placed above the blog route, the URL /blog/2 would match blog_show instead of blog since
the {slug} parameter of blog_show has no requirements. By using proper ordering and clever
requirements, you can accomplish just about anything.
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:
Listing 10-12
1 homepage:
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
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 91
Adding HTTP Method Requirements
In addition to the URL, you can also match on the method of the incoming request (i.e. GET, HEAD,
POST, PUT, DELETE). Suppose you have a contact form with two controllers - one for displaying the
form (on a GET request) and one for processing the form when it's submitted (on a POST request). This
can be accomplished with the following route configuration:
Listing 10-13
1 contact:
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.
If no _method requirement is specified, the route will match on all methods.
Like the other requirements, the _method requirement is parsed as a regular expression. To match GET or
POST requests, you can use GET|POST.
Advanced Routing Example
At this point, you have everything you need to create a powerful routing structure in Symfony. The
following is an example of just how flexible the routing system can be:
Listing 10-14
1 article_show:
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 dot between placeholders instead of a
slash. URLs matching this route might look like:
• /articles/en/2010/my-post
• /articles/fr/2010/my-post.rss
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 92
The Special _format Routing Parameter
This example also highlights the special _format routing parameter. When using this parameter,
the matched value becomes the "request format" of the Request object. Ultimately, the request
format is used for such things such as setting the Content-Type of the response (e.g. a json request
format translates into a Content-Type of application/json). It can also be used in the controller
to render a different template for each value of _format. The _format parameter is a very powerful
way to render the same content in different formats.
Special Routing Parameters
As you've seen, each routing parameter or default value is eventually available as an argument in the
controller method. Additionally, there are three parameters that are special: each adds a unique piece of
functionality inside your application:
• _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);
Controller Naming Pattern
Every route must have a _controller parameter, which dictates which controller should be executed
when that route is matched. This parameter uses a simple string pattern called the logical controller name,
which Symfony maps to a specific PHP method and class. The pattern has three parts, each separated by
a colon:
bundle:controller:action
For example, a _controller value of AcmeBlogBundle:Blog:show means:
Bundle
Controller Class
Method Name
AcmeBlogBundle
BlogController
showAction
The controller might look like this:
Listing 10-15
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/BlogBundle/Controller/BlogController.php
namespace Acme\BlogBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BlogController extends Controller
{
public function showAction($slug)
{
// ...
}
}
Notice that Symfony adds the string Controller to the class name (Blog => BlogController) and
Action to the method name (show => showAction).
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 93
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).
Route Parameters and Controller Arguments
The route parameters (e.g. {slug}) are especially important because each is made available as an
argument to the controller method:
Listing 10-16
1 public function showAction($slug)
2 {
3
// ...
4 }
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.
Including External Routing Resources
All routes are loaded via a single configuration file - usually app/config/routing.yml (see Creating
Routes above). Commonly, however, you'll want to load routes from other places, like a routing file that
lives inside a bundle. This can be done by "importing" that file:
Listing 10-17
1 # app/config/routing.yml
2 acme_hello:
3
resource: "@AcmeHelloBundle/Resources/config/routing.yml"
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 94
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 10-18
1 # src/Acme/HelloBundle/Resources/config/routing.yml
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.
Prefixing Imported Routes
You can also choose to provide a "prefix" for the imported routes. For example, suppose you want the
acme_hello route to have a final pattern of /admin/hello/{name} instead of simply /hello/{name}:
Listing 10-19
1 # app/config/routing.yml
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.
You can also define routes using annotations. See the FrameworkExtraBundle documentation to see
how.
Visualizing & Debugging Routes
While adding and customizing routes, it's helpful to be able to visualize and get detailed information
about your routes. A great way to see every route in your application is via the router:debug console
command. Execute the command by running the following from the root of your project.
Listing 10-20
1 $ php app/console router:debug
The command will print a helpful list of all the configured routes in your application:
Listing 10-21
1
2
3
4
5
6
homepage
contact
contact_process
article_show
blog
blog_show
ANY
GET
POST
ANY
ANY
ANY
/
/contact
/contact
/articles/{culture}/{year}/{title}.{_format}
/blog/{page}
/blog/{slug}
You can also get very specific information on a single route by including the route name after the
command:
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 95
Listing 10-22
1 $ php app/console router:debug article_show
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:
Listing 10-23
1
2
3
4
5
$params = $router->match('/blog/my-blog-post');
// array('slug' => 'my-blog-post', '_controller' => 'AcmeBlogBundle:Blog:show')
$uri = $router->generate('blog_show', array('slug' => 'my-blog-post'));
// /blog/my-blog-post
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 10-24
1 class MainController extends Controller
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 10-25
1 var url = Routing.generate('blog_show', { "slug": 'my-blog-post'});
For more information, see the documentation for that bundle.
Generating Absolute URLs
By default, the router will generate relative URLs (e.g. /blog). To generate an absolute URL, simply pass
true to the third argument of the generate() method:
Listing 10-26
1 $router->generate('blog_show', array('slug' => 'my-blog-post'), true);
2 // http://www.example.com/blog/my-blog-post
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()
3. https://github.com/FriendsOfSymfony/FOSJsRoutingBundle
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 96
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:
Listing 10-27
1 $router->getContext()->setHost('www.example.com');
Generating URLs with Query Strings
The generate method takes an array of wildcard values to generate the URI. But if you pass extra ones,
they will be added to the URI as a query string:
Listing 10-28
1 $router->generate('blog', array('page' => 2, 'category' => 'Symfony'));
2 // /blog/2?category=Symfony
Generating URLs from a template
The most common place to generate a URL is from within a template when linking between pages in
your application. This is done just as before, but using a template helper function:
Listing 10-29
1 <a href="{{ path('blog_show', {'slug': 'my-blog-post'}) }}">
2
Read this blog post.
3 </a>
Absolute URLs can also be generated.
Listing 10-30
1 <a href="{{ url('blog_show', {'slug': 'my-blog-post'}) }}">
2
Read this blog post.
3 </a>
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.
Learn more from the Cookbook
• How to force routes to always use HTTPS or HTTP
PDF brought to you by
generated on October 26, 2012
Chapter 10: Routing | 97
Chapter 11
Creating and using Templates
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:
Listing 11-1
1 <!DOCTYPE html>
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>
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 98
But Symfony2 packages an even more powerful templating language called Twig1. Twig allows you to
write concise, readable templates that are more friendly to web designers and, in several ways, more
powerful than PHP templates:
Listing 11-2
1 <!DOCTYPE html>
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>
Twig defines two types of special syntax:
• {{ ... }}: "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:
Listing 11-3
1 {{ title|upper }}
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:
Listing 11-4
1 {% for i in 0..10 %}
2
<div class="{{ cycle(['odd', 'even'], i) }}">
3
<!-- some HTML here -->
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
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 99
4
</div>
5 {% endfor %}
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, automatic and
contextual output escaping, 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 11-5
1 <ul>
2
{% for user in users if user.active %}
3
<li>{{ user.username }}</li>
4
{% else %}
5
<li>No users found</li>
6
{% endfor %}
7 </ul>
Twig Template Caching
Twig is fast. Each Twig template is compiled down to a native PHP class that is rendered at runtime. The
compiled classes are located in the app/cache/{environment}/twig directory (where {environment}
is the environment, such as dev or prod) and in some cases can be useful while debugging. See
Environments for more information on environments.
When debug mode is enabled (common in the dev environment), a Twig template will be automatically
recompiled when changes are made to it. This means that during development you can happily make
changes to a Twig template and instantly see the changes without needing to worry about clearing any
cache.
When debug mode is disabled (common in the prod environment), however, you must clear the Twig
cache directory so that the Twig templates will regenerate. Remember to do this when deploying your
application.
Template Inheritance and Layouts
More often than not, templates in a project share common elements, like the header, footer, sidebar
or more. In Symfony2, we like to think about this problem differently: a template can be decorated by
another one. This works exactly the same as PHP classes: template inheritance allows you to build a base
"layout" template that contains all the common elements of your site defined as blocks (think "PHP class
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 100
with base methods"). A child template can extend the base layout and override any of its blocks (think
"PHP subclass that overrides certain methods of its parent class").
First, build a base layout file:
Listing 11-6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{# app/Resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% block title %}Test Application{% endblock %}</title>
</head>
<body>
<div id="sidebar">
{% block sidebar %}
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
{% endblock %}
</div>
<div id="content">
{% block body %}{% endblock %}
</div>
</body>
</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:
Listing 11-7
1
2
3
4
5
6
7
8
9
10
11
{# src/Acme/BlogBundle/Resources/views/Blog/index.html.twig #}
{% extends '::base.html.twig' %}
{% block title %}My cool blog posts{% endblock %}
{% block body %}
{% for entry in blog_entries %}
<h2>{{ entry.title }}</h2>
<p>{{ entry.body }}</p>
{% endfor %}
{% endblock %}
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.
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 101
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:
Listing 11-8
1 <!DOCTYPE html>
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).
• If you need to get the content of a block from the parent template, you can use the {{
parent() }} function. This is useful if you want to add to the contents of a parent block
instead of completely overriding it:
Listing 11-9
1 {% block sidebar %}
2
<h3>Table of Contents</h3>
3
...
4
{{ parent() }}
5 {% endblock %}
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 102
Template Naming and Locations
By default, templates can live in two different locations:
• app/Resources/views/: The applications views directory can contain application-wide base
templates (i.e. your application's layouts) as well as templates that override bundle templates
(see Overriding Bundle Templates);
• path/to/bundle/Resources/views/: Each bundle houses its templates in its Resources/
views directory (and subdirectories). The majority of templates will live inside a bundle.
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:
• AcmeBlogBundle:Blog:index.html.twig: This syntax is used to specify a template for a
specific page. The three parts of the string, each separated by a colon (:), mean the following:
• AcmeBlogBundle: (bundle) the template lives inside the AcmeBlogBundle
(e.g. src/Acme/BlogBundle);
• Blog: (controller) indicates that the template lives inside the Blog
subdirectory of Resources/views;
• index.html.twig: (template) the actual name of the file is
index.html.twig.
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.
Template Suffix
The bundle:controller:template format of each template specifies where the template file is located.
Every template name also has two extensions that specify the format and engine for that template.
• AcmeBlogBundle:Blog:index.html.twig - HTML format, Twig engine
• AcmeBlogBundle:Blog:index.html.php - HTML format, PHP engine
• AcmeBlogBundle:Blog:index.css.twig - CSS format, Twig engine
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,
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 103
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.
Tags and Helpers
You already understand the basics of templates, how they're named and how to use template inheritance.
The hardest parts are already behind you. In this section, you'll learn about a large group of tools available
to help perform the most common template tasks such as including other templates, linking to pages and
including images.
Symfony2 comes bundled with several specialized Twig tags and functions that ease the work of the
template designer. In PHP, the templating system provides an extensible helper system that provides
useful features in a template context.
We've already seen a few built-in Twig tags ({% block %} & {% extends %}) as well as an example of a
PHP helper ($view['slots']). Let's learn a few more.
Including other Templates
You'll often want to include the same template or code fragment on several different pages. For example,
in an application with "news articles", the template code displaying an article might be used on the article
detail page, on a page displaying the most popular articles, or in a list of the latest articles.
When you need to reuse a chunk of PHP code, you typically move the code to a new PHP class or
function. The same is true for templates. By moving the reused template code into its own template, it
can be included from any other template. First, create the template that you'll need to reuse.
Listing 11-10
1
2
3
4
5
6
7
{# src/Acme/ArticleBundle/Resources/views/Article/articleDetails.html.twig #}
<h2>{{ article.title }}</h2>
<h3 class="byline">by {{ article.authorName }}</h3>
<p>
{{ article.body }}
</p>
Including this template from any other template is simple:
Listing 11-11
1
2
3
4
5
6
7
8
9
10
{# src/Acme/ArticleBundle/Resources/views/Article/list.html.twig #}
{% extends 'AcmeArticleBundle::layout.html.twig' %}
{% block body %}
<h1>Recent Articles<h1>
{% for article in articles %}
{% include 'AcmeArticleBundle:Article:articleDetails.html.twig' with {'article':
article} %}
{% endfor %}
{% endblock %}
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 104
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:
Listing 11-12
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/ArticleBundle/Controller/ArticleController.php
class ArticleController extends Controller
{
public function recentArticlesAction($max = 3)
{
// make a database call or other logic to get the "$max" most recent articles
$articles = ...;
return $this->render('AcmeArticleBundle:Article:recentList.html.twig',
array('articles' => $articles));
}
}
The recentList template is perfectly straightforward:
Listing 11-13
1 {# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
2 {% for article in articles %}
3
<a href="/article/{{ article.slug }}">
4
{{ article.title }}
5
</a>
6 {% endfor %}
Notice that we've cheated and hardcoded the article URL in this example (e.g. /article/*slug*).
This is a bad practice. In the next section, you'll learn how to do this correctly.
To include the controller, you'll need to refer to it using the standard string syntax for controllers (i.e.
bundle:controller:action):
Listing 11-14
1
2
3
4
5
6
7
{# app/Resources/views/base.html.twig #}
{# ... #}
<div id="sidebar">
{% render "AcmeArticleBundle:Article:recentArticles" with {'max': 3} %}
</div>
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 105
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 11-15
1 _welcome:
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 11-16
1 <a href="{{ path('_welcome') }}">Home</a>
As expected, this will generate the URL /. Let's see how this works with a more complicated route:
Listing 11-17
1 article_show:
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:
Listing 11-18
1 {# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
2 {% for article in articles %}
3
<a href="{{ path('article_show', {'slug': article.slug}) }}">
4
{{ article.title }}
5
</a>
6 {% endfor %}
You can also generate an absolute URL by using the url Twig function:
Listing 11-19
1 <a href="{{ url('_welcome') }}">Home</a>
The same can be done in PHP templates by passing a third argument to the generate() method:
Listing 11-20
1 <a href="<?php echo $view['router']->generate('_welcome', array(), true) ?>">Home</a>
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:
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 106
Listing 11-21
1 <img src="{{ asset('images/logo.png') }}" alt="Symfony!" />
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.
Including Stylesheets and Javascripts in Twig
No site would be complete without including Javascript files and stylesheets. In Symfony, the inclusion
of these assets is handled elegantly by taking advantage of Symfony's template inheritance.
This section will teach you the philosophy behind including stylesheet and Javascript assets in
Symfony. Symfony also packages another library, called Assetic, which follows this philosophy but
allows you to do much more interesting things with those assets. For more information on using
Assetic see How to Use Assetic for Asset Management.
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:
Listing 11-22
1 {# 'app/Resources/views/base.html.twig' #}
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:
5. http://example.com
6. http://example.com/my_app
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 107
Listing 11-23
1
2
3
4
5
6
7
8
9
10
{# src/Acme/DemoBundle/Resources/views/Contact/contact.html.twig #}
{% extends '::base.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<link href="{{ asset('/css/contact.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{# ... #}
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").
Listing 11-24
1 <link href="{{ asset('bundles/acmedemo/css/contact.css') }}" type="text/css"
rel="stylesheet" />
The end result is a page that includes both the main.css and contact.css stylesheets.
Global Template Variables
During each request, Symfony2 will set a global template variable app in both Twig and PHP template
engines by default. The app variable is a GlobalVariables7 instance which will give you access to some
application specific variables automatically:
•
•
•
•
•
•
Listing 11-25
app.security - The security context.
app.user - The current user object.
app.request - The request object.
app.session - The session object.
app.environment - The current environment (dev, prod, etc).
app.debug - True if in debug mode. False otherwise.
1 <p>Username: {{ app.user.username }}</p>
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.
7. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.html
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 108
Configuring and using the templating Service
The heart of the template system in Symfony2 is the templating Engine. This special object is responsible
for rendering templates and returning their content. When you render a template in a controller, for
example, you're actually using the templating engine service. For example:
Listing 11-26
1 return $this->render('AcmeArticleBundle:Article:index.html.twig');
is equivalent to:
$engine
=
$this->container->get('templating');
>render('AcmeArticleBundle:Article:index.html.twig');
$content
=
$engine-
return $response = new Response($content);
The templating engine (or "service") is preconfigured to work automatically inside Symfony2. It can, of
course, be configured further in the application configuration file:
Listing 11-27
1 # app/config/config.yml
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).
Overriding Bundle Templates
The Symfony2 community prides itself on creating and maintaining high quality bundles (see
KnpBundles.com8) for a large number of different features. Once you use a third-party bundle, you'll likely
need to override and customize one or more of its templates.
Suppose you've included the imaginary open-source AcmeBlogBundle in your project (e.g. in the src/
Acme/BlogBundle directory). And while you're really happy with everything, you want to override the
blog "list" page to customize the markup specifically for your application. By digging into the Blog
controller of the AcmeBlogBundle, you find the following:
Listing 11-28
1 public function indexAction()
2 {
3
// some logic to retrieve the blogs
4
$blogs = ...;
5
6
$this->render('AcmeBlogBundle:Blog:index.html.twig', array('blogs' => $blogs));
7 }
When the AcmeBlogBundle:Blog:index.html.twig is rendered, Symfony2 actually looks in two
different locations for the template:
1. app/Resources/AcmeBlogBundle/views/Blog/index.html.twig
2. src/Acme/BlogBundle/Resources/views/Blog/index.html.twig
8. http://knpbundles.com
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 109
To override the bundle template, just copy the index.html.twig template from the bundle to app/
Resources/AcmeBlogBundle/views/Blog/index.html.twig (the app/Resources/AcmeBlogBundle
directory won't exist, so you'll need to create it). You're now free to customize the template.
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.
This logic also applies to base bundle templates. Suppose also that each template in AcmeBlogBundle
inherits from a base template called AcmeBlogBundle::layout.html.twig. Just as before, Symfony2 will
look in the following two places for the template:
1. app/Resources/AcmeBlogBundle/views/layout.html.twig
2. src/Acme/BlogBundle/Resources/views/layout.html.twig
Once again, to override the template, just copy it from the bundle to app/Resources/AcmeBlogBundle/
views/layout.html.twig. You're now free to customize this copy as you see fit.
If you take a step back, you'll see that Symfony2 always starts by looking in the app/Resources/
{BUNDLE_NAME}/views/ directory for a template. If the template doesn't exist there, it continues by
checking inside the Resources/views directory of the bundle itself. This means that all bundle templates
can be overridden by placing them in the correct app/Resources subdirectory.
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.
Overriding Core Templates
Since the Symfony2 framework itself is just a bundle, core templates can be overridden in the same way.
For example, the core TwigBundle contains a number of different "exception" and "error" templates that
can be overridden by copying each from the Resources/views/Exception directory of the TwigBundle
to, you guessed it, the app/Resources/TwigBundle/views/Exception directory.
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 sectionspecific elements;
Listing 11-29
1 {# src/Acme/BlogBundle/Resources/views/layout.html.twig #}
2 {% extends '::base.html.twig' %}
3
4 {% block body %}
5
<h1>Blog Application</h1>
6
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 110
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.
Listing 11-30
1
2
3
4
5
6
7
8
9
{# src/Acme/BlogBundle/Resources/views/Blog/index.html.twig #}
{% extends 'AcmeBlogBundle::layout.html.twig' %}
{% block content %}
{% for entry in blog_entries %}
<h2>{{ entry.title }}</h2>
<p>{{ entry.body }}</p>
{% endfor %}
{% endblock %}
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 11-31
1 Hello {{ name }}
Imagine that the user enters the following code as his/her name:
Listing 11-32
1 <script>alert('hello!')</script>
Without any output escaping, the resulting template will cause a JavaScript alert box to pop up:
Listing 11-33
1 Hello <script>alert('hello!')</script>
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 11-34
9. http://en.wikipedia.org/wiki/Cross-site_scripting
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 111
1 Hello &lt;script&gt;alert(&#39;helloe&#39;)&lt;/script&gt;
The Twig and PHP templating systems approach the problem in different ways. If you're using Twig,
output escaping is on by default and you're protected. In PHP, output escaping is not automatic, meaning
you'll need to manually escape where necessary.
Output Escaping in Twig
If you're using Twig templates, then output escaping is on by default. This means that you're protected
out-of-the-box from the unintentional consequences of user-submitted code. By default, the output
escaping assumes that content is being escaped for HTML output.
In some cases, you'll need to disable output escaping when you're rendering a variable that is trusted and
contains markup that should not be escaped. Suppose that administrative users are able to write articles
that contain HTML code. By default, Twig will escape the article body. To render it normally, add the
raw filter: {{ article.body|raw }}.
You can also disable output escaping inside a {% block %} area or for an entire template. For more
information, see Output Escaping10 in the Twig documentation.
Output Escaping in PHP
Output escaping is not automatic when using PHP templates. This means that unless you explicitly
choose to escape a variable, you're not protected. To use output escaping, use the special escape() view
method:
Listing 11-35
1 Hello <?php echo $view->escape($name) ?>
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:
Listing 11-36
1 var myMsg = 'Hello <?php echo $view->escape($name, 'js') ?>';
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:
Listing 11-37
1 # app/config/config.yml
2 services:
3
acme_hello.twig.extension.debug:
4
class:
Twig_Extension_Debug
10. http://twig.sensiolabs.org/doc/api.html#escaper-extension
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 112
5
6
tags:
- { name: 'twig.extension' }
Template parameters can then be dumped using the dump function:
Listing 11-38
1
2
3
4
5
6
7
8
{# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
{{ dump(articles) }}
{% for article in articles %}
<a href="/article/{{ article.slug }}">
{{ article.title }}
</a>
{% 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:
• XML template name: AcmeArticleBundle:Article:index.xml.twig
• XML template filename: index.xml.twig
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 11-39
1 public function indexAction()
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:
Listing 11-40
1 <a href="{{ path('article_show', {'id': 123, '_format': 'pdf'}) }}">
2
PDF Version
3 </a>
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 113
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:
Listing 11-41
1
2
3
4
5
// creates a Response object whose content is the rendered template
$response = $this->render('AcmeArticleBundle:Article:index.html.twig');
// creates a Response object whose content is simple text
$response = new Response('response content');
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.
Learn more from the Cookbook
• How to use PHP instead of Twig for Templates
• How to customize Error Pages
• How to write a custom Twig Extension
PDF brought to you by
generated on October 26, 2012
Chapter 11: Creating and using Templates | 114
Chapter 12
Databases and Doctrine
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.
A Simple Example: A Product
The easiest way to understand how Doctrine works is to see it in action. In this section, you'll configure
your database, create a Product object, persist it to the database and fetch it back out.
Code along with the example
If you want to follow along with the example in this chapter, create an AcmeStoreBundle via:
Listing 12-1
1 $ php app/console generate:bundle --namespace=Acme/StoreBundle
1. http://www.doctrine-project.org/
2. http://www.mongodb.org/
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 115
Configuring the Database
Before you really begin, you'll need to configure your database connection information. By convention,
this information is usually configured in an app/config/parameters.ini file:
Listing 12-2
1 ; app/config/parameters.ini
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:
Listing 12-3
1 # app/config/config.yml
2 doctrine:
3
dbal:
4
driver:
"%database_driver%"
5
host:
"%database_host%"
6
dbname:
"%database_name%"
7
user:
"%database_user%"
8
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.
Now that Doctrine knows about your database, you can have it create the database for you:
Listing 12-4
1 $ php app/console doctrine:database:create
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 116
Setting Up The Database
One mistake even seasoned developers make when starting a Symfony2 project is forgetting to
setup default charset and collation on their database, ending up with latin type collations, which
are default for most databases. They might even remember to do it the very first time, but forget
that it's all gone after running a relatively common command during development:
Listing 12-5
1 $ php app/console doctrine:database:drop --force
2 $ php 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 12-6
1 [mysqld]
2 collation-server = utf8_general_ci
3 character-set-server = utf8
Creating an Entity Class
Suppose you're building an application where products need to be displayed. Without even thinking
about Doctrine or databases, you already know that you need a Product object to represent those
products. Create this class inside the Entity directory of your AcmeStoreBundle:
Listing 12-7
1
2
3
4
5
6
7
8
9
10
11
// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
class Product
{
protected $name;
protected $price;
protected $description;
}
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.
Once you learn the concepts behind Doctrine, you can have Doctrine create this entity class for
you:
Listing 12-8
1 $ php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product"
--fields="name:string(255) price:float description:text"
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 117
Add Mapping Information
Doctrine allows you to work with databases in a much more interesting way than just fetching rows of
a column-based table into an array. Instead, Doctrine allows you to persist entire objects to the database
and fetch entire objects out of the database. This works by mapping a PHP class to a database table, and
the properties of that PHP class to columns on the table:
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.
Listing 12-9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="product")
*/
class Product
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string", length=100)
*/
protected $name;
/**
* @ORM\Column(type="decimal", scale=2)
*/
protected $price;
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 118
29
30
31
32
33 }
/**
* @ORM\Column(type="text")
*/
protected $description;
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 12-10
1 /**
2 * @IgnoreAnnotation("fn")
3 */
4 class Product
Generating Getters and Setters
Even though Doctrine now knows how to persist a Product object to the database, the class itself isn't
really useful yet. Since Product is just a regular PHP class, you need to create getter and setter methods
(e.g. getName(), setName()) in order to access its properties (since the properties are protected).
Fortunately, Doctrine can do this for you by running:
Listing 12-11
1 $ php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product
3.
4.
5.
6.
http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html
http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html#quoting-reserved-words
http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html#persistent-classes
http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html#property-mapping
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 119
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).
More about doctrine:generate:entities
With the doctrine:generate:entities command you can:
• generate getters and setters,
• generate repository classes configured with the
@ORM\Entity(repositoryClass="...") annotation,
• generate the appropriate constructor for 1:n and n:m relations.
The doctrine:generate:entities command saves a backup of the original Product.php named
Product.php~. In some cases, the presence of this file can cause a "Cannot redeclare class" error.
It can be safely removed.
Note that you don't need to use this command. Doctrine doesn't rely on code generation. Like with
normal PHP classes, you just need to make sure that your protected/private properties have getter
and setter methods. Since this is a common thing to do when using Doctrine, this command was
created.
You can also generate all known entities (i.e. any PHP class with Doctrine mapping information) of a
bundle or an entire namespace:
Listing 12-12
1 $ php app/console doctrine:generate:entities AcmeStoreBundle
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.
Creating the Database Tables/Schema
You now have a usable Product class with mapping information so that Doctrine knows exactly how to
persist it. Of course, you don't yet have the corresponding product table in your database. Fortunately,
Doctrine can automatically create all the database tables needed for every known entity in your
application. To do this, run:
Listing 12-13
1 $ php app/console doctrine:schema:update --force
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.
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 120
Your database now has a fully-functional product table with columns that match the metadata you've
specified.
Persisting Objects to the Database
Now that you have a mapped Product entity and corresponding product table, you're ready to persist
data to the database. From inside a controller, this is pretty easy. Add the following method to the
DefaultController of the bundle:
Listing 12-14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Acme/StoreBundle/Controller/DefaultController.php
// ...
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
public function createAction()
{
$product = new Product();
$product->setName('A Foo Bar');
$product->setPrice('19.99');
$product->setDescription('Lorem ipsum dolor');
$em = $this->getDoctrine()->getEntityManager();
$em->persist($product);
$em->flush();
return new Response('Created product id '.$product->getId());
}
If you're following along with this example, you'll need to create a route that points to this action
to see it work.
Let's walk through this example:
• lines 9-12 In this section, you instantiate and work with the $product object like any other,
normal PHP object;
• line 14 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 15 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 16 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.
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 121
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.
Fetching Objects from the Database
Fetching an object back out of the database is even easier. For example, suppose you've configured a
route to display a specific Product based on its id value:
Listing 12-15
1 public function showAction($id)
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 12-16
1 $repository = $this->getDoctrine()
2
->getRepository('AcmeStoreBundle:Product');
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:
Listing 12-17
1
2
3
4
5
6
7
8
9
10
11
12
// query by the primary key (usually "id")
$product = $repository->find($id);
// dynamic method names to find based on a column value
$product = $repository->findOneById($id);
$product = $repository->findOneByName('foo');
// find *all* products
$products = $repository->findAll();
// find a group of products based on an arbitrary column value
$products = $repository->findByPrice(19.99);
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 122
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 12-18
1
2
3
4
5
6
7
8
// query for one product matching be name and price
$product = $repository->findOneBy(array('name' => 'foo', 'price' => 19.99));
// query for all products matching the name, ordered by price
$product = $repository->findBy(
array('name' => 'foo'),
array('price' => 'ASC')
);
When you render any page, you can see how many queries were made in the bottom right corner
of the web debug toolbar.
If you click the icon, the profiler will open, showing you the exact queries that were made.
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:
Listing 12-19
1 public function updateAction($id)
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
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 123
10
11
12
13
14 }
$product->setName('New product name!');
$em->flush();
return $this->redirect($this->generateUrl('homepage'));
Updating an object involves just three steps:
1. fetching the object from Doctrine;
2. modifying the object;
3. calling flush() on the entity manager
Notice that calling $em->persist($product) isn't necessary. Recall that this method simply tells
Doctrine to manage or "watch" the $product object. In this case, since you fetched the $product object
from Doctrine, it's already managed.
Deleting an Object
Deleting an object is very similar, but requires a call to the remove() method of the entity manager:
Listing 12-20
1 $em->remove($product);
2 $em->flush();
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.
Querying for Objects
You've already seen how the repository object allows you to run basic queries without any work:
Listing 12-21
1 $repository->find($id);
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.
Querying for Objects with DQL
Imagine that you want to query for products, but only return products that cost more than 19.99,
ordered from cheapest to most expensive. From inside a controller, do the following:
Listing 12-22
1
2
3
4
5
6
$em = $this->getDoctrine()->getEntityManager();
$query = $em->createQuery(
'SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC'
)->setParameter('price', '19.99');
$products = $query->getResult();
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 124
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 12-23
1 $product = $query->getSingleResult();
The getSingleResult() method throws a Doctrine\ORM\NoResultException exception if no
results are returned and a Doctrine\ORM\NonUniqueResultException if more than one result is
returned. If you use this method, you may need to wrap it in a try-catch block and ensure that only
one result is returned (if you're querying on something that could feasibly return more than one
result):
Listing 12-24
1
2
3
4
5
6
7
8
9
$query = $em->createQuery('SELECT ...')
->setMaxResults(1);
try {
$product = $query->getSingleResult();
} catch (\Doctrine\Orm\NoResultException $e) {
$product = null;
}
// ...
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:
Listing 12-25
1 ... WHERE p.price > :price ...
You can then set the value of the price placeholder by calling the setParameter() method:
Listing 12-26
1 ->setParameter('price', '19.99')
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:
Listing 12-27
1 ->setParameters(array(
2
'price' => '19.99',
3
'name' => 'Foo',
4 ))
7. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/dql-doctrine-query-language.html
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 125
Using Doctrine's Query Builder
Instead of writing the queries directly, you can alternatively use Doctrine's QueryBuilder to do the same
job using a nice, object-oriented interface. If you use an IDE, you can also take advantage of autocompletion as you type the method names. From inside a controller:
Listing 12-28
1 $repository = $this->getDoctrine()
2
->getRepository('AcmeStoreBundle:Product');
3
4 $query = $repository->createQueryBuilder('p')
5
->where('p.price > :price')
6
->setParameter('price', '19.99')
7
->orderBy('p.price', 'ASC')
8
->getQuery();
9
10 $products = $query->getResult();
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.
Custom Repository Classes
In the previous sections, you began constructing and using more complex queries from inside a
controller. In order to isolate, test and reuse these queries, it's a good idea to create a custom repository
class for your entity and add methods with your query logic there.
To do this, add the name of the repository class to your mapping definition.
Listing 12-29
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="Acme\StoreBundle\Entity\ProductRepository")
*/
class Product
{
//...
}
Doctrine can generate the repository class for you by running the same command used earlier to generate
the missing getter and setter methods:
Listing 12-30
1 $ php app/console doctrine:generate:entities Acme
Next, add a new method - findAllOrderedByName() - to the newly generated repository class. This
method will query for all of the Product entities, ordered alphabetically.
Listing 12-31
1 // src/Acme/StoreBundle/Entity/ProductRepository.php
2 namespace Acme\StoreBundle\Entity;
3
8. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/query-builder.html
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 126
4
5
6
7
8
9
10
11
12
13
14
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
public function findAllOrderedByName()
{
return $this->getEntityManager()
->createQuery('SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC')
->getResult();
}
}
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:
Listing 12-32
1 $em = $this->getDoctrine()->getEntityManager();
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.
Listing 12-33
1 $ php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Category"
--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.
Relationship Mapping Metadata
To relate the Category and Product entities, start by creating a products property on the Category class:
Listing 12-34
1
2
3
4
5
6
7
// src/Acme/StoreBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 127
8
9
10
11
12
13
14
15
16
17
18
19 }
// ...
/**
* @ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
protected $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
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:
Listing 12-35
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/StoreBundle/Entity/Product.php
// ...
class Product
{
// ...
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
protected $category;
}
Finally, now that you've added a new property to both the Category and Product classes, tell Doctrine
to generate the missing getter and setter methods for you:
Listing 12-36
1 $ php app/console doctrine:generate:entities Acme
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.
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 128
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.
Before you continue, be sure to tell Doctrine to add the new category table, and product.category_id
column, and new foreign key:
Listing 12-37
1 $ php app/console doctrine:schema:update --force
This task should only be really used during development. For a more robust method of
systematically updating your production database, read about Doctrine migrations.
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 129
Saving Related Entities
Now, let's see the code in action. Imagine you're inside a controller:
Listing 12-38
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
use Acme\StoreBundle\Entity\Category;
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
class DefaultController extends Controller
{
public function createProductAction()
{
$category = new Category();
$category->setName('Main Products');
$product = new Product();
$product->setName('Foo');
$product->setPrice(19.99);
// relate this product to the category
$product->setCategory($category);
$em = $this->getDoctrine()->getEntityManager();
$em->persist($category);
$em->persist($product);
$em->flush();
return new Response(
'Created product id: '.$product->getId().' and category id:
'.$category->getId()
);
}
}
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.
Fetching Related Objects
When you need to fetch associated objects, your workflow looks just like it did before. First, fetch a
$product object and then access its related Category:
Listing 12-39
1 public function showAction($id)
2 {
3
$product = $this->getDoctrine()
4
->getRepository('AcmeStoreBundle:Product')
5
->find($id);
6
7
$categoryName = $product->getCategory()->getName();
8
9
// ...
10 }
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-
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 130
>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:
Listing 12-40
1 public function showProductAction($id)
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
you call ->getProducts()). The $products variable is an array of all Product objects that relate to the
given Category object via their category_id value.
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 131
Relationships and Proxy Classes
This "lazy loading" is possible because, when necessary, Doctrine returns a "proxy" object in place
of the true object. Look again at the above example:
Listing 12-41
1
2
3
4
5
6
7
8
$product = $this->getDoctrine()
->getRepository('AcmeStoreBundle:Product')
->find($id);
$category = $product->getCategory();
// prints "Proxies\AcmeStoreBundleEntityCategoryProxy"
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.
Joining to Related Records
In the above examples, two queries were made - one for the original object (e.g. a Category) and one for
the related object(s) (e.g. the Product objects).
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:
Listing 12-42
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Acme/StoreBundle/Entity/ProductRepository.php
public function findOneByIdJoinedToCategory($id)
{
$query = $this->getEntityManager()
->createQuery('
SELECT p, c FROM AcmeStoreBundle:Product p
JOIN p.category c
WHERE p.id = :id'
)->setParameter('id', $id);
try {
return $query->getSingleResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
Now, you can use this method in your controller to query for a Product object and its related Category
with just one query:
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 132
Listing 12-43
1 public function showAction($id)
2 {
3
$product = $this->getDoctrine()
4
->getRepository('AcmeStoreBundle:Product')
5
->findOneByIdJoinedToCategory($id);
6
7
$category = $product->getCategory();
8
9
// ...
10 }
More Information on Associations
This section has been an introduction to one common type of entity relationship, the one-to-many
relationship. For more advanced details and examples of how to use other types of relations (e.g. oneto-one, many-to-many), see Doctrine's Association Mapping Documentation9.
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:
Listing 12-44
1
2
3
4
5
6
7
8
/**
* @ORM\Entity()
* @ORM\HasLifecycleCallbacks()
*/
class Product
{
// ...
}
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):
9. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/association-mapping.html
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 133
Listing 12-45
/**
* @ORM\PrePersist
*/
public function setCreatedValue()
{
$this->created = new \DateTime();
}
1
2
3
4
5
6
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
Lifecycle Callbacks and Event Listeners
Notice that the setCreatedValue() method receives no arguments. This is always the case
for lifecycle callbacks and is intentional: lifecycle callbacks should be simple methods that are
concerned with internally transforming data in the entity (e.g. setting a created/updated field,
generating a slug value).
If you need to do some heavier lifting - like perform logging or send an email - you should register
an external class as an event listener or subscriber and give it access to whatever resources you
need. For more information, see How to Register Event Listeners and Subscribers.
Doctrine Extensions: Timestampable, Sluggable, etc.
Doctrine is quite flexible, and a number of third-party extensions are available that allow you to
easily perform repeated and common tasks on your entities. These include thing such as Sluggable,
Timestampable, Loggable, Translatable, and Tree.
For more information on how to find and use these extensions, see the cookbook article about using
common Doctrine extensions.
10. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/events.html#lifecycle-events
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 134
Doctrine Field Types Reference
Doctrine comes with a large number of field types available. Each of these maps a PHP data type to a
specific column type in whatever database you're using. The following types are supported in Doctrine:
• Strings
• string (used for shorter strings)
• text (used for larger 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)
For more information, see Doctrine's Mapping Types documentation12.
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:
Listing 12-46
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* A string field with length 255 that cannot be null
* (reflecting the default values for the "type", "length" and *nullable* options)
*
* @ORM\Column()
*/
protected $name;
/**
* A string field of length 150 that persists to an "email_address" column
* and has a unique index.
*
* @ORM\Column(name="email_address", unique=true, length=150)
*/
protected $email;
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
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 135
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 12-47
1 $ php app/console
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 12-48
1 $ php app/console help doctrine:database:create
Some notable or interesting tasks include:
• doctrine:ensure-production-settings - checks to see if the current environment is
configured efficiently for production. This should always be run in the prod environment:
Listing 12-49
1 $ php app/console doctrine:ensure-production-settings --no-debug --env=prod
Don't forget to add the --no-debug switch, because the debug flag is always set to true,
even if the environment is set to prod.
• doctrine:mapping:import - allows Doctrine to introspect an existing database and create
mapping information. For more information, see How to generate Entities from an Existing
Database.
• doctrine:mapping:info - tells you all of the entities that Doctrine is aware of and whether or
not there are any basic errors with the mapping.
• doctrine:query:dql and doctrine:query:sql - allow you to execute DQL or SQL queries
directly from the command line.
To be able to load data fixtures to your database, you will need to have the
DoctrineFixturesBundle bundle installed. To learn how to do it, read the
"DoctrineFixturesBundle" entry of the documentation.
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.
13. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/basic-mapping.html#property-mapping
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 136
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.
PDF brought to you by
generated on October 26, 2012
Chapter 12: Databases and Doctrine | 137
Chapter 13
Databases and Propel
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.
A Simple Example: A Product
In this section, you'll configure your database, create a Product object, persist it to the database and fetch
it back out.
Code along with the example
If you want to follow along with the example in this chapter, create an AcmeStoreBundle via:
Listing 13-1
1 $ php app/console generate:bundle --namespace=Acme/StoreBundle
Configuring the Database
Before you can start, you'll need to configure your database connection information. By convention, this
information is usually configured in an app/config/parameters.ini file:
Listing 13-2
1 ; app/config/parameters.ini
2 [parameters]
3
database_driver
= mysql
4
database_host
= localhost
5
database_name
= test_project
6
database_user
= root
7
database_password = password
8
database_charset = UTF8
1. http://www.propelorm.org/cookbook/symfony2/working-with-symfony2.html#installation
PDF brought to you by
generated on October 26, 2012
Chapter 13: Databases and Propel | 138
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:
Listing 13-3
1 propel:
2
dbal:
3
driver:
"%database_driver%"
4
user:
"%database_user%"
5
password:
"%database_password%"
6
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:
Listing 13-4
1 $ php app/console propel:database:create
In this example, you have one configured connection, named default. If you want to configure
more than one connection, read the PropelBundle configuration section.
Creating a Model Class
In the Propel world, ActiveRecord classes are known as models because classes generated by Propel
contain some business logic.
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:
Listing 13-5
1 <?xml version="1.0" encoding="UTF-8"?>
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>
Building the Model
After creating your schema.xml, generate your model from it by running:
Listing 13-6
1 $ php app/console propel:model:build
This generates each model class to quickly develop your application in the Model/ directory the
AcmeStoreBundle bundle.
PDF brought to you by
generated on October 26, 2012
Chapter 13: Databases and Propel | 139
Creating the Database Tables/Schema
Now you have a usable Product class and all you need to persist it. Of course, you don't yet have
the corresponding product table in your database. Fortunately, Propel can automatically create all the
database tables needed for every known model in your application. To do this, run:
Listing 13-7
1 $ php app/console propel:sql:build
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.
Persisting Objects to the Database
Now that you have a Product object and corresponding product table, you're ready to persist data
to the database. From inside a controller, this is pretty easy. Add the following method to the
DefaultController of the bundle:
Listing 13-8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Acme/StoreBundle/Controller/DefaultController.php
// ...
use Acme\StoreBundle\Model\Product;
use Symfony\Component\HttpFoundation\Response;
public function createAction()
{
$product = new Product();
$product->setName('A Foo Bar');
$product->setPrice(19.99);
$product->setDescription('Lorem ipsum dolor');
$product->save();
return new Response('Created product id '.$product->getId());
}
In this piece of code, you instantiate and work with the $product object. When you call the save()
method on it, you persist it to the database. No need to use other services, the object knows how to
persist itself.
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.
Fetching Objects from the Database
Fetching an object back from the database is even easier. For example, suppose you've configured a route
to display a specific Product based on its id value:
Listing 13-9
PDF brought to you by
generated on October 26, 2012
Chapter 13: Databases and Propel | 140
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...
use Acme\StoreBundle\Model\ProductQuery;
public function showAction($id)
{
$product = ProductQuery::create()
->findPk($id);
if (!$product) {
throw $this->createNotFoundException('No product found for id '.$id);
}
// ... do something, like pass the $product object into a template
}
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:
Listing 13-10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
use Acme\StoreBundle\Model\ProductQuery;
public function updateAction($id)
{
$product = ProductQuery::create()
->findPk($id);
if (!$product) {
throw $this->createNotFoundException('No product found for id '.$id);
}
$product->setName('New product name!');
$product->save();
return $this->redirect($this->generateUrl('homepage'));
}
Updating an object involves just three steps:
1. fetching the object from Propel;
2. modifying the object;
3. saving it.
Deleting an Object
Deleting an object is very similar, but requires a call to the delete() method on the object:
Listing 13-11
1 $product->delete();
Querying for Objects
Propel provides generated Query classes to run both basic and complex queries without any work:
PDF brought to you by
generated on October 26, 2012
Chapter 13: Databases and Propel | 141
Listing 13-12
1 \Acme\StoreBundle\Model\ProductQuery::create()->findPk($id);
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 13-13
1 $products = \Acme\StoreBundle\Model\ProductQuery::create()
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 13-14
1
2
3
4
5
6
7
8
9
// src/Acme/StoreBundle/Model/ProductQuery.php
class ProductQuery extends BaseProductQuery
{
public function filterByExpensivePrice()
{
return $this
->filterByPrice(array('min' => 1000))
}
}
But note that Propel generates a lot of methods for you and a simple findAllOrderedByName() can be
written without any effort:
Listing 13-15
1 \Acme\StoreBundle\Model\ProductQuery::create()
2
->orderByName()
3
->find();
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:
Listing 13-16
1 <database name="default" namespace="Acme\StoreBundle\Model" defaultIdMethod="native">
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">
PDF brought to you by
generated on October 26, 2012
Chapter 13: Databases and Propel | 142
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>
Create the classes:
Listing 13-17
1 $ php app/console propel:model:build
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.
Listing 13-18
1 $ php app/console propel:migration:generate-diff
2 $ php app/console propel:migration:migrate
Your database has been updated, you can continue to write your application.
Saving Related Objects
Now, let's see the code in action. Imagine you're inside a controller:
Listing 13-19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ...
use Acme\StoreBundle\Model\Category;
use Acme\StoreBundle\Model\Product;
use Symfony\Component\HttpFoundation\Response;
class DefaultController extends Controller
{
public function createProductAction()
{
$category = new Category();
$category->setName('Main Products');
$product = new Product();
$product->setName('Foo');
$product->setPrice(19.99);
// relate this product to the category
$product->setCategory($category);
// save the whole
$product->save();
return new Response(
'Created product id: '.$product->getId().' and category id:
'.$category->getId()
);
}
}
PDF brought to you by
generated on October 26, 2012
Chapter 13: Databases and Propel | 143
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.
Fetching Related Objects
When you need to fetch associated objects, your workflow looks just like it did before. First, fetch a
$product object and then access its related Category:
Listing 13-20
1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
use Acme\StoreBundle\Model\ProductQuery;
public function showAction($id)
{
$product = ProductQuery::create()
->joinWithCategory()
->findPk($id);
$categoryName = $product->getCategory()->getName();
// ...
}
Note, in the above example, only one query was made.
More information on Associations
You will find more information on relations by reading the dedicated chapter on Relationships2.
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:
Listing 13-21
1
2
3
4
5
6
7
8
9
10
11
// src/Acme/StoreBundle/Model/Product.php
// ...
class Product extends BaseProduct
{
public function preInsert(\PropelPDO $con = null)
{
// do something before the object is inserted
}
}
Propel provides the following hooks:
• preInsert() code executed before insertion of a new object
• postInsert() code executed after insertion of a new object
2. http://www.propelorm.org/documentation/04-relationships.html
PDF brought to you by
generated on October 26, 2012
Chapter 13: Databases and Propel | 144
•
•
•
•
•
•
preUpdate() code executed before update of an existing object
postUpdate() code executed after update of an existing object
preSave() code executed before saving an object (new or existing)
postSave() code executed after saving an object (new or existing)
preDelete() code executed before deleting an object
postDelete() code executed after deleting an object
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.
3. http://www.propelorm.org/cookbook/symfony2/working-with-symfony2#the_commands
PDF brought to you by
generated on October 26, 2012
Chapter 13: Databases and Propel | 145
Chapter 14
Testing
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.
The PHPUnit Testing Framework
Symfony2 integrates with an independent library - called PHPUnit - to give you a rich testing framework.
This chapter won't cover PHPUnit itself, but it has its own excellent documentation1.
Symfony2 works with PHPUnit 3.5.11 or later.
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:
Listing 14-1
1 # specify the configuration directory on the command line
2 $ phpunit -c app/
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.
Code coverage can be generated with the --coverage-html option.
1. http://www.phpunit.de/manual/3.5/en/
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 146
Unit Tests
A unit test is usually a test against a specific PHP class. If you want to test the overall behavior of your
application, see the section about Functional Tests.
Writing Symfony2 unit tests is no different than writing standard PHPUnit unit tests. Suppose, for
example, that you have an incredibly simple class called Calculator in the Utility/ directory of your
bundle:
Listing 14-2
1
2
3
4
5
6
7
8
9
10
// src/Acme/DemoBundle/Utility/Calculator.php
namespace Acme\DemoBundle\Utility;
class Calculator
{
public function add($a, $b)
{
return $a + $b;
}
}
To test this, create a CalculatorTest file in the Tests/Utility directory of your bundle:
Listing 14-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
namespace Acme\DemoBundle\Tests\Utility;
use Acme\DemoBundle\Utility\Calculator;
class CalculatorTest extends \PHPUnit_Framework_TestCase
{
public function testAdd()
{
$calc = new Calculator();
$result = $calc->add(30, 12);
// assert that our calculator added the numbers correctly!
$this->assertEquals(42, $result);
}
}
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:
Listing 14-4
1
2
3
4
5
6
7
8
# run all tests in the Utility directory
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/
# run tests for the Calculator class
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
# run all tests for the entire Bundle
$ phpunit -c app src/Acme/DemoBundle/
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 147
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.
Your First Functional Test
Functional tests are simple PHP files that typically live in the Tests/Controller directory of your
bundle. If you want to test the pages handled by your DemoController class, start by creating a new
DemoControllerTest.php file that extends a special WebTestCase class.
For example, the Symfony2 Standard Edition provides a simple functional test for its DemoController
(DemoControllerTest2) that reads as follows:
Listing 14-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
namespace Acme\DemoBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class DemoControllerTest extends WebTestCase
{
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET', '/demo/hello/Fabien');
$this->assertGreaterThan(0, $crawler->filter('html:contains("Hello
Fabien")')->count());
}
}
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:
Listing 14-6
1 <phpunit>
2
<!-- ... -->
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:
2. https://github.com/symfony/symfony-standard/blob/master/src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 148
Listing 14-7
1 $crawler = $client->request('GET', '/demo/hello/Fabien');
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:
Listing 14-8
1 $link = $crawler->filter('a:contains("Greet")')->eq(1)->link();
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:
Listing 14-9
1
2
3
4
5
6
7
8
$form = $crawler->selectButton('submit')->form();
// set some values
$form['name'] = 'Lucas';
$form['form_name[subject]'] = 'Hey there!';
// submit the form
$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.
Now that you can easily navigate through an application, use assertions to test that it actually does what
you expect it to. Use the Crawler to make assertions on the DOM:
Listing 14-10
1 // Assert that the response matches a given CSS selector.
2 $this->assertGreaterThan(0, $crawler->filter('h1')->count());
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 14-11
1 $this->assertRegExp('/Hello Fabien/', $client->getResponse()->getContent());
3. http://api.symfony.com/2.0/Symfony/Component/DomCrawler/Crawler.html
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 149
More about the request() method:
The full signature of the request() method is:
Listing 14-12
1 request(
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:
Listing 14-13
1 $client->request(
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
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 150
Useful Assertions
To get you started faster, here is a list of the most common and useful test assertions:
Listing 14-14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Assert that there is more than one h2 tag with the class "subtitle"
$this->assertGreaterThan(0, $crawler->filter('h2.subtitle')->count());
// Assert that there are exactly 4 h2 tags on the page
$this->assertCount(4, $crawler->filter('h2'));
// Assert that the "Content-Type" header is "application/json"
$this->assertTrue($client->getResponse()->headers->contains('Content-Type',
'application/json'));
// Assert that the response content matches a regexp.
$this->assertRegExp('/foo/', $client->getResponse()->getContent());
// Assert that the response status code is 2xx
$this->assertTrue($client->getResponse()->isSuccessful());
// Assert that the response status code is 404
$this->assertTrue($client->getResponse()->isNotFound());
// Assert a specific 200 status code
$this->assertEquals(200, $client->getResponse()->getStatusCode());
// Assert that the response is a redirect to /demo/contact
$this->assertTrue($client->getResponse()->isRedirect('/demo/contact'));
// or simply check that the response is a redirect to any URL
$this->assertTrue($client->getResponse()->isRedirect());
Working with the Test Client
The Test Client simulates an HTTP client like a browser and makes requests into your Symfony2
application:
Listing 14-15
1 $crawler = $client->request('GET', '/hello/Fabien');
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:
Listing 14-16
1
2
3
4
5
$link = $crawler->selectLink('Go elsewhere...')->link();
$crawler = $client->click($link);
$form = $crawler->selectButton('validate')->form();
$crawler = $client->submit($form, array('name' => 'Fabien'));
The click() and submit() methods both return a Crawler object. These methods are the best way to
browse your application as it takes care of a lot of things for you, like detecting the HTTP method from
a form and giving you a nice API for uploading files.
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 151
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:
Listing 14-17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Directly submit a form (but using the Crawler is easier!)
$client->request('POST', '/submit', array('name' => 'Fabien'));
// Form submission with a file upload
use Symfony\Component\HttpFoundation\File\UploadedFile;
$photo = new UploadedFile(
'/path/to/photo.jpg',
'photo.jpg',
'image/jpeg',
123
);
// or
$photo = array(
'tmp_name' => '/path/to/photo.jpg',
'name' => 'photo.jpg',
'type' => 'image/jpeg',
'size' => 123,
'error' => UPLOAD_ERR_OK
);
$client->request(
'POST',
'/submit',
array('name' => 'Fabien'),
array('photo' => $photo)
);
// Perform a DELETE requests, and pass HTTP headers
$client->request(
'DELETE',
'/post/12',
array(),
array(),
array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word')
);
Last but not least, you can force each request to be executed in its own PHP process to avoid any sideeffects when working with several clients in the same script:
Listing 14-18
1 $client->insulate();
Browsing
The Client supports many operations that can be done in a real browser:
Listing 14-19
1 $client->back();
2 $client->forward();
3 $client->reload();
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 152
4
5 // Clears all cookies and the history
6 $client->restart();
Accessing Internal Objects
If you use the client to test your application, you might want to access the client's internal objects:
Listing 14-20
1 $history
= $client->getHistory();
2 $cookieJar = $client->getCookieJar();
You can also get the objects related to the latest request:
Listing 14-21
1 $request = $client->getRequest();
2 $response = $client->getResponse();
3 $crawler = $client->getCrawler();
If your requests are not insulated, you can also access the Container and the Kernel:
Listing 14-22
1 $container = $client->getContainer();
2 $kernel
= $client->getKernel();
Accessing the Container
It's highly recommended that a functional test only tests the Response. But under certain very rare
circumstances, you might want to access some internal objects to write assertions. In such cases, you can
access the dependency injection container:
Listing 14-23
1 $container = $client->getContainer();
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.
Accessing the Profiler Data
On each request, the Symfony profiler collects and stores a lot of data about the internal handling of that
request. For example, the profiler could be used to verify that a given page executes less than a certain
number of database queries when loading.
To get the Profiler for the last request, do the following:
Listing 14-24
1 $profile = $client->getProfile();
For specific details on using the profiler inside a test, see the How to use the Profiler in a Functional Test
cookbook entry.
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 153
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 14-25
1 $crawler = $client->followRedirect();
If you want the client to automatically follow all redirects, you can force him with the
followRedirects() method:
Listing 14-26
1 $client->followRedirects();
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 14-27
1 $newCrawler = $crawler->filter('input[type=submit]')
2
->last()
3
->parents()
4
->first()
5 ;
Many other methods are also available:
Method
Description
filter('h1.title')
Nodes that match the CSS selector
filterXpath('h1')
Nodes that match the XPath expression
eq(1)
Node for the specified index
first()
First node
last()
Last node
siblings()
Siblings
nextAll()
All following siblings
previousAll()
All preceding siblings
parents()
Returns the parent nodes
children()
Returns children nodes
reduce($lambda)
Nodes for which the callable does not return false
Since each of these methods returns a new Crawler instance, you can narrow down your node selection
by chaining the method calls:
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 154
Listing 14-28
1 $crawler
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:
Listing 14-29
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Returns the attribute value for the first node
$crawler->attr('class');
// Returns the node value for the first node
$crawler->text();
// Extracts an array of attributes for all nodes (_text returns the node value)
// returns an array for each element in crawler, each with the value and href
$info = $crawler->extract(array('_text', 'href'));
// Executes a lambda for each node and return an array of results
$data = $crawler->each(function ($node, $i)
{
return $node->attr('href');
});
Links
To select links, you can use the traversing methods above or the convenient selectLink() shortcut:
Listing 14-30
1 $crawler->selectLink('Click here');
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 14-31
1 $link = $crawler->selectLink('Click here')->link();
2
3 $client->click($link);
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 155
Forms
Just like links, you select forms with the selectButton() method:
Listing 14-32
1 $buttonCrawlerNode = $crawler->selectButton('submit');
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:
• The value attribute value;
• The id or alt attribute value for images;
• The id or name attribute value for button tags.
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 14-33
1 $form = $buttonCrawlerNode->form();
When calling the form() method, you can also pass an array of field values that overrides the default
ones:
Listing 14-34
1 $form = $buttonCrawlerNode->form(array(
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:
Listing 14-35
1 $form = $buttonCrawlerNode->form(array(), 'DELETE');
The Client can submit Form instances:
Listing 14-36
1 $client->submit($form);
The field values can also be passed as a second argument of the submit() method:
Listing 14-37
1 $client->submit($form, array(
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:
Listing 14-38
1 // Change the value of a field
2 $form['name'] = 'Fabien';
3 $form['my_form[subject]'] = 'Symfony rocks!';
There is also a nice API to manipulate the values of the fields according to their type:
Listing 14-39
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 156
1
2
3
4
5
6
7
8
// Select an option or a radio
$form['country']->select('France');
// Tick a checkbox
$form['like_symfony']->tick();
// Upload a file
$form['photo']->upload('/path/to/lucas.jpg');
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
format (it converts the keys with square brackets notation - e.g. my_form[subject] - to PHP
arrays).
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 14-40
1 # app/config/config_test.yml
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 14-41
1 $client = static::createClient(array(
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 14-42
1 $client = static::createClient(array(), array(
2
'HTTP_HOST'
=> 'en.example.com',
3
'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
4 ));
You can also override HTTP headers on a per request basis:
Listing 14-43
1 $client->request('GET', '/', array(), array(), array(
2
'HTTP_HOST'
=> 'en.example.com',
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 157
3
'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
4 ));
The test client is available as a service in the container in the test environment (or wherever the
framework.test option is enabled). This means you can override the service entirely if you need to.
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:
Listing 14-44
1 <!-- hello/phpunit.xml.dist -->
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:
Listing 14-45
1 <filter>
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>
Learn more
•
•
•
•
The DomCrawler Component
The CssSelector Component
How to simulate HTTP Authentication in a Functional Test
How to test the Interaction of several Clients
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 158
• How to use the Profiler in a Functional Test
• How to customize the Bootstrap Process before running Tests
PDF brought to you by
generated on October 26, 2012
Chapter 14: Testing | 159
Chapter 15
Validation
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.
The Basics of Validation
The best way to understand validation is to see it in action. To start, suppose you've created a plain-oldPHP object that you need to use somewhere in your application:
Listing 15-1
1
2
3
4
5
6
7
// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;
class Author
{
public $name;
}
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:
Listing 15-2
1 # src/Acme/BlogBundle/Resources/config/validation.yml
2 Acme\BlogBundle\Entity\Author:
3
properties:
1. https://github.com/symfony/Validator
2. http://jcp.org/en/jsr/detail?id=303
PDF brought to you by
generated on October 26, 2012
Chapter 15: Validation | 160
4
5
name:
- NotBlank: ~
Protected and private properties can also be validated, as well as "getter" methods (see validatorconstraint-targets).
Using the validator Service
Next, to actually validate an Author object, use the validate method on the validator service (class
Validator3). The job of the validator is easy: to read the constraints (i.e. rules) of a class and verify
whether or not the data on the object satisfies those constraints. If validation fails, an array of errors is
returned. Take this simple example from inside a controller:
Listing 15-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ...
use Symfony\Component\HttpFoundation\Response;
use Acme\BlogBundle\Entity\Author;
public function indexAction()
{
$author = new Author();
// ... do something to the $author object
$validator = $this->get('validator');
$errors = $validator->validate($author);
if (count($errors) > 0) {
return new Response(print_r($errors, true));
} else {
return new Response('The author is valid! Yes!');
}
}
If the $name property is empty, you will see the following error message:
Listing 15-4
1 Acme\BlogBundle\Author.name:
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.
You could also pass the collection of errors into a template.
Listing 15-5
1 if (count($errors) > 0) {
2
return $this->render('AcmeBlogBundle:Author:validate.html.twig', array(
3
'errors' => $errors,
4
));
3. http://api.symfony.com/2.0/Symfony/Component/Validator/Validator.html
PDF brought to you by
generated on October 26, 2012
Chapter 15: Validation | 161
5 } else {
6
// ...
7 }
Inside the template, you can output the list of errors exactly as needed:
Listing 15-6
1
2
3
4
5
6
7
{# src/Acme/BlogBundle/Resources/views/Author/validate.html.twig #}
<h3>The author has the following errors</h3>
<ul>
{% for error in errors %}
<li>{{ error.message }}</li>
{% endfor %}
</ul>
Each validation error (called a "constraint violation"), is represented by a ConstraintViolation4
object.
Validation and Forms
The validator service can be used at any time to validate any object. In reality, however, you'll
usually work with the validator indirectly when working with forms. Symfony's form library uses the
validator service internally to validate the underlying object after values have been submitted and
bound. The constraint violations on the object are converted into FieldError objects that can easily be
displayed with your form. The typical form submission workflow looks like the following from inside a
controller:
Listing 15-7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...
use Acme\BlogBundle\Entity\Author;
use Acme\BlogBundle\Form\AuthorType;
use Symfony\Component\HttpFoundation\Request;
public function updateAction(Request $request)
{
$author = new Author();
$form = $this->createForm(new AuthorType(), $author);
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
// the validation passed, do something with the $author object
return $this->redirect($this->generateUrl(...));
}
}
return $this->render('BlogBundle:Author:form.html.twig', array(
'form' => $form->createView(),
));
}
4. http://api.symfony.com/2.0/Symfony/Component/Validator/ConstraintViolation.html
PDF brought to you by
generated on October 26, 2012
Chapter 15: Validation | 162
This example uses an AuthorType form class, which is not shown here.
For more information, see the Forms chapter.
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:
Listing 15-8
1 # app/config/config.yml
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
False
Type
String Constraints
•
•
•
•
•
•
Email
MinLength
MaxLength
Url
Regex
Ip
PDF brought to you by
generated on October 26, 2012
Chapter 15: Validation | 163
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":
Listing 15-9
1 # src/Acme/BlogBundle/Resources/config/validation.yml
2 Acme\BlogBundle\Entity\Author:
3
properties:
4
gender:
5
- Choice: { choices: [male, female], message: Choose a valid gender. }
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.
Listing 15-10
1 # src/Acme/BlogBundle/Resources/config/validation.yml
2 Acme\BlogBundle\Entity\Author:
3
properties:
PDF brought to you by
generated on October 26, 2012
Chapter 15: Validation | 164
4
5
gender:
- 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).
Translation Constraint Messages
For information on translating the constraint messages, see Translating Constraint Messages.
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.
Listing 15-11
1 # src/Acme/BlogBundle/Resources/config/validation.yml
2 Acme\BlogBundle\Entity\Author:
3
properties:
4
firstName:
5
- NotBlank: ~
6
- MinLength: 3
Getters
Constraints can also be applied to the return value of a method. Symfony2 allows you to add a constraint
to any public method whose name starts with "get" or "is". In this guide, both of these types of methods
are referred to as "getters".
The benefit of this technique is that it allows you to validate your object dynamically. For example,
suppose you want to make sure that a password field doesn't match the first name of the user (for security
reasons). You can do this by creating an isPasswordLegal method, and then asserting that this method
must return true:
Listing 15-12
1 # src/Acme/BlogBundle/Resources/config/validation.yml
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 15-13
PDF brought to you by
generated on October 26, 2012
Chapter 15: Validation | 165
1 public function isPasswordLegal()
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:
Listing 15-14
1 # src/Acme/BlogBundle/Resources/config/validation.yml
2 Acme\BlogBundle\Entity\User:
3
properties:
4
email:
5
- Email: { groups: [registration] }
6
password:
7
- NotBlank: { groups: [registration] }
8
- MinLength: { limit: 7, groups: [registration] }
9
city:
10
- MinLength: 2
With this configuration, there are two validation groups:
• Default - contains the constraints not assigned to any other group;
• registration - contains the constraints on the email and password fields only.
To tell the validator to use a specific group, pass one or more group names as the second argument to the
validate() method:
Listing 15-15
1 $errors = $validator->validate($author, array('registration'));
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.
PDF brought to you by
generated on October 26, 2012
Chapter 15: Validation | 166
Validating Values and Arrays
So far, you've seen how you can validate entire objects. But sometimes, you just want to validate a simple
value - like to verify that a string is a valid email address. This is actually pretty easy to do. From inside a
controller, it looks like this:
Listing 15-16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// add this to the top of your class
use Symfony\Component\Validator\Constraints\Email;
public function addEmailAction($email)
{
$emailConstraint = new Email();
// all constraint "options" can be set this way
$emailConstraint->message = 'Invalid email address';
// use the validator to validate the value
$errorList = $this->get('validator')->validateValue($email, $emailConstraint);
if (count($errorList) == 0) {
// this IS a valid email address, do something
} else {
// this is *not* a valid email address
$errorMessage = $errorList[0]->getMessage()
// ... do something with the error
}
// ...
}
By calling validateValue on the validator, you can pass in a raw value and the constraint object that you
want to validate that value against. A full list of the available constraints - as well as the full class name
for each constraint - is available in the constraints reference section .
The validateValue method returns a ConstraintViolationList5 object, which acts just like an array
of errors. Each error in the collection is a ConstraintViolation6 object, which holds the error message
on its getMessage method.
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.
Learn more from the Cookbook
• How to create a Custom Validation Constraint
5. http://api.symfony.com/2.0/Symfony/Component/Validator/ConstraintViolationList.html
6. http://api.symfony.com/2.0/Symfony/Component/Validator/ConstraintViolation.html
PDF brought to you by
generated on October 26, 2012
Chapter 15: Validation | 167
Chapter 16
Forms
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.
Creating a Simple Form
Suppose you're building a simple todo list application that will need to display "tasks". Because your users
will need to edit and create tasks, you're going to need to build a form. But before you begin, first focus
on the generic Task class that represents and stores the data for a single task:
Listing 16-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\Entity;
class Task
{
protected $task;
protected $dueDate;
public function getTask()
{
return $this->task;
}
public function setTask($task)
{
1. https://github.com/symfony/Form
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 168
16
17
18
19
20
21
22
23
24
25
26
27 }
$this->task = $task;
}
public function getDueDate()
{
return $this->dueDate;
}
public function setDueDate(\DateTime $dueDate = null)
{
$this->dueDate = $dueDate;
}
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 16-2
1 $ php app/console generate:bundle --namespace=Acme/TaskBundle
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.
Building the Form
Now that you've created a Task class, the next step is to create and render the actual HTML form. In
Symfony2, this is done by building a form object and then rendering it in a template. For now, this can
all be done from inside a controller:
Listing 16-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Acme/TaskBundle/Controller/DefaultController.php
namespace Acme\TaskBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Acme\TaskBundle\Entity\Task;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
public function newAction(Request $request)
{
// create a task and give it some dummy data for this example
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTime('tomorrow'));
$form = $this->createFormBuilder($task)
->add('task', 'text')
->add('dueDate', 'date')
->getForm();
return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 169
25
26 }
}
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).
Rendering the Form
Now that the form has been created, the next step is to render it. This is done by passing a special form
"view" object to your template (notice the $form->createView() in the controller above) and using a set
of form helper functions:
Listing 16-4
1 {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
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
each form field individually so you can control how the form looks. You'll learn how to do that in the
"Rendering a Form in a Template" section.
Before moving on, notice how the rendered task input field has the value of the task property from the
$task object (i.e. "Write a blog post"). This is the first job of a form: to take data from an object and
translate it into a format that's suitable for being rendered in an HTML form.
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 170
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()).
Handling Form Submissions
The second job of a form is to translate user-submitted data back to the properties of an object. To
make this happen, the submitted data from the user must be bound to the form. Add the following
functionality to your controller:
Listing 16-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...
public function newAction(Request $request)
{
// just setup a fresh $task object (remove the dummy data)
$task = new Task();
$form = $this->createFormBuilder($task)
->add('task', 'text')
->add('dueDate', 'date')
->getForm();
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
// perform some action, such as saving the task to the database
return $this->redirect($this->generateUrl('task_success'));
}
}
// ...
}
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:
1. When initially loading the page in a browser, the request method is GET and the form is simply
created and rendered;
2. When the user submits the form (i.e. the method is POST) with invalid data (validation is covered
in the next section), the form is bound and then rendered, this time displaying all validation
errors;
3. When the user submits the form with valid data, the form is bound and you have the
opportunity to perform some actions using the $task object (e.g. persisting it to the database)
before redirecting the user to some other page (e.g. a "thank you" or "success" page).
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 171
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.
Listing 16-6
1 # Acme/TaskBundle/Resources/config/validation.yml
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.
Validation Groups
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 16-7
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 172
1 $form = $this->createFormBuilder($users, array(
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 16-8
1 public function getDefaultOptions(array $options)
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.
Built-in Field Types
Symfony comes standard with a large group of field types that cover all of the common form fields and
data types you'll encounter:
Text Fields
•
•
•
•
•
•
•
•
•
•
text
textarea
email
integer
money
number
password
percent
search
url
Choice Fields
•
•
•
•
•
•
choice
entity
country
language
locale
timezone
Date and Time Fields
•
•
•
•
date
datetime
time
birthday
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 173
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.
Field Type Options
Each field type has a number of options that can be used to configure it. For example, the dueDate field
is currently being rendered as 3 select boxes. However, the date field can be configured to be rendered as
a single text box (where the user would enter the date as a string in the box):
Listing 16-9
1 ->add('dueDate', 'date', array('widget' => 'single_text'))
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.
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 174
The required option
The most common option is the required option, which can be applied to any field. By default,
the required option is set to true, meaning that HTML5-ready browsers will apply client-side
validation if the field is left blank. If you don't want this behavior, either set the required option
on your field to false or disable HTML5 validation.
Also note that setting the required option to true will not result in server-side validation to be
applied. In other words, if a user submits a blank value for the field (either with an old browser or
web service, for example), it will be accepted as a valid value unless you use Symfony's NotBlank
or NotNull validation constraint.
In other words, the required option is "nice", but true server-side validation should always be
used.
The label option
The label for the form field can be set using the label option, which can be applied to any field:
Listing 16-10
1 ->add('dueDate', 'date', array(
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.
Field Type Guessing
Now that you've added validation metadata to the Task class, Symfony already knows a bit about your
fields. If you allow it, Symfony can "guess" the type of your field and set it up for you. In this example,
Symfony can guess from the validation rules that both the task field is a normal text field and the
dueDate field is a date field:
Listing 16-11
1 public function newAction()
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.
If your form uses a specific validation group, the field type guesser will still consider all validation
constraints when guessing your field types (including constraints that are not part of the validation
group(s) being used).
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 175
Field Type Options Guessing
In addition to guessing the "type" for a field, Symfony can also try to guess the correct values of a number
of field options.
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:
Listing 16-12
1 ->add('task', null, array('max_length' => 4))
Rendering a Form in a Template
So far, you've seen how an entire form can be rendered with just one line of code. Of course, you'll usually
need much more flexibility when rendering:
Listing 16-13
1 {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
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>
Let's take a look at each part:
• 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;
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 176
• 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 16-14
1 {{ form.vars.value.task }}
Rendering each Field by Hand
The form_row helper is great because you can very quickly render each field of your form (and the
markup used for the "row" can be customized as well). But since life isn't always so simple, you can also
render each field entirely by hand. The end-product of the following is the same as when you used the
form_row helper:
Listing 16-15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{{ form_errors(form) }}
<div>
{{ form_label(form.task) }}
{{ form_errors(form.task) }}
{{ form_widget(form.task) }}
</div>
<div>
{{ form_label(form.dueDate) }}
{{ form_errors(form.dueDate) }}
{{ form_widget(form.dueDate) }}
</div>
{{ form_rest(form) }}
If the auto-generated label for a field isn't quite right, you can explicitly specify it:
Listing 16-16
1 {{ form_label(form.task, 'Task Description') }}
Some field types have additional rendering options that can be passed to the widget. These options are
documented with each type, but one common options is attr, which allows you to modify attributes on
the form element. The following would add the task_field class to the rendered input text field:
Listing 16-17
1 {{ form_widget(form.task, { 'attr': {'class': 'task_field'} }) }}
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:
Listing 16-18
1 {{ form.task.vars.id }}
To get the value used for the form field's name attribute you need to use the full_name value:
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 177
Listing 16-19
1 {{ form.task.vars.full_name }}
Twig Template Function Reference
If you're using Twig, a full reference of the form rendering functions is available in the reference manual.
Read this to know everything about the helpers available and the options that can be used with each.
Creating Form Classes
As you've seen, a form can be created and used directly in a controller. However, a better practice is
to build the form in a separate, standalone PHP class, which can then be reused anywhere in your
application. Create a new class that will house the logic for building the task form:
Listing 16-20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class TaskType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('task');
$builder->add('dueDate', null, array('widget' => 'single_text'));
}
public function getName()
{
return 'task';
}
}
This new class contains all the directions needed to create the task form (note that the getName() method
should return a unique identifier for this form "type"). It can be used to quickly build a form object in the
controller:
Listing 16-21
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/TaskBundle/Controller/DefaultController.php
// add this new use statement at the top of the class
use Acme\TaskBundle\Form\Type\TaskType;
public function newAction()
{
$task = ...;
$form = $this->createForm(new TaskType(), $task);
// ...
}
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.
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 178
Setting the data_class
Every form needs to know the name of the class that holds the underlying data (e.g.
Acme\TaskBundle\Entity\Task). Usually, this is just guessed based off of the object passed to the
second argument to createForm (i.e. $task). Later, when you begin embedding forms, this will no
longer be sufficient. So, while not always necessary, it's generally a good idea to explicitly specify
the data_class option by adding the following to your form type class:
Listing 16-22
1 public function getDefaultOptions(array $options)
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 16-23
1 public function buildForm(FormBuilder $builder, array $options)
2 {
3
$builder->add('task');
4
$builder->add('dueDate', null, array('property_path' => false));
5 }
Additionally, if there are any fields on the form that aren't included in the submitted data, those
fields will be explicitly set to null.
The field data can be accessed in a controller with:
Listing 16-24
1 $form->get('dueDate')->getData();
Forms and Doctrine
The goal of a form is to translate data from an object (e.g. Task) to an HTML form and then translate
user-submitted data back to the original object. As such, the topic of persisting the Task object to the
database is entirely unrelated to the topic of forms. But, if you've configured the Task class to be persisted
via Doctrine (i.e. you've added mapping metadata for it), then persisting it after a form submission can be
done when the form is valid:
Listing 16-25
1 if ($form->isValid()) {
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:
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 179
Listing 16-26
1 $task = $form->getData();
For more information, see the Doctrine ORM chapter.
The key thing to understand is that when the form is bound, the submitted data is transferred to the
underlying object immediately. If you want to persist that data, you simply need to persist the object itself
(which already contains the submitted data).
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.
Embedding a Single Object
Suppose that each Task belongs to a simple Category object. Start, of course, by creating the Category
object:
Listing 16-27
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/TaskBundle/Entity/Category.php
namespace Acme\TaskBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Category
{
/**
* @Assert\NotBlank()
*/
public $name;
}
Next, add a new category property to the Task class:
Listing 16-28
1 // ...
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;
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 180
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:
Listing 16-29
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/Acme/TaskBundle/Form/Type/CategoryType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class CategoryType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\TaskBundle\Entity\Category',
);
}
public function getName()
{
return 'category';
}
}
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:
Listing 16-30
1 public function buildForm(FormBuilder $builder, array $options)
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:
Listing 16-31
1
2
3
4
5
6
7
8
9
{# ... #}
<h3>Category</h3>
<div class="category">
{{ form_row(form.category.name) }}
</div>
{{ form_rest(form) }}
{# ... #}
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 181
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.
Embedding a Collection of Forms
You can also embed a collection of forms into one form (imagine a Category form with many Product
sub-forms). This is done by using the collection field type.
For more information see the "How to Embed a Collection of Forms" cookbook entry and the collection
field type reference.
Form Theming
Every part of how a form is rendered can be customized. You're free to change how each form "row"
renders, change the markup used to render errors, or even customize how a textarea tag should be
rendered. Nothing is off-limits, and different customizations can be used in different places.
Symfony uses templates to render each and every part of a form, such as label tags, input tags, error
messages and everything else.
In Twig, each form "fragment" is represented by a Twig block. To customize any part of how a form
renders, you just need to override the appropriate block.
In PHP, each form "fragment" is rendered via an individual template file. To customize any part of how a
form renders, you just need to override the existing template by creating a new one.
To understand how this works, let's customize the form_row fragment and add a class attribute to the div
element that surrounds each row. To do this, create a new template file that will store the new markup:
Listing 16-32
1
2
3
4
5
6
7
8
9
10
{# src/Acme/TaskBundle/Resources/views/Form/fields.html.twig #}
{% block field_row %}
{% spaceless %}
<div class="form_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% 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 16-33
{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
{% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' %}
{% form_theme form 'AcmeTaskBundle:Form:fields.html.twig'
'AcmeTaskBundle:Form:fields2.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).
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 182
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.
Form Fragment Naming
In Symfony, every part of a form that is rendered - HTML form elements, errors, labels, etc - is defined in
a base theme, which is a collection of blocks in Twig and a collection of template files in PHP.
In Twig, every block needed is defined in a single template file (form_div_layout.html.twig2) that lives
inside the Twig Bridge3. Inside this file, you can see every block needed to render a form and every default
field type.
In PHP, the fragments are individual template files. By default they are located in the Resources/views/
Form directory of the framework bundle (view on GitHub4).
Each fragment name follows the same basic pattern and is broken up into two pieces, separated by a
single underscore character (_). A few examples are:
• field_row - used by form_row to render most fields;
• textarea_widget - used by form_widget to render a textarea field type;
• field_errors - used by form_errors to render errors for a field;
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:
label
(e.g. field_label)
renders the field's label
widget
(e.g. field_widget)
renders the field's HTML representation
errors
(e.g. field_errors)
renders the field's errors
row
(e.g. field_row)
renders the field's entire row (label, widget & errors)
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).
Template Fragment Inheritance
In some cases, the fragment you want to customize will appear to be missing. For example, there is no
textarea_errors fragment in the default themes provided with Symfony. So how are the errors for a
textarea field rendered?
2. https://github.com/symfony/symfony/blob/2.0/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
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 183
The answer is: via the field_errors fragment. When Symfony renders the errors for a textarea type, it
looks first for a textarea_errors fragment before falling back to the field_errors fragment. Each field
type has a parent type (the parent type of textarea is field), and Symfony uses the fragment for the
parent type if the base fragment doesn't exist.
So, to override the errors for only textarea fields, copy the field_errors fragment, rename it to
textarea_errors and customize it. To override the default error rendering for all fields, copy and
customize the field_errors fragment directly.
The "parent" type of each field type is available in the form type reference for each field type.
Global Form Theming
In the above example, you used the form_theme helper (in Twig) to "import" the custom form fragments
into just that form. You can also tell Symfony to import form customizations across your entire project.
Twig
To automatically include the customized blocks from the fields.html.twig template created earlier in
all templates, modify your application configuration file:
Listing 16-34
1 # app/config/config.yml
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.
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 184
Customizing Form Output all in a Single File with Twig
In Twig, you can also customize a form block right inside the template where that customization
is needed:
Listing 16-35
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% extends '::base.html.twig' %}
{# import "_self" as the form theme #}
{% form_theme form _self %}
{# make the form fragment customization #}
{% block field_row %}
{# custom field row output #}
{% endblock field_row %}
{% block content %}
{# ... #}
{{ form_row(form.task) }}
{% 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:
Listing 16-36
1 # app/config/config.yml
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.
CSRF Protection
CSRF - or Cross-site request forgery5 - is a method by which a malicious user attempts to make your
legitimate users unknowingly submit data that they don't intend to submit. Fortunately, CSRF attacks
can be prevented by using a CSRF token inside your forms.
5. http://en.wikipedia.org/wiki/Cross-site_request_forgery
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 185
The good news is that, by default, Symfony embeds and validates CSRF tokens automatically for you.
This means that you can take advantage of the CSRF protection without doing anything. In fact, every
form in this chapter has taken advantage of the CSRF protection!
CSRF protection works by adding a hidden field to your form - called _token by default - that contains a
value that only you and your user knows. This ensures that the user - not some other entity - is submitting
the given data. Symfony automatically validates the presence and accuracy of this token.
The _token field is a hidden field and will be automatically rendered if you include the form_rest()
function in your template, which ensures that all un-rendered fields are output.
The CSRF token can be customized on a form-by-form basis. For example:
Listing 16-37
1 class TaskType extends AbstractType
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.
Using a Form without a Class
In most cases, a form is tied to an object, and the fields of the form get and store their data on the
properties of that object. This is exactly what you've seen so far in this chapter with the Task class.
But sometimes, you may just want to use a form without a class, and get back an array of the submitted
data. This is actually really easy:
Listing 16-38
1
2
3
4
5
6
7
8
9
10
11
// make sure you've imported the Request namespace above the class
use Symfony\Component\HttpFoundation\Request
// ...
public function contactAction(Request $request)
{
$defaultData = array('message' => 'Type your message here');
$form = $this->createFormBuilder($defaultData)
->add('name', 'text')
->add('email', 'email')
->add('message', 'textarea')
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 186
12
13
14
15
16
17
18
19
20
21
22 }
->getForm();
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
// data is an array with "name", "email", and "message" keys
$data = $form->getData();
}
// ... render the form
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:
Listing 16-39
1 $this->get('request')->request->get('name');
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:
Listing 16-40
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// import the namespaces above your controller class
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\MinLength;
use Symfony\Component\Validator\Constraints\Collection;
$collectionConstraint = new Collection(array(
'name' => new MinLength(5),
'email' => new Email(array('message' => 'Invalid email address')),
));
// create a form, no default values, pass in the constraint option
$form = $this->createFormBuilder(null, array(
'validation_constraint' => $collectionConstraint,
))->add('email', 'email')
// ...
;
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 187
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:
Listing 16-41
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Acme\TaskBundle\Form\Type;
use
use
use
use
use
Symfony\Component\Form\AbstractType;
Symfony\Component\Form\FormBuilder;
Symfony\Component\Validator\Constraints\Email;
Symfony\Component\Validator\Constraints\MinLength;
Symfony\Component\Validator\Constraints\Collection;
class ContactType extends AbstractType
{
// ...
public function getDefaultOptions(array $options)
{
$collectionConstraint = new Collection(array(
'name' => new MinLength(5),
'email' => new Email(array('message' => 'Invalid email address')),
));
return array('validation_constraint' => $collectionConstraint);
}
}
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
list where you can keep adding more fields via Javascript before submitting). See the cookbook for these
topics. Also, be sure to lean on the field type reference documentation, which includes examples of how to
use each field type and its options.
Learn more from the Cookbook
•
•
•
•
•
•
How to handle File Uploads with Doctrine
File Field Reference
Creating Custom Field Types
How to customize Form Rendering
How to Dynamically Generate Forms Using Form Events
How to use Data Transformers
PDF brought to you by
generated on October 26, 2012
Chapter 16: Forms | 188
Chapter 17
Security
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.
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 189
Basic Example: HTTP Authentication
The security component can be configured via your application configuration. In fact, most standard
security setups are just a matter of using the right configuration. The following configuration tells
Symfony to secure any URL matching /admin/* and to ask the user for credentials using basic HTTP
authentication (i.e. the old-school username/password box):
Listing 17-1
1 # app/config/security.yml
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:
•
•
•
•
There are two users in the system (ryan and admin);
Users authenticate themselves via the basic HTTP authentication prompt;
Any URL matching /admin/* is secured, and only the admin user can access it;
All URLs not matching /admin/* are accessible by all users (and the user is never prompted to
login).
Let's look briefly at how security works and how each part of the configuration comes into play.
How Security Works: Authentication and Authorization
Symfony's security system works by determining who a user is (i.e. authentication) and then checking to
see if that user should have access to a specific resource or URL.
Firewalls (Authentication)
When a user makes a request to a URL that's protected by a firewall, the security system is activated. The
job of the firewall is to determine whether or not the user needs to be authenticated, and if he does, to
send a response back to the user initiating the authentication process.
1. https://github.com/symfony/Security
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 190
A firewall is activated when the URL of an incoming request matches the configured firewall's regular
expression pattern config value. In this example, the pattern (^/) will match every incoming request.
The fact that the firewall is activated does not mean, however, that the HTTP authentication username
and password box is displayed for every URL. For example, any user can access /foo without being
prompted to authenticate.
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.
Access Controls (Authorization)
If a user requests /admin/foo, however, the process behaves differently. This is because of the
access_control configuration section that says that any URL matching the regular expression pattern
^/admin (i.e. /admin or anything matching /admin/*) requires the ROLE_ADMIN role. Roles are the basis
for most authorization: a user can access /admin/foo only if it has the ROLE_ADMIN role.
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 191
Like before, when the user originally makes the request, the firewall doesn't ask for any identification.
However, as soon as the access control layer denies the user access (because the anonymous user doesn't
have the ROLE_ADMIN role), the firewall jumps into action and initiates the authentication process. The
authentication process depends on the authentication mechanism you're using. For example, if you're
using the form login authentication method, the user will be redirected to the login page. If you're using
HTTP authentication, the user will be sent an HTTP 401 response so that the user sees the username and
password box.
The user now has the opportunity to submit its credentials back to the application. If the credentials are
valid, the original request can be re-tried.
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 192
In this example, the user ryan successfully authenticates with the firewall. But since ryan doesn't have
the ROLE_ADMIN role, he's still denied access to /admin/foo. Ultimately, this means that the user will see
some sort of message indicating that access has been denied.
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:
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 193
The request flow when a user requests a protected resource is straightforward, but incredibly flexible. As
you'll see later, authentication can be handled in any number of ways, including via a form login, X.509
certificate, or by authenticating the user via Twitter. Regardless of the authentication method, the request
flow is always the same:
1. A user accesses a protected resource;
2. The application redirects the user to the login form;
3. The user submits its credentials (e.g. username/password);
4. The firewall authenticates the user;
5. The authenticated user re-tries the original request.
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.
Using a Traditional Login Form
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.
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 194
To load users from the database, please read How to load Security Users from the Database (the
Entity Provider). By reading that article and this section, you can create a full login form system
that loads users from the database.
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 17-2
1 # app/config/security.yml
2 security:
3
firewalls:
4
secured_area:
5
pattern:
^/
6
anonymous: ~
7
form_login:
8
login_path:
9
check_path:
/login
/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 17-3
1 form_login: ~
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 17-4
1 # app/config/routing.yml
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.
Notice that the name of the login route isn't important. What's important is that the URL of the route
(/login) matches the login_path config value, as that's where the security system will redirect users that
need to login.
Next, create the controller that will display the login form:
Listing 17-5
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 195
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Acme/SecurityBundle/Controller/SecurityController.php;
namespace Acme\SecurityBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;
class SecurityController extends Controller
{
public function loginAction()
{
$request = $this->getRequest();
$session = $request->getSession();
// get the login error if there is one
if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
} else {
$error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
$session->remove(SecurityContext::AUTHENTICATION_ERROR);
}
return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
// last username entered by the user
'last_username' => $session->get(SecurityContext::LAST_USERNAME),
'error'
=> $error,
));
}
}
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:
Listing 17-6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
{% if error %}
<div>{{ error.message }}</div>
{% endif %}
<form action="{{ path('login_check') }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
{#
If you want to control the URL the user is redirected to on success (more details
below)
<input type="hidden" name="_target_path" value="/account" />
#}
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 196
<button type="submit">login</button>
</form>
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
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 197
Avoid Common Pitfalls
When setting up your login form, watch out for a few common pitfalls.
1. Create the correct routes
First, be sure that you've defined the /login and /login_check routes correctly and that they
correspond to the login_path and check_path config values. A misconfiguration here can mean
that you're redirected to a 404 page instead of the login page, or that submitting the login form
does nothing (you just see the login form over and over again).
2. Be sure the login page isn't secure
Also, be sure that the login page does not require any roles to be viewed. For example, the following
configuration - which requires the ROLE_ADMIN role for all URLs (including the /login URL), will
cause a redirect loop:
Listing 17-7
1 access_control:
2
- { path: ^/, roles: ROLE_ADMIN }
Removing the access control on the /login URL fixes the problem:
Listing 17-8
1 access_control:
2
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
3
- { path: ^/, roles: ROLE_ADMIN }
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:
Listing 17-9
1 firewalls:
2
login_firewall:
3
pattern:
4
anonymous:
5
secured_area:
6
pattern:
7
form_login:
^/login$
~
^/
~
3. Be sure ``/login_check`` is behind a firewall
Next, make sure that your check_path URL (e.g. /login_check) is behind the firewall you're
using for your form login (in this example, the single firewall matches all URLs, including
/login_check). If /login_check doesn't match any firewall, you'll receive a Unable to find the
controller for path "/login_check" exception.
4. Multiple firewalls don't share security context
If you're using multiple firewalls and you authenticate against one firewall, you will not be
authenticated against any other firewalls automatically. Different firewalls are like different
security systems. That's why, for most applications, having one main firewall is enough.
Authorization
The first step in security is always authentication: the process of verifying who the user is. With Symfony,
authentication can be done in any way - via a form login, basic HTTP Authentication, or even via
Facebook.
Once the user has been authenticated, authorization begins. Authorization provides a standard and
powerful way to decide if a user can access any resource (a URL, a model object, a method call, ...). This
works by assigning specific roles to each user, and then requiring different roles for different resources.
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 198
The process of authorization has two different sides:
1. The user has a specific set of roles;
2. A resource requires a specific role in order to be accessed.
In this section, you'll focus on how to secure different resources (e.g. URLs, method calls, etc) with
different roles. Later, you'll learn more about how roles are created and assigned to users.
Securing Specific URL Patterns
The most basic way to secure part of your application is to secure an entire URL pattern. You've seen
this already in the first example of this chapter, where anything matching the regular expression pattern
^/admin requires the ROLE_ADMIN role.
You can define as many URL patterns as you need - each is a regular expression.
Listing 17-10
1 # app/config/security.yml
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).
Here is an example of how you might secure this route from outside access:
Listing 17-11
1 # app/config/security.yml
2 security:
3
# ...
4
access_control:
5
- { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
3. http://api.symfony.com/2.0/Symfony/Component/Security/Core/Exception/AccessDeniedException.html
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 199
Securing by Channel
Much like securing based on IP, requiring the use of SSL is as simple as adding a new access_control
entry:
Listing 17-12
1 # app/config/security.yml
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:
Listing 17-13
1
2
3
4
5
6
7
8
9
10
11
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
public function helloAction($name)
{
if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
// ...
}
You can also choose to install and use the optional JMSSecurityExtraBundle, which can secure your
controller using annotations:
Listing 17-14
1
2
3
4
5
6
7
8
9
10
// ...
use JMS\SecurityExtraBundle\Annotation\Secure;
/**
* @Secure(roles="ROLE_ADMIN")
*/
public function helloAction($name)
{
// ...
}
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.
Securing other Services
In fact, anything in Symfony can be protected using a strategy similar to the one seen in the previous
section. For example, suppose you have a service (i.e. a PHP class) whose job is to send emails from one
user to another. You can restrict use of this class - no matter where it's being used from - to users that
have a specific role.
For more information on how you can use the security component to secure different services and
methods in your application, see How to secure any Service or Method in your Application.
4. https://github.com/schmittjoh/JMSSecurityExtraBundle
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 200
Access Control Lists (ACLs): Securing Individual Database Objects
Imagine you are designing a blog system where your users can comment on your posts. Now, you want
a user to be able to edit his own comments, but not those of other users. Also, as the admin user, you
yourself want to be able to edit all comments.
The security component comes with an optional access control list (ACL) system that you can use when
you need to control access to individual instances of an object in your system. Without ACL, you can
secure your system so that only certain users can edit blog comments in general. But with ACL, you can
restrict or allow access on a comment-by-comment basis.
For more information, see the cookbook article: How to use Access Control Lists (ACLs).
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.
Where do Users come from? (User Providers)
During authentication, the user submits a set of credentials (usually a username and password). The job
of the authentication system is to match those credentials against some pool of users. So where does this
list of users come from?
In Symfony2, users can come from anywhere - a configuration file, a database table, a web service, or
anything else you can dream up. Anything that provides one or more users to the authentication system
is known as a "user provider". Symfony2 comes standard with the two most common user providers: one
that loads users from a configuration file and one that loads users from a database table.
Specifying Users in a Configuration File
The easiest way to specify your users is directly in a configuration file. In fact, you've seen this already in
the example in this chapter.
Listing 17-15
1 # app/config/security.yml
2 security:
3
# ...
4
providers:
5
default_provider:
6
users:
7
ryan: { password: ryanpass, roles: 'ROLE_USER' }
8
admin: { password: kitten, roles: 'ROLE_ADMIN' }
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:
5. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/User.html
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 201
Listing 17-16
1 users:
2
- { name: 77, password: pass, roles: 'ROLE_USER' }
3
- { name: user-name, password: pass, roles: 'ROLE_USER' }
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.
Loading Users from the Database
If you'd like to load your users via the Doctrine ORM, you can easily do this by creating a User class and
configuring the entity provider.
With this approach, you'll first create your own User class, which will be stored in the database.
Listing 17-17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class User implements UserInterface
{
/**
* @ORM\Column(type="string", length=255)
*/
protected $username;
// ...
}
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 17-18
1 # app/config/security.yml
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.
6. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserInterface.html
7. http://php.net/manual/en/class.serializable.php
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 202
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.
Encoding the User's Password
So far, for simplicity, all the examples have stored the users' passwords in plain text (whether those users
are stored in a configuration file or in a database somewhere). Of course, in a real application, you'll want
to encode your users' passwords for security reasons. This is easily accomplished by mapping your User
class to one of several built-in "encoders". For example, to store your users in memory, but obscure their
passwords via sha1, do the following:
Listing 17-19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# app/config/security.yml
security:
# ...
providers:
in_memory:
users:
ryan: { password: bb87a29949f3a1ee0559f8a57357487151281386, roles:
'ROLE_USER' }
admin: { password: 74913f5cd5f61ec0bcfdb775414c2fb3d161b620, roles:
'ROLE_ADMIN' }
encoders:
Symfony\Component\Security\Core\User\User:
algorithm: sha1
iterations: 1
encode_as_base64: false
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:
Listing 17-20
1 # app/config/security.yml
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).
8. http://www.functions-online.com/sha1.html
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 203
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:
Listing 17-21
1
2
3
4
5
6
$factory = $this->get('security.encoder_factory');
$user = new Acme\UserBundle\Entity\User();
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword('ryanpass', $user->getSalt());
$user->setPassword($password);
Retrieving the User Object
After authentication, the User object of the current user can be accessed via the security.context
service. From inside a controller, this will look like:
Listing 17-22
1 public function indexAction()
2 {
3
$user = $this->get('security.context')->getToken()->getUser();
4 }
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 17-23
1 <p>Username: {{ app.user.username }}</p>
Using Multiple User Providers
Each authentication mechanism (e.g. HTTP Authentication, form login, etc) uses exactly one user
provider, and will use the first declared user provider by default. But what if you want to specify a few
users via configuration and the rest of your users in the database? This is possible by creating a new
provider that chains the two together:
Listing 17-24
1 # app/config/security.yml
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.
9. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.html#getUser()
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 204
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 17-25
1 # app/config/security.yml
2 security:
3
providers:
4
main_provider:
5
users:
6
foo: { password: test }
7
entity: { class: Acme\UserBundle\Entity\User, property: username }
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:
Listing 17-26
1 # app/config/security.yml
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:
Listing 17-27
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 205
1 # app/config/security.yml
2 security:
3
role_hierarchy:
4
ROLE_ADMIN:
ROLE_USER
5
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
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 17-28
1 # app/config/security.yml
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 17-29
1 logout: ~
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 17-30
1 # app/config/routing.yml
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.
Access Control in Templates
If you want to check if the current user has a role inside a template, use the built-in helper function:
Listing 17-31
1 {% if is_granted('ROLE_ADMIN') %}
2
<a href="...">Delete</a>
3 {% endif %}
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 206
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).
Access Control in Controllers
If you want to check if the current user has a role in your controller, use the isGranted method of the
security context:
Listing 17-32
1 public function indexAction()
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:
Listing 17-33
1 # app/config/security.yml
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
To switch back to the original user, use the special _exit username:
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:
10. http://example.com/somewhere?_switch_user=thomas
11. http://example.com/somewhere?_switch_user=_exit
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 207
Listing 17-34
1 # app/config/security.yml
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 17-35
1 # app/config/security.yml
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.
Learn more from the Cookbook
•
•
•
•
Forcing HTTP/HTTPS
Blacklist users by IP address with a custom voter
Access Control Lists (ACLs)
How to add "Remember Me" Login Functionality
PDF brought to you by
generated on October 26, 2012
Chapter 17: Security | 208
Chapter 18
HTTP Cache
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.
Caching on the Shoulders of Giants
The most effective way to improve performance of an application is to cache the full output of a page and
then bypass the application entirely on each subsequent request. Of course, this isn't always possible for
highly dynamic websites, or is it? In this chapter, we'll show you how the Symfony2 cache system works
and why we think this is the best possible approach.
The Symfony2 cache system is different because it relies on the simplicity and power of the HTTP cache
as defined in the HTTP specification. Instead of reinventing a caching methodology, Symfony2 embraces
the standard that defines basic communication on the Web. Once you understand the fundamental
HTTP validation and expiration caching models, you'll be ready to master the Symfony2 cache system.
For the purposes of learning how to cache with Symfony2, we'll cover the subject in four steps:
• 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).
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 209
• Step 4: Edge Side Includes (ESI) allow HTTP cache to be used to cache page fragments (even
nested fragments) independently. With ESI, you can even cache an entire page for 60 minutes,
but an embedded sidebar for only 5 minutes.
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.
Caching with a Gateway Cache
When caching with HTTP, the cache is separated from your application entirely and sits between your
application and the client making the request.
The job of the cache is to accept requests from the client and pass them back to your application. The
cache will also receive responses back from your application and forward them on to the client. The cache
is the "middle-man" of the request-response communication between the client and your application.
Along the way, the cache will store each response that is deemed "cacheable" (See Introduction to HTTP
Caching). If the same resource is requested again, the cache sends the cached response to the client,
ignoring your application entirely.
This type of cache is known as a HTTP gateway cache and many exist such as Varnish3, Squid in reverse
proxy mode4, and the Symfony2 reverse proxy.
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
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 210
Symfony2 Reverse Proxy
Symfony2 comes with a reverse proxy (also called a gateway cache) written in PHP. Enable it and
cacheable responses from your application will start to be cached right away. Installing it is just as easy.
Each new Symfony2 application comes with a pre-configured caching kernel (AppCache) that wraps the
default one (AppKernel). The caching Kernel is the reverse proxy.
To enable caching, modify the code of a front controller to use the caching kernel:
Listing 18-1
1
2
3
4
5
6
7
8
9
10
11
12
// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
require_once __DIR__.'/../app/AppCache.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
// wrap the default AppKernel with the AppCache one
$kernel = new AppCache($kernel);
$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 18-2
1 error_log($kernel->getLog());
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:
Listing 18-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/AppCache.php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
class AppCache extends HttpCache
{
protected function getOptions()
{
return array(
'debug'
'default_ttl'
'private_headers'
'allow_reload'
'allow_revalidate'
'stale_while_revalidate'
'stale_if_error'
);
}
}
PDF brought to you by
generated on October 26, 2012
=>
=>
=>
=>
=>
=>
=>
false,
0,
array('Authorization', 'Cookie'),
false,
false,
2,
60,
Chapter 18: HTTP Cache | 211
Unless overridden in getOptions(), the debug option will be set to automatically be the debug
value of the wrapped AppKernel.
Here is a list of the main options:
• 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 CacheControl directive. (default: Authorization and Cookie);
• allow_reload: Specifies whether the client can force a cache reload by including a CacheControl "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.
Changing from one Reverse Proxy to Another
The Symfony2 reverse proxy is a great tool to use when developing your website or when you
deploy your website to a shared host where you cannot install anything beyond PHP code. But
being written in PHP, it cannot be as fast as a proxy written in C. That's why we highly recommend
you to use Varnish or Squid on your production servers if possible. The good news is that the
switch from one proxy server to another is easy and transparent as no code modification is needed
in your application. Start easy with the Symfony2 reverse proxy and upgrade later to Varnish when
your traffic increases.
For more information on using Varnish with Symfony2, see the How to use Varnish cookbook
chapter.
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.
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 212
Introduction to HTTP Caching
To take advantage of the available cache layers, your application must be able to communicate which
responses are cacheable and the rules that govern when/how that cache should become stale. This is done
by setting HTTP cache headers on the response.
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.
The Cache-Control Header
The Cache-Control header is unique in that it contains not one, but various pieces of information about
the cacheability of a response. Each piece of information is separated by a comma:
Cache-Control: private, max-age=0, must-revalidate
Cache-Control: max-age=3600, must-revalidate
Symfony provides an abstraction around the Cache-Control header to make its creation more
manageable:
Listing 18-4
1
2
3
4
5
6
7
8
9
10
11
12
$response = new Response();
// mark the response as either public or private
$response->setPublic();
$response->setPrivate();
// set the private or shared max age
$response->setMaxAge(600);
$response->setSharedMaxAge(600);
// set a custom Cache-Control directive
$response->headers->addCacheControlDirective('must-revalidate', true);
Public vs Private Responses
Both gateway and proxy caches are considered "shared" caches as the cached content is shared by more
than one user. If a user-specific response were ever mistakenly stored by a shared cache, it might be
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 213
returned later to any number of different users. Imagine if your account information were cached and
then returned to every subsequent user who asked for their account page!
To handle this situation, every response may be set to be public or private:
• 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.
Caching Rules and Defaults
HTTP 1.1 allows caching anything by default unless there is an explicit Cache-Control header. In
practice, most caches do nothing when requests have a cookie, an authorization header, use a non-safe
method (i.e. PUT, POST, DELETE), or when responses have a redirect status code.
Symfony2 automatically sets a sensible and conservative Cache-Control header when none is set by the
developer by following these rules:
• If no cache header is defined (Cache-Control, Expires, ETag or Last-Modified), CacheControl is set to no-cache, meaning that the response will not be cached;
• If Cache-Control is empty (but one of the other cache headers is present), its value is set to
private, must-revalidate;
• But if at least one Cache-Control directive is set, and no 'public' or private directives have
been explicitly added, Symfony2 adds the private directive automatically (except when smaxage is set).
HTTP Expiration and Validation
The HTTP specification defines two caching models:
• 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".
• When pages are really dynamic (i.e. their representation changes often), the validation model6
is often necessary. With this model, the cache stores the response, but asks the server on
each request whether or not the cached response is still valid. The application uses a unique
5. http://tools.ietf.org/html/rfc2616#section-13.2
6. http://tools.ietf.org/html/rfc2616#section-13.3
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 214
response identifier (the Etag header) and/or a timestamp (the Last-Modified header) to check
if the page has changed since being cached.
The goal of both models is to never generate the same response twice by relying on a cache to store and
return "fresh" responses.
Reading the HTTP Specification
The HTTP specification defines a simple but powerful language in which clients and servers can
communicate. As a web developer, the request-response model of the specification dominates our
work. Unfortunately, the actual specification document - RFC 26167 - can be difficult to read.
There is an on-going effort (HTTP Bis8) to rewrite the RFC 2616. It does not describe a new version
of HTTP, but mostly clarifies the original HTTP specification. The organization is also improved as
the specification is split into seven parts; everything related to HTTP caching can be found in two
dedicated parts (P4 - Conditional Requests9 and P6 - Caching: Browser and intermediary caches).
As a web developer, we strongly urge you to read the specification. Its clarity and power - even
more than ten years after its creation - is invaluable. Don't be put-off by the appearance of the spec
- its contents are much more beautiful than its cover.
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.
Expiration with the Expires Header
According to the HTTP specification, "the Expires header field gives the date/time after which the
response is considered stale." The Expires header can be set with the setExpires() Response method.
It takes a DateTime instance as an argument:
Listing 18-5
1 $date = new DateTime();
2 $date->modify('+600 seconds');
3
4 $response->setExpires($date);
The resulting HTTP header will look like this:
Listing 18-6
1 Expires: Thu, 01 Mar 2011 16:00:00 GMT
The setExpires() method automatically converts the date to the GMT timezone as required by
the specification.
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
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
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 215
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."
Expiration with the Cache-Control Header
Because of the Expires header limitations, most of the time, you should use the Cache-Control header
instead. Recall that the Cache-Control header is used to specify many different cache directives. For
expiration, there are two directives, max-age and s-maxage. The first one is used by all caches, whereas
the second one is only taken into account by shared caches:
Listing 18-7
1
2
3
4
5
6
// Sets the number of seconds after which the response
// should no longer be considered fresh
$response->setMaxAge(600);
// Same as above but only for shared caches
$response->setSharedMaxAge(600);
The Cache-Control header would take on the following format (it may have additional directives):
Listing 18-8
1 Cache-Control: max-age=600, s-maxage=600
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.
Validation with the ETag Header
The ETag header is a string header (called the "entity-tag") that uniquely identifies one representation of
the target resource. It's entirely generated and set by your application so that you can tell, for example,
if the /about resource that's stored by the cache is up-to-date with what your application would return.
An ETag is like a fingerprint and is used to quickly compare if two different versions of a resource are
equivalent. Like fingerprints, each ETag must be unique across all representations of the same resource.
Let's walk through a simple implementation that generates the ETag as the md5 of the content:
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 216
Listing 18-9
1 public function indexAction()
2 {
3
$response = $this->render('MyBundle:Main:index.html.twig');
4
$response->setETag(md5($response->getContent()));
5
$response->setPublic(); // make sure the response is public/cacheable
6
$response->isNotModified($this->getRequest());
7
8
return $response;
9 }
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.
Validation with the Last-Modified Header
The Last-Modified header is the second form of validation. According to the HTTP specification,
"The Last-Modified header field indicates the date and time at which the origin server believes the
representation was last modified." In other words, the application decides whether or not the cached
content has been updated based on whether or not it's been updated since the response was cached.
For instance, you can use the latest update date for all the objects needed to compute the resource
representation as the value for the Last-Modified header value:
Listing 18-10
1 public function showAction($articleSlug)
2 {
3
// ...
4
5
$articleDate = new \DateTime($article->getUpdatedAt());
6
$authorDate = new \DateTime($author->getUpdatedAt());
7
8
$date = $authorDate > $articleDate ? $authorDate : $articleDate;
9
10
$response->setLastModified($date);
11
// Set response as public. Otherwise it will be private by default.
12
$response->setPublic();
13
14
if ($response->isNotModified($this->getRequest())) {
15
return $response;
16
}
17
18
// do more work to populate the response will the full content
19
20
return $response;
21 }
10. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html#setETag()
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 217
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.
Optimizing your Code with Validation
The main goal of any caching strategy is to lighten the load on the application. Put another way, the
less you do in your application to return a 304 response, the better. The Response::isNotModified()
method does exactly that by exposing a simple and efficient pattern:
Listing 18-11
1 public function showAction($articleSlug)
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
// Set response as public. Otherwise it will be private by default.
15
$response->setPublic();
16
17
// Check that the Response is not modified for the given Request
18
if ($response->isNotModified($this->getRequest())) {
19
// return the 304 Response immediately
20
return $response;
21
} else {
22
// do more work here - like retrieving more data
23
$comments = ...;
24
25
// or render a template with the $response you've already started
26
return $this->render(
27
'MyBundle:MyController:article.html.twig',
28
array('article' => $article, 'comments' => $comments),
29
$response
30
);
31
}
32 }
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).
11. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Response.html#setNotModified()
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 218
Varying the Response
So far, we've assumed that each URI has exactly one representation of the target resource. By default,
HTTP caching is done by using the URI of the resource as the cache key. If two people request the same
URI of a cacheable resource, the second person will receive the cached version.
Sometimes this isn't enough and different versions of the same URI need to be cached based on one or
more request header values. For instance, if you compress pages when the client supports it, any given
URI has two representations: one when the client supports compression, and one when it does not. This
determination is done by the value of the Accept-Encoding request header.
In this case, we need the cache to store both a compressed and uncompressed version of the response for
the particular URI and return them based on the request's Accept-Encoding value. This is done by using
the Vary response header, which is a comma-separated list of different headers whose values trigger a
different representation of the requested resource:
Listing 18-12
1 Vary: Accept-Encoding, User-Agent
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:
Listing 18-13
1
2
3
4
5
// set one vary header
$response->setVary('Accept-Encoding');
// set multiple vary headers
$response->setVary(array('Accept-Encoding', 'User-Agent'));
The setVary() method takes a header name or an array of header names for which the response varies.
Expiration and Validation
You can of course use both validation and expiration within the same Response. As expiration wins over
validation, you can easily benefit from the best of both worlds. In other words, by using both expiration
and validation, you can instruct the cache to serve the cached content, while checking back at some
interval (the expiration) to verify that the content is still valid.
More Response Methods
The Response class provides many more methods related to the cache. Here are the most useful ones:
Listing 18-14
1
2
3
4
5
// Marks the Response stale
$response->expire();
// Force the response to return a proper 304 response with no content
$response->setNotModified();
Additionally, most cache-related HTTP headers can be set via the single setCache() method:
Listing 18-15
1 // Set cache settings in one call
2 $response->setCache(array(
3
'etag'
=> $etag,
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 219
4
5
6
7
8
9 ));
'last_modified'
'max_age'
's_maxage'
'public'
// 'private'
=>
=>
=>
=>
=>
$date,
10,
10,
true,
true,
Using Edge Side Includes
Gateway caches are a great way to make your website perform better. But they have one limitation: they
can only cache whole pages. If you can't cache whole pages or if parts of a page has "more" dynamic parts,
you are out of luck. Fortunately, Symfony2 provides a solution for these cases, based on a technology
called ESI12, or Edge Side Includes. Akamaï wrote this specification almost 10 years ago, and it allows
specific parts of a page to have a different caching strategy than the main page.
The ESI specification describes tags you can embed in your pages to communicate with the gateway
cache. Only one tag is implemented in Symfony2, include, as this is the only useful one outside of
Akamaï context:
Listing 18-16
1 <!doctype html>
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.
Using ESI in Symfony2
First, to use ESI, be sure to enable it in your application configuration:
Listing 18-17
12. http://www.w3.org/TR/esi-lang
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 220
1 # app/config/config.yml
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 18-18
1 public function indexAction()
2 {
3
$response = $this->render('MyBundle:MyController:index.html.twig');
4
// set the shared max age - the also marks the response as public
5
$response->setSharedMaxAge(600);
6
7
return $response;
8 }
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 18-19
1 {% render '...:news' with {}, {'standalone': true} %}
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 18-20
1 public function newsAction()
2 {
3
// ...
4
5
$response->setSharedMaxAge(60);
6 }
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
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 221
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:
Listing 18-21
1 # app/config/routing.yml
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.
The render helper supports two other useful options:
• 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.
Since invalidation is a topic specific to each type of reverse proxy, if you don't worry about
invalidation, you can switch between reverse proxies without changing anything in your
application code.
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:
Listing 18-22
1 // app/AppCache.php
2
3 use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
4
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 222
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.
Learn more from the Cookbook
• How to use Varnish to speed up my Website
PDF brought to you by
generated on October 26, 2012
Chapter 18: HTTP Cache | 223
Chapter 19
Translations
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:
Listing 19-1
1
2
3
4
5
// text will *always* print out in English
echo 'Hello World';
// text can be translated into the end-user's language or default to English
echo $translator->trans('Hello World');
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
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 224
Configuration
Translations are handled by a Translator service that uses the user's locale to lookup and return
translated messages. Before using it, enable the Translator in your configuration:
Listing 19-2
1 # app/config/config.yml
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 19-3
1 public function indexAction()
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:
Listing 19-4
1
2
3
4
5
6
7
8
9
10
11
12
<!-- messages.fr.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Symfony2 is great</source>
<target>J'aime Symfony2</target>
</trans-unit>
</body>
</file>
</xliff>
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.
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()
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 225
The Translation Process
To actually translate the message, Symfony2 uses a simple process:
• 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:
Listing 19-5
1 public function indexAction($name)
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":
Listing 19-6
1 public function indexAction($name)
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 19-7
1
2
3
4
5
6
7
8
9
10
11
12
<!-- messages.fr.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Hello %name%</source>
<target>Bonjour %name%</target>
</trans-unit>
</body>
</file>
</xliff>
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 226
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.
As we've seen, creating a translation is a two-step process:
1. Abstract the message that needs to be translated by processing it through the Translator.
2. Create a translation for the message in each locale that you choose to support.
The second step is done by creating message catalogues that define the translations for any number of
different locales.
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:
Symfony2 is Great => J'aime Symfony2
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 19-8
1 $ php app/console cache:clear
Translation Locations and Naming Conventions
Symfony2 looks for message files (i.e. translations) in two locations:
• For messages found in a bundle, the corresponding message files should live in the Resources/
translations/ directory of the bundle;
• To override any bundle translations, place message files in the app/Resources/translations
directory.
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:
• xliff: XLIFF file;
• php: PHP file;
• yml: YAML file.
6. http://www.php.net/manual/en/function.strtr.php
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 227
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):
Listing 19-9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- src/Acme/DemoBundle/Resources/translations/messages.fr.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Symfony2 is great</source>
<target>J'aime Symfony2</target>
</trans-unit>
<trans-unit id="2">
<source>symfony2.great</source>
<target>J'aime Symfony2</target>
</trans-unit>
</body>
</file>
</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
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 228
Using Real or Keyword Messages
This example illustrates the two different philosophies when creating messages to be translated:
Listing 19-10
1 $t = $translator->trans('Symfony2 is great');
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 19-11
1 symfony2:
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 19-12
1
2
3
4
symfony2.is.great: Symfony2 is great
symfony2.is.amazing: Symfony2 is amazing
symfony2.has.bundles: Symfony2 has bundles
user.login: Login
Using Message Domains
As we've seen, message files are organized into the different locales that they translate. The message
files can also be organized further into "domains". When creating message files, the domain is the first
portion of the filename. The default domain is messages. For example, suppose that, for organization,
translations were split into three different domains: messages, admin and navigation. The French
translation would have the following message files:
• messages.fr.xliff
• admin.fr.xliff
• navigation.fr.xliff
When translating strings that are not in the default domain (messages), you must specify the domain as
the third argument of trans():
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 229
Listing 19-13
1 $this->get('translator')->trans('Symfony2 is great', array(), 'admin');
Symfony2 will now look for the message in the admin domain of the user's locale.
Handling the User's Locale
The locale of the current user is stored in the session and is accessible via the session service:
Listing 19-14
1 $locale = $this->get('session')->getLocale();
2
3 $this->get('session')->setLocale('en_US');
Fallback and Default Locale
If the locale hasn't been set explicitly in the session, the fallback_locale configuration parameter will
be used by the Translator. The parameter defaults to en (see Configuration).
Alternatively, you can guarantee that a locale is set on the user's session by defining a default_locale
for the session service:
Listing 19-15
1 # app/config/config.yml
2 framework:
3
session: { default_locale: en }
The Locale and the URL
Since the locale of the user is stored in the session, it may be tempting to use the same URL to display a
resource in many different languages based on the user's locale. For example, http://www.example.com/
contact could show content in English for one user and French for another user. Unfortunately, this
violates a fundamental rule of the Web: that a particular URL returns the same resource regardless of the
user. To further muddy the problem, which version of the content would be indexed by search engines?
A better policy is to include the locale in the URL. This is fully-supported by the routing system using the
special _locale parameter:
Listing 19-16
1 contact:
2
pattern:
/{_locale}/contact
3
defaults: { _controller: AcmeDemoBundle:Contact:index, _locale: en }
4
requirements:
5
_locale: en|fr|de
When using the special _locale parameter in a route, the matched locale will automatically be set on the
user's session. In other words, if a user visits the URI /fr/contact, the locale fr will automatically be set
as the locale for the user's session.
You can now use the user's locale to create routes to other translated pages in your application.
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:
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 230
Listing 19-17
1 (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 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 19-18
1 'There is one apple|There are %count% apples'
To translate pluralized messages, use the transChoice()9 method:
Listing 19-19
1 $t = $this->get('translator')->transChoice(
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 19-20
1 'Il y a %count% pomme|Il y a %count% pommes'
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
French, but for Russian, you'd may want a hint to know which rule matches which string. To help
translators, you can optionally "tag" each string:
Listing 19-21
1 'one: There is one apple|some: There are %count% apples'
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.
Explicit Interval Pluralization
The easiest way to pluralize a message is to let Symfony2 use internal logic to choose which string to
use based on a given number. Sometimes, you'll need more control or want a different translation for
specific cases (for 0, or when the count is negative, for example). For such cases, you can use explicit
math intervals:
9. http://api.symfony.com/2.0/Symfony/Component/Translation/Translator.html#transChoice()
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 231
Listing 19-22
1 '{0} There are no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf]
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:
Listing 19-23
1 '{0} There are no apples|[20,Inf] There are many apples|There is one apple|a_few: There 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:
Listing 19-24
1 {1,2,3,4}
Or numbers between two other numbers:
Listing 19-25
1 [1, +Inf[
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.
Translations in Templates
Most of the time, translation occurs in templates. Symfony2 provides native support for both Twig and
PHP templates.
Twig Templates
Symfony2 provides specialized Twig tags (trans and transchoice) to help with message translation of
static blocks of text:
Listing 19-26
1 {% trans %}Hello %name%{% endtrans %}
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 %}
10. http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation
11. http://api.symfony.com/2.0/Symfony/Component/Translation/Interval.html
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 232
You can also specify the message domain and pass some additional variables:
Listing 19-27
1
2
3
4
5
6
7
{% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %}
{% trans with {'%name%': 'Fabien'} from "app" into "fr" %}Hello %name%{% endtrans %}
{% transchoice count with {'%name%': 'Fabien'} from "app" %}
{0} There is no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}
The trans and transchoice filters can be used to translate variable texts and complex expressions:
Listing 19-28
1
2
3
4
5
6
7
{{ message|trans }}
{{ message|transchoice(5) }}
{{ message|trans({'%name%': 'Fabien'}, "app") }}
{{ 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:
Listing 19-29
1
2
3
4
5
6
7
8
9
10
11
12
{# text translated between tags is never escaped #}
{% trans %}
<h3>foo</h3>
{% endtrans %}
{% set message = '<h3>foo</h3>' %}
{# a variable translated via a filter is escaped by default #}
{{ message|trans|raw }}
{# but static strings are never escaped #}
{{ '<h3>foo</h3>'|trans }}
PHP Templates
The translator service is accessible in PHP templates through the translator helper:
Listing 19-30
1 <?php echo $view['translator']->trans('Symfony2 is great') ?>
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 ) ?>
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 233
Forcing the Translator Locale
When translating a message, Symfony2 uses the locale from the user's session or the fallback locale if
necessary. You can also manually specify the locale to use for translation:
Listing 19-31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$this->get('translator')->trans(
'Symfony2 is great',
array(),
'messages',
'fr_FR',
);
$this->get('translator')->transChoice(
'{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
10,
array('%count%' => 10),
'messages',
'fr_FR',
);
Translating Database Content
The translation of database content should be handled by Doctrine through the Translatable Extension12.
For more information, see the documentation for that library.
Translating Constraint Messages
The best way to understand constraint translation is to see it in action. To start, suppose you've created
a plain-old-PHP object that you need to use somewhere in your application:
Listing 19-32
1
2
3
4
5
6
7
// src/Acme/BlogBundle/Entity/Author.php
namespace Acme\BlogBundle\Entity;
class Author
{
public $name;
}
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 19-33
1 # src/Acme/BlogBundle/Resources/config/validation.yml
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.
Listing 19-34
12. https://github.com/l3pp4rd/DoctrineExtensions
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 234
1
2
3
4
5
6
7
8
9
10
11
12
<!-- validators.en.xliff -->
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>author.name.not_blank</source>
<target>Please enter an author name.</target>
</trans-unit>
</body>
</file>
</xliff>
Summary
With the Symfony2 Translation component, creating an internationalized application no longer needs to
be a painful process and boils down to just a few basic steps:
• Abstract messages in your application by wrapping each in either the trans()13 or
transChoice()14 methods;
• Translate each message into multiple locales by creating translation message files. Symfony2
discovers and processes each file because its name follows a specific convention;
• Manage the user's locale, which is stored in the session.
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()
PDF brought to you by
generated on October 26, 2012
Chapter 19: Translations | 235
Chapter 20
Service Container
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.
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 236
So what's the big deal then? The advantage of thinking about "services" is that you begin to think about
separating each piece of functionality in your application into a series of services. Since each service does
just one job, you can easily access each service and use its functionality wherever you need it. Each service
can also be more easily tested and configured since it's separated from the other functionality in your
application. This idea is called service-oriented architecture1 and is not unique to Symfony2 or even PHP.
Structuring your application around a set of independent service classes is a well-known and trusted
object-oriented best-practice. These skills are key to being a good developer in almost any language.
What is a Service Container?
A Service Container (or dependency injection container) is simply a PHP object that manages the
instantiation of services (i.e. objects). For example, suppose we have a simple PHP class that delivers
email messages. Without a service container, we must manually create the object whenever we need it:
Listing 20-1
1 use Acme\HelloBundle\Mailer;
2
3 $mailer = new Mailer('sendmail');
4 $mailer->send('ryan@foobar.net', ...);
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.
Creating/Configuring Services in the Container
A better answer is to let the service container create the Mailer object for you. In order for this to work,
we must teach the container how to create the Mailer service. This is done via configuration, which can
be specified in YAML, XML or PHP:
Listing 20-2
1 # app/config/config.yml
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:
Listing 20-3
1 class HelloController extends Controller
2 {
1. http://wikipedia.org/wiki/Service-oriented_architecture
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 237
3
4
5
6
7
8
9
10
11 }
// ...
public function sendEmailAction()
{
// ...
$mailer = $this->get('my_mailer');
$mailer->send('ryan@foobar.net', ...);
}
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 20-4
# app/config/config.yml
parameters:
my_mailer.class:
my_mailer.transport:
services:
my_mailer:
class:
arguments:
Acme\HelloBundle\Mailer
sendmail
"%my_mailer.class%"
[%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:
Listing 20-5
<argument type="string">http://symfony.com/?foo=%%s&bar=%%d</argument>
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;
• when creating a service in a bundle (we'll show this shortly), using parameters allows the
service to be easily customized in your application.
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 238
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.
Listing 20-6
1 # app/config/config.yml
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
Importing other Container Configuration Resources
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.
Importing Configuration with imports
So far, we've placed our my_mailer service container definition directly in the application configuration
file (e.g. app/config/config.yml). Of course, since the Mailer class itself lives inside the
AcmeHelloBundle, it makes more sense to put the my_mailer container definition inside the bundle as
well.
First, move the my_mailer container definition into a new container resource file inside
AcmeHelloBundle. If the Resources or Resources/config directories don't exist, create them.
Listing 20-7
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
my_mailer.class:
Acme\HelloBundle\Mailer
my_mailer.transport: sendmail
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 239
services:
my_mailer:
class:
arguments:
"%my_mailer.class%"
[%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 20-8
# app/config/config.yml
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.
Importing Configuration via Container Extensions
When developing in Symfony2, you'll most commonly use the imports directive to import container
configuration from the bundles you've created specifically for your application. Third-party bundle
container configuration, including Symfony2 core services, are usually loaded using another method
that's more flexible and easy to configure in your application.
Here's how it works. Internally, each bundle defines its services very much like we've seen so far. Namely,
a bundle uses one or more configuration resource files (usually XML) to specify the parameters and
services for that bundle. However, instead of importing each of these resources directly from your
application configuration using the imports directive, you can simply invoke a service container extension
inside the bundle that does the work for you. A service container extension is a PHP class created by the
bundle author to accomplish two things:
• 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 20-9
1 # app/config/config.yml
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
# ...
When the configuration is parsed, the container looks for an extension that can handle the framework
configuration directive. The extension in question, which lives in the FrameworkBundle, is invoked and
the service configuration for the FrameworkBundle is loaded. If you remove the framework key from
your application configuration file entirely, the core Symfony2 services won't be loaded. The point is that
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 240
you're in control: the Symfony2 framework doesn't contain any magic or perform any actions that you
don't have control over.
Of course you can do much more than simply "activate" the service container extension of the
FrameworkBundle. Each extension allows you to easily customize the bundle, without worrying about
how the internal services are defined.
In this case, the extension allows you to customize the charset, error_handler, csrf_protection,
router configuration and much more. Internally, the FrameworkBundle uses the options specified here
to define and configure the services specific to it. The bundle takes care of creating all the necessary
parameters and services for the service container, while still allowing much of the configuration to
be easily customized. As an added bonus, most service container extensions are also smart enough to
perform validation - notifying you of options that are missing or the wrong data type.
When installing or configuring a bundle, see the bundle's documentation for how the services for the
bundle should be installed and configured. The options available for the core bundles can be found inside
the Reference Guide.
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.
Referencing (Injecting) Services
So far, our original my_mailer service is simple: it takes just one argument in its constructor, which is
easily configurable. As you'll see, the real power of the container is realized when you need to create a
service that depends on one or more other services in the container.
Let's start with an example. Suppose we have a new service, NewsletterManager, that helps to manage
the preparation and delivery of an email message to a collection of addresses. Of course the my_mailer
service is already really good at delivering email messages, so we'll use it inside NewsletterManager to
handle the actual delivery of the messages. This pretend class might look something like this:
Listing 20-10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Acme/HelloBundle/Newsletter/NewsletterManager.php
namespace Acme\HelloBundle\Newsletter;
use Acme\HelloBundle\Mailer;
class NewsletterManager
{
protected $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
// ...
}
Without using the service container, we can create a new NewsletterManager fairly easily from inside a
controller:
Listing 20-11
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 241
1 public function sendNewsletterAction()
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 20-12
# src/Acme/HelloBundle/Resources/config/services.yml
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 welldefined 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.
Optional Dependencies: Setter Injection
Injecting dependencies into the constructor in this manner is an excellent way of ensuring that the
dependency is available to use. If you have optional dependencies for a class, then "setter injection" may
be a better option. This means injecting the dependency using a method call rather than through the
constructor. The class would look like this:
Listing 20-13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace Acme\HelloBundle\Newsletter;
use Acme\HelloBundle\Mailer;
class NewsletterManager
{
protected $mailer;
public function setMailer(Mailer $mailer)
{
$this->mailer = $mailer;
}
// ...
}
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 242
Injecting the dependency by the setter method just needs a change of syntax:
Listing 20-14
# src/Acme/HelloBundle/Resources/config/services.yml
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".
Making References Optional
Sometimes, one of your services may have an optional dependency, meaning that the dependency is
not required for your service to work properly. In the example above, the my_mailer service must exist,
otherwise an exception will be thrown. By modifying the newsletter_manager service definition, you
can make this reference optional. The container will then inject it if it exists and do nothing if it doesn't:
Listing 20-15
# src/Acme/HelloBundle/Resources/config/services.yml
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 20-16
1 public function __construct(Mailer $mailer = null)
2 {
3
// ...
4 }
Core Symfony and Third-Party Bundle Services
Since Symfony2 and all third-party bundles configure and retrieve their services via the container, you can
easily access them or even use them in your own services. To keep things simple, Symfony2 by default
does not require that controllers be defined as services. Furthermore Symfony2 injects the entire service
container into your controller. For example, to handle the storage of information on a user's session,
Symfony2 provides a session service, which you can access inside a standard controller as follows:
Listing 20-17
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 243
1 public function indexAction($bar)
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:
Listing 20-18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Acme\HelloBundle\Newsletter;
use Symfony\Component\Templating\EngineInterface;
class NewsletterManager
{
protected $mailer;
protected $templating;
public function __construct(\Swift_Mailer $mailer, EngineInterface $templating)
{
$this->mailer = $mailer;
$this->templating = $templating;
}
// ...
}
Configuring the service container is easy:
Listing 20-19
services:
newsletter_manager:
class:
"%newsletter_manager.class%"
arguments: [@mailer, @templating]
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.
Be sure that swiftmailer entry appears in your application configuration. As we mentioned
in Importing Configuration via Container Extensions, the swiftmailer key invokes the service
extension from the SwiftmailerBundle, which registers the mailer service.
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 244
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:
Listing 20-20
1 services:
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).
For a list of all the tags available in the core Symfony Framework, check out The Dependency Injection
Tags.
Debugging Services
You can find out what services are registered with the container using the console. To show all services
and the class for each service, run:
Listing 20-21
1 $ php app/console container:debug
By default only public services are shown, but you can also view private services:
Listing 20-22
1 $ php app/console container:debug --show-private
You can get more detailed information about a particular service by specifying its id:
Listing 20-23
1 $ php app/console container:debug my_mailer
Learn more
•
•
•
•
•
•
•
•
•
Compiling the Container
Working with Container Parameters and Definitions
Using a Factory to Create Services
Managing Common Dependencies with Parent Services
Working with Tagged Services
How to define Controllers as Services
How to work with Scopes
How to work with Compiler Passes in Bundles
Advanced Container Configuration
PDF brought to you by
generated on October 26, 2012
Chapter 20: Service Container | 245
Chapter 21
Performance
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.
Use a Byte Code Cache (e.g. APC)
One of the best (and easiest) things that you should do to improve your performance is to use a "byte
code cache". The idea of a byte code cache is to remove the need to constantly recompile the PHP source
code. There are a number of byte code caches1 available, some of which are open source. The most widely
used byte code cache is probably APC2
Using a byte code cache really has no downside, and Symfony2 has been architected to perform really
well in this type of environment.
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
PDF brought to you by
generated on October 26, 2012
Chapter 21: Performance | 246
Use an Autoloader that caches (e.g. ApcUniversalClassLoader)
By default, the Symfony2 standard edition uses the UniversalClassLoader in the autoloader.php3 file.
This autoloader is easy to use, as it will automatically find any new classes that you've placed in the
registered directories.
Unfortunately, this comes at a cost, as the loader iterates over all configured namespaces to find a
particular file, making file_exists calls until it finally finds the file it's looking for.
The simplest solution is to cache the location of each class after it's located the first time. Symfony comes
with a class - ApcUniversalClassLoader - loader that extends the UniversalClassLoader and stores
the class locations in APC.
To use this class loader, simply adapt your autoloader.php as follows:
Listing 21-1
1
2
3
4
5
6
7
// app/autoload.php
require __DIR__.'/../vendor/symfony/src/Symfony/Component/ClassLoader/
ApcUniversalClassLoader.php';
use Symfony\Component\ClassLoader\ApcUniversalClassLoader;
$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.
Use Bootstrap Files
To ensure optimal flexibility and code reuse, Symfony2 applications leverage a variety of classes and 3rd
party components. But loading all of these classes from separate files on each request can result in some
overhead. To reduce this overhead, the Symfony2 Standard Edition provides a script to generate a socalled bootstrap file4, consisting of multiple classes definitions in a single file. By including this file (which
contains a copy of many of the core classes), Symfony no longer needs to include any of the source files
containing those classes. This will reduce disc IO quite a bit.
If you're using the Symfony2 Standard Edition, then you're probably already using the bootstrap file. To
be sure, open your front controller (usually app.php) and check to make sure that the following line
exists:
Listing 21-2
1 require_once __DIR__.'/../app/bootstrap.php.cache';
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);
• when debugging, one will need to place break points inside the bootstrap file.
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
PDF brought to you by
generated on October 26, 2012
Chapter 21: Performance | 247
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.
Bootstrap Files and Byte Code Caches
Even when using a byte code cache, performance will improve when using a bootstrap file since there
will be fewer files to monitor for changes. Of course if this feature is disabled in the byte code cache (e.g.
apc.stat=0 in APC), there is no longer a reason to use a bootstrap file.
PDF brought to you by
generated on October 26, 2012
Chapter 21: Performance | 248
Chapter 22
Internals
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
PDF brought to you by
generated on October 26, 2012
Chapter 22: Internals | 249
• The Session5 class and SessionStorageInterface6 interface abstract session management
session_*() functions.
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).
Read more about Dependency Injection and 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:
Listing 22-1
1 function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
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:
Listing 22-2
1 public function getController(Request $request);
2
3 public function getArguments(Request $request, $controller);
The getController()14 method returns the Controller (a PHP callable) associated with the given
Request. The default implementation (ControllerResolver15) looks for a _controller request attribute
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
14. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.html#getController()
PDF brought to you by
generated on October 26, 2012
Chapter 22: Internals | 250
that
represents
the
controller
name
Bundle\BlogBundle\PostController:indexAction).
(a
"class::method"
string,
like
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.
Matching Controller method arguments from Request attributes
For each method argument, Symfony2 tries to get the value of a Request attribute with the same
name. If it is not defined, the argument default value is used if defined:
Listing 22-3
1
2
3
4
5
6
// Symfony2 will look for an 'id' attribute (mandatory)
// and an 'admin' one (optional)
public function showAction($id, $admin = true)
{
// ...
}
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.
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()
PDF brought to you by
generated on October 26, 2012
Chapter 22: Internals | 251
Internal Requests
At any time during the handling of a request (the 'master' one), a sub-request can be handled. You can
pass the request type to the handle() method (its second argument):
• 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()
returns
the
type
of
the
request
(HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST);
• getKernel() - returns the Kernel handling the request;
• getRequest() - returns the current Request being handled.
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:
Listing 22-4
1 use Symfony\Component\HttpKernel\HttpKernelInterface;
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).
kernel.controller Event
Event Class: FilterControllerEvent22
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
PDF brought to you by
generated on October 26, 2012
Chapter 22: Internals | 252
This event is not used by FrameworkBundle, but can be an entry point used to modify the controller that
should be executed:
Listing 22-5
1
2
3
4
5
6
7
8
9
10
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
// ...
// the controller can be changed to any PHP callable
$event->setController($controller);
}
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:
Listing 22-6
1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpFoundation\Response;
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$val = $event->getControllerResult();
$response = new Response();
// some how customize the Response from the return value
$event->setResponse($response);
}
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 22-7
1 public function onKernelResponse(FilterResponseEvent $event)
2 {
3
$response = $event->getResponse();
4
// ... modify the response object
5 }
The FrameworkBundle registers several listeners:
• ProfilerListener25: collects data for the current request;
• WebDebugToolbarListener26: injects the Web Debug Toolbar;
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
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
PDF brought to you by
generated on October 26, 2012
Chapter 22: Internals | 253
• ResponseListener27: fixes the Response Content-Type based on the request format;
• EsiListener28: adds a Surrogate-Control HTTP header when the Response needs to be
parsed for ESI tags.
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:
Listing 22-8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
$response = new Response();
// setup the Response object based on the caught exception
$event->setResponse($response);
// you can alternatively set a new Exception
// $exception = new \Exception('Some special exception');
// $event->setException($exception);
}
The Event Dispatcher
The event dispatcher is a standalone component that is responsible for much of the underlying logic
and flow behind a Symfony request. For more information, see the Event Dispatcher Component
Documentation.
Profiler
When enabled, the Symfony2 profiler collects useful information about each request made to your
application and store them for later analysis. Use the profiler in the development environment to help you
to debug your code and enhance performance; use it in the production environment to explore problems
after the fact.
You rarely have to deal with the profiler directly as Symfony2 provides visualizer tools like the Web
Debug Toolbar and the Web Profiler. If you use the Symfony2 Standard Edition, the profiler, the web
debug toolbar, and the web profiler are all already configured with sensible settings.
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).
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
PDF brought to you by
generated on October 26, 2012
Chapter 22: Internals | 254
Visualizing Profiling Data
Using the Web Debug Toolbar
In the development environment, the web debug toolbar is available at the bottom of all pages. It displays
a good summary of the profiling data that gives you instant access to a lot of useful information when
something does not work as expected.
If the summary provided by the Web Debug Toolbar is not enough, click on the token link (a string made
of 13 random characters) to access the Web Profiler.
If the token is not clickable, it means that the profiler routes are not registered (see below for
configuration information).
Analyzing Profiling data with the Web Profiler
The Web Profiler is a visualization tool for profiling data that you can use in development to debug your
code and enhance performance; but it can also be used to explore problems that occur in production. It
exposes all information collected by the profiler in a web interface.
Accessing the Profiling information
You don't need to use the default visualizer to access the profiling information. But how can you retrieve
profiling information for a specific request after the fact? When the profiler stores data about a Request,
it also associates a token with it; this token is available in the X-Debug-Token HTTP header of the
Response:
Listing 22-9
1 $profile = $container->get('profiler')->loadProfileFromResponse($response);
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.
Use the find() method to access tokens based on some criteria:
Listing 22-10
1
2
3
4
5
6
7
8
// get the latest 10 tokens
$tokens = $container->get('profiler')->find('', '', 10);
// get the latest 10 tokens for all URL containing /admin/
$tokens = $container->get('profiler')->find('', '/admin/', 10);
// get the latest 10 tokens for local requests
$tokens = $container->get('profiler')->find('127.0.0.1', '', 10);
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:
Listing 22-11
1 // on the production machine
2 $profile = $container->get('profiler')->loadProfile($token);
3 $data = $profiler->export($profile);
PDF brought to you by
generated on October 26, 2012
Chapter 22: Internals | 255
4
5 // on the development machine
6 $profiler->import($data);
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:
Listing 22-12
1
2
3
4
5
6
7
8
9
# load the profiler
framework:
profiler: { only_exceptions: false }
# enable the web profiler
web_profiler:
toolbar: true
intercept_redirects: true
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:
Listing 22-13
_profiler:
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
want to get information when the client IP comes from a specific address, or for a limited portion of the
website? You can use a request matcher:
Listing 22-14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# enables the profiler only for request coming for the 192.168.0.0 network
framework:
profiler:
matcher: { ip: 192.168.0.0/24 }
# enables the profiler only for the /admin URLs
framework:
profiler:
matcher: { path: "^/admin/" }
# combine rules
framework:
profiler:
matcher: { ip: 192.168.0.0/24, path: "^/admin/" }
# use a custom matcher instance defined in the "custom_matcher" service
framework:
PDF brought to you by
generated on October 26, 2012
Chapter 22: Internals | 256
18
19
profiler:
matcher: { service: custom_matcher }
Learn more from the Cookbook
•
•
•
•
How to use the Profiler in a Functional Test
How to create a custom Data Collector
How to extend a Class without using Inheritance
How to customize a Method Behavior without using Inheritance
PDF brought to you by
generated on October 26, 2012
Chapter 22: Internals | 257
Chapter 23
The Symfony2 Stable API
The Symfony2 stable API is a subset of all Symfony2 published public methods (components and core
bundles) that share the following properties:
•
•
•
•
The namespace and class name won't change;
The method name won't change;
The method signature (arguments and return value type) won't change;
The semantic of what the method does won't change.
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.
As of Symfony 2.0, the following components have a public tagged API:
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
BrowserKit
ClassLoader
Console
CssSelector
DependencyInjection
DomCrawler
EventDispatcher
Finder
HttpFoundation
HttpKernel
Locale
Process
Routing
Templating
Translation
PDF brought to you by
generated on October 26, 2012
Chapter 23: The Symfony2 Stable API | 258
• Validator
• Yaml
PDF brought to you by
generated on October 26, 2012
Chapter 23: The Symfony2 Stable API | 259
Part III
The Cookbook
Chapter 24
How to Create and store a Symfony2 Project in
git
Though this entry is specifically about git, the same generic principles will apply if you're storing
your project in Subversion.
Once you've read through Creating Pages in Symfony2 and become familiar with using Symfony, you'll
no-doubt be ready to start your own project. In this cookbook article, you'll learn the best way to start a
new Symfony2 project that's stored using the git1 source control management system.
Initial Project Setup
To get started, you'll need to download Symfony and initialize your local git repository:
1. Download the Symfony2 Standard Edition2 without vendors.
2. Unzip/untar the distribution. It will create a folder called Symfony with your new project
structure, config files, etc. Rename it to whatever you like.
3. Create a new file called .gitignore at the root of your new project (e.g. next to the deps file)
and paste the following into it. Files matching these patterns will be ignored by git:
Listing 24-1
1
2
3
4
5
6
/web/bundles/
/app/bootstrap*
/app/cache/*
/app/logs/*
/vendor/
/app/config/parameters.ini
1. http://git-scm.com/
2. http://symfony.com/download
PDF brought to you by
generated on October 26, 2012
Chapter 24: How to Create and store a Symfony2 Project in git | 261
You may also want to create a .gitignore file that can be used system-wide, in which case, you can
find more information here: Github .gitignore3 This way you can exclude files/folders often used by
your IDE for all of your projects.
4. Copy
app/config/parameters.ini
to
app/config/parameters.ini.dist.
The
parameters.ini file is ignored by git (see above) so that machine-specific settings like database
passwords aren't committed. By creating the parameters.ini.dist file, new developers can
quickly clone the project, copy this file to parameters.ini, customize it, and start developing.
5. Initialize your git repository:
Listing 24-2
1 $ git init
6. Add all of the initial files to git:
Listing 24-3
1 $ git add .
7. Create an initial commit with your started project:
Listing 24-4
1 $ git commit -m "Initial commit"
8. Finally, download all of the third-party vendor libraries:
Listing 24-5
1 $ php bin/vendors install
At this point, you have a fully-functional Symfony2 project that's correctly committed to git. You can
immediately begin development, committing the new changes to your git repository.
After execution of the command:
Listing 24-6
1 $ php bin/vendors install
your project will contain complete the git history of all the bundles and libraries defined in the
deps file. It can be as much as 100 MB! If you save the current versions of all your dependencies
with the command:
Listing 24-7
1 $ php bin/vendors lock
then you can remove the git history directories with the following command:
Listing 24-8
1 $ find vendor -name .git -type d | xargs rm -rf
The command removes all .git directories contained inside the vendor directory.
If you want to update bundles defined in deps file after this, you will have to reinstall them:
Listing 24-9
1 $ php bin/vendors install --reinstall
You can continue to follow along with the Creating Pages in Symfony2 chapter to learn more about how
to configure and develop inside your application.
3. http://help.github.com/ignore-files/
PDF brought to you by
generated on October 26, 2012
Chapter 24: How to Create and store a Symfony2 Project in git | 262
The Symfony2 Standard Edition comes with some example functionality. To remove the sample
code, follow the instructions on the Standard Edition Readme4.
Managing Vendor Libraries with bin/vendors and deps
How does it work?
Every Symfony project uses a group of third-party "vendor" libraries. One way or another the goal is to
download these files into your vendor/ directory and, ideally, to give you some sane way to manage the
exact version you need for each.
By default, these libraries are downloaded by running a php bin/vendors install "downloader" script.
This script reads from the deps file at the root of your project. This is an ini-formatted script, which
holds a list of each of the external libraries you need, the directory each should be downloaded to, and
(optionally) the version to be downloaded. The bin/vendors script uses git to downloaded these, solely
because these external libraries themselves tend to be stored via git. The bin/vendors script also reads
the deps.lock file, which allows you to pin each library to an exact git commit hash.
It's important to realize that these vendor libraries are not actually part of your repository. Instead, they're
simply un-tracked files that are downloaded into the vendor/ directory by the bin/vendors script. But
since all the information needed to download these files is saved in deps and deps.lock (which are
stored) in our repository), any other developer can use our project, run php bin/vendors install, and
download the exact same set of vendor libraries. This means that you're controlling exactly what each
vendor library looks like, without needing to actually commit them to your repository.
So, whenever a developer uses your project, he/she should run the php bin/vendors install script to
ensure that all of the needed vendor libraries are downloaded.
Upgrading Symfony
Since Symfony is just a group of third-party libraries and third-party libraries are entirely controlled
through deps and deps.lock, upgrading Symfony means simply upgrading each of these files to
match their state in the latest Symfony Standard Edition.
Of course, if you've added new entries to deps or deps.lock, be sure to replace only the original
parts (i.e. be sure not to also delete any of your custom entries).
There is also a php bin/vendors update command, but this has nothing to do with upgrading
your project and you will normally not need to use it. This command is used to freeze the versions
of all of your vendor libraries by updating them to the version specified in deps and recording it
into the deps.lock file.
Hacking vendor libraries
Sometimes, you want a specific branch, tag, or commit of a library to be downloaded or upgraded. You
can set that directly to the deps file :
Listing 24-10
4. https://github.com/symfony/symfony-standard/blob/master/README.md
PDF brought to you by
generated on October 26, 2012
Chapter 24: How to Create and store a Symfony2 Project in git | 263
1 [AcmeAwesomeBundle]
2
git=http://github.com/johndoe/Acme/AwesomeBundle.git
3
target=/bundles/Acme/AwesomeBundle
4
version=the-awesome-version
• The git option sets the URL of the library. It can use various protocols, like http:// as well
as git://.
• The target option specifies where the repository will live : plain Symfony bundles should go
under the vendor/bundles/Acme directory, other third-party libraries usually go to vendor/
my-awesome-library-name. The target directory defaults to this last option when not
specified.
• The version option allows you to set a specific revision. You can use a tag
(version=origin/0.42) or a branch name (refs/remotes/origin/awesome-branch). It
defaults to origin/HEAD.
Updating workflow
When you execute the php bin/vendors install, for every library, the script first checks if the install
directory exists.
If it does not (and ONLY if it does not), it runs a git clone.
Then, it does a git fetch origin and a git reset --hard the-awesome-version.
This means that the repository will only be cloned once. If you want to perform any change of the git
remote, you MUST delete the entire target directory, not only its content.
Vendors and Submodules
Instead of using the deps, bin/vendors system for managing your vendor libraries, you may instead
choose to use native git submodules5. There is nothing wrong with this approach, though the deps system
is the official way to solve this problem and git submodules can be difficult to work with at times.
Storing your Project on a Remote Server
You now have a fully-functional Symfony2 project stored in git. However, in most cases, you'll also want
to store your project on a remote server both for backup purposes, and so that other developers can
collaborate on the project.
The easiest way to store your project on a remote server is via GitHub6. Public repositories are free,
however you will need to pay a monthly fee to host private repositories.
Alternatively, you can store your git repository on any server by creating a barebones repository7 and then
pushing to it. One library that helps manage this is Gitolite8.
5. http://git-scm.com/book/en/Git-Tools-Submodules
6. https://github.com/
7. http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository
8. https://github.com/sitaramc/gitolite
PDF brought to you by
generated on October 26, 2012
Chapter 24: How to Create and store a Symfony2 Project in git | 264
Chapter 25
How to Create and store a Symfony2 Project in
Subversion
This entry is specifically about Subversion, and based on principles found in How to Create and
store a Symfony2 Project in git.
Once you've read through Creating Pages in Symfony2 and become familiar with using Symfony, you'll
no-doubt be ready to start your own project. The preferred method to manage Symfony2 projects is using
git1 but some prefer to use Subversion2 which is totally fine!. In this cookbook article, you'll learn how to
manage your project using svn3 in a similar manner you would do with git4.
This is a method to tracking your Symfony2 project in a Subversion repository. There are several
ways to do and this one is simply one that works.
The Subversion Repository
For this article we will suppose that your repository layout follows the widespread standard structure:
Listing 25-1
1 myproject/
2
branches/
3
tags/
4
trunk/
1. http://git-scm.com/
2. http://subversion.apache.org/
3. http://subversion.apache.org/
4. http://git-scm.com/
PDF brought to you by
generated on October 26, 2012
Chapter 25: How to Create and store a Symfony2 Project in Subversion | 265
Most subversion hosting should follow this standard practice. This is the recommended layout in
Version Control with Subversion5 and the layout used by most free hosting (see Subversion hosting
solutions).
Initial Project Setup
To get started, you'll need to download Symfony2 and get the basic Subversion setup:
1. Download the Symfony2 Standard Edition6 with or without vendors.
2. Unzip/untar the distribution. It will create a folder called Symfony with your new project
structure, config files, etc. Rename it to whatever you like.
3. Checkout the Subversion repository that will host this project. Let's say it is hosted on Google
code7 and called myproject:
Listing 25-2
1 $ svn checkout http://myproject.googlecode.com/svn/trunk myproject
4. Copy the Symfony2 project files in the subversion folder:
Listing 25-3
1 $ mv Symfony/* myproject/
5. Let's now set the ignore rules. Not everything should be stored in your subversion repository.
Some files (like the cache) are generated and others (like the database configuration) are meant
to be customized on each machine. This makes use of the svn:ignore property, so that we can
ignore specific files.
Listing 25-4
1
2
3
4
5
6
7
8
9
10
11
12
$ cd myproject/
$ svn add --depth=empty app app/cache app/logs app/config web
$
$
$
$
$
svn
svn
svn
svn
svn
propset
propset
propset
propset
propset
svn:ignore
svn:ignore
svn:ignore
svn:ignore
svn:ignore
"vendor" .
"bootstrap*" app/
"parameters.ini" app/config/
"*" app/cache/
"*" app/logs/
$ svn propset svn:ignore "bundles" web
$ svn ci -m "commit basic symfony ignore list (vendor, app/bootstrap*, app/config/
parameters.ini, app/cache/*, app/logs/*, web/bundles)"
6. The rest of the files can now be added and committed to the project:
Listing 25-5
1 $ svn add --force .
2 $ svn ci -m "add basic Symfony Standard 2.X.Y"
7. Copy
app/config/parameters.ini
to
app/config/parameters.ini.dist.
The
parameters.ini file is ignored by svn (see above) so that machine-specific settings like database
passwords aren't committed. By creating the parameters.ini.dist file, new developers can
quickly clone the project, copy this file to parameters.ini, customize it, and start developing.
5. http://svnbook.red-bean.com/
6. http://symfony.com/download
7. http://code.google.com/hosting/
PDF brought to you by
generated on October 26, 2012
Chapter 25: How to Create and store a Symfony2 Project in Subversion | 266
8. Finally, download all of the third-party vendor libraries:
Listing 25-6
1 $ php bin/vendors install
git8 has to be installed to run bin/vendors, this is the protocol used to fetch vendor libraries.
This only means that git is used as a tool to basically help download the libraries in the vendor/
directory.
At this point, you have a fully-functional Symfony2 project stored in your Subversion repository. The
development can start with commits in the Subversion repository.
You can continue to follow along with the Creating Pages in Symfony2 chapter to learn more about how
to configure and develop inside your application.
The Symfony2 Standard Edition comes with some example functionality. To remove the sample
code, follow the instructions on the Standard Edition Readme9.
Managing Vendor Libraries with bin/vendors and deps
How does it work?
Every Symfony project uses a group of third-party "vendor" libraries. One way or another the goal is to
download these files into your vendor/ directory and, ideally, to give you some sane way to manage the
exact version you need for each.
By default, these libraries are downloaded by running a php bin/vendors install "downloader" script.
This script reads from the deps file at the root of your project. This is an ini-formatted script, which
holds a list of each of the external libraries you need, the directory each should be downloaded to, and
(optionally) the version to be downloaded. The bin/vendors script uses git to downloaded these, solely
because these external libraries themselves tend to be stored via git. The bin/vendors script also reads
the deps.lock file, which allows you to pin each library to an exact git commit hash.
It's important to realize that these vendor libraries are not actually part of your repository. Instead, they're
simply un-tracked files that are downloaded into the vendor/ directory by the bin/vendors script. But
since all the information needed to download these files is saved in deps and deps.lock (which are
stored) in our repository), any other developer can use our project, run php bin/vendors install, and
download the exact same set of vendor libraries. This means that you're controlling exactly what each
vendor library looks like, without needing to actually commit them to your repository.
So, whenever a developer uses your project, he/she should run the php bin/vendors install script to
ensure that all of the needed vendor libraries are downloaded.
8. http://git-scm.com/
9. https://github.com/symfony/symfony-standard/blob/master/README.md
PDF brought to you by
generated on October 26, 2012
Chapter 25: How to Create and store a Symfony2 Project in Subversion | 267
Upgrading Symfony
Since Symfony is just a group of third-party libraries and third-party libraries are entirely controlled
through deps and deps.lock, upgrading Symfony means simply upgrading each of these files to
match their state in the latest Symfony Standard Edition.
Of course, if you've added new entries to deps or deps.lock, be sure to replace only the original
parts (i.e. be sure not to also delete any of your custom entries).
There is also a php bin/vendors update command, but this has nothing to do with upgrading
your project and you will normally not need to use it. This command is used to freeze the versions
of all of your vendor libraries by updating them to the version specified in deps and recording it
into the deps.lock file.
Hacking vendor libraries
Sometimes, you want a specific branch, tag, or commit of a library to be downloaded or upgraded. You
can set that directly to the deps file :
Listing 25-7
1 [AcmeAwesomeBundle]
2
git=http://github.com/johndoe/Acme/AwesomeBundle.git
3
target=/bundles/Acme/AwesomeBundle
4
version=the-awesome-version
• The git option sets the URL of the library. It can use various protocols, like http:// as well
as git://.
• The target option specifies where the repository will live : plain Symfony bundles should go
under the vendor/bundles/Acme directory, other third-party libraries usually go to vendor/
my-awesome-library-name. The target directory defaults to this last option when not
specified.
• The version option allows you to set a specific revision. You can use a tag
(version=origin/0.42) or a branch name (refs/remotes/origin/awesome-branch). It
defaults to origin/HEAD.
Updating workflow
When you execute the php bin/vendors install, for every library, the script first checks if the install
directory exists.
If it does not (and ONLY if it does not), it runs a git clone.
Then, it does a git fetch origin and a git reset --hard the-awesome-version.
This means that the repository will only be cloned once. If you want to perform any change of the git
remote, you MUST delete the entire target directory, not only its content.
PDF brought to you by
generated on October 26, 2012
Chapter 25: How to Create and store a Symfony2 Project in Subversion | 268
Subversion hosting solutions
The biggest difference between git10 and svn11 is that Subversion needs a central repository to work. You
then have several solutions:
• Self hosting: create your own repository and access it either through the filesystem or the
network. To help in this task you can read Version Control with Subversion.
• Third party hosting: there are a lot of serious free hosting solutions available like GitHub12,
Google code13, SourceForge14 or Gna15. Some of them offer git hosting as well.
10. http://git-scm.com/
11. http://subversion.apache.org/
12. http://github.com/
13. http://code.google.com/hosting/
14. http://sourceforge.net/
15. http://gna.org/
PDF brought to you by
generated on October 26, 2012
Chapter 25: How to Create and store a Symfony2 Project in Subversion | 269
Chapter 26
How to customize Error Pages
When any exception is thrown in Symfony2, the exception is caught inside the Kernel class and
eventually forwarded to a special controller, TwigBundle:Exception:show for handling. This controller,
which lives inside the core TwigBundle, determines which error template to display and the status code
that should be set for the given exception.
Error pages can be customized in two different ways, depending on how much control you need:
1. Customize the error templates of the different error pages (explained below);
2. Replace the default exception controller TwigBundle::Exception:show with your own
controller and handle it however you want (see exception_controller in the Twig reference);
The customization of exception handling is actually much more powerful than what's written here.
An internal event, kernel.exception, is thrown which allows complete control over exception
handling. For more information, see kernel.exception Event.
All of the error templates live inside TwigBundle. To override the templates, we simply rely on the
standard method for overriding templates that live inside a bundle. For more information, see Overriding
Bundle Templates.
For example, to override the default error template that's shown to the end-user, create a new template
located at app/Resources/TwigBundle/views/Exception/error.html.twig:
Listing 26-1
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>An Error Occurred: {{ status_text }}</title>
</head>
<body>
<h1>Oops! An Error Occurred</h1>
<h2>The server returned a "{{ status_code }} {{ status_text }}".</h2>
</body>
</html>
PDF brought to you by
generated on October 26, 2012
Chapter 26: How to customize Error Pages | 270
If you're not familiar with Twig, don't worry. Twig is a simple, powerful and optional templating
engine that integrates with Symfony2. For more information about Twig see Creating and using
Templates.
In addition to the standard HTML error page, Symfony provides a default error page for many of the
most common response formats, including JSON (error.json.twig), XML (error.xml.twig) and even
Javascript (error.js.twig), to name a few. To override any of these templates, just create a new file with
the same name in the app/Resources/TwigBundle/views/Exception directory. This is the standard way
of overriding any template that lives inside a bundle.
Customizing the 404 Page and other Error Pages
You can also customize specific error templates according to the HTTP status code. For instance, create
a app/Resources/TwigBundle/views/Exception/error404.html.twig template to display a special
page for 404 (page not found) errors.
Symfony uses the following algorithm to determine which template to use:
• First, it looks for a template for the given format and status code (like error404.json.twig);
• If it does not exist, it looks for a template for the given format (like error.json.twig);
• If it does not exist, it falls back to the HTML template (like error.html.twig).
To see the full list of default error templates, see the Resources/views/Exception directory of
the TwigBundle. In a standard Symfony2 installation, the TwigBundle can be found at vendor/
symfony/src/Symfony/Bundle/TwigBundle. Often, the easiest way to customize an error page
is to copy it from the TwigBundle into app/Resources/TwigBundle/views/Exception and then
modify it.
The debug-friendly exception pages shown to the developer can even be customized in the same
way by creating templates such as exception.html.twig for the standard HTML exception page
or exception.json.twig for the JSON exception page.
PDF brought to you by
generated on October 26, 2012
Chapter 26: How to customize Error Pages | 271
Chapter 27
How to define Controllers as Services
In the book, you've learned how easily a controller can be used when it extends the base Controller1
class. While this works fine, controllers can also be specified as services.
To refer to a controller that's defined as a service, use the single colon (:) notation. For example, suppose
we've defined a service called my_controller and we want to forward to a method called indexAction()
inside the service:
Listing 27-1
1 $this->forward('my_controller:indexAction', array('foo' => $bar));
You need to use the same notation when defining the route _controller value:
Listing 27-2
1 my_controller:
2
pattern:
/
3
defaults: { _controller: my_controller:indexAction }
To use a controller in this way, it must be defined in the service container configuration. For more
information, see the Service Container chapter.
When using a controller defined as a service, it will most likely not extend the base Controller class.
Instead of relying on its shortcut methods, you'll interact directly with the services that you need.
Fortunately, this is usually pretty easy and the base Controller class itself is a great source on how to
perform many common tasks.
Specifying a controller as a service takes a little bit more work. The primary advantage is that the
entire controller or any services passed to the controller can be modified via the service container
configuration. This is especially useful when developing an open-source bundle or any bundle that
will be used in many different projects. So, even if you don't specify your controllers as services,
you'll likely see this done in some open-source Symfony2 bundles.
1. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Controller/Controller.html
PDF brought to you by
generated on October 26, 2012
Chapter 27: How to define Controllers as Services | 272
Using Annotation Routing
When using annotations to setup routing when using a controller defined as a service, you need to specify
your service as follows:
Listing 27-3
1
2
3
4
5
6
7
/**
* @Route("/blog", service="my_bundle.annot_controller")
* @Cache(expires="tomorrow")
*/
class AnnotController extends Controller
{
}
In this example, my_bundle.annot_controller should be the id of the AnnotController instance
defined in the service container. This is documented in the @Route and @Method chapter.
PDF brought to you by
generated on October 26, 2012
Chapter 27: How to define Controllers as Services | 273
Chapter 28
How to force routes to always use HTTPS or
HTTP
Sometimes, you want to secure some routes and be sure that they are always accessed via the HTTPS
protocol. The Routing component allows you to enforce the URI scheme via the _scheme requirement:
Listing 28-1
1 secure:
2
pattern: /secure
3
defaults: { _controller: AcmeDemoBundle:Main:secure }
4
requirements:
5
_scheme: https
The above configuration forces the secure route to always use HTTPS.
When generating the secure URL, and if the current scheme is HTTP, Symfony will automatically
generate an absolute URL with HTTPS as the scheme:
Listing 28-2
1
2
3
4
5
6
7
{# If the current scheme is HTTPS #}
{{ path('secure') }}
# generates /secure
{# If the current scheme is HTTP #}
{{ path('secure') }}
{# generates https://example.com/secure #}
The requirement is also enforced for incoming requests. If you try to access the /secure path with HTTP,
you will automatically be redirected to the same URL, but with the HTTPS scheme.
The above example uses https for the _scheme, but you can also force a URL to always use http.
The Security component provides another way to enforce HTTP or HTTPs via the
requires_channel setting. This alternative method is better suited to secure an "area" of your
website (all URLs under /admin) or when you want to secure URLs defined in a third party bundle.
PDF brought to you by
generated on October 26, 2012
Chapter 28: How to force routes to always use HTTPS or HTTP | 274
Chapter 29
How to allow a "/" character in a route
parameter
Sometimes, you need to compose URLs with parameters that can contain a slash /. For example, take the
classic /hello/{name} route. By default, /hello/Fabien will match this route but not /hello/Fabien/
Kris. This is because Symfony uses this character as separator between route parts.
This guide covers how you can modify a route so that /hello/Fabien/Kris matches the /hello/{name}
route, where {name} equals Fabien/Kris.
Configure the Route
By default, the symfony routing components requires that the parameters match the following regex
pattern: [^/]+. This means that all characters are allowed except /.
You must explicitly allow / to be part of your parameter by specifying a more permissive regex pattern.
Listing 29-1
1 _hello:
2
pattern: /hello/{name}
3
defaults: { _controller: AcmeDemoBundle:Demo:hello }
4
requirements:
5
name: ".+"
That's it! Now, the {name} parameter can contain the / character.
PDF brought to you by
generated on October 26, 2012
Chapter 29: How to allow a "/" character in a route parameter | 275
Chapter 30
How to configure a redirect to another route
without a custom controller
This guide explains how to configure a redirect from one route to another without using a custom
controller.
Let's assume that there is no useful default controller for the / path of your application and you want to
redirect theses requests to /app.
Your configuration will look like this:
Listing 30-1
1 AppBundle:
2
resource: "@App/Controller/"
3
type:
annotation
4
prefix:
/app
5
6 root:
7
pattern: /
8
defaults:
9
_controller: FrameworkBundle:Redirect:urlRedirect
10
path: /app
11
permanent: true
Your AppBundle is registered to handle all requests under /app.
We configure a route for the / path and let RedirectController1 handle it. This controller is built-in
and offers two methods for redirecting request:
• redirect redirects to another route. You must provide the route parameter with the name of
the route you want to redirect to.
• urlRedirect redirects to another path. You must provide the path parameter containing the
path of the resource you want to redirect to.
The permanent switch tells both methods to issue a 301 HTTP status code.
1. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.html
PDF brought to you by
generated on October 26, 2012
Chapter 30: How to configure a redirect to another route without a custom controller | 276
Chapter 31
How to use HTTP Methods beyond GET and
POST in Routes
The HTTP method of a request is one of the requirements that can be checked when seeing if it matches
a route. This is introduced in the routing chapter of the book "Routing" with examples using GET and
POST. You can also use other HTTP verbs in this way. For example, if you have a blog post entry then
you could use the same URL pattern to show it, make changes to it and delete it by matching on GET,
PUT and DELETE.
Listing 31-1
1 blog_show:
2
pattern: /blog/{slug}
3
defaults: { _controller: AcmeDemoBundle:Blog:show }
4
requirements:
5
_method: GET
6
7 blog_update:
8
pattern: /blog/{slug}
9
defaults: { _controller: AcmeDemoBundle:Blog:update }
10
requirements:
11
_method: PUT
12
13 blog_delete:
14
pattern: /blog/{slug}
15
defaults: { _controller: AcmeDemoBundle:Blog:delete }
16
requirements:
17
_method: DELETE
Unfortunately, life isn't quite this simple, since most browsers do not support sending PUT and DELETE
requests. Fortunately Symfony2 provides you with a simple way of working around this limitation. By
including a _method parameter in the query string or parameters of an HTTP request, Symfony2 will use
this as the method when matching routes. This can be done easily in forms with a hidden field. Suppose
you have a form for editing a blog post:
Listing 31-2
PDF brought to you by
generated on October 26, 2012
Chapter 31: How to use HTTP Methods beyond GET and POST in Routes | 277
1 <form action="{{ path('blog_update', {'slug': blog.slug}) }}" method="post">
2
<input type="hidden" name="_method" value="PUT" />
3
{{ form_widget(form) }}
4
<input type="submit" value="Update" />
5 </form>
The submitted request will now match the blog_update route and the updateAction will be used to
process the form.
Likewise the delete form could be changed to look like this:
Listing 31-3
1 <form action="{{ path('blog_delete', {'slug': blog.slug}) }}" method="post">
2
<input type="hidden" name="_method" value="DELETE" />
3
{{ form_widget(delete_form) }}
4
<input type="submit" value="Delete" />
5 </form>
It will then match the blog_delete route.
PDF brought to you by
generated on October 26, 2012
Chapter 31: How to use HTTP Methods beyond GET and POST in Routes | 278
Chapter 32
How to Use Assetic for Asset Management
Assetic combines two major ideas: assets and filters. The assets are files such as CSS, JavaScript and image
files. The filters are things that can be applied to these files before they are served to the browser. This
allows a separation between the asset files stored in the application and the files actually presented to the
user.
Without Assetic, you just serve the files that are stored in the application directly:
Listing 32-1
1 <script src="{{ asset('js/script.js') }}" type="text/javascript" />
But with Assetic, you can manipulate these assets however you want (or load them from anywhere) before
serving them. This means you can:
• Minify and combine all of your CSS and JS files
• Run all (or just some) of your CSS or JS files through some sort of compiler, such as LESS,
SASS or CoffeeScript
• Run image optimizations on your images
Assets
Using Assetic provides many advantages over directly serving the files. The files do not need to be stored
where they are served from and can be drawn from various sources such as from within a bundle:
Listing 32-2
1 {% javascripts '@AcmeFooBundle/Resources/public/js/*' %}
2
<script type="text/javascript" src="{{ asset_url }}"></script>
3 {% endjavascripts %}
To bring in CSS stylesheets, you can use the same methodologies seen in this entry, except with
the stylesheets tag:
Listing 32-3
PDF brought to you by
generated on October 26, 2012
Chapter 32: How to Use Assetic for Asset Management | 279
1 {% stylesheets '@AcmeFooBundle/Resources/public/css/*' %}
2
<link rel="stylesheet" href="{{ asset_url }}" />
3 {% endstylesheets %}
In this example, all of the files in the Resources/public/js/ directory of the AcmeFooBundle will be
loaded and served from a different location. The actual rendered tag might simply look like:
Listing 32-4
1 <script src="/app_dev.php/js/abcd123.js"></script>
This is a key point: once you let Assetic handle your assets, the files are served from a different
location. This can cause problems with CSS files that reference images by their relative path.
However, this can be fixed by using the cssrewrite filter, which updates paths in CSS files to
reflect their new location.
Combining Assets
You can also combine several files into one. This helps to reduce the number of HTTP requests, which is
great for front end performance. It also allows you to maintain the files more easily by splitting them into
manageable parts. This can help with re-usability as you can easily split project-specific files from those
which can be used in other applications, but still serve them as a single file:
Listing 32-5
1 {% javascripts
2
'@AcmeFooBundle/Resources/public/js/*'
3
'@AcmeBarBundle/Resources/public/js/form.js'
4
'@AcmeBarBundle/Resources/public/js/calendar.js' %}
5
<script src="{{ asset_url }}"></script>
6 {% endjavascripts %}
In the dev environment, each file is still served individually, so that you can debug problems more easily.
However, in the prod environment, this will be rendered as a single script tag.
If you're new to Assetic and try to use your application in the prod environment (by using the
app.php controller), you'll likely see that all of your CSS and JS breaks. Don't worry! This is on
purpose. For details on using Assetic in the prod environment, see Dumping Asset Files.
And combining files doesn't only apply to your files. You can also use Assetic to combine third party
assets, such as jQuery, with your own into a single file:
Listing 32-6
1 {% javascripts
2
'@AcmeFooBundle/Resources/public/js/thirdparty/jquery.js'
3
'@AcmeFooBundle/Resources/public/js/*' %}
4
<script src="{{ asset_url }}"></script>
5 {% endjavascripts %}
Filters
Once they're managed by Assetic, you can apply filters to your assets before they are served. This includes
filters that compress the output of your assets for smaller file sizes (and better front-end optimization).
PDF brought to you by
generated on October 26, 2012
Chapter 32: How to Use Assetic for Asset Management | 280
Other filters can compile JavaScript file from CoffeeScript files and process SASS into CSS. In fact, Assetic
has a long list of available filters.
Many of the filters do not do the work directly, but use existing third-party libraries to do the heavylifting. This means that you'll often need to install a third-party library to use a filter. The great advantage
of using Assetic to invoke these libraries (as opposed to using them directly) is that instead of having to
run them manually after you work on the files, Assetic will take care of this for you and remove this step
altogether from your development and deployment processes.
To use a filter, you first need to specify it in the Assetic configuration. Adding a filter here doesn't mean
it's being used - it just means that it's available to use (we'll use the filter below).
For example to use the JavaScript YUI Compressor the following config should be added:
Listing 32-7
1 # app/config/config.yml
2 assetic:
3
filters:
4
yui_js:
5
jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
Now, to actually use the filter on a group of JavaScript files, add it into your template:
Listing 32-8
1 {% javascripts '@AcmeFooBundle/Resources/public/js/*' filter='yui_js' %}
2
<script src="{{ asset_url }}"></script>
3 {% endjavascripts %}
A more detailed guide about configuring and using Assetic filters as well as details of Assetic's debug
mode can be found in How to Minify JavaScripts and Stylesheets with YUI Compressor.
Controlling the URL used
If you wish to, you can control the URLs that Assetic produces. This is done from the template and is
relative to the public document root:
Listing 32-9
1 {% javascripts '@AcmeFooBundle/Resources/public/js/*' output='js/compiled/main.js' %}
2
<script src="{{ asset_url }}"></script>
3 {% endjavascripts %}
Symfony also contains a method for cache busting, where the final URL generated by Assetic
contains a query parameter that can be incremented via configuration on each deployment. For
more information, see the assets_version configuration option.
Dumping Asset Files
In the dev environment, Assetic generates paths to CSS and JavaScript files that don't physically exist on
your computer. But they render nonetheless because an internal Symfony controller opens the files and
serves back the content (after running any filters).
This kind of dynamic serving of processed assets is great because it means that you can immediately see
the new state of any asset files you change. It's also bad, because it can be quite slow. If you're using a lot
of filters, it might be downright frustrating.
Fortunately, Assetic provides a way to dump your assets to real files, instead of being generated
dynamically.
PDF brought to you by
generated on October 26, 2012
Chapter 32: How to Use Assetic for Asset Management | 281
Dumping Asset Files in the prod environment
In the prod environment, your JS and CSS files are represented by a single tag each. In other words,
instead of seeing each JavaScript file you're including in your source, you'll likely just see something like
this:
Listing 32-10
1 <script src="/app_dev.php/js/abcd123.js"></script>
Moreover, that file does not actually exist, nor is it dynamically rendered by Symfony (as the asset files
are in the dev environment). This is on purpose - letting Symfony generate these files dynamically in a
production environment is just too slow.
Instead, each time you use your app in the prod environment (and therefore, each time you deploy), you
should run the following task:
Listing 32-11
1 $ php app/console assetic:dump --env=prod --no-debug
This will physically generate and write each file that you need (e.g. /js/abcd123.js). If you update any
of your assets, you'll need to run this again to regenerate the file.
Dumping Asset Files in the dev environment
By default, each asset path generated in the dev environment is handled dynamically by Symfony. This
has no disadvantage (you can see your changes immediately), except that assets can load noticeably slow.
If you feel like your assets are loading too slowly, follow this guide.
First, tell Symfony to stop trying to process these files dynamically. Make the following change in your
config_dev.yml file:
Listing 32-12
1 # app/config/config_dev.yml
2 assetic:
3
use_controller: false
Next, since Symfony is no longer generating these assets for you, you'll need to dump them manually. To
do so, run the following:
Listing 32-13
1 $ php app/console assetic:dump
This physically writes all of the asset files you need for your dev environment. The big disadvantage is
that you need to run this each time you update an asset. Fortunately, by passing the --watch option, the
command will automatically regenerate assets as they change:
Listing 32-14
1 $ php app/console assetic:dump --watch
Since running this command in the dev environment may generate a bunch of files, it's usually a good
idea to point your generated assets files to some isolated directory (e.g. /js/compiled), to keep things
organized:
Listing 32-15
1 {% javascripts '@AcmeFooBundle/Resources/public/js/*' output='js/compiled/main.js' %}
2
<script src="{{ asset_url }}"></script>
3 {% endjavascripts %}
PDF brought to you by
generated on October 26, 2012
Chapter 32: How to Use Assetic for Asset Management | 282
Chapter 33
How to Minify JavaScripts and Stylesheets with
YUI Compressor
Yahoo! provides an excellent utility for minifying JavaScripts and stylesheets so they travel over the wire
faster, the YUI Compressor1. Thanks to Assetic, you can take advantage of this tool very easily.
Download the YUI Compressor JAR
The YUI Compressor is written in Java and distributed as a JAR. Download the JAR2 from the Yahoo! site
and save it to app/Resources/java/yuicompressor.jar.
Configure the YUI Filters
Now you need to configure two Assetic filters in your application, one for minifying JavaScripts with the
YUI Compressor and one for minifying stylesheets:
Listing 33-1
1 # app/config/config.yml
2 assetic:
3
# java: "/usr/bin/java"
4
filters:
5
yui_css:
6
jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
7
yui_js:
8
jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
1. http://developer.yahoo.com/yui/compressor/
2. http://yuilibrary.com/projects/yuicompressor/
PDF brought to you by
generated on October 26, 2012
Chapter 33: How to Minify JavaScripts and Stylesheets with YUI Compressor | 283
Windows users need to remember to update config to proper java location. In Windows7 x64 bit
by default it's C:\Program Files (x86)\Java\jre6\bin\java.exe.
You now have access to two new Assetic filters in your application: yui_css and yui_js. These will use
the YUI Compressor to minify stylesheets and JavaScripts, respectively.
Minify your Assets
You have YUI Compressor configured now, but nothing is going to happen until you apply one of these
filters to an asset. Since your assets are a part of the view layer, this work is done in your templates:
Listing 33-2
1 {% javascripts '@AcmeFooBundle/Resources/public/js/*' filter='yui_js' %}
2
<script src="{{ asset_url }}"></script>
3 {% endjavascripts %}
The above example assumes that you have a bundle called AcmeFooBundle and your JavaScript
files are in the Resources/public/js directory under your bundle. This isn't important however you can include your Javascript files no matter where they are.
With the addition of the yui_js filter to the asset tags above, you should now see minified JavaScripts
coming over the wire much faster. The same process can be repeated to minify your stylesheets.
Listing 33-3
1 {% stylesheets '@AcmeFooBundle/Resources/public/css/*' filter='yui_css' %}
2
<link rel="stylesheet" type="text/css" media="screen" href="{{ asset_url }}" />
3 {% endstylesheets %}
Disable Minification in Debug Mode
Minified JavaScripts and Stylesheets are very difficult to read, let alone debug. Because of this, Assetic lets
you disable a certain filter when your application is in debug mode. You can do this by prefixing the filter
name in your template with a question mark: ?. This tells Assetic to only apply this filter when debug
mode is off.
Listing 33-4
1 {% javascripts '@AcmeFooBundle/Resources/public/js/*' filter='?yui_js' %}
2
<script src="{{ asset_url }}"></script>
3 {% endjavascripts %}
Instead of adding the filter to the asset tags, you can also globally enable it by adding the applyto attribute to the filter configuration, for example in the yui_js filter apply_to: "\.js$". To
only have the filter applied in production, add this to the config_prod file rather than the common
config file. For details on applying filters by file extension, see Filtering based on a File Extension.
PDF brought to you by
generated on October 26, 2012
Chapter 33: How to Minify JavaScripts and Stylesheets with YUI Compressor | 284
Chapter 34
How to Use Assetic For Image Optimization
with Twig Functions
Amongst its many filters, Assetic has four filters which can be used for on-the-fly image optimization.
This allows you to get the benefits of smaller file sizes without having to use an image editor to process
each image. The results are cached and can be dumped for production so there is no performance hit for
your end users.
Using Jpegoptim
Jpegoptim1 is a utility for optimizing JPEG files. To use it with Assetic, add the following to the Assetic
config:
Listing 34-1
1 # app/config/config.yml
2 assetic:
3
filters:
4
jpegoptim:
5
bin: path/to/jpegoptim
Notice that to use jpegoptim, you must have it already installed on your system. The bin option
points to the location of the compiled binary.
It can now be used from a template:
Listing 34-2
1 {% image '@AcmeFooBundle/Resources/public/images/example.jpg'
2
filter='jpegoptim' output='/images/example.jpg' %}
3
<img src="{{ asset_url }}" alt="Example"/>
4 {% endimage %}
1. http://www.kokkonen.net/tjko/projects.html
PDF brought to you by
generated on October 26, 2012
Chapter 34: How to Use Assetic For Image Optimization with Twig Functions | 285
Removing all EXIF Data
By default, running this filter only removes some of the meta information stored in the file. Any EXIF
data and comments are not removed, but you can remove these by using the strip_all option:
Listing 34-3
1 # app/config/config.yml
2 assetic:
3
filters:
4
jpegoptim:
5
bin: path/to/jpegoptim
6
strip_all: true
Lowering Maximum Quality
The quality level of the JPEG is not affected by default. You can gain further file size reductions by setting
the max quality setting lower than the current level of the images. This will of course be at the expense of
image quality:
Listing 34-4
1 # app/config/config.yml
2 assetic:
3
filters:
4
jpegoptim:
5
bin: path/to/jpegoptim
6
max: 70
Shorter syntax: Twig Function
If you're using Twig, it's possible to achieve all of this with a shorter syntax by enabling and using a
special Twig function. Start by adding the following config:
Listing 34-5
1 # app/config/config.yml
2 assetic:
3
filters:
4
jpegoptim:
5
bin: path/to/jpegoptim
6
twig:
7
functions:
8
jpegoptim: ~
The Twig template can now be changed to the following:
Listing 34-6
1 <img src="{{ jpegoptim('@AcmeFooBundle/Resources/public/images/example.jpg') }}"
alt="Example"/>
You can specify the output directory in the config in the following way:
Listing 34-7
1 # app/config/config.yml
2 assetic:
3
filters:
4
jpegoptim:
5
bin: path/to/jpegoptim
6
twig:
PDF brought to you by
generated on October 26, 2012
Chapter 34: How to Use Assetic For Image Optimization with Twig Functions | 286
7
8
functions:
jpegoptim: { output: images/*.jpg }
PDF brought to you by
generated on October 26, 2012
Chapter 34: How to Use Assetic For Image Optimization with Twig Functions | 287
Chapter 35
How to Apply an Assetic Filter to a Specific File
Extension
Assetic filters can be applied to individual files, groups of files or even, as you'll see here, files that have a
specific extension. To show you how to handle each option, let's suppose that you want to use Assetic's
CoffeeScript filter, which compiles CoffeeScript files into Javascript.
The main configuration is just the paths to coffee and node. These default respectively to /usr/bin/
coffee and /usr/bin/node:
Listing 35-1
1 # app/config/config.yml
2 assetic:
3
filters:
4
coffee:
5
bin: /usr/bin/coffee
6
node: /usr/bin/node
Filter a Single File
You can now serve up a single CoffeeScript file as JavaScript from within your templates:
Listing 35-2
1 {% javascripts '@AcmeFooBundle/Resources/public/js/example.coffee' filter='coffee' %}
2
<script src="{{ asset_url }}" type="text/javascript"></script>
3 {% endjavascripts %}
This is all that's needed to compile this CoffeeScript file and server it as the compiled JavaScript.
Filter Multiple Files
You can also combine multiple CoffeeScript files into a single output file:
PDF brought to you by
generated on October 26, 2012
Chapter 35: How to Apply an Assetic Filter to a Specific File Extension | 288
Listing 35-3
1 {% javascripts '@AcmeFooBundle/Resources/public/js/example.coffee'
2
'@AcmeFooBundle/Resources/public/js/another.coffee'
3
filter='coffee' %}
4
<script src="{{ asset_url }}" type="text/javascript"></script>
5 {% endjavascripts %}
Both the files will now be served up as a single file compiled into regular JavaScript.
Filtering based on a File Extension
One of the great advantages of using Assetic is reducing the number of asset files to lower HTTP requests.
In order to make full use of this, it would be good to combine all your JavaScript and CoffeeScript files
together since they will ultimately all be served as JavaScript. Unfortunately just adding the JavaScript
files to the files to be combined as above will not work as the regular JavaScript files will not survive the
CoffeeScript compilation.
This problem can be avoided by using the apply_to option in the config, which allows you to specify
that a filter should always be applied to particular file extensions. In this case you can specify that the
Coffee filter is applied to all .coffee files:
Listing 35-4
# app/config/config.yml
assetic:
filters:
coffee:
bin: /usr/bin/coffee
node: /usr/bin/node
apply_to: "\.coffee$"
With this, you no longer need to specify the coffee filter in the template. You can also list regular
JavaScript files, all of which will be combined and rendered as a single JavaScript file (with only the
.coffee files being run through the CoffeeScript filter):
Listing 35-5
1 {% javascripts '@AcmeFooBundle/Resources/public/js/example.coffee'
2
'@AcmeFooBundle/Resources/public/js/another.coffee'
3
'@AcmeFooBundle/Resources/public/js/regular.js' %}
4
<script src="{{ asset_url }}" type="text/javascript"></script>
5 {% endjavascripts %}
PDF brought to you by
generated on October 26, 2012
Chapter 35: How to Apply an Assetic Filter to a Specific File Extension | 289
Chapter 36
How to handle File Uploads with Doctrine
Handling file uploads with Doctrine entities is no different than handling any other file upload. In other
words, you're free to move the file in your controller after handling a form submission. For examples of
how to do this, see the file type reference page.
If you choose to, you can also integrate the file upload into your entity lifecycle (i.e. creation, update and
removal). In this case, as your entity is created, updated, and removed from Doctrine, the file uploading
and removal processing will take place automatically (without needing to do anything in your controller);
To make this work, you'll need to take care of a number of details, which will be covered in this cookbook
entry.
Basic Setup
First, create a simple Doctrine Entity class to work with:
Listing 36-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Acme/DemoBundle/Entity/Document.php
namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
*/
class Document
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* @ORM\Column(type="string", length=255)
PDF brought to you by
generated on October 26, 2012
Chapter 36: How to handle File Uploads with Doctrine | 290
21
* @Assert\NotBlank
22
*/
23
public $name;
24
25
/**
26
* @ORM\Column(type="string", length=255, nullable=true)
27
*/
28
public $path;
29
30
public function getAbsolutePath()
31
{
32
return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->path;
33
}
34
35
public function getWebPath()
36
{
37
return null === $this->path ? null : $this->getUploadDir().'/'.$this->path;
38
}
39
40
protected function getUploadRootDir()
41
{
42
// the absolute directory path where uploaded documents should be saved
43
return __DIR__.'/../../../../web/'.$this->getUploadDir();
44
}
45
46
protected function getUploadDir()
47
{
48
// get rid of the __DIR__ so it doesn't screw when displaying uploaded doc/image
49 in the view.
50
return 'uploads/documents';
51
}
}
The Document entity has a name and it is associated with a file. The path property stores the relative path
to the file and is persisted to the database. The getAbsolutePath() is a convenience method that returns
the absolute path to the file while the getWebPath() is a convenience method that returns the web path,
which can be used in a template to link to the uploaded file.
If you have not done so already, you should probably read the file type documentation first to
understand how the basic upload process works.
If you're using annotations to specify your validation rules (as shown in this example), be sure that
you've enabled validation by annotation (see validation configuration).
To handle the actual file upload in the form, use a "virtual" file field. For example, if you're building
your form directly in a controller, it might look like this:
Listing 36-2
1 public function uploadAction()
2 {
3
// ...
4
5
$form = $this->createFormBuilder($document)
6
->add('name')
PDF brought to you by
generated on October 26, 2012
Chapter 36: How to handle File Uploads with Doctrine | 291
7
8
9
10
11 }
->add('file')
->getForm();
// ...
Next, create this property on your Document class and add some validation rules:
Listing 36-3
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/DemoBundle/Entity/Document.php
// ...
class Document
{
/**
* @Assert\File(maxSize="6000000")
*/
public $file;
// ...
}
As you are using the File constraint, Symfony2 will automatically guess that the form field is a
file upload input. That's why you did not have to set it explicitly when creating the form above
(->add('file')).
The following controller shows you how to handle the entire process:
Listing 36-4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use Acme\DemoBundle\Entity\Document;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
// ...
/**
* @Template()
*/
public function uploadAction()
{
$document = new Document();
$form = $this->createFormBuilder($document)
->add('name')
->add('file')
->getForm()
;
if ($this->getRequest()->getMethod() === 'POST') {
$form->bindRequest($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($document);
$em->flush();
$this->redirect($this->generateUrl(...));
}
}
PDF brought to you by
generated on October 26, 2012
Chapter 36: How to handle File Uploads with Doctrine | 292
29
30 }
return array('form' => $form->createView());
When writing the template, don't forget to set the enctype attribute:
Listing 36-5
<h1>Upload File</h1>
<form action="#" method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<input type="submit" value="Upload Document" />
</form>
The previous controller will automatically persist the Document entity with the submitted name, but it
will do nothing about the file and the path property will be blank.
An easy way to handle the file upload is to move it just before the entity is persisted and then set the path
property accordingly. Start by calling a new upload() method on the Document class, which you'll create
in a moment to handle the file upload:
Listing 36-6
1 if ($form->isValid()) {
2
$em = $this->getDoctrine()->getEntityManager();
3
4
$document->upload();
5
6
$em->persist($document);
7
$em->flush();
8
9
$this->redirect(...);
10 }
The upload() method will take advantage of the UploadedFile1 object, which is what's returned after a
file field is submitted:
Listing 36-7
1 public function upload()
2 {
3
// the file property can be empty if the field is not required
4
if (null === $this->file) {
5
return;
6
}
7
8
// we use the original file name here but you should
9
// sanitize it at least to avoid any security issues
10
11
// move takes the target directory and then the target filename to move to
12
$this->file->move($this->getUploadRootDir(), $this->file->getClientOriginalName());
13
14
// set the path property to the filename where you'ved saved the file
15
$this->path = $this->file->getClientOriginalName();
16
17
// clean up the file property as you won't need it anymore
18
$this->file = null;
19 }
1. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/File/UploadedFile.html
PDF brought to you by
generated on October 26, 2012
Chapter 36: How to handle File Uploads with Doctrine | 293
Using Lifecycle Callbacks
Even if this implementation works, it suffers from a major flaw: What if there is a problem when the
entity is persisted? The file would have already moved to its final location even though the entity's path
property didn't persist correctly.
To avoid these issues, you should change the implementation so that the database operation and the
moving of the file become atomic: if there is a problem persisting the entity or if the file cannot be moved,
then nothing should happen.
To do this, you need to move the file right as Doctrine persists the entity to the database. This can be
accomplished by hooking into an entity lifecycle callback:
Listing 36-8
1
2
3
4
5
6
7
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
}
Next, refactor the Document class to take advantage of these callbacks:
Listing 36-9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
/**
* @ORM\PrePersist()
* @ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->file) {
// do whatever you want to generate a unique name
$this->path = sha1(uniqid(mt_rand(), true)).'.'.$this->file->guessExtension();
}
}
/**
* @ORM\PostPersist()
* @ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file) {
return;
}
// if there is an error when moving the file, an exception will
// be automatically thrown by move(). This will properly prevent
// the entity from being persisted to the database on error
$this->file->move($this->getUploadRootDir(), $this->path);
unset($this->file);
PDF brought to you by
generated on October 26, 2012
Chapter 36: How to handle File Uploads with Doctrine | 294
37
38
39
40
41
42
43
44
45
46
47
48 }
}
/**
* @ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
The class now does everything you need: it generates a unique filename before persisting, moves the file
after persisting, and removes the file if the entity is ever deleted.
Now that the moving of the file is handled atomically by the entity, the call to $document->upload()
should be removed from the controller:
Listing 36-10
1 if ($form->isValid()) {
2
$em = $this->getDoctrine()->getEntityManager();
3
4
$em->persist($document);
5
$em->flush();
6
7
$this->redirect(...);
8 }
The @ORM\PrePersist() and @ORM\PostPersist() event callbacks are triggered before and after
the entity is persisted to the database. On the other hand, the @ORM\PreUpdate() and
@ORM\PostUpdate() event callbacks are called when the entity is updated.
The PreUpdate and PostUpdate callbacks are only triggered if there is a change in one of the
entity's field that are persisted. This means that, by default, if you modify only the $file property,
these events will not be triggered, as the property itself is not directly persisted via Doctrine. One
solution would be to use an updated field that's persisted to Doctrine, and to modify it manually
when changing the file.
Using the id as the filename
If you want to use the id as the name of the file, the implementation is slightly different as you need to
save the extension under the path property, instead of the actual filename:
Listing 36-11
1
2
3
4
5
6
7
8
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/
class Document
{
PDF brought to you by
generated on October 26, 2012
Chapter 36: How to handle File Uploads with Doctrine | 295
9
// a property used temporarily while deleting
10
private $filenameForRemove;
11
12
/**
13
* @ORM\PrePersist()
14
* @ORM\PreUpdate()
15
*/
16
public function preUpload()
17
{
18
if (null !== $this->file) {
19
$this->path = $this->file->guessExtension();
20
}
21
}
22
23
/**
24
* @ORM\PostPersist()
25
* @ORM\PostUpdate()
26
*/
27
public function upload()
28
{
29
if (null === $this->file) {
30
return;
31
}
32
33
// you must throw an exception here if the file cannot be moved
34
// so that the entity is not persisted to the database
35
// which the UploadedFile move() method does
36
$this->file->move($this->getUploadRootDir(),
37 $this->id.'.'.$this->file->guessExtension());
38
39
unset($this->file);
40
}
41
42
/**
43
* @ORM\PreRemove()
44
*/
45
public function storeFilenameForRemove()
46
{
47
$this->filenameForRemove = $this->getAbsolutePath();
48
}
49
50
/**
51
* @ORM\PostRemove()
52
*/
53
public function removeUpload()
54
{
55
if ($this->filenameForRemove) {
56
unlink($this->filenameForRemove);
57
}
58
}
59
60
public function getAbsolutePath()
61
{
62
return null === $this->path ? null :
63 $this->getUploadRootDir().'/'.$this->id.'.'.$this->path;
}
}
PDF brought to you by
generated on October 26, 2012
Chapter 36: How to handle File Uploads with Doctrine | 296
You'll notice in this case that you need to do a little bit more work in order to remove the file. Before it's
removed, you must store the file path (since it depends on the id). Then, once the object has been fully
removed from the database, you can safely delete the file (in PostRemove).
PDF brought to you by
generated on October 26, 2012
Chapter 36: How to handle File Uploads with Doctrine | 297
Chapter 37
How to use Doctrine Extensions:
Timestampable, Sluggable, Translatable, etc.
Doctrine2 is very flexible, and the community has already created a series of useful Doctrine extensions
to help you with common entity-related tasks.
One library in particular - the DoctrineExtensions1 library - provides integration functionality for
Sluggable2, Translatable3, Timestampable4, Loggable5, Tree6 and Sortable7 behaviors.
The usage for each of these extensions is explained in that repository.
However, to install/activate each extension you must register and activate an Event Listener. To do this,
you have two options:
1. Use the StofDoctrineExtensionsBundle8, which integrates the above library.
2. Implement this services directly by following the documentation for integration with Symfony2:
Install Gedmo Doctrine2 extensions in Symfony29
1. https://github.com/l3pp4rd/DoctrineExtensions
2. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/sluggable.md
3. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/translatable.md
4. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/timestampable.md
5. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/loggable.md
6. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/tree.md
7. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/sortable.md
8. https://github.com/stof/StofDoctrineExtensionsBundle
9. https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/symfony2.md
PDF brought to you by
generated on October 26, 2012
Chapter 37: How to use Doctrine Extensions: Timestampable, Sluggable, Translatable, etc. | 298
Chapter 38
How to Register Event Listeners and
Subscribers
Doctrine packages a rich event system that fires events when almost anything happens inside the system.
For you, this means that you can create arbitrary services and tell Doctrine to notify those objects
whenever a certain action (e.g. prePersist) happens within Doctrine. This could be useful, for example,
to create an independent search index whenever an object in your database is saved.
Doctrine defines two types of objects that can listen to Doctrine events: listeners and subscribers. Both
are very similar, but listeners are a bit more straightforward. For more, see The Event System1 on
Doctrine's website.
Configuring the Listener/Subscriber
To register a service to act as an event listener or subscriber you just have to tag it with the appropriate
name. Depending on your use-case, you can hook a listener into every DBAL connection and ORM entity
manager or just into one specific DBAL connection and all the entity managers that use this connection.
Listing 38-1
1 doctrine:
2
dbal:
3
default_connection: default
4
connections:
5
default:
6
driver: pdo_sqlite
7
memory: true
8
9 services:
10
my.listener:
11
class: Acme\SearchBundle\Listener\SearchIndexer
12
tags:
13
- { name: doctrine.event_listener, event: postPersist }
14
my.listener2:
1. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/events.html
PDF brought to you by
generated on October 26, 2012
Chapter 38: How to Register Event Listeners and Subscribers | 299
15
16
17
18
19
20
21
class: Acme\SearchBundle\Listener\SearchIndexer2
tags:
- { name: doctrine.event_listener, event: postPersist, connection: default }
my.subscriber:
class: Acme\SearchBundle\Listener\SearchIndexerSubscriber
tags:
- { name: doctrine.event_subscriber, connection: default }
Creating the Listener Class
In the previous example, a service my.listener was configured as a Doctrine listener on the event
postPersist. That class behind that service must have a postPersist method, which will be called
when the event is thrown:
Listing 38-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Acme/SearchBundle/Listener/SearchIndexer.php
namespace Acme\SearchBundle\Listener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\StoreBundle\Entity\Product;
class SearchIndexer
{
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
// perhaps you only want to act on some "Product" entity
if ($entity instanceof Product) {
// do something with the Product
}
}
}
In each event, you have access to a LifecycleEventArgs object, which gives you access to both the entity
object of the event and the entity manager itself.
One important thing to notice is that a listener will be listening for all entities in your application. So,
if you're interested in only handling a specific type of entity (e.g. a Product entity but not a BlogPost
entity), you should check for the class name of the entity in your method (as shown above).
PDF brought to you by
generated on October 26, 2012
Chapter 38: How to Register Event Listeners and Subscribers | 300
Chapter 39
How to use Doctrine's DBAL Layer
This article is about Doctrine DBAL's layer. Typically, you'll work with the higher level Doctrine
ORM layer, which simply uses the DBAL behind the scenes to actually communicate with the
database. To read more about the Doctrine ORM, see "Databases and Doctrine".
The Doctrine1 Database Abstraction Layer (DBAL) is an abstraction layer that sits on top of PDO2 and
offers an intuitive and flexible API for communicating with the most popular relational databases. In
other words, the DBAL library makes it easy to execute queries and perform other database actions.
Read the official Doctrine DBAL Documentation3 to learn all the details and capabilities of
Doctrine's DBAL library.
To get started, configure the database connection parameters:
Listing 39-1
1 # app/config/config.yml
2 doctrine:
3
dbal:
4
driver:
pdo_mysql
5
dbname:
Symfony2
6
user:
root
7
password: null
8
charset: UTF8
For full DBAL configuration options, see Doctrine DBAL Configuration.
You can then access the Doctrine DBAL connection by accessing the database_connection service:
Listing 39-2
1. http://www.doctrine-project.org
2. http://www.php.net/pdo
3. http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/index.html
PDF brought to you by
generated on October 26, 2012
Chapter 39: How to use Doctrine's DBAL Layer | 301
1 class UserController extends Controller
2 {
3
public function indexAction()
4
{
5
$conn = $this->get('database_connection');
6
$users = $conn->fetchAll('SELECT * FROM users');
7
8
// ...
9
}
10 }
Registering Custom Mapping Types
You can register custom mapping types through Symfony's configuration. They will be added to all
configured connections. For more information on custom mapping types, read Doctrine's Custom
Mapping Types4 section of their documentation.
Listing 39-3
1 # app/config/config.yml
2 doctrine:
3
dbal:
4
types:
5
custom_first: Acme\HelloBundle\Type\CustomFirst
6
custom_second: Acme\HelloBundle\Type\CustomSecond
Registering Custom Mapping Types in the SchemaTool
The SchemaTool is used to inspect the database to compare the schema. To achieve this task, it needs to
know which mapping type needs to be used for each database types. Registering new ones can be done
through the configuration.
Let's map the ENUM type (not suppoorted by DBAL by default) to a the string mapping type:
Listing 39-4
1 # app/config/config.yml
2 doctrine:
3
dbal:
4
connections:
5
default:
6
// Other connections parameters
7
mapping_types:
8
enum: string
4. http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#custom-mapping-types
PDF brought to you by
generated on October 26, 2012
Chapter 39: How to use Doctrine's DBAL Layer | 302
Chapter 40
How to generate Entities from an Existing
Database
When starting work on a brand new project that uses a database, two different situations comes
naturally. In most cases, the database model is designed and built from scratch. Sometimes, however,
you'll start with an existing and probably unchangeable database model. Fortunately, Doctrine comes
with a bunch of tools to help generate model classes from your existing database.
As the Doctrine tools documentation1 says, reverse engineering is a one-time process to get started
on a project. Doctrine is able to convert approximately 70-80% of the necessary mapping
information based on fields, indexes and foreign key constraints. Doctrine can't discover inverse
associations, inheritance types, entities with foreign keys as primary keys or semantical operations
on associations such as cascade or lifecycle events. Some additional work on the generated entities
will be necessary afterwards to design each to fit your domain model specificities.
This tutorial assumes you're using a simple blog application with the following two tables: blog_post
and blog_comment. A comment record is linked to a post record thanks to a foreign key constraint.
Listing 40-1
1 CREATE TABLE `blog_post` (
2
`id` bigint(20) NOT NULL AUTO_INCREMENT,
3
`title` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
4
`content` longtext COLLATE utf8_unicode_ci NOT NULL,
5
`created_at` datetime NOT NULL,
6
PRIMARY KEY (`id`)
7 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
8
9 CREATE TABLE `blog_comment` (
10
`id` bigint(20) NOT NULL AUTO_INCREMENT,
11
`post_id` bigint(20) NOT NULL,
12
`author` varchar(20) COLLATE utf8_unicode_ci NOT NULL,
13
`content` longtext COLLATE utf8_unicode_ci NOT NULL,
14
`created_at` datetime NOT NULL,
1. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/tools.html#reverse-engineering
PDF brought to you by
generated on October 26, 2012
Chapter 40: How to generate Entities from an Existing Database | 303
15
PRIMARY KEY (`id`),
16
KEY `blog_comment_post_id_idx` (`post_id`),
17
CONSTRAINT `blog_post_id` FOREIGN KEY (`post_id`) REFERENCES `blog_post` (`id`) ON
18 DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Before diving into the recipe, be sure your database connection parameters are correctly setup in the
app/config/parameters.ini file (or wherever your database configuration is kept) and that you have
initialized a bundle that will host your future entity class. In this tutorial, we will assume that an
AcmeBlogBundle exists and is located under the src/Acme/BlogBundle folder.
The first step towards building entity classes from an existing database is to ask Doctrine to introspect
the database and generate the corresponding metadata files. Metadata files describe the entity class to
generate based on tables fields.
Listing 40-2
1 $ php app/console doctrine:mapping:convert xml ./src/Acme/BlogBundle/Resources/config/
doctrine/metadata/orm --from-database --force
This command line tool asks Doctrine to introspect the database and generate the XML metadata files
under the src/Acme/BlogBundle/Resources/config/doctrine/metadata/orm folder of your bundle.
It's also possible to generate metadata class in YAML format by changing the first argument to yml.
The generated BlogPost.dcm.xml metadata file looks as follows:
Listing 40-3
1 <?xml version="1.0" encoding="utf-8"?>
2 <doctrine-mapping>
3
<entity name="BlogPost" table="blog_post">
4
<change-tracking-policy>DEFERRED_IMPLICIT</change-tracking-policy>
5
<id name="id" type="bigint" column="id">
6
<generator strategy="IDENTITY"/>
7
</id>
8
<field name="title" type="string" column="title" length="100"/>
9
<field name="content" type="text" column="content"/>
10
<field name="isPublished" type="boolean" column="is_published"/>
11
<field name="createdAt" type="datetime" column="created_at"/>
12
<field name="updatedAt" type="datetime" column="updated_at"/>
13
<field name="slug" type="string" column="slug" length="255"/>
14
<lifecycle-callbacks/>
15
</entity>
16 </doctrine-mapping>
If you have oneToMany relationships between your entities, you will need to edit the generated xml
or yml files to add a section on the specific entities for oneToMany defining the inversedBy and the
mappedBy pieces.
Once the metadata files are generated, you can ask Doctrine to import the schema and build related entity
classes by executing the following two commands.
Listing 40-4
1 $ php app/console doctrine:mapping:import AcmeBlogBundle annotation
2 $ php app/console doctrine:generate:entities AcmeBlogBundle
PDF brought to you by
generated on October 26, 2012
Chapter 40: How to generate Entities from an Existing Database | 304
The first command generates entity classes with an annotations mapping, but you can of course change
the annotation argument to xml or yml. The newly created BlogComment entity class looks as follow:
Listing 40-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
// src/Acme/BlogBundle/Entity/BlogComment.php
namespace Acme\BlogBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Acme\BlogBundle\Entity\BlogComment
*
* @ORM\Table(name="blog_comment")
* @ORM\Entity
*/
class BlogComment
{
/**
* @var bigint $id
*
* @ORM\Column(name="id", type="bigint", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string $author
*
* @ORM\Column(name="author", type="string", length=100, nullable=false)
*/
private $author;
/**
* @var text $content
*
* @ORM\Column(name="content", type="text", nullable=false)
*/
private $content;
/**
* @var datetime $createdAt
*
* @ORM\Column(name="created_at", type="datetime", nullable=false)
*/
private $createdAt;
/**
* @var BlogPost
*
* @ORM\ManyToOne(targetEntity="BlogPost")
* @ORM\JoinColumn(name="post_id", referencedColumnName="id")
*/
private $post;
}
As you can see, Doctrine converts all table fields to pure private and annotated class properties. The most
impressive thing is that it also discovered the relationship with the BlogPost entity class based on the
PDF brought to you by
generated on October 26, 2012
Chapter 40: How to generate Entities from an Existing Database | 305
foreign key constraint. Consequently, you can find a private $post property mapped with a BlogPost
entity in the BlogComment entity class.
The last command generated all getters and setters for your two BlogPost and BlogComment entity class
properties. The generated entities are now ready to be used. Have fun!
PDF brought to you by
generated on October 26, 2012
Chapter 40: How to generate Entities from an Existing Database | 306
Chapter 41
How to work with Multiple Entity Managers
and Connections
You can use multiple Doctrine entity managers or connections in a Symfony2 application. This is
necessary if you are using different databases or even vendors with entirely different sets of entities. In
other words, one entity manager that connects to one database will handle some entities while another
entity manager that connects to another database might handle the rest.
Using multiple entity managers is pretty easy, but more advanced and not usually required. Be sure
you actually need multiple entity managers before adding in this layer of complexity.
The following configuration code shows how you can configure two entity managers:
Listing 41-1
doctrine:
dbal:
default_connection: default
connections:
default:
driver: %database_driver%
host:
%database_host%
port:
%database_port%
dbname: %database_name%
user:
%database_user%
password: %database_password%
charset: UTF8
customer:
driver: %database_driver2%
host:
%database_host2%
port:
%database_port2%
dbname: %database_name2%
user:
%database_user2%
password: %database_password2%
charset: UTF8
PDF brought to you by
generated on October 26, 2012
Chapter 41: How to work with Multiple Entity Managers and Connections | 307
orm:
default_entity_manager: default
entity_managers:
default:
connection:
default
mappings:
AcmeDemoBundle: ~
AcmeStoreBundle: ~
customer:
connection:
customer
mappings:
AcmeCustomerBundle: ~
In this case, you've defined two entity managers and called them default and customer. The default
entity manager manages entities in the AcmeDemoBundle and AcmeStoreBundle, while the customer
entity manager manages entities in the AcmeCustomerBundle. You've also defined two connections, one
for each entity manager.
When working with multiple connections and entity managers, you should be explicit about which
configuration you want. If you do omit the name of the connection or entity manager, the default
(i.e. default) is used.
When working with multiple connections to create your databases:
Listing 41-2
1
2
3
4
5
# Play only with "default" connection
$ php app/console doctrine:database:create
# Play only with "customer" connection
$ php app/console doctrine:database:create --connection=customer
When working with multiple entity managers to update your schema:
Listing 41-3
1
2
3
4
5
# Play only with "default" mappings
$ php app/console doctrine:schema:update --force
# Play only with "customer" mappings
$ php app/console doctrine:schema:update --force --em=customer
If you do omit the entity manager's name when asking for it, the default entity manager (i.e. default) is
returned:
Listing 41-4
1 class UserController extends Controller
2 {
3
public function indexAction()
4
{
5
// both return the "default" em
6
$em = $this->get('doctrine')->getManager();
7
$em = $this->get('doctrine')->getManager('default');
8
9
$customerEm = $this->get('doctrine')->getManager('customer');
10
}
11 }
You can now use Doctrine just as you did before - using the default entity manager to persist and fetch
entities that it manages and the customer entity manager to persist and fetch its entities.
The same applies to repository call:
PDF brought to you by
generated on October 26, 2012
Chapter 41: How to work with Multiple Entity Managers and Connections | 308
Listing 41-5
1 class UserController extends Controller
2 {
3
public function indexAction()
4
{
5
// Retrieves a repository managed by the "default" em
6
$products = $this->get('doctrine')
7
->getRepository('AcmeStoreBundle:Product')
8
->findAll();
9
10
// Explicit way to deal with the "default" em
11
$products = $this->get('doctrine')
12
->getRepository('AcmeStoreBundle:Product', 'default')
13
->findAll();
14
15
// Retrieves a repository managed by the "customer" em
16
$customers = $this->get('doctrine')
17
->getRepository('AcmeCustomerBundle:Customer', 'customer')
18
->findAll();
19
}
20 }
PDF brought to you by
generated on October 26, 2012
Chapter 41: How to work with Multiple Entity Managers and Connections | 309
Chapter 42
How to Register Custom DQL Functions
Doctrine allows you to specify custom DQL functions. For more information on this topic, read
Doctrine's cookbook article "DQL User Defined Functions1".
In Symfony, you can register your custom DQL functions as follows:
Listing 42-1
1 # app/config/config.yml
2 doctrine:
3
orm:
4
# ...
5
entity_managers:
6
default:
7
# ...
8
dql:
9
string_functions:
10
test_string: Acme\HelloBundle\DQL\StringFunction
11
second_string: Acme\HelloBundle\DQL\SecondStringFunction
12
numeric_functions:
13
test_numeric: Acme\HelloBundle\DQL\NumericFunction
14
datetime_functions:
15
test_datetime: Acme\HelloBundle\DQL\DatetimeFunction
1. http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/cookbook/dql-user-defined-functions.html
PDF brought to you by
generated on October 26, 2012
Chapter 42: How to Register Custom DQL Functions | 310
Chapter 43
How to implement a simple Registration Form
Some forms have extra fields whose values don't need to be stored in the database. For example, you
may want to create a registration form with some extra fields (like a "terms accepted" checkbox field) and
embed the form that actually stores the account information.
The simple User model
You have a simple User entity mapped to the database:
Listing 43-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Acme/AccountBundle/Entity/User.php
namespace Acme\AccountBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* @ORM\Entity
* @UniqueEntity(fields="email", message="Email already taken")
*/
class User
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank()
* @Assert\Email()
*/
protected $email;
PDF brought to you by
generated on October 26, 2012
Chapter 43: How to implement a simple Registration Form | 311
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 }
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank()
*/
protected $plainPassword;
public function getId()
{
return $this->id;
}
public function getEmail()
{
return $this->email;
}
public function setEmail($email)
{
$this->email = $email;
}
public function getPlainPassword()
{
return $this->plainPassword;
}
public function setPlainPassword($password)
{
$this->plainPassword = $password;
}
This User entity contains three fields and two of them (email and plainPassword) should display on the
form. The email property must be unique in the database, this is enforced by adding this validation at the
top of the class.
If you want to integrate this User within the security system, you need to implement the
UserInterface of the security component.
Create a Form for the Model
Next, create the form for the User model:
Listing 43-2
1
2
3
4
5
6
7
8
9
// src/Acme/AccountBundle/Form/Type/UserType.php
namespace Acme\AccountBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class UserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
PDF brought to you by
generated on October 26, 2012
Chapter 43: How to implement a simple Registration Form | 312
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 }
{
$builder->add('email', 'email');
$builder->add('plainPassword', 'repeated', array(
'first_name' => 'password',
'second_name' => 'confirm',
'type' => 'password',
));
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'Acme\AccountBundle\Entity\User');
}
public function getName()
{
return 'user';
}
There are just two fields: email and plainPassword (repeated to confirm the entered password). The
data_class option tells the form the name of data class (i.e. your User entity).
To explore more things about the form component, read Forms.
Embedding the User form into a Registration Form
The form that you'll use for the registration page is not the same as the form used to simply modify the
User (i.e. UserType). The registration form will contain further fields like "accept the terms", whose value
won't be stored in the database.
Start by creating a simple class which represents the "registration":
Listing 43-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Acme/AccountBundle/Form/Model/Registration.php
namespace Acme\AccountBundle\Form\Model;
use Symfony\Component\Validator\Constraints as Assert;
use Acme\AccountBundle\Entity\User;
class Registration
{
/**
* @Assert\Type(type="Acme\AccountBundle\Entity\User")
*/
protected $user;
/**
* @Assert\NotBlank()
* @Assert\True()
*/
protected $termsAccepted;
PDF brought to you by
generated on October 26, 2012
Chapter 43: How to implement a simple Registration Form | 313
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 }
public function setUser(User $user)
{
$this->user = $user;
}
public function getUser()
{
return $this->user;
}
public function getTermsAccepted()
{
return $this->termsAccepted;
}
public function setTermsAccepted($termsAccepted)
{
$this->termsAccepted = (Boolean) $termsAccepted;
}
Next, create the form for this Registration model:
Listing 43-4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Acme/AccountBundle/Form/Type/RegistrationType.php
namespace Acme\AccountBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class RegistrationType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('user', new UserType());
$builder->add('terms', 'checkbox', array('property_path' => 'termsAccepted'));
}
public function getName()
{
return 'registration';
}
}
You don't need to use special method for embedding the UserType form. A form is a field, too - so you
can add this like any other field, with the expectation that the Registration.user property will hold an
instance of the User class.
Handling the Form Submission
Next, you need a controller to handle the form. Start by creating a simple controller for displaying the
registration form:
Listing 43-5
1 // src/Acme/AccountBundle/Controller/AccountController.php
2 namespace Acme\AccountBundle\Controller;
3
PDF brought to you by
generated on October 26, 2012
Chapter 43: How to implement a simple Registration Form | 314
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Acme\AccountBundle\Form\Type\RegistrationType;
use Acme\AccountBundle\Form\Model\Registration;
class AccountController extends Controller
{
public function registerAction()
{
$form = $this->createForm(new RegistrationType(), new Registration());
return $this->render('AcmeAccountBundle:Account:register.html.twig', array('form'
=> $form->createView()));
}
}
and its template:
Listing 43-6
1 {# src/Acme/AccountBundle/Resources/views/Account/register.html.twig #}
2 <form action="{{ path('create')}}" method="post" {{ form_enctype(form) }}>
3
{{ form_widget(form) }}
4
5
<input type="submit" />
6 </form>
Finally, create the controller which handles the form submission. This performs the validation and saves
the data into the database:
Listing 43-7
1 public function createAction()
2 {
3
$em = $this->getDoctrine()->getEntityManager();
4
5
$form = $this->createForm(new RegistrationType(), new Registration());
6
7
$form->bindRequest($this->getRequest());
8
9
if ($form->isValid()) {
10
$registration = $form->getData();
11
12
$em->persist($registration->getUser());
13
$em->flush();
14
15
return $this->redirect(...);
16
}
17
18
return $this->render('AcmeAccountBundle:Account:register.html.twig', array('form' =>
19 $form->createView()));
}
That's it! Your form now validates, and allows you to save the User object to the database. The extra
terms checkbox on the Registration model class is used during validation, but not actually used
afterwards when saving the User to the database.
PDF brought to you by
generated on October 26, 2012
Chapter 43: How to implement a simple Registration Form | 315
Chapter 44
How to customize Form Rendering
Symfony gives you a wide variety of ways to customize how a form is rendered. In this guide, you'll learn
how to customize every possible part of your form with as little effort as possible whether you use Twig
or PHP as your templating engine.
Form Rendering Basics
Recall that the label, error and HTML widget of a form field can easily be rendered by using the form_row
Twig function or the row PHP helper method:
Listing 44-1
1 {{ form_row(form.age) }}
You can also render each of the three parts of the field individually:
Listing 44-2
1 <div>
2
{{ form_label(form.age) }}
3
{{ form_errors(form.age) }}
4
{{ form_widget(form.age) }}
5 </div>
In both cases, the form label, errors and HTML widget are rendered by using a set of markup that ships
standard with Symfony. For example, both of the above templates would render:
Listing 44-3
1 <div>
2
<label for="form_age">Age</label>
3
<ul>
4
<li>This field is required</li>
5
</ul>
6
<input type="number" id="form_age" name="form[age]" />
7 </div>
To quickly prototype and test a form, you can render the entire form with just one line:
Listing 44-4
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 316
1 {{ form_widget(form) }}
The remainder of this recipe will explain how every part of the form's markup can be modified at
several different levels. For more information about form rendering in general, see Rendering a Form in a
Template.
What are Form Themes?
Symfony uses form fragments - a small piece of a template that renders just one part of a form - to render
every part of a form - - field labels, errors, input text fields, select tags, etc
The fragments are defined as blocks in Twig and as template files in PHP.
A theme is nothing more than a set of fragments that you want to use when rendering a form. In other
words, if you want to customize one portion of how a form is rendered, you'll import a theme which
contains a customization of the appropriate form fragments.
Symfony comes with a default theme (form_div_layout.html.twig1 in Twig and FrameworkBundle:Form
in PHP) that defines each and every fragment needed to render every part of a form.
In the next section you will learn how to customize a theme by overriding some or all of its fragments.
For example, when the widget of a integer type field is rendered, an input number field is generated
Listing 44-5
1 {{ form_widget(form.age) }}
renders:
Listing 44-6
1 <input type="number" id="form_age" name="form[age]" required="required" value="33" />
Internally, Symfony uses the integer_widget fragment to render the field. This is because the field type
is integer and you're rendering its widget (as opposed to its label or errors).
In Twig that would default to the block integer_widget from the form_div_layout.html.twig2 template.
In PHP it would rather be the integer_widget.html.php file located in FrameworkBundle/Resources/
views/Form folder.
The default implementation of the integer_widget fragment looks like this:
Listing 44-7
1 {# form_div_layout.html.twig #}
2 {% block integer_widget %}
3
{% set type = type|default('number') %}
4
{{ block('field_widget') }}
5 {% endblock integer_widget %}
As you can see, this fragment itself renders another fragment - field_widget:
Listing 44-8
1 {# form_div_layout.html.twig #}
2 {% block field_widget %}
3
{% set type = type|default('text') %}
4
<input type="{{ type }}" {{ block('widget_attributes') }} value="{{ value }}" />
5 {% endblock field_widget %}
1. https://github.com/symfony/symfony/blob/2.0/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
2. https://github.com/symfony/symfony/blob/2.0/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 317
The point is, the fragments dictate the HTML output of each part of a form. To customize the form
output, you just need to identify and override the correct fragment. A set of these form fragment
customizations is known as a form "theme". When rendering a form, you can choose which form
theme(s) you want to apply.
In Twig a theme is a single template file and the fragments are the blocks defined in this file.
In PHP a theme is a folder and the fragments are individual template files in this folder.
Knowing which block to customize
In this example, the customized fragment name is integer_widget because you want to override
the HTML widget for all integer field types. If you need to customize textarea fields, you would
customize textarea_widget.
As you can see, the fragment name is a combination of the field type and which part of the field is
being rendered (e.g. widget, label, errors, row). As such, to customize how errors are rendered
for just input text fields, you should customize the text_errors fragment.
More commonly, however, you'll want to customize how errors are displayed across all fields.
You can do this by customizing the field_errors fragment. This takes advantage of field type
inheritance. Specifically, since the text type extends from the field type, the form component
will first look for the type-specific fragment (e.g. text_errors) before falling back to its parent
fragment name if it doesn't exist (e.g. field_errors).
For more information on this topic, see Form Fragment Naming.
Form Theming
To see the power of form theming, suppose you want to wrap every input number field with a div tag.
The key to doing this is to customize the integer_widget fragment.
Form Theming in Twig
When customizing the form field block in Twig, you have two options on where the customized form
block can live:
Method
Pros
Cons
Inside the same template as the
form
Quick and easy
Can't be reused in other templates
Inside a separate template
Can be reused by many
templates
Requires an extra template to be
created
Both methods have the same effect but are better in different situations.
Method 1: Inside the same Template as the Form
The easiest way to customize the integer_widget block is to customize it directly in the template that's
actually rendering the form.
Listing 44-9
1 {% extends '::base.html.twig' %}
2
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 318
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% form_theme form _self %}
{% block integer_widget %}
<div class="integer_widget">
{% set type = type|default('number') %}
{{ block('field_widget') }}
</div>
{% endblock %}
{% block content %}
{# ... render the form #}
{{ form_row(form.age) }}
{% endblock %}
By using the special {% form_theme form _self %} tag, Twig looks inside the same template for
any overridden form blocks. Assuming the form.age field is an integer type field, when its widget is
rendered, the customized integer_widget block will be used.
The disadvantage of this method is that the customized form block can't be reused when rendering other
forms in other templates. In other words, this method is most useful when making form customizations
that are specific to a single form in your application. If you want to reuse a form customization across
several (or all) forms in your application, read on to the next section.
Method 2: Inside a Separate Template
You can also choose to put the customized integer_widget form block in a separate template entirely.
The code and end-result are the same, but you can now re-use the form customization across many
templates:
Listing 44-10
1 {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
2 {% block integer_widget %}
3
<div class="integer_widget">
4
{% set type = type|default('number') %}
5
{{ block('field_widget') }}
6
</div>
7 {% endblock %}
Now that you've created the customized form block, you need to tell Symfony to use it. Inside the
template where you're actually rendering your form, tell Symfony to use the template via the form_theme
tag:
Listing 44-11
1 {% form_theme form 'AcmeDemoBundle:Form:fields.html.twig' %}
2
3 {{ form_widget(form.age) }}
When the form.age widget is rendered, Symfony will use the integer_widget block from the new
template and the input tag will be wrapped in the div element specified in the customized block.
Form Theming in PHP
When using PHP as a templating engine, the only method to customize a fragment is to create a new
template file - this is similar to the second method used by Twig.
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 319
The template file must be named after the fragment. You must create a integer_widget.html.php file
in order to customize the integer_widget fragment.
Listing 44-12
1 <!-- src/Acme/DemoBundle/Resources/views/Form/integer_widget.html.php -->
2 <div class="integer_widget">
3
<?php echo $view['form']->renderBlock('field_widget', array('type' => isset($type) ?
4 $type : "number")) ?>
</div>
Now that you've created the customized form template, you need to tell Symfony to use it. Inside the
template where you're actually rendering your form, tell Symfony to use the theme via the setTheme
helper method:
Listing 44-13
1 <?php $view['form']->setTheme($form, array('AcmeDemoBundle:Form')) ;?>
2
3 <?php $view['form']->widget($form['age']) ?>
When the form.age widget is rendered, Symfony will use the customized integer_widget.html.php
template and the input tag will be wrapped in the div element.
Referencing Base Form Blocks (Twig specific)
So far, to override a particular form block, the best method is to copy the default block from
form_div_layout.html.twig3, paste it into a different template, and then customize it. In many cases, you
can avoid doing this by referencing the base block when customizing it.
This is easy to do, but varies slightly depending on if your form block customizations are in the same
template as the form or a separate template.
Referencing Blocks from inside the same Template as the Form
Import the blocks by adding a use tag in the template where you're rendering the form:
Listing 44-14
1 {% use 'form_div_layout.html.twig' with integer_widget as base_integer_widget %}
Now, when the blocks from form_div_layout.html.twig4 are imported, the integer_widget block is
called base_integer_widget. This means that when you redefine the integer_widget block, you can
reference the default markup via base_integer_widget:
Listing 44-15
1 {% block integer_widget %}
2
<div class="integer_widget">
3
{{ block('base_integer_widget') }}
4
</div>
5 {% endblock %}
Referencing Base Blocks from an External Template
If your form customizations live inside an external template, you can reference the base block by using
the parent() Twig function:
Listing 44-16
3. https://github.com/symfony/symfony/blob/2.0/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
4. https://github.com/symfony/symfony/blob/2.0/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 320
1
2
3
4
5
6
7
8
{# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
{% extends 'form_div_layout.html.twig' %}
{% block integer_widget %}
<div class="integer_widget">
{{ parent() }}
</div>
{% endblock %}
It is not possible to reference the base block when using PHP as the templating engine. You have
to manually copy the content from the base block to your new template file.
Making Application-wide Customizations
If you'd like a certain form customization to be global to your application, you can accomplish this by
making the form customizations in an external template and then importing it inside your application
configuration:
Twig
By using the following configuration, any customized form blocks inside the
AcmeDemoBundle:Form:fields.html.twig template will be used globally when a form is rendered.
Listing 44-17
1 # app/config/config.yml
2 twig:
3
form:
4
resources:
5
- 'AcmeDemoBundle:Form:fields.html.twig'
6
# ...
By default, Twig uses a div layout when rendering forms. Some people, however, may prefer to render
forms in a table layout. Use the form_table_layout.html.twig resource to use such a layout:
Listing 44-18
1 # app/config/config.yml
2 twig:
3
form:
4
resources: ['form_table_layout.html.twig']
5
# ...
If you only want to make the change in one template, add the following line to your template file rather
than adding the template as a resource:
Listing 44-19
1 {% form_theme form 'form_table_layout.html.twig' %}
Note that the form variable in the above code is the form view variable that you passed to your template.
PHP
By using the following configuration, any customized form fragments inside the src/Acme/DemoBundle/
Resources/views/Form folder will be used globally when a form is rendered.
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 321
Listing 44-20
1 # app/config/config.yml
2 framework:
3
templating:
4
form:
5
resources:
6
- 'AcmeDemoBundle:Form'
7
# ...
By default, the PHP engine uses a div layout when rendering forms. Some people, however, may prefer to
render forms in a table layout. Use the FrameworkBundle:FormTable resource to use such a layout:
Listing 44-21
1 # app/config/config.yml
2 framework:
3
templating:
4
form:
5
resources:
6
- 'FrameworkBundle:FormTable'
If you only want to make the change in one template, add the following line to your template file rather
than adding the template as a resource:
Listing 44-22
1 <?php $view['form']->setTheme($form, array('FrameworkBundle:FormTable')); ?>
Note that the $form variable in the above code is the form view variable that you passed to your template.
How to customize an Individual field
So far, you've seen the different ways you can customize the widget output of all text field types. You
can also customize individual fields. For example, suppose you have two text fields - first_name and
last_name - but you only want to customize one of the fields. This can be accomplished by customizing
a fragment whose name is a combination of the field id attribute and which part of the field is being
customized. For example:
Listing 44-23
1
2
3
4
5
6
7
8
9
{% form_theme form _self %}
{% block _product_name_widget %}
<div class="text_widget">
{{ block('field_widget') }}
</div>
{% endblock %}
{{ form_widget(form.name) }}
Here, the _product_name_widget fragment defines the template to use for the field whose id is
product_name (and name is product[name]).
The product portion of the field is the form name, which may be set manually or generated
automatically based on your form type name (e.g. ProductType equates to product). If you're not
sure what your form name is, just view the source of your generated form.
You can also override the markup for an entire field row using the same method:
Listing 44-24
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 322
1
2
3
4
5
6
7
8
9
10
{# _product_name_row.html.twig #}
{% form_theme form _self %}
{% block _product_name_row %}
<div class="name_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endblock %}
Other Common Customizations
So far, this recipe has shown you several different ways to customize a single piece of how a form is
rendered. The key is to customize a specific fragment that corresponds to the portion of the form you
want to control (see naming form blocks).
In the next sections, you'll see how you can make several common form customizations. To apply these
customizations, use one of the methods described in the Form Theming section.
Customizing Error Output
The form component only handles how the validation errors are rendered, and not the actual
validation error messages. The error messages themselves are determined by the validation
constraints you apply to your objects. For more information, see the chapter on validation.
There are many different ways to customize how errors are rendered when a form is submitted with
errors. The error messages for a field are rendered when you use the form_errors helper:
Listing 44-25
1 {{ form_errors(form.age) }}
By default, the errors are rendered inside an unordered list:
Listing 44-26
1 <ul>
2
<li>This field is required</li>
3 </ul>
To override how errors are rendered for all fields, simply copy, paste and customize the field_errors
fragment.
Listing 44-27
1 {# fields_errors.html.twig #}
2 {% block field_errors %}
3
{% spaceless %}
4
{% if errors|length > 0 %}
5
<ul class="error_list">
6
{% for error in errors %}
7
<li>{{ error.messageTemplate|trans(error.messageParameters, 'validators')
8 }}</li>
9
{% endfor %}
10
</ul>
11
{% endif %}
12
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 323
{% endspaceless %}
{% endblock field_errors %}
See Form Theming for how to apply this customization.
You can also customize the error output for just one specific field type. For example, certain errors that
are more global to your form (i.e. not specific to just one field) are rendered separately, usually at the top
of your form:
Listing 44-28
1 {{ form_errors(form) }}
To customize only the markup used for these errors, follow the same directions as above, but now call
the block form_errors (Twig) / the file form_errors.html.php (PHP). Now, when errors for the form
type are rendered, your customized fragment will be used instead of the default field_errors.
Customizing the "Form Row"
When you can manage it, the easiest way to render a form field is via the form_row function, which
renders the label, errors and HTML widget of a field. To customize the markup used for rendering all
form field rows, override the field_row fragment. For example, suppose you want to add a class to the
div element around each row:
Listing 44-29
1 {# field_row.html.twig #}
2 {% block field_row %}
3
<div class="form_row">
4
{{ form_label(form) }}
5
{{ form_errors(form) }}
6
{{ form_widget(form) }}
7
</div>
8 {% endblock field_row %}
See Form Theming for how to apply this customization.
Adding a "Required" Asterisk to Field Labels
If you want to denote all of your required fields with a required asterisk (*), you can do this by
customizing the field_label fragment.
In Twig, if you're making the form customization inside the same template as your form, modify the use
tag and add the following:
Listing 44-30
1 {% use 'form_div_layout.html.twig' with field_label as base_field_label %}
2
3 {% block field_label %}
4
{{ block('base_field_label') }}
5
6
{% if required %}
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 324
7
<span class="required" title="This field is required">*</span>
8
{% endif %}
9 {% endblock %}
In Twig, if you're making the form customization inside a separate template, use the following:
Listing 44-31
1 {% extends 'form_div_layout.html.twig' %}
2
3 {% block field_label %}
4
{{ parent() }}
5
6
{% if required %}
7
<span class="required" title="This field is required">*</span>
8
{% endif %}
9 {% endblock %}
When using PHP as a templating engine you have to copy the content from the original template:
Listing 44-32
1
2
3
4
5
6
7
8
9
<!-- field_label.html.php -->
<!-- original content -->
<label for="<?php echo $view->escape($id) ?>" <?php foreach($attr as $k => $v) {
printf('%s="%s" ', $view->escape($k), $view->escape($v)); } ?>><?php echo
$view->escape($view['translator']->trans($label)) ?></label>
<!-- customization -->
<?php if ($required) : ?>
<span class="required" title="This field is required">*</span>
<?php endif ?>
See Form Theming for how to apply this customization.
Adding "help" messages
You can also customize your form widgets to have an optional "help" message.
In Twig, If you're making the form customization inside the same template as your form, modify the use
tag and add the following:
Listing 44-33
1 {% use 'form_div_layout.html.twig' with field_widget as base_field_widget %}
2
3 {% block field_widget %}
4
{{ block('base_field_widget') }}
5
6
{% if help is defined %}
7
<span class="help">{{ help }}</span>
8
{% endif %}
9 {% endblock %}
In twig, If you're making the form customization inside a separate template, use the following:
Listing 44-34
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 325
1 {% extends 'form_div_layout.html.twig' %}
2
3 {% block field_widget %}
4
{{ parent() }}
5
6
{% if help is defined %}
7
<span class="help">{{ help }}</span>
8
{% endif %}
9 {% endblock %}
When using PHP as a templating engine you have to copy the content from the original template:
Listing 44-35
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- field_widget.html.php -->
<!-- Original content -->
<input
type="<?php echo isset($type) ? $view->escape($type) : "text" ?>"
value="<?php echo $view->escape($value) ?>"
<?php echo $view['form']->renderBlock('attributes') ?>
/>
<!-- Customization -->
<?php if (isset($help)) : ?>
<span class="help"><?php echo $view->escape($help) ?></span>
<?php endif ?>
To render a help message below a field, pass in a help variable:
Listing 44-36
1 {{ form_widget(form.title, {'help': 'foobar'}) }}
See Form Theming for how to apply this customization.
Using Form Variables
Most of the functions available for rendering different parts of a form (e.g. the form widget, form label,
form widget, etc) also allow you to make certain customizations directly. Look at the following example:
Listing 44-37
1 {# render a widget, but add a "foo" class to it #}
2 {{ form_widget(form.name, { 'attr': {'class': 'foo'} }) }}
The array passed as the second argument contains form "variables". For more details about this concept
in Twig, see More about Form "Variables".
PDF brought to you by
generated on October 26, 2012
Chapter 44: How to customize Form Rendering | 326
Chapter 45
How to use Data Transformers
You'll often find the need to transform the data the user entered in a form into something else for use
in your program. You could easily do this manually in your controller, but what if you want to use this
specific form in different places?
Say you have a one-to-one relation of Task to Issue, e.g. a Task optionally has an issue linked to it. Adding
a listbox with all possible issues can eventually lead to a really long listbox in which it is impossible to find
something. You might want to add a textbox instead, where the user can simply enter the issue number.
You could try to do this in your controller, but it's not the best solution. It would be better if this issue
were automatically converted to an Issue object. This is where Data Transformers come into play.
Creating the Transformer
First, create an IssueToNumberTransformer class - this class will be responsible for converting to and
from the issue number and the Issue object:
Listing 45-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Acme/TaskBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace Acme\TaskBundle\Form\DataTransformer;
use
use
use
use
Symfony\Component\Form\DataTransformerInterface;
Symfony\Component\Form\Exception\TransformationFailedException;
Doctrine\Common\Persistence\ObjectManager;
Acme\TaskBundle\Entity\Issue;
class IssueToNumberTransformer implements DataTransformerInterface
{
/**
* @var ObjectManager
*/
private $om;
/**
* @param ObjectManager $om
*/
public function __construct(ObjectManager $om)
PDF brought to you by
generated on October 26, 2012
Chapter 45: How to use Data Transformers | 327
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66 }
{
$this->om = $om;
}
/**
* Transforms an object (issue) to a string (number).
*
* @param Issue|null $issue
* @return string
*/
public function transform($issue)
{
if (null === $issue) {
return "";
}
return $issue->getNumber();
}
/**
* Transforms a string (number) to an object (issue).
*
* @param string $number
* @return Issue|null
* @throws TransformationFailedException if object (issue) is not found.
*/
public function reverseTransform($number)
{
if (!$number) {
return null;
}
$issue = $this->om
->getRepository('AcmeTaskBundle:Issue')
->findOneBy(array('number' => $number))
;
if (null === $issue) {
throw new TransformationFailedException(sprintf(
'An issue with number "%s" does not exist!',
$number
));
}
return $issue;
}
If you want a new issue to be created when an unknown number is entered, you can instantiate it
rather than throwing the TransformationFailedException.
Using the Transformer
Now that you have the transformer built, you just need to add it to your issue field in some form.
PDF brought to you by
generated on October 26, 2012
Chapter 45: How to use Data Transformers | 328
You can also use transformers without creating a new custom form type by calling
prependNormTransformer (or appendClientTransformer - see Norm and Client
Transformers) on any field builder:
Listing 45-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;
class TaskType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
// ...
// this assumes that the entity manager was passed in as an option
$entityManager = $options['em'];
$transformer = new IssueToNumberTransformer($entityManager);
// add a normal text field, but add our transformer to it
$builder->add(
$builder->create('issue', 'text')
->prependNormTransformer($transformer)
);
}
// ...
}
This example requires that you pass in the entity manager as an option when creating your form. Later,
you'll learn how you could create a custom issue field type to avoid needing to do this in your controller:
Listing 45-3
1 $taskForm = $this->createForm(new TaskType(), $task, array(
2
'em' => $this->getDoctrine()->getEntityManager(),
3 ));
Cool, you're done! Your user will be able to enter an issue number into the text field and it will be
transformed back into an Issue object. This means that, after a successful bind, the Form framework will
pass a real Issue object to Task::setIssue() instead of the issue number.
If the issue isn't found, a form error will be created for that field and its error message can be controlled
with the invalid_message field option.
Notice that adding a transformer requires using a slightly more complicated syntax when adding
the field. The following is wrong, as the transformer would be applied to the entire form, instead
of just this field:
Listing 45-4
1 // THIS IS WRONG - TRANSFORMER WILL BE APPLIED TO THE ENTIRE FORM
2 // see above example for correct code
3 $builder->add('issue', 'text')
4
->prependNormTransformer($transformer);
Norm and Client Transformers
In the above example, the transformer was used as a "norm" transformer. In fact, there are two different
type of transformers and three different types of underlying data.
In any form, the 3 different types of data are:
PDF brought to you by
generated on October 26, 2012
Chapter 45: How to use Data Transformers | 329
1) App data - This is the data in the format used in your application (e.g. an Issue object). If you call
Form::getData or Form::setData, you're dealing with the "app" data.
2) Norm Data - This is a normalized version of your data, and is commonly the same as your "app" data
(though not in our example). It's not commonly used directly.
3) Client Data - This is the format that's used to fill in the form fields themselves. It's also the format in
which the user will submit the data. When you call Form::bind($data), the $data is in the "client" data
format.
The 2 different types of transformers help convert to and from each of these types of data:
Norm transformers:
• transform: "app data" => "norm data"
• reverseTransform: "norm data" => "app data"
Client transformers:
• transform: "norm data" => "client data"
• reverseTransform: "client data" => "norm data"
Which transformer you need depends on your situation.
To use the client transformer, call appendClientTransformer.
So why did we use the norm transformer?
In our example, the field is a text field, and we always expect a text field to be a simple, scalar format
in the "norm" and "client" formats. For this reason, the most appropriate transformer was the "norm"
transformer (which converts to/from the norm format - string issue number - to the app format - Issue
object).
The difference between the transformers is subtle and you should always think about what the "norm"
data for a field should really be. For example, the "norm" data for a text field is a string, but is a DateTime
object for a date field.
Using Transformers in a custom field type
In the above example, you applied the transformer to a normal text field. This was easy, but has two
downsides:
1) You need to always remember to apply the transformer whenever you're adding a field for issue
numbers
2) You need to worry about passing in the em option whenever you're creating a form that uses the
transformer.
Because of these, you may choose to create a create a custom field type. First, create the custom field type
class:
Listing 45-5
1
2
3
4
5
6
7
8
// src/Acme/TaskBundle/Form/Type/IssueSelectorType.php
namespace Acme\TaskBundle\Form\Type;
use
use
use
use
Symfony\Component\Form\AbstractType;
Symfony\Component\Form\FormBuilder;
Acme\TaskBundle\Form\DataTransformer\IssueToNumberTransformer;
Doctrine\Common\Persistence\ObjectManager;
PDF brought to you by
generated on October 26, 2012
Chapter 45: How to use Data Transformers | 330
9 class IssueSelectorType extends AbstractType
10 {
11
/**
12
* @var ObjectManager
13
*/
14
private $om;
15
16
/**
17
* @param ObjectManager $om
18
*/
19
public function __construct(ObjectManager $om)
20
{
21
$this->om = $om;
22
}
23
24
public function buildForm(FormBuilder $builder, array $options)
25
{
26
$transformer = new IssueToNumberTransformer($this->om);
27
$builder->prependNormTransformer($transformer);
28
}
29
30
public function getDefaultOptions(array $options)
31
{
32
return array(
33
'invalid_message' => 'The selected issue does not exist',
34
);
35
}
36
37
public function getParent(array $options)
38
{
39
return 'text';
40
}
41
42
public function getName()
43
{
44
return 'issue_selector';
45
}
46 }
Next, register your type as a service and tag it with form.type so that it's recognized as a custom field
type:
Listing 45-6
1 services:
2
acme_demo.type.issue_selector:
3
class: Acme\TaskBundle\Form\Type\IssueSelectorType
4
arguments: ["@doctrine.orm.entity_manager"]
5
tags:
6
- { name: form.type, alias: issue_selector }
Now, whenever you need to use your special issue_selector field type, it's quite easy:
Listing 45-7
1
2
3
4
5
6
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
PDF brought to you by
generated on October 26, 2012
Chapter 45: How to use Data Transformers | 331
7 class TaskType extends AbstractType
8 {
9
public function buildForm(FormBuilder $builder, array $options)
10
{
11
$builder
12
->add('task')
13
->add('dueDate', null, array('widget' => 'single_text'));
14
->add('issue', 'issue_selector');
15
}
16
17
public function getName()
18
{
19
return 'task';
20
}
21 }
PDF brought to you by
generated on October 26, 2012
Chapter 45: How to use Data Transformers | 332
Chapter 46
How to Dynamically Generate Forms Using
Form Events
Before jumping right into dynamic form generation, let's have a quick review of what a bare form class
looks like:
Listing 46-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Acme/DemoBundle/Form/Type/ProductType.php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class ProductType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
$builder->add('price');
}
public function getName()
{
return 'product';
}
}
If this particular section of code isn't already familiar to you, you probably need to take a step back
and first review the Forms chapter before proceeding.
Let's assume for a moment that this form utilizes an imaginary "Product" class that has only two relevant
properties ("name" and "price"). The form generated from this class will look the exact same regardless
of a new Product is being created or if an existing product is being edited (e.g. a product fetched from the
database).
PDF brought to you by
generated on October 26, 2012
Chapter 46: How to Dynamically Generate Forms Using Form Events | 333
Suppose now, that you don't want the user to be able to change the name value once the object has been
created. To do this, you can rely on Symfony's Event Dispatcher system to analyze the data on the object
and modify the form based on the Product object's data. In this entry, you'll learn how to add this level
of flexibility to your forms.
Adding An Event Subscriber To A Form Class
So, instead of directly adding that "name" widget via our ProductType form class, let's delegate the
responsibility of creating that particular field to an Event Subscriber:
Listing 46-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Acme/DemoBundle/Form/Type/ProductType.php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Acme\DemoBundle\Form\EventListener\AddNameFieldSubscriber;
class ProductType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$subscriber = new AddNameFieldSubscriber($builder->getFormFactory());
$builder->addEventSubscriber($subscriber);
$builder->add('price');
}
public function getName()
{
return 'product';
}
}
The event subscriber is passed the FormFactory object in its constructor so that our new subscriber is
capable of creating the form widget once it is notified of the dispatched event during form creation.
Inside the Event Subscriber Class
The goal is to create a "name" field only if the underlying Product object is new (e.g. hasn't been persisted
to the database). Based on that, the subscriber might look like the following:
Listing 46-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/DemoBundle/Form/EventListener/AddNameFieldSubscriber.php
namespace Acme\DemoBundle\Form\EventListener;
use
use
use
use
Symfony\Component\Form\Event\DataEvent;
Symfony\Component\Form\FormFactoryInterface;
Symfony\Component\EventDispatcher\EventSubscriberInterface;
Symfony\Component\Form\FormEvents;
class AddNameFieldSubscriber implements EventSubscriberInterface
{
private $factory;
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
PDF brought to you by
generated on October 26, 2012
Chapter 46: How to Dynamically Generate Forms Using Form Events | 334
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 }
}
public static function getSubscribedEvents()
{
// Tells the dispatcher that we want to listen on the form.pre_set_data
// event and that the preSetData method should be called.
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public function preSetData(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
//
//
//
//
//
if
During form creation setData() is called with null as an argument
by the FormBuilder constructor. We're only concerned with when
setData is called with an actual Entity object in it (whether new,
or fetched with Doctrine). This if statement let's us skip right
over the null condition.
(null === $data) {
return;
}
// check if the product object is "new"
if (!$data->getId()) {
$form->add($this->factory->createNamed('text', 'name'));
}
}
It is easy to misunderstand the purpose of the if (null === $data) segment of this event
subscriber. To fully understand its role, you might consider also taking a look at the Form class1
and paying special attention to where setData() is called at the end of the constructor, as well as
the setData() method itself.
The FormEvents::PRE_SET_DATA line actually resolves to the string form.pre_set_data. The
FormEvents class2 serves an organizational purpose. It is a centralized location in which you can find all
of the various form events available.
While this example could have used the form.set_data event or even the form.post_set_data events
just as effectively, by using form.pre_set_data we guarantee that the data being retrieved from the
Event object has in no way been modified by any other subscribers or listeners. This is because
form.pre_set_data passes a DataEvent3 object instead of the FilterDataEvent4 object passed by the
form.set_data event. DataEvent5, unlike its child FilterDataEvent6, lacks a setData() method.
You may view the full list of form events via the FormEvents class7, found in the form bundle.
1. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Form.php
2. https://github.com/symfony/Form/blob/master/FormEvents.php
3. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Event/DataEvent.php
4. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Event/FilterDataEvent.php
5. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Event/DataEvent.php
6. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Event/FilterDataEvent.php
7. https://github.com/symfony/Form/blob/master/FormEvents.php
PDF brought to you by
generated on October 26, 2012
Chapter 46: How to Dynamically Generate Forms Using Form Events | 335
Chapter 47
How to Embed a Collection of Forms
In this entry, you'll learn how to create a form that embeds a collection of many other forms. This could
be useful, for example, if you had a Task class and you wanted to edit/create/remove many Tag objects
related to that Task, right inside the same form.
In this entry, we'll loosely assume that you're using Doctrine as your database store. But if you're
not using Doctrine (e.g. Propel or just a database connection), it's all very similar. There are only a
few parts of this tutorial that really care about "persistence".
If you are using Doctrine, you'll need to add the Doctrine metadata, including the ManyToMany
association mapping definition on the Task's tags property.
Let's start there: suppose that each Task belongs to multiple Tags objects. Start by creating a simple Task
class:
Listing 47-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
class Task
{
protected $description;
protected $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
public function getDescription()
{
return $this->description;
}
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 336
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 }
public function setDescription($description)
{
$this->description = $description;
}
public function getTags()
{
return $this->tags;
}
public function setTags(ArrayCollection $tags)
{
$this->tags = $tags;
}
The ArrayCollection is specific to Doctrine and is basically the same as using an array (but it
must be an ArrayCollection) if you're using Doctrine.
Now, create a Tag class. As you saw above, a Task can have many Tag objects:
Listing 47-2
1
2
3
4
5
6
7
// src/Acme/TaskBundle/Entity/Tag.php
namespace Acme\TaskBundle\Entity;
class Tag
{
public $name;
}
The name property is public here, but it can just as easily be protected or private (but then it would
need getName and setName methods).
Now let's get to the forms. Create a form class so that a Tag object can be modified by the user:
Listing 47-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Acme/TaskBundle/Form/Type/TagType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class TagType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\TaskBundle\Entity\Tag',
);
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 337
19
20
21
22
23
24
25 }
}
public function getName()
{
return 'tag';
}
With this, we have enough to render a tag form by itself. But since the end goal is to allow the tags of a
Task to be modified right inside the task form itself, create a form for the Task class.
Notice that we embed a collection of TagType forms using the collection field type:
Listing 47-4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class TaskType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('description');
$builder->add('tags', 'collection', array('type' => new TagType()));
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\TaskBundle\Entity\Task',
);
}
public function getName()
{
return 'task';
}
}
In your controller, you'll now initialize a new instance of TaskType:
Listing 47-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/TaskBundle/Controller/TaskController.php
namespace Acme\TaskBundle\Controller;
use
use
use
use
use
Acme\TaskBundle\Entity\Task;
Acme\TaskBundle\Entity\Tag;
Acme\TaskBundle\Form\Type\TaskType;
Symfony\Component\HttpFoundation\Request;
Symfony\Bundle\FrameworkBundle\Controller\Controller;
class TaskController extends Controller
{
public function newAction(Request $request)
{
$task = new Task();
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 338
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 }
// dummy code - this is here just so that the Task has some tags
// otherwise, this isn't an interesting example
$tag1 = new Tag();
$tag1->name = 'tag1';
$task->getTags()->add($tag1);
$tag2 = new Tag();
$tag2->name = 'tag2';
$task->getTags()->add($tag2);
// end dummy code
$form = $this->createForm(new TaskType(), $task);
// process the form on POST
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
// maybe do some form processing, like saving the Task and Tag objects
}
}
return $this->render('AcmeTaskBundle:Task:new.html.twig', array(
'form' => $form->createView(),
));
}
The corresponding template is now able to render both the description field for the task form as well
as all the TagType forms for any tags that are already related to this Task. In the above controller, I added
some dummy code so that you can see this in action (since a Task has zero tags when first created).
Listing 47-6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{# src/Acme/TaskBundle/Resources/views/Task/new.html.twig #}
{# ... #}
<form action="..." method="POST" {{ form_enctype(form) }}>
{# render the task's only field: description #}
{{ form_row(form.description) }}
<h3>Tags</h3>
<ul class="tags">
{# iterate over each existing tag and render its only field: name #}
{% for tag in form.tags %}
<li>{{ form_row(tag.name) }}</li>
{% endfor %}
</ul>
{{ form_rest(form) }}
{# ... #}
</form>
When the user submits the form, the submitted data for the Tags fields are used to construct an
ArrayCollection of Tag objects, which is then set on the tag field of the Task instance.
The Tags collection is accessible naturally via $task->getTags() and can be persisted to the database or
used however you need.
So far, this works great, but this doesn't allow you to dynamically add new tags or delete existing tags.
So, while editing existing tags will work great, your user can't actually add any new tags yet.
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 339
In this entry, we embed only one collection, but you are not limited to this. You can also embed
nested collection as many level down as you like. But if you use Xdebug in your development setup,
you may receive a Maximum function nesting level of '100' reached, aborting! error.
This is due to the xdebug.max_nesting_level PHP setting, which defaults to 100.
This directive limits recursion to 100 calls which may not be enough for rendering the form in
the template if you render the whole form at once (e.g form_widget(form)). To fix this you can
set this directive to a higher value (either via a PHP ini file or via ini_set1, for example in app/
autoload.php) or render each form field by hand using form_row.
Allowing "new" tags with the "prototype"
Allowing the user to dynamically add new tags means that we'll need to use some JavaScript. Previously
we added two tags to our form in the controller. Now we need to let the user add as many tag forms as
he needs directly in the browser. This will be done through a bit of JavaScript.
The first thing we need to do is to let the form collection know that it will receive an unknown number
of tags. So far we've added two tags and the form type expects to receive exactly two, otherwise an error
will be thrown: This form should not contain extra fields. To make this flexible, we add the
allow_add option to our collection field:
Listing 47-7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/TaskBundle/Form/Type/TaskType.php
// ...
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('description');
$builder->add('tags', 'collection', array(
'type' => new TagType(),
'allow_add' => true,
'by_reference' => false,
));
}
Note that we also added 'by_reference' => false. Normally, the form framework would modify the
tags on a Task object without actually ever calling setTags. By setting by_reference to false, setTags will be
called. This will be important later as you'll see.
In addition to telling the field to accept any number of submitted objects, the allow_add also makes a
"prototype" variable available to you. This "prototype" is a little "template" that contains all the HTML
to be able to render any new "tag" forms. To render it, make the following change to your template:
Listing 47-8
1 <ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e }}">
2
...
3 </ul>
If you render your whole "tags" sub-form at once (e.g. form_row(form.tags)), then the prototype
is automatically available on the outer div as the data-prototype attribute, similar to what you
see above.
1. http://php.net/manual/en/function.ini-set.php
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 340
The form.tags.vars.prototype is form element that looks and feels just like the individual
form_widget(tag) elements inside our for loop. This means that you can call form_widget,
form_row, or form_label on it. You could even choose to render only one of its fields (e.g. the
name field):
Listing 47-9
1 {{ form_widget(form.tags.vars.prototype.name)|e }}
On the rendered page, the result will look something like this:
Listing 47-10
1 <ul class="tags" data-prototype="&lt;div&gt;&lt;label class=&quot;
required&quot;&gt;$$name$$&lt;/label&gt;&lt;div
id=&quot;task_tags_$$name$$&quot;&gt;&lt;div&gt;&lt;label
for=&quot;task_tags_$$name$$_name&quot; class=&quot; required&quot;&gt;Name&lt;/
label&gt;&lt;input type=&quot;text&quot; id=&quot;task_tags_$$name$$_name&quot;
name=&quot;task[tags][$$name$$][name]&quot; required=&quot;required&quot;
maxlength=&quot;255&quot; /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;">
The goal of this section will be to use JavaScript to read this attribute and dynamically add new tag forms
when the user clicks a "Add a tag" link. To make things simple, we'll use jQuery and assume you have it
included somewhere on your page.
Add a script tag somewhere on your page so we can start writing some JavaScript.
First, add a link to the bottom of the "tags" list via JavaScript. Second, bind to the "click" event of that
link so we can add a new tag form (addTagForm will be show next):
Listing 47-11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Get the div that holds the collection of tags
var collectionHolder = $('ul.tags');
// setup an "add a tag" link
var $addTagLink = $('<a href="#" class="add_tag_link">Add a tag</a>');
var $newLinkLi = $('<li></li>').append($addTagLink);
jQuery(document).ready(function() {
// add the "add a tag" anchor and li to the tags ul
collectionHolder.append($newLinkLi);
$addTagLink.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
addTagForm(collectionHolder, $newLinkLi);
});
});
The addTagForm function's job will be to use the data-prototype attribute to dynamically add a new
form when this link is clicked. The data-prototype HTML contains the tag text input element with
a name of task[tags][$$name$$][name] and id of task_tags_$$name$$_name. The $$name is a little
"placeholder", which we'll replace with a unique, incrementing number (e.g. task[tags][3][name]).
The actual code needed to make this all work can vary quite a bit, but here's one example:
Listing 47-12
1 function addTagForm(collectionHolder, $newLinkLi) {
2
// Get the data-prototype we explained earlier
3
var prototype = collectionHolder.attr('data-prototype');
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 341
4
5
6
7
8
9
10
11
12 }
// Replace '$$name$$' in the prototype's HTML to
// instead be a number based on the current collection's length.
var newForm = prototype.replace(/\$\$name\$\$/g, collectionHolder.children().length);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
Now, each time a user clicks the Add a tag link, a new sub form will appear on the page. When we
submit, any new tag forms will be converted into new Tag objects and added to the tags property of the
Task object.
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 342
Doctrine: Cascading Relations and saving the "Inverse" side
To get the new tags to save in Doctrine, you need to consider a couple more things. First, unless
you iterate over all of the new Tag objects and call $em->persist($tag) on each, you'll receive an
error from Doctrine:
A new entity was found through the relationship 'AcmeTaskBundleEntityTask#tags'
that was not configured to cascade persist operations for entity...
To fix this, you may choose to "cascade" the persist operation automatically from the Task object
to any related tags. To do this, add the cascade option to your ManyToMany metadata:
Listing 47-13
1
2
3
4
5
6
7
8
// src/Acme/TaskBundle/Entity/Task.php
// ...
/**
* @ORM\ManyToMany(targetEntity="Tag", cascade={"persist"})
*/
protected $tags;
A second potential issue deals with the Owning Side and Inverse Side2 of Doctrine relationships. In
this example, if the "owning" side of the relationship is "Task", then persistence will work fine as
the tags are properly added to the Task. However, if the owning side is on "Tag", then you'll need
to do a little bit more work to ensure that the correct side of the relationship is modified.
The trick is to make sure that the single "Task" is set on each "Tag". One easy way to do this is to
add some extra logic to setTags(), which is called by the form framework since by_reference is set
to false:
Listing 47-14
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/TaskBundle/Entity/Task.php
// ...
public function setTags(ArrayCollection $tags)
{
foreach ($tags as $tag) {
$tag->addTask($this);
}
$this->tags = $tags;
}
Inside Tag, just make sure you have an addTask method:
Listing 47-15
1
2
3
4
5
6
7
8
9
10
// src/Acme/TaskBundle/Entity/Tag.php
// ...
public function addTask(Task $task)
{
if (!$this->tasks->contains($task)) {
$this->tasks->add($task);
}
}
If you have a OneToMany relationship, then the workaround is similar, except that you can simply
call setTask from inside setTags.
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 343
Allowing tags to be removed
The next step is to allow the deletion of a particular item in the collection. The solution is similar to
allowing tags to be added.
Start by adding the allow_delete option in the form Type:
Listing 47-16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/TaskBundle/Form/Type/TaskType.php
// ...
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('description');
$builder->add('tags', 'collection', array(
'type' => new TagType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
));
}
Templates Modifications
The allow_delete option has one consequence: if an item of a collection isn't sent on submission, the
related data is removed from the collection on the server. The solution is thus to remove the form element
from the DOM.
First, add a "delete this tag" link to each tag form:
Listing 47-17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jQuery(document).ready(function() {
// add a delete link to all of the existing tag form li elements
collectionHolder.find('li').each(function() {
addTagFormDeleteLink($(this));
});
// ... the rest of the block from above
});
function addTagForm() {
// ...
// add a delete link to the new form
addTagFormDeleteLink($newFormLi);
}
The addTagFormDeleteLink function will look something like this:
Listing 47-18
1 function addTagFormDeleteLink($tagFormLi) {
2
var $removeFormA = $('<a href="#">delete this tag</a>');
3
$tagFormLi.append($removeFormA);
4
5
$removeFormA.on('click', function(e) {
6
// prevent the link from creating a "#" on the URL
2. http://docs.doctrine-project.org/en/latest/reference/unitofwork-associations.html
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 344
7
8
9
10
11
12 }
e.preventDefault();
// remove the li for the tag form
$tagFormLi.remove();
});
When a tag form is removed from the DOM and submitted, the removed Tag object will not be included
in the collection passed to setTags. Depending on your persistence layer, this may or may not be enough
to actually remove the relationship between the removed Tag and Task object.
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 345
Doctrine: Ensuring the database persistence
When removing objects in this way, you may need to do a little bit more work to ensure that the
relationship between the Task and the removed Tag is properly removed.
In Doctrine, you have two side of the relationship: the owning side and the inverse side. Normally
in this case you'll have a ManyToMany relation and the deleted tags will disappear and persist
correctly (adding new tags also works effortlessly).
But if you have an OneToMany relation or a ManyToMany with a mappedBy on the Task entity
(meaning Task is the "inverse" side), you'll need to do more work for the removed tags to persist
correctly.
In this case, you can modify the controller to remove the relationship on the removed tag. This
assumes that you have some editAction which is handling the "update" of your Task:
Listing 47-19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// src/Acme/TaskBundle/Controller/TaskController.php
// ...
public function editAction($id, Request $request)
{
$em = $this->getDoctrine()->getEntityManager();
$task = $em->getRepository('AcmeTaskBundle:Task')->find($id);
if (!$task) {
throw $this->createNotFoundException('No task found for is '.$id);
}
$originalTags = array();
// Create an array of the current Tag objects in the database
foreach ($task->getTags() as $tag) $originalTags[] = $tag;
$editForm = $this->createForm(new TaskType(), $task);
if ('POST' === $request->getMethod()) {
$editForm->bindRequest($this->getRequest());
if ($editForm->isValid()) {
// filter $originalTags to contain tags no longer present
foreach ($task->getTags() as $tag) {
foreach ($originalTags as $key => $toDel) {
if ($toDel->getId() === $tag->getId()) {
unset($originalTags[$key]);
}
}
}
// remove the relationship between the tag and the Task
foreach ($originalTags as $tag) {
// remove the Task from the Tag
$tag->getTasks()->removeElement($task);
// if it were a ManyToOne relationship, remove the relationship like
this
PDF brought to you by
generated on October 26, 2012
// $tag->setTask(null);
$em->persist($tag);
Chapter 47: How to Embed a Collection of Forms | 346
46
// if you wanted to delete the Tag entirely, you can also do that
47
// $em->remove($tag);
48
}
49
50
$em->persist($task);
51
$em->flush();
52
53
// redirect back to some edit page
54
return $this->redirect($this->generateUrl('task_edit', array('id' =>
55 $id)));
56
}
57
}
58
// render some form template
}
As you can see, adding and removing the elements correctly can be tricky. Unless you have a
ManyToMany relationship where Task is the "owning" side, you'll need to do extra work to make
sure that the relationship is properly updated (whether you're adding new tags or removing existing
tags) on each Tag object itself.
PDF brought to you by
generated on October 26, 2012
Chapter 47: How to Embed a Collection of Forms | 347
Chapter 48
How to Create a Custom Form Field Type
Symfony comes with a bunch of core field types available for building forms. However there are situations
where we want to create a custom form field type for a specific purpose. This recipe assumes we need a
field definition that holds a person's gender, based on the existing choice field. This section explains how
the field is defined, how we can customize its layout and finally, how we can register it for use in our
application.
Defining the Field Type
In order to create the custom field type, first we have to create the class representing the field. In our
situation the class holding the field type will be called GenderType and the file will be stored in the default
location for form fields, which is <BundleName>\Form\Type. Make sure the field extends AbstractType1:
Listing 48-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Acme/DemoBundle/Form/Type/GenderType.php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class GenderType extends AbstractType
{
public function getDefaultOptions(array $options)
{
return array(
'choices' => array(
'm' => 'Male',
'f' => 'Female',
)
);
}
public function getParent(array $options)
{
1. http://api.symfony.com/2.0/Symfony/Component/Form/AbstractType.html
PDF brought to you by
generated on October 26, 2012
Chapter 48: How to Create a Custom Form Field Type | 348
21
22
23
24
25
26
27
28 }
return 'choice';
}
public function getName()
{
return 'gender';
}
The location of this file is not important - the Form\Type directory is just a convention.
Here, the return value of the getParent function indicates that we're extending the choice field type.
This means that, by default, we inherit all of the logic and rendering of that field type. To see some of the
logic, check out the ChoiceType2 class. There are three methods that are particularly important:
• buildForm() - Each field type has a buildForm method, which is where you configure and
build any field(s). Notice that this is the same method you use to setup your forms, and it
works the same here.
• buildView() - This method is used to set any extra variables you'll need when rendering your
field in a template. For example, in ChoiceType3, a multiple variable is set and used in the
template to set (or not set) the multiple attribute on the select field. See Creating a Template
for the Field for more details.
• getDefaultOptions() - This defines options for your form type that can be used in
buildForm() and buildView(). There are a lot of options common to all fields (see
FieldType4), but you can create any others that you need here.
If you're creating a field that consists of many fields, then be sure to set your "parent" type as form
or something that extends form. Also, if you need to modify the "view" of any of your child types
from your parent type, use the buildViewBottomUp() method.
The getName() method returns an identifier which should be unique in your application. This is used in
various places, such as when customizing how your form type will be rendered.
The goal of our field was to extend the choice type to enable selection of a gender. This is achieved by
fixing the choices to a list of possible genders.
Creating a Template for the Field
Each field type is rendered by a template fragment, which is determined in part by the value of your
getName() method. For more information, see What are Form Themes?.
In this case, since our parent field is choice, we don't need to do any work as our custom field type
will automatically be rendered like a choice type. But for the sake of this example, let's suppose that
when our field is "expanded" (i.e. radio buttons or checkboxes, instead of a select field), we want to
always render it in a ul element. In your form theme template (see above link for details), create a
gender_widget block to handle this:
2. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
3. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
4. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/FieldType.php
PDF brought to you by
generated on October 26, 2012
Chapter 48: How to Create a Custom Form Field Type | 349
Listing 48-2
1 {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
2 {% block gender_widget %}
3
{% spaceless %}
4
{% if expanded %}
5
<ul {{ block('widget_container_attributes') }}>
6
{% for child in form %}
7
<li>
8
{{ form_widget(child) }}
9
{{ form_label(child) }}
10
</li>
11
{% endfor %}
12
</ul>
13
{% else %}
14
{# just let the choice widget render the select tag #}
15
{{ block('choice_widget') }}
16
{% endif %}
17
{% endspaceless %}
18 {% endblock %}
Make sure the correct widget prefix is used. In this example the name should be gender_widget,
according to the value returned by getName. Further, the main config file should point to the
custom form template so that it's used when rendering all forms.
Listing 48-3
1 # app/config/config.yml
2 twig:
3
form:
4
resources:
5
- 'AcmeDemoBundle:Form:fields.html.twig'
Using the Field Type
You can now use your custom field type immediately, simply by creating a new instance of the type in
one of your forms:
Listing 48-4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/DemoBundle/Form/Type/AuthorType.php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class AuthorType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('gender_code', new GenderType(), array(
'empty_value' => 'Choose a gender',
));
}
}
But this only works because the GenderType() is very simple. What if the gender codes were stored
in configuration or in a database? The next section explains how more complex field types solve this
problem.
PDF brought to you by
generated on October 26, 2012
Chapter 48: How to Create a Custom Form Field Type | 350
Creating your Field Type as a Service
So far, this entry has assumed that you have a very simple custom field type. But if you need access to
configuration, a database connection, or some other service, then you'll want to register your custom type
as a service. For example, suppose that we're storing the gender parameters in configuration:
Listing 48-5
1 # app/config/config.yml
2 parameters:
3
genders:
4
m: Male
5
f: Female
To use the parameter, we'll define our custom field type as a service, injecting the genders parameter
value as the first argument to its to-be-created __construct function:
Listing 48-6
1 # src/Acme/DemoBundle/Resources/config/services.yml
2 services:
3
form.type.gender:
4
class: Acme\DemoBundle\Form\Type\GenderType
5
arguments:
6
- "%genders%"
7
tags:
8
- { name: form.type, alias: gender }
Make sure the services file is being imported. See Importing Configuration with imports for details.
Be sure that the alias attribute of the tag corresponds with the value returned by the getName method
defined earlier. We'll see the importance of this in a moment when we use the custom field type. But first,
add a __construct argument to GenderType, which receives the gender configuration:
Listing 48-7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Acme/DemoBundle/Form/Type/GenderType.php
namespace Acme\DemoBundle\Form\Type;
// ...
class GenderType extends AbstractType
{
private $genderChoices;
public function __construct(array $genderChoices)
{
$this->genderChoices = $genderChoices;
}
public function getDefaultOptions(array $options)
{
return array(
'choices' => $this->genderChoices,
);
}
// ...
}
PDF brought to you by
generated on October 26, 2012
Chapter 48: How to Create a Custom Form Field Type | 351
Great! The GenderType is now fueled by the configuration parameters and registered as a service. And
because we used the form.type alias in its configuration, using the field is now much easier:
Listing 48-8
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Acme/DemoBundle/Form/Type/AuthorType.php
namespace Acme\DemoBundle\Form\Type;
// ...
class AuthorType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('gender_code', 'gender', array(
'empty_value' => 'Choose a gender',
));
}
}
Notice that instead of instantiating a new instance, we can just refer to it by the alias used in our service
configuration, gender. Have fun!
PDF brought to you by
generated on October 26, 2012
Chapter 48: How to Create a Custom Form Field Type | 352
Chapter 49
How to Create a Form Type Extension
Custom form field types are great when you need field types with a specific purpose, such as a gender
selector, or a VAT number input.
But sometimes, you don't really need to add new field types - you want to add features on top of existing
types. This is where form type extensions come in.
Form type extensions have 2 main use-cases:
1. You want to add a generic feature to several types (such as adding a "help" text to every field
type);
2. You want to add a specific feature to a single type (such as adding a "download" feature to
the "file" field type).
In both those cases, it might be possible to achieve your goal with custom form rendering, or custom
form field types. But using form type extensions can be cleaner (by limiting the amount of business logic
in templates) and more flexible (you can add several type extensions to a single form type).
Form type extensions can achieve most of what custom field types can do, but instead of being field types
of their own, they plug into existing types.
Imagine that you manage a Media entity, and that each media is associated to a file. Your Media form
uses a file type, but when editing the entity, you would like to see its image automatically rendered next
to the file input.
You could of course do this by customizing how this field is rendered in a template. But field type
extensions allow you to do this in a nice DRY fashion.
Defining the Form Type Extension
Your first task will be to create the form type extension class. Let's call it ImageTypeExtension. By
standard, form extensions usually live in the Form\Extension directory of one of your bundles.
When creating a form type extension, you can either implement the FormTypeExtensionInterface1
interface or extend the AbstractTypeExtension2 class. In most cases, it's easier to extend the abstract
class:
1. http://api.symfony.com/2.0/Symfony/Component/Form/FormTypeExtensionInterface.html
2. http://api.symfony.com/2.0/Symfony/Component/Form/AbstractTypeExtension.html
PDF brought to you by
generated on October 26, 2012
Chapter 49: How to Create a Form Type Extension | 353
Listing 49-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php
namespace Acme\DemoBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
class ImageTypeExtension extends AbstractTypeExtension
{
/**
* Returns the name of the type being extended.
*
* @return string The name of the type being extended
*/
public function getExtendedType()
{
return 'file';
}
}
The only method you must implement is the getExtendedType function. It is used to indicate the name
of the form type that will be extended by your extension.
The value you return in the getExtendedType method corresponds to the value returned by the
getName method in the form type class you wish to extend.
In addition to the getExtendedType function, you will probably want to override one of the following
methods:
•
•
•
•
•
buildForm()
buildView()
getDefaultOptions()
getAllowedOptionValues()
buildViewBottomUp()
For more information on what those methods do, you can refer to the Creating Custom Field Types
cookbook article.
Registering your Form Type Extension as a Service
The next step is to make Symfony aware of your extension. All you need to do is to declare it as a service
by using the form.type_extension tag:
Listing 49-2
1 services:
2
acme_demo_bundle.image_type_extension:
3
class: Acme\DemoBundle\Form\Type\ImageTypeExtension
4
tags:
5
- { name: form.type_extension, alias: file }
The alias key of the tag is the type of field that this extension should be applied to. In your case, as you
want to extend the file field type, you will use file as an alias.
PDF brought to you by
generated on October 26, 2012
Chapter 49: How to Create a Form Type Extension | 354
Adding the extension Business Logic
The goal of your extension is to display nice images next to file inputs (when the underlying model
contains images). For that purpose, let's assume that you use an approach similar to the one described in
How to handle File Uploads with Doctrine: you have a Media model with a file property (corresponding
to the file field in the form) and a path property (corresponding to the image path in the database):
Listing 49-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/Acme/DemoBundle/Entity/Media.php
namespace Acme\DemoBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Media
{
// ...
/**
* @var string The path - typically stored in the database
*/
private $path;
/**
* @var \Symfony\Component\HttpFoundation\File\UploadedFile
* @Assert\File(maxSize="2M")
*/
public $file;
// ...
/**
* Get the image url
*
* @return null|string
*/
public function getWebPath()
{
// ... $webPath being the full image url, to be used in templates
return $webPath;
}
Your form type extension class will need to do two things in order to extend the file form type:
1. Override the getDefaultOptions method in order to add an image_path option;
2. Override the buildForm and buildView methods in order to pass the image url to the view.
The logic is the following: when adding a form field of type file, you will be able to specify a new option:
image_path. This option will tell the file field how to get the actual image url in order to display it in the
view:
Listing 49-4
1
2
3
4
5
6
7
8
9
// src/Acme/DemoBundle/Form/Extension/ImageTypeExtension.php
namespace Acme\DemoBundle\Form\Extension;
use
use
use
use
use
Symfony\Component\Form\AbstractTypeExtension;
Symfony\Component\Form\FormBuilder;
Symfony\Component\Form\FormView;
Symfony\Component\Form\FormInterface;
Symfony\Component\Form\Util\PropertyPath;
PDF brought to you by
generated on October 26, 2012
Chapter 49: How to Create a Form Type Extension | 355
10 class ImageTypeExtension extends AbstractTypeExtension
11 {
12
/**
13
* Returns the name of the type being extended.
14
*
15
* @return string The name of the type being extended
16
*/
17
public function getExtendedType()
18
{
19
return 'file';
20
}
21
22
/**
23
* Add the image_path option
24
*
25
* @param array $options
26
*/
27
public function getDefaultOptions(array $options)
28
{
29
return array('image_path' => null);
30
}
31
32
/**
33
* Store the image_path option as a builder attribute
34
*
35
* @param \Symfony\Component\Form\FormBuilder $builder
36
* @param array $options
37
*/
38
public function buildForm(FormBuilder $builder, array $options)
39
{
40
if (null !== $options['image_path']) {
41
$builder->setAttribute('image_path', $options['image_path']);
42
}
43
}
44
45
/**
46
* Pass the image url to the view
47
*
48
* @param \Symfony\Component\Form\FormView $view
49
* @param \Symfony\Component\Form\FormInterface $form
50
*/
51
public function buildView(FormView $view, FormInterface $form)
52
{
53
if ($form->hasAttribute('image_path')) {
54
$parentData = $form->getParent()->getData();
55
56
$propertyPath = new PropertyPath($form->getAttribute('image_path'));
57
$imageUrl = $propertyPath->getValue($parentData);
58
// set an "image_url" variable that will be available when rendering this field
59
$view->set('image_url', $imageUrl);
60
}
61
}
62
63 }
PDF brought to you by
generated on October 26, 2012
Chapter 49: How to Create a Form Type Extension | 356
Override the File Widget Template Fragment
Each field type is rendered by a template fragment. Those template fragments can be overridden in order
to customize form rendering. For more information, you can refer to the What are Form Themes? article.
In your extension class, you have added a new variable (image_url), but you still need to take advantage
of this new variable in your templates. Specifically, you need to override the file_widget block:
Listing 49-5
1
2
3
4
5
6
7
8
9
10
11
12
13
{# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
{% extends 'form_div_layout.html.twig' %}
{% block file_widget %}
{% spaceless %}
{{ block('field_widget') }}
{% if image_url is not null %}
<img src="{{ asset(image_url) }}"/>
{% endif %}
{% endspaceless %}
{% endblock %}
You will need to change your config file or explicitly specify how you want your form to be
themed in order for Symfony to use your overridden block. See What are Form Themes? for more
information.
Using the Form Type Extension
From now on, when adding a field of type file in your form, you can specify an image_path option that
will be used to display an image next to the file field. For example:
Listing 49-6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Acme/DemoBundle/Form/Type/MediaType.php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class MediaType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('name', 'text')
->add('file', 'file', array('image_path' => 'webPath'));
}
public function getName()
{
return 'media';
}
}
When displaying the form, if the underlying model has already been associated with an image, you will
see it displayed next to the file input.
PDF brought to you by
generated on October 26, 2012
Chapter 49: How to Create a Form Type Extension | 357
Chapter 50
How to use the Virtual Form Field Option
The virtual form field option can be very useful when you have some duplicated fields in different
entities.
For example, imagine you have two entities, a Company and a Customer:
Listing 50-1
Listing 50-2
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Acme/HelloBundle/Entity/Company.php
namespace Acme\HelloBundle\Entity;
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Acme/HelloBundle/Entity/Customer.php
namespace Acme\HelloBundle\Entity;
class Company
{
private $name;
private $website;
private
private
private
private
$address;
$zipcode;
$city;
$country;
}
class Customer
{
private $firstName;
private $lastName;
private
private
private
private
$address;
$zipcode;
$city;
$country;
}
Like you can see, each entity shares a few of the same fields: address, zipcode, city, country.
Now, you want to build two forms: one for a Company and the second for a Customer.
PDF brought to you by
generated on October 26, 2012
Chapter 50: How to use the Virtual Form Field Option | 358
Start by creating a very simple CompanyType and CustomerType:
Listing 50-3
Listing 50-4
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/HelloBundle/Form/Type/CompanyType.php
namespace Acme\HelloBundle\Form\Type;
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/HelloBundle/Form/Type/CustomerType.php
namespace Acme\HelloBundle\Form\Type;
class CompanyType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('name', 'text')
->add('website', 'text');
}
}
class CustomerType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('firstName', 'text')
->add('lastName', 'text');
}
}
Now, we have to deal with the four duplicated fields. Here is a (simple) location form type:
Listing 50-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Acme/HelloBundle/Form/Type/LocationType.php
namespace Acme\HelloBundle\Form\Type;
class LocationType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('address', 'textarea')
->add('zipcode', 'text')
->add('city', 'text')
->add('country', 'text');
}
public function getDefaultOptions(array $options)
{
return array(
'virtual' => true,
);
}
public function getName()
{
return 'location';
}
}
PDF brought to you by
generated on October 26, 2012
Chapter 50: How to use the Virtual Form Field Option | 359
We don't actually have a location field in each of our entities, so we can't directly link our LocationType
to our CompanyType or CustomerType. But we absolutely want to have a dedicated form type to deal with
location (remember, DRY!).
The virtual form field option is the solution.
We can set the option 'virtual' => true in the getDefaultOptions method of LocationType and
directly start using it in the two original form types.
Look at the result:
Listing 50-6
1
2
3
4
5
// CompanyType
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('foo', new LocationType());
}
Listing 50-7
1
2
3
4
5
// CustomerType
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('bar', new LocationType());
}
With the virtual option set to false (default behavior), the Form Component expects each underlying
object to have a foo (or bar) property that is either some object or array which contains the four location
fields. Of course, we don't have this object/array in our entities and we don't want it!
With the virtual option set to true, the Form component skips the foo (or bar) property, and instead
"gets" and "sets" the 4 location fields directly on the underlying object!
Instead of setting the virtual option inside LocationType, you can (just like with any options)
also pass it in as an array option to the third argument of $builder->add().
PDF brought to you by
generated on October 26, 2012
Chapter 50: How to use the Virtual Form Field Option | 360
Chapter 51
How to create a Custom Validation Constraint
You can create a custom constraint by extending the base constraint class, Constraint1. As an example
we're going to create a simple validator that checks if a string contains only alphanumeric characters.
Creating Constraint class
First you need to create a Constraint class and extend Constraint2:
Listing 51-1
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/DemoBundle/Validator/constraints/ContainsAlphanumeric.php
namespace Acme\DemoBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class ContainsAlphanumeric extends Constraint
{
public $message = 'The string "%string%" contains an illegal character: it can only
contain letters or numbers.';
}
The @Annotation annotation is necessary for this new constraint in order to make it available for
use in classes via annotations. Options for your constraint are represented as public properties on
the constraint class.
1. http://api.symfony.com/2.0/Symfony/Component/Validator/Constraint.html
2. http://api.symfony.com/2.0/Symfony/Component/Validator/Constraint.html
PDF brought to you by
generated on October 26, 2012
Chapter 51: How to create a Custom Validation Constraint | 361
Creating the Validator itself
As you can see, a constraint class is fairly minimal. The actual validation is performed by a another
"constraint validator" class. The constraint validator class is specified by the constraint's validatedBy()
method, which includes some simple default logic:
Listing 51-2
1
2
3
4
5
// in the base Symfony\Component\Validator\Constraint class
public function validatedBy()
{
return get_class($this).'Validator';
}
In other words, if you create a custom Constraint (e.g. MyConstraint), Symfony2 will automatically
look for another class, MyConstraintValidator when actually performing the validation.
The validator class is also simple, and only has one required method: isValid:
Listing 51-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Acme/DemoBundle/Validator/Constraints/ContainsAlphanumericValidator.php
namespace Acme\DemoBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ContainsAlphanumericValidator extends ConstraintValidator
{
public function isValid($value, Constraint $constraint)
{
if (!preg_match('/^[a-zA-Za0-9]+$/', $value, $matches)) {
$this->setMessage($constraint->message, array('%string%' => $value));
return false;
}
return true;
}
}
Don't forget to call setMessage to construct an error message when the value is invalid.
Using the new Validator
Using custom validators is very easy, just as the ones provided by Symfony2 itself:
Listing 51-4
1 # src/Acme/BlogBundle/Resources/config/validation.yml
2 Acme\DemoBundle\Entity\AcmeEntity:
3
properties:
4
name:
5
- NotBlank: ~
6
- Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric: ~
If your constraint contains options, then they should be public properties on the custom Constraint class
you created earlier. These options can be configured like options on core Symfony constraints.
PDF brought to you by
generated on October 26, 2012
Chapter 51: How to create a Custom Validation Constraint | 362
Constraint Validators with Dependencies
If your constraint validator has dependencies, such as a database connection, it will need to be configured
as a service in the dependency injection container. This service must include the
validator.constraint_validator tag and an alias attribute:
Listing 51-5
1 services:
2
validator.unique.your_validator_name:
3
class: Fully\Qualified\Validator\Class\Name
4
tags:
5
- { name: validator.constraint_validator, alias: alias_name }
Your constraint class should now use this alias to reference the appropriate validator:
Listing 51-6
1 public function validatedBy()
2 {
3
return 'alias_name';
4 }
As mentioned above, Symfony2 will automatically look for a class named after the constraint, with
Validator appended. If your constraint validator is defined as a service, it's important that you override
the validatedBy() method to return the alias used when defining your service, otherwise Symfony2
won't use the constraint validator service, and will instantiate the class instead, without any dependencies
injected.
Class Constraint Validator
Beside validating a class property, a constraint can have a class scope by providing a target:
Listing 51-7
1 public function getTargets()
2 {
3
return self::CLASS_CONSTRAINT;
4 }
With this, the validator isValid() method gets an object as its first argument:
Listing 51-8
1 class ProtocolClassValidator extends ConstraintValidator
2 {
3
public function isValid($protocol, Constraint $constraint)
4
{
5
if ($protocol->getFoo() != $protocol->getBar()) {
6
7
$propertyPath = $this->context->getPropertyPath() . 'foo';
8
$this->context->setPropertyPath($propertyPath);
9
$this->context->addViolation($constraint->getMessage(), array(), null);
10
11
return false;
12
}
13
14
return true;
15
}
16 }
Note that a class constraint validator is applied to the class itself, and not to the property:
Listing 51-9
PDF brought to you by
generated on October 26, 2012
Chapter 51: How to create a Custom Validation Constraint | 363
1 # src/Acme/BlogBundle/Resources/config/validation.yml
2 Acme\DemoBundle\Entity\AcmeEntity:
3
constraints:
4
- ContainsAlphanumeric
PDF brought to you by
generated on October 26, 2012
Chapter 51: How to create a Custom Validation Constraint | 364
Chapter 52
How to Master and Create new Environments
Every application is the combination of code and a set of configuration that dictates how that code should
function. The configuration may define the database being used, whether or not something should be
cached, or how verbose logging should be. In Symfony2, the idea of "environments" is the idea that the
same codebase can be run using multiple different configurations. For example, the dev environment
should use configuration that makes development easy and friendly, while the prod environment should
use a set of configuration optimized for speed.
Different Environments, Different Configuration Files
A typical Symfony2 application begins with three environments: dev, prod, and test. As discussed, each
"environment" simply represents a way to execute the same codebase with different configuration. It
should be no surprise then that each environment loads its own individual configuration file. If you're
using the YAML configuration format, the following files are used:
• for the dev environment: app/config/config_dev.yml
• for the prod environment: app/config/config_prod.yml
• for the test environment: app/config/config_test.yml
This works via a simple standard that's used by default inside the AppKernel class:
Listing 52-1
1
2
3
4
5
6
7
8
9
10
11
12
13
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
// ...
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
}
PDF brought to you by
generated on October 26, 2012
Chapter 52: How to Master and Create new Environments | 365
As you can see, when Symfony2 is loaded, it uses the given environment to determine which
configuration file to load. This accomplishes the goal of multiple environments in an elegant, powerful
and transparent way.
Of course, in reality, each environment differs only somewhat from others. Generally, all environments
will share a large base of common configuration. Opening the "dev" configuration file, you can see how
this is accomplished easily and transparently:
Listing 52-2
1 imports:
2
- { resource: config.yml }
3 # ...
To share common configuration, each environment's configuration file simply first imports from a central
configuration file (config.yml). The remainder of the file can then deviate from the default configuration
by overriding individual parameters. For example, by default, the web_profiler toolbar is disabled.
However, in the dev environment, the toolbar is activated by modifying the default value in the dev
configuration file:
Listing 52-3
1 # app/config/config_dev.yml
2 imports:
3
- { resource: config.yml }
4
5 web_profiler:
6
toolbar: true
7
# ...
Executing an Application in Different Environments
To execute the application in each environment, load up the application using either the app.php (for the
prod environment) or the app_dev.php (for the dev environment) front controller:
Listing 52-4
1 http://localhost/app.php
2 http://localhost/app_dev.php
-> *prod* environment
-> *dev* environment
The given URLs assume that your web server is configured to use the web/ directory of the
application as its root. Read more in Installing Symfony2.
If you open up one of these files, you'll quickly see that the environment used by each is explicitly set:
Listing 52-5
1
2
3
4
5
6
7
8
9
<?php
require_once __DIR__.'/../app/bootstrap_cache.php';
require_once __DIR__.'/../app/AppCache.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppCache(new AppKernel('prod', false));
$kernel->handle(Request::createFromGlobals())->send();
As you can see, the prod key specifies that this environment will run in the prod environment. A
Symfony2 application can be executed in any environment by using this code and changing the
environment string.
PDF brought to you by
generated on October 26, 2012
Chapter 52: How to Master and Create new Environments | 366
The test environment is used when writing functional tests and is not accessible in the browser
directly via a front controller. In other words, unlike the other environments, there is no
app_test.php front controller file.
Debug Mode
Important, but unrelated to the topic of environments is the false key on line 8 of the front
controller above. This specifies whether or not the application should run in "debug mode".
Regardless of the environment, a Symfony2 application can be run with debug mode set to true
or false. This affects many things in the application, such as whether or not errors should be
displayed or if cache files are dynamically rebuilt on each request. Though not a requirement,
debug mode is generally set to true for the dev and test environments and false for the prod
environment.
Internally, the value of the debug mode becomes the kernel.debug parameter used inside the
service container. If you look inside the application configuration file, you'll see the parameter used,
for example, to turn logging on or off when using the Doctrine DBAL:
Listing 52-6
1 doctrine:
2
dbal:
3
logging: "%kernel.debug%"
4
# ...
Creating a New Environment
By default, a Symfony2 application has three environments that handle most cases. Of course, since an
environment is nothing more than a string that corresponds to a set of configuration, creating a new
environment is quite easy.
Suppose, for example, that before deployment, you need to benchmark your application. One way
to benchmark the application is to use near-production settings, but with Symfony2's web_profiler
enabled. This allows Symfony2 to record information about your application while benchmarking.
The best way to accomplish this is via a new environment called, for example, benchmark. Start by
creating a new configuration file:
Listing 52-7
1 # app/config/config_benchmark.yml
2 imports:
3
- { resource: config_prod.yml }
4
5 framework:
6
profiler: { only_exceptions: false }
And with this simple addition, the application now supports a new environment called benchmark.
This new configuration file imports the configuration from the prod environment and modifies it. This
guarantees that the new environment is identical to the prod environment, except for any changes
explicitly made here.
Because you'll want this environment to be accessible via a browser, you should also create a front
controller for it. Copy the web/app.php file to web/app_benchmark.php and edit the environment to be
benchmark:
Listing 52-8
PDF brought to you by
generated on October 26, 2012
Chapter 52: How to Master and Create new Environments | 367
1
2
3
4
5
6
7
8
9
<?php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/../app/AppKernel.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('benchmark', false);
$kernel->handle(Request::createFromGlobals())->send();
The new environment is now accessible via:
Listing 52-9
1 http://localhost/app_benchmark.php
Some environments, like the dev environment, are never meant to be accessed on any deployed
server by the general public. This is because certain environments, for debugging purposes, may
give too much information about the application or underlying infrastructure. To be sure these
environments aren't accessible, the front controller is usually protected from external IP addresses
via the following code at the top of the controller:
Listing 52-10
1 if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1'))) {
2
die('You are not allowed to access this file. Check
3 '.basename(__FILE__).' for more information.');
}
Environments and the Cache Directory
Symfony2 takes advantage of caching in many ways: the application configuration, routing configuration,
Twig templates and more are cached to PHP objects stored in files on the filesystem.
By default, these cached files are largely stored in the app/cache directory. However, each environment
caches its own set of files:
Listing 52-11
1 app/cache/dev
2 app/cache/prod
- cache directory for the *dev* environment
- cache directory for the *prod* environment
Sometimes, when debugging, it may be helpful to inspect a cached file to understand how something
is working. When doing so, remember to look in the directory of the environment you're using (most
commonly dev while developing and debugging). While it can vary, the app/cache/dev directory
includes the following:
• appDevDebugProjectContainer.php - the cached "service container" that represents the
cached application configuration;
• appdevUrlGenerator.php - the PHP class generated from the routing configuration and used
when generating URLs;
• appdevUrlMatcher.php - the PHP class used for route matching - look here to see the
compiled regular expression logic used to match incoming URLs to different routes;
• twig/ - this directory contains all the cached Twig templates.
PDF brought to you by
generated on October 26, 2012
Chapter 52: How to Master and Create new Environments | 368
You can easily change the directory location and name. For more information read the article How
to override Symfony's Default Directory Structure.
Going Further
Read the article on How to Set External Parameters in the Service Container.
PDF brought to you by
generated on October 26, 2012
Chapter 52: How to Master and Create new Environments | 369
Chapter 53
How to override Symfony's Default Directory
Structure
Symfony automatically ships with a default directory structure. You can easily override this directory
structure to create your own. The default directory structure is:
Listing 53-1
1
2
3
4
5
6
7
8
9
10
11
12
app/
cache/
config/
logs/
...
src/
...
vendor/
...
web/
app.php
...
Override the cache directory
You can override the cache directory by overriding the getCacheDir method in the AppKernel class of
you application:
Listing 53-2
1
2
3
4
5
6
7
8
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
// ...
public function getCacheDir()
PDF brought to you by
generated on October 26, 2012
Chapter 53: How to override Symfony's Default Directory Structure | 370
9
10
11
12 }
{
return $this->rootDir.'/'.$this->environment.'/cache/';
}
$this->rootDir is the absolute path to the app directory and $this->environment is the current
environment (i.e. dev). In this case we have changed the location of the cache directory to app/
{environment}/cache.
You should keep the cache directory different for each environment, otherwise some unexpected
behaviour may happen. Each environment generates its own cached config files, and so each needs
its own directory to store those cache files.
Override the logs directory
Overriding the logs directory is the same as overriding the cache directory, the only difference is that
you need to override the getLogDir method:
Listing 53-3
1
2
3
4
5
6
7
8
9
10
11
12
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
// ...
public function getLogDir()
{
return $this->rootDir.'/'.$this->environment.'/logs/';
}
}
Here we have changed the location of the directory to app/{environment}/logs.
Override the web directory
If you need to rename or move your web directory, the only thing you need to guarantee is that the path
to the app directory is still correct in your app.php and app_dev.php front controllers. If you simply
renamed the directory, you're fine. But if you moved it in some way, you may need to modify the paths
inside these files:
Listing 53-4
1 require_once __DIR__.'/../Symfony/app/bootstrap.php.cache';
2 require_once __DIR__.'/../Symfony/app/AppKernel.php';
Some shared hosts have a public_html web directory root. Renaming your web directory from
web to public_html is one way to make your Symfony project work on your shared host. Another
way is to deploy your application to a directory outside of your web root, delete your public_html
directory, and then replace it with a symbolic link to the web in your project.
PDF brought to you by
generated on October 26, 2012
Chapter 53: How to override Symfony's Default Directory Structure | 371
If you use the AsseticBundle you need to configure this, so it can use the correct web directory:
Listing 53-5
1 # app/config/config.yml
2
3 # ...
4 assetic:
5
# ...
6
read_from: "%kernel.root_dir%/../../public_html"
Now you just need to dump the assets again and your application should work:
Listing 53-6
1 $ php app/console assetic:dump --env=prod --no-debug
PDF brought to you by
generated on October 26, 2012
Chapter 53: How to override Symfony's Default Directory Structure | 372
Chapter 54
How to Set External Parameters in the Service
Container
In the chapter How to Master and Create new Environments, you learned how to manage your application
configuration. At times, it may benefit your application to store certain credentials outside of your project
code. Database configuration is one such example. The flexibility of the symfony service container allows
you to easily do this.
Environment Variables
Symfony will grab any environment variable prefixed with SYMFONY__ and set it as a parameter in the
service container. Double underscores are replaced with a period, as a period is not a valid character in
an environment variable name.
For example, if you're using Apache, environment variables can be set using the following VirtualHost
configuration:
Listing 54-1
1 <VirtualHost *:80>
2
ServerName
Symfony2
3
DocumentRoot
"/path/to/symfony_2_app/web"
4
DirectoryIndex index.php index.html
5
SetEnv
SYMFONY__DATABASE__USER user
6
SetEnv
SYMFONY__DATABASE__PASSWORD secret
7
8
<Directory "/path/to/symfony_2_app/web">
9
AllowOverride All
10
Allow from All
11
</Directory>
12 </VirtualHost>
PDF brought to you by
generated on October 26, 2012
Chapter 54: How to Set External Parameters in the Service Container | 373
The example above is for an Apache configuration, using the SetEnv1 directive. However, this will
work for any web server which supports the setting of environment variables.
Also, in order for your console to work (which does not use Apache), you must export these as
shell variables. On a Unix system, you can run the following:
Listing 54-2
1 $ export SYMFONY__DATABASE__USER=user
2 $ export SYMFONY__DATABASE__PASSWORD=secret
Now that you have declared an environment variable, it will be present in the PHP $_SERVER global
variable. Symfony then automatically sets all $_SERVER variables prefixed with SYMFONY__ as parameters
in the service container.
You can now reference these parameters wherever you need them.
Listing 54-3
1 doctrine:
2
dbal:
3
driver
4
dbname:
5
user:
6
password:
pdo_mysql
symfony2_project
"%database.user%"
"%database.password%"
Constants
The container also has support for setting PHP constants as parameters. To take advantage of this feature,
map the name of your constant to a parameter key, and define the type as constant.
Listing 54-4
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<parameters>
<parameter key="global.constant.value"
type="constant">GLOBAL_CONSTANT</parameter>
<parameter key="my_class.constant.value"
type="constant">My_Class::CONSTANT_NAME</parameter>
</parameters>
</container>
This only works for XML configuration. If you're not using XML, simply import an XML file to
take advantage of this functionality:
Listing 54-5
1 # app/config/config.yml
2 imports:
3
- { resource: parameters.xml }
1. http://httpd.apache.org/docs/current/env.html
PDF brought to you by
generated on October 26, 2012
Chapter 54: How to Set External Parameters in the Service Container | 374
Miscellaneous Configuration
The imports directive can be used to pull in parameters stored elsewhere. Importing a PHP file gives
you the flexibility to add whatever is needed in the container. The following imports a file named
parameters.php.
Listing 54-6
1 # app/config/config.yml
2 imports:
3
- { resource: parameters.php }
A resource file can be one of many types. PHP, XML, YAML, INI, and closure resources are all
supported by the imports directive.
In parameters.php, tell the service container the parameters that you wish to set. This is useful when
important configuration is in a nonstandard format. The example below includes a Drupal database's
configuration in the symfony service container.
Listing 54-7
1 // app/config/parameters.php
2 include_once('/path/to/drupal/sites/default/settings.php');
3 $container->setParameter('drupal.database.url', $db_url);
PDF brought to you by
generated on October 26, 2012
Chapter 54: How to Set External Parameters in the Service Container | 375
Chapter 55
How to use PdoSessionStorage to store
Sessions in the Database
The default session storage of Symfony2 writes the session information to file(s). Most medium to large
websites use a database to store the session values instead of files, because databases are easier to use and
scale in a multi-webserver environment.
Symfony2 has a built-in solution for database session storage called PdoSessionStorage1. To use it, you
just need to change some parameters in config.yml (or the configuration format of your choice):
Listing 55-1
# app/config/config.yml
framework:
session:
# ...
storage_id:
session.storage.pdo
parameters:
pdo.db_options:
db_table:
db_id_col:
db_data_col:
db_time_col:
session
session_id
session_value
session_time
services:
pdo:
class: PDO
arguments:
dsn:
"mysql:dbname=mydatabase"
user:
myuser
password: mypassword
session.storage.pdo:
class:
Symfony\Component\HttpFoundation\SessionStorage\PdoSessionStorage
arguments: [@pdo, %session.storage.options%, %pdo.db_options%]
1. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/SessionStorage/PdoSessionStorage.html
PDF brought to you by
generated on October 26, 2012
Chapter 55: How to use PdoSessionStorage to store Sessions in the Database | 376
•
•
•
•
db_table: The name of the session table in your database
db_id_col: The name of the id column in your session table (VARCHAR(255) or larger)
db_data_col: The name of the value column in your session table (TEXT or CLOB)
db_time_col: The name of the time column in your session table (INTEGER)
Sharing your Database Connection Information
With the given configuration, the database connection settings are defined for the session storage
connection only. This is OK when you use a separate database for the session data.
But if you'd like to store the session data in the same database as the rest of your project's data, you
can use the connection settings from the parameter.ini by referencing the database-related parameters
defined there:
Listing 55-2
pdo:
class: PDO
arguments:
- "mysql:dbname=%database_name%"
- %database_user%
- %database_password%
Example SQL Statements
MySQL
The SQL statement for creating the needed database table might look like the following (MySQL):
Listing 55-3
1 CREATE TABLE `session` (
2
`session_id` varchar(255) NOT NULL,
3
`session_value` text NOT NULL,
4
`session_time` int(11) NOT NULL,
5
PRIMARY KEY (`session_id`)
6 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
PostgreSQL
For PostgreSQL, the statement should look like this:
Listing 55-4
1 CREATE TABLE session (
2
session_id character varying(255) NOT NULL,
3
session_value text NOT NULL,
4
session_time integer NOT NULL,
5
CONSTRAINT session_pkey PRIMARY KEY (session_id)
6 );
PDF brought to you by
generated on October 26, 2012
Chapter 55: How to use PdoSessionStorage to store Sessions in the Database | 377
Chapter 56
How to use the Apache Router
Symfony2, while fast out of the box, also provides various ways to increase that speed with a little bit of
tweaking. One of these ways is by letting apache handle routes directly, rather than using Symfony2 for
this task.
Change Router Configuration Parameters
To dump Apache routes we must first tweak some configuration parameters to tell Symfony2 to use the
ApacheUrlMatcher instead of the default one:
Listing 56-1
1 # app/config/config_prod.yml
2 parameters:
3
router.options.matcher.cache_class: ~ # disable router cache
4
router.options.matcher_class: Symfony\Component\Routing\Matcher\ApacheUrlMatcher
Note that ApacheUrlMatcher1 extends UrlMatcher2 so even if you don't regenerate the url_rewrite
rules, everything will work (because at the end of ApacheUrlMatcher::match() a call to
parent::match() is done).
Generating mod_rewrite rules
To test that it's working, let's create a very basic route for demo bundle:
Listing 56-2
1 # app/config/routing.yml
2 hello:
3
pattern: /hello/{name}
4
defaults: { _controller: AcmeDemoBundle:Demo:hello }
1. http://api.symfony.com/2.0/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.html
2. http://api.symfony.com/2.0/Symfony/Component/Routing/Matcher/UrlMatcher.html
PDF brought to you by
generated on October 26, 2012
Chapter 56: How to use the Apache Router | 378
Now we generate url_rewrite rules:
Listing 56-3
1 $ php app/console router:dump-apache -e=prod --no-debug
Which should roughly output the following:
Listing 56-4
1
2
3
4
5
6
7
# skip "real" requests
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule .* - [QSA,L]
# hello
RewriteCond %{REQUEST_URI} ^/hello/([^/]+?)$
RewriteRule .* app.php
[QSA,L,E=_ROUTING__route:hello,E=_ROUTING_name:%1,E=_ROUTING__controller:AcmeDemoBundle\:Demo\:hello]
You can now rewrite web/.htaccess to use the new rules, so with our example it should look like this:
Listing 56-5
1 <IfModule mod_rewrite.c>
2
RewriteEngine On
3
4
# skip "real" requests
5
RewriteCond %{REQUEST_FILENAME} -f
6
RewriteRule .* - [QSA,L]
7
8
# hello
9
RewriteCond %{REQUEST_URI} ^/hello/([^/]+?)$
10
RewriteRule .* app.php
11 [QSA,L,E=_ROUTING__route:hello,E=_ROUTING_name:%1,E=_ROUTING__controller:AcmeDemoBundle\:Demo\:hello]
</IfModule>
Procedure above should be done each time you add/change a route if you want to take full
advantage of this setup
That's it! You're now all set to use Apache Route rules.
Additional tweaks
To save a little bit of processing time, change occurrences of Request to ApacheRequest in web/app.php:
Listing 56-6
1
2
3
4
5
6
7
8
9
10
11
12
// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
//require_once __DIR__.'/../app/AppCache.php';
use Symfony\Component\HttpFoundation\ApacheRequest;
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
//$kernel = new AppCache($kernel);
$kernel->handle(ApacheRequest::createFromGlobals())->send();
PDF brought to you by
generated on October 26, 2012
Chapter 56: How to use the Apache Router | 379
Chapter 57
How to create an Event Listener
Symfony has various events and hooks that can be used to trigger custom behavior in your application.
Those events are thrown by the HttpKernel component and can be viewed in the KernelEvents1 class.
To hook into an event and add your own custom logic, you have to create a service that will act as an
event listener on that event. In this entry, we will create a service that will act as an Exception Listener,
allowing us to modify how exceptions are shown by our application. The KernelEvents::EXCEPTION
event is just one of the core kernel events:
Listing 57-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Acme/DemoBundle/Listener/AcmeExceptionListener.php
namespace Acme\DemoBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
class AcmeExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
// We get the exception object from the received event
$exception = $event->getException();
$message = 'My Error says: ' . $exception->getMessage();
// Customize our response object to display our exception details
$response = new Response();
$response->setContent($message);
$response->setStatusCode($exception->getStatusCode());
// Send our modified response object to the event
$event->setResponse($response);
}
}
1. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/KernelEvents.html
PDF brought to you by
generated on October 26, 2012
Chapter 57: How to create an Event Listener | 380
Each event receives a slightly different type of $event object. For the kernel.exception event, it
is GetResponseForExceptionEvent2. To see what type of object each event listener receives, see
KernelEvents3.
Now that the class is created, we just need to register it as a service and notify Symfony that it is a
"listener" on the kernel.exception event by using a special "tag":
Listing 57-2
1 # app/config/config.yml
2 services:
3
kernel.listener.your_listener_name:
4
class: Acme\DemoBundle\Listener\AcmeExceptionListener
5
tags:
6
- { name: kernel.event_listener, event: kernel.exception, method:
onKernelException }
There is an additional tag option priority that is optional and defaults to 0. This value can be
from -255 to 255, and the listeners will be executed in the order of their priority. This is useful
when you need to guarantee that one listener is executed before another.
Request events, checking types
A single page can make several requests (one master request, and then multiple sub-requests), which is
why when working with the KernelEvents::REQUEST event, you might need to check the type of the
request. This can be easily done as follow:
Listing 57-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Acme/DemoBundle/Listener/AcmeRequestListener.php
namespace Acme\DemoBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernel;
class AcmeRequestListener
{
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) {
// don't do anything if it's not the master request
return;
}
// ...
}
}
Two types of request are available in the HttpKernelInterface4 interface:
HttpKernelInterface::MASTER_REQUEST and HttpKernelInterface::SUB_REQUEST.
2. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Event/GetResponseForExceptionEvent.html
3. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/KernelEvents.html
4. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/HttpKernelInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 57: How to create an Event Listener | 381
Chapter 58
How to work with Scopes
This entry is all about scopes, a somewhat advanced topic related to the Service Container. If you've ever
gotten an error mentioning "scopes" when creating services, or need to create a service that depends on
the request service, then this entry is for you.
Understanding Scopes
The scope of a service controls how long an instance of a service is used by the container. The
Dependency Injection component provides two generic scopes:
• container (the default one): The same instance is used each time you request it from this
container.
• prototype: A new instance is created each time you request the service.
The FrameworkBundle also defines a third scope: request. This scope is tied to the request, meaning a
new instance is created for each subrequest and is unavailable outside the request (for instance in the
CLI).
Scopes add a constraint on the dependencies of a service: a service cannot depend on services from
a narrower scope. For example, if you create a generic my_foo service, but try to inject the request
component, you'll receive a ScopeWideningInjectionException1 when compiling the container. Read
the sidebar below for more details.
1. http://api.symfony.com/2.0/Symfony/Component/DependencyInjection/Exception/ScopeWideningInjectionException.html
PDF brought to you by
generated on October 26, 2012
Chapter 58: How to work with Scopes | 382
Scopes and Dependencies
Imagine you've configured a my_mailer service. You haven't configured the scope of the service, so
it defaults to container. In other words, everytime you ask the container for the my_mailer service,
you get the same object back. This is usually how you want your services to work.
Imagine, however, that you need the request service in your my_mailer service, maybe because
you're reading the URL of the current request. So, you add it as a constructor argument. Let's look
at why this presents a problem:
• When requesting my_mailer, an instance of my_mailer (let's call it MailerA) is created
and the request service (let's call it RequestA) is passed to it. Life is good!
• You've now made a subrequest in Symfony, which is a fancy way of saying that you've
called, for example, the {% render ... %} Twig function, which executes another
controller. Internally, the old request service (RequestA) is actually replaced by a new
request instance (RequestB). This happens in the background, and it's totally normal.
• In your embedded controller, you once again ask for the my_mailer service. Since your
service is in the container scope, the same instance (MailerA) is just re-used. But here's
the problem: the MailerA instance still contains the old RequestA object, which is now
not the correct request object to have (RequestB is now the current request service). This
is subtle, but the mis-match could cause major problems, which is why it's not allowed.
So, that's the reason why scopes exist, and how they can cause problems. Keep reading
to find out the common solutions.
A service can of course depend on a service from a wider scope without any issue.
Setting the Scope in the Definition
The scope of a service is set in the definition of the service:
Listing 58-1
1 # src/Acme/HelloBundle/Resources/config/services.yml
2 services:
3
greeting_card_manager:
4
class: Acme\HelloBundle\Mail\GreetingCardManager
5
scope: request
If you don't specify the scope, it defaults to container, which is what you want most of the time. Unless
your service depends on another service that's scoped to a narrower scope (most commonly, the request
service), you probably don't need to set the scope.
Using a Service from a narrower Scope
If your service depends on a scoped service, the best solution is to put it in the same scope (or a narrower
one). Usually, this means putting your new service in the request scope.
But this is not always possible (for instance, a twig extension must be in the container scope as the Twig
environment needs it as a dependency). In these cases, you should pass the entire container into your
service and retrieve your dependency from the container each time we need it to be sure you have the
right instance:
PDF brought to you by
generated on October 26, 2012
Chapter 58: How to work with Scopes | 383
Listing 58-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Acme/HelloBundle/Mail/Mailer.php
namespace Acme\HelloBundle\Mail;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Mailer
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function sendEmail()
{
$request = $this->container->get('request');
// ... do something using the request here
}
}
Take care not to store the request in a property of the object for a future call of the service as it
would cause the same issue described in the first section (except that Symfony cannot detect that
you are wrong).
The service config for this class would look something like this:
Listing 58-3
1 # src/Acme/HelloBundle/Resources/config/services.yml
2 parameters:
3
# ...
4
my_mailer.class: Acme\HelloBundle\Mail\Mailer
5 services:
6
my_mailer:
7
class:
"%my_mailer.class%"
8
arguments:
9
- "@service_container"
10
# scope: container can be omitted as it is the default
Injecting the whole container into a service is generally not a good idea (only inject what you need).
In some rare cases, it's necessary when you have a service in the container scope that needs a
service in the request scope.
If you define a controller as a service then you can get the Request object without injecting the container
by having it passed in as an argument of your action method. See The Request as a Controller Argument
for details.
PDF brought to you by
generated on October 26, 2012
Chapter 58: How to work with Scopes | 384
Chapter 59
How to work with Compiler Passes in Bundles
Compiler passes give you an opportunity to manipulate other service definitions that have been registered
with the service container. You can read about how to create them in the components section "Compiling
the Container". To register a compiler pass from a bundle you need to add it to the build method of the
bundle definition class:
Listing 59-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Acme/MailerBundle/AcmeMailerBundle.php
namespace Acme\MailerBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Acme\MailerBundle\DependencyInjection\Compiler\CustomCompilerPass;
class AcmeMailerBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new CustomCompilerPass());
}
}
One of the most common use-cases of compiler passes is to work with tagged services (read more about
tags in the components section "Working with Tagged Services"). If you are using custom tags in a bundle
then by convention, tag names consist of the name of the bundle (lowercase, underscores as separators),
followed by a dot, and finally the "real" name. For example, if you want to introduce some sort of
"transport" tag in your AcmeMailerBundle, you should call it acme_mailer.transport.
PDF brought to you by
generated on October 26, 2012
Chapter 59: How to work with Compiler Passes in Bundles | 385
Chapter 60
How to use Best Practices for Structuring
Bundles
A bundle is a directory that has a well-defined structure and can host anything from classes to controllers
and web resources. Even if bundles are very flexible, you should follow some best practices if you want
to distribute them.
Bundle Name
A bundle is also a PHP namespace. The namespace must follow the technical interoperability standards1
for PHP 5.3 namespaces and class names: it starts with a vendor segment, followed by zero or more
category segments, and it ends with the namespace short name, which must end with a Bundle suffix.
A namespace becomes a bundle as soon as you add a bundle class to it. The bundle class name must
follow these simple rules:
•
•
•
•
Use only alphanumeric characters and underscores;
Use a CamelCased name;
Use a descriptive and short name (no more than 2 words);
Prefix the name with the concatenation of the vendor (and optionally the category
namespaces);
• Suffix the name with Bundle.
Here are some valid bundle namespaces and class names:
Namespace
Bundle Class Name
Acme\Bundle\BlogBundle
AcmeBlogBundle
Acme\Bundle\Social\BlogBundle AcmeSocialBlogBundle
Acme\BlogBundle
AcmeBlogBundle
1. http://symfony.com/PSR0
PDF brought to you by
generated on October 26, 2012
Chapter 60: How to use Best Practices for Structuring Bundles | 386
By convention, the getName() method of the bundle class should return the class name.
If you share your bundle publicly, you must use the bundle class name as the name of the
repository (AcmeBlogBundle and not BlogBundle for instance).
Symfony2 core Bundles do not prefix the Bundle class with Symfony and always add a Bundle
subnamespace; for example: FrameworkBundle2.
Each bundle has an alias, which is the lower-cased short version of the bundle name using underscores
(acme_hello for AcmeHelloBundle, or acme_social_blog for Acme\Social\BlogBundle for instance).
This alias is used to enforce uniqueness within a bundle (see below for some usage examples).
Directory Structure
The basic directory structure of a HelloBundle bundle must read as follows:
Listing 60-1
1 XXX/...
2
HelloBundle/
3
HelloBundle.php
4
Controller/
5
Resources/
6
meta/
7
LICENSE
8
config/
9
doc/
10
index.rst
11
translations/
12
views/
13
public/
14
Tests/
The XXX directory(ies) reflects the namespace structure of the bundle.
The following files are mandatory:
• HelloBundle.php;
• Resources/meta/LICENSE: The full license for the code;
• Resources/doc/index.rst: The root file for the Bundle documentation.
These conventions ensure that automated tools can rely on this default structure to work.
The depth of sub-directories should be kept to the minimal for most used classes and files (2 levels at a
maximum). More levels can be defined for non-strategic, less-used files.
The bundle directory is read-only. If you need to write temporary files, store them under the cache/ or
log/ directory of the host application. Tools can generate files in the bundle directory structure, but only
if the generated files are going to be part of the repository.
The following classes and files have specific emplacements:
2. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/FrameworkBundle.html
PDF brought to you by
generated on October 26, 2012
Chapter 60: How to use Best Practices for Structuring Bundles | 387
Type
Directory
Commands
Command/
Controllers
Controller/
Service Container Extensions DependencyInjection/
Event Listeners
EventListener/
Configuration
Resources/config/
Web Resources
Resources/public/
Translation files
Resources/translations/
Templates
Resources/views/
Unit and Functional Tests
Tests/
Classes
The bundle directory structure is used as the namespace hierarchy. For instance, a HelloController
controller is stored in Bundle/HelloBundle/Controller/HelloController.php and the fully qualified
class name is Bundle\HelloBundle\Controller\HelloController.
All classes and files must follow the Symfony2 coding standards.
Some classes should be seen as facades and should be as short as possible, like Commands, Helpers,
Listeners, and Controllers.
Classes that connect to the Event Dispatcher should be suffixed with Listener.
Exceptions classes should be stored in an Exception sub-namespace.
Vendors
A bundle must not embed third-party PHP libraries. It should rely on the standard Symfony2 autoloading
instead.
A bundle should not embed third-party libraries written in JavaScript, CSS, or any other language.
Tests
A bundle should come with a test suite written with PHPUnit and stored under the Tests/ directory.
Tests should follow the following principles:
• The test suite must be executable with a simple phpunit command run from a sample
application;
• The functional tests should only be used to test the response output and some profiling
information if you have some;
• The tests should cover at least 95% of the code base.
A test suite must not contain AllTests.php scripts, but must rely on the existence of a
phpunit.xml.dist file.
PDF brought to you by
generated on October 26, 2012
Chapter 60: How to use Best Practices for Structuring Bundles | 388
Documentation
All classes and functions must come with full PHPDoc.
Extensive documentation should also be provided in the reStructuredText format, under the Resources/
doc/ directory; the Resources/doc/index.rst file is the only mandatory file and must be the entry point
for the documentation.
Controllers
As a best practice, controllers in a bundle that's meant to be distributed to others must not extend the
Controller3 base class. They can implement ContainerAwareInterface4 or extend ContainerAware5
instead.
If you have a look at Controller6 methods, you will see that they are only nice shortcuts to ease
the learning curve.
Routing
If the bundle provides routes, they must be prefixed with the bundle alias. For an AcmeBlogBundle for
instance, all routes must be prefixed with acme_blog_.
Templates
If a bundle provides templates, they must use Twig. A bundle must not provide a main layout, except if
it provides a full working application.
Translation Files
If a bundle provides message translations, they must be defined in the XLIFF format; the domain should
be named after the bundle name (bundle.hello).
A bundle must not override existing messages from another bundle.
Configuration
To provide more flexibility, a bundle can provide configurable settings by using the Symfony2 built-in
mechanisms.
For simple configuration settings, rely on the default parameters entry of the Symfony2 configuration.
Symfony2 parameters are simple key/value pairs; a value being any valid PHP value. Each parameter
name should start with the bundle alias, though this is just a best-practice suggestion. The rest of the
parameter name will use a period (.) to separate different parts (e.g. acme_hello.email.from).
3. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Controller/Controller.html
4. http://api.symfony.com/2.0/Symfony/Component/DependencyInjection/ContainerAwareInterface.html
5. http://api.symfony.com/2.0/Symfony/Component/DependencyInjection/ContainerAware.html
6. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Controller/Controller.html
PDF brought to you by
generated on October 26, 2012
Chapter 60: How to use Best Practices for Structuring Bundles | 389
The end user can provide values in any configuration file:
Listing 60-2
1 # app/config/config.yml
2 parameters:
3
acme_hello.email.from: fabien@example.com
Retrieve the configuration parameters in your code from the container:
Listing 60-3
1 $container->getParameter('acme_hello.email.from');
Even if this mechanism is simple enough, you are highly encouraged to use the semantic configuration
described in the cookbook.
If you are defining services, they should also be prefixed with the bundle alias.
Learn more from the Cookbook
• How to expose a Semantic Configuration for a Bundle
PDF brought to you by
generated on October 26, 2012
Chapter 60: How to use Best Practices for Structuring Bundles | 390
Chapter 61
How to use Bundle Inheritance to Override
parts of a Bundle
When working with third-party bundles, you'll probably come across a situation where you want to
override a file in that third-party bundle with a file in one of your own bundles. Symfony gives you a very
convenient way to override things like controllers, templates, and other files in a bundle's Resources/
directory.
For example, suppose that you're installing the FOSUserBundle1, but you want to override its base
layout.html.twig template, as well as one of its controllers. Suppose also that you have your own
AcmeUserBundle where you want the overridden files to live. Start by registering the FOSUserBundle as
the "parent" of your bundle:
Listing 61-1
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/UserBundle/AcmeUserBundle.php
namespace Acme\UserBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeUserBundle extends Bundle
{
public function getParent()
{
return 'FOSUserBundle';
}
}
By making this simple change, you can now override several parts of the FOSUserBundle simply by
creating a file with the same name.
Despite the method name, there is no parent/child relationship between the bundles, it is just a
way to extend and override an existing bundle.
1. https://github.com/friendsofsymfony/fosuserbundle
PDF brought to you by
generated on October 26, 2012
Chapter 61: How to use Bundle Inheritance to Override parts of a Bundle | 391
Overriding Controllers
Suppose you want to add some functionality to the registerAction of a RegistrationController that
lives inside FOSUserBundle. To do so, just create your own RegistrationController.php file, override
the bundle's original method, and change its functionality:
Listing 61-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/UserBundle/Controller/RegistrationController.php
namespace Acme\UserBundle\Controller;
use FOS\UserBundle\Controller\RegistrationController as BaseController;
class RegistrationController extends BaseController
{
public function registerAction()
{
$response = parent::registerAction();
// ... do custom stuff
return $response;
}
}
Depending on how severely you need to change the behavior, you might call
parent::registerAction() or completely replace its logic with your own.
Overriding controllers in this way only works if the bundle refers to the controller using the
standard FOSUserBundle:Registration:register syntax in routes and templates. This is the
best practice.
Overriding Resources: Templates, Routing, Validation, etc
Most resources can also be overridden, simply by creating a file in the same location as your parent
bundle.
For example, it's very common to need to override the FOSUserBundle's layout.html.twig template so
that it uses your application's base layout. Since the file lives at Resources/views/layout.html.twig in
the FOSUserBundle, you can create your own file in the same location of AcmeUserBundle. Symfony will
ignore the file that lives inside the FOSUserBundle entirely, and use your file instead.
The same goes for routing files, validation configuration and other resources.
The overriding of resources only works when you refer to resources with the @FosUserBundle/
Resources/config/routing/security.xml method. If you refer to resources without using the
@BundleName shortcut, they can't be overridden in this way.
Translation files do not work in the same way as described above. All translation files are
accumulated into a set of "pools" (one for each) domain. Symfony loads translation files from
bundles first (in the order that the bundles are initialized) and then from your app/Resources
PDF brought to you by
generated on October 26, 2012
Chapter 61: How to use Bundle Inheritance to Override parts of a Bundle | 392
directory. If the same translation is specified in two resources, the translation from the resource
that's loaded last will win.
PDF brought to you by
generated on October 26, 2012
Chapter 61: How to use Bundle Inheritance to Override parts of a Bundle | 393
Chapter 62
How to Override any Part of a Bundle
This document is a quick reference for how to override different parts of third-party bundles.
Templates
For information on overriding templates, see * Overriding Bundle Templates. * How to use Bundle
Inheritance to Override parts of a Bundle
Routing
Routing is never automatically imported in Symfony2. If you want to include the routes from any
bundle, then they must be manually imported from somewhere in your application (e.g. app/config/
routing.yml).
The easiest way to "override" a bundle's routing is to never import it at all. Instead of importing a thirdparty bundle's routing, simply copying that routing file into your application, modify it, and import it
instead.
Controllers
Assuming the third-party bundle involved uses non-service controllers (which is almost always the case),
you can easily override controllers via bundle inheritance. For more information, see How to use Bundle
Inheritance to Override parts of a Bundle.
Services & Configuration
In order to override/extend a service, there are two options. First, you can set the parameter holding the
service's class name to your own class by setting it in app/config/config.yml. This of course is only
possible if the class name is defined as a parameter in the service config of the bundle containing the
service. For example, to override the class used for Symfony's translator service, you would override the
PDF brought to you by
generated on October 26, 2012
Chapter 62: How to Override any Part of a Bundle | 394
translator.class parameter. Knowing exactly which parameter to override may take some research.
For the translator, the parameter is defined and used in the Resources/config/translation.xml file in
the core FrameworkBundle:
Listing 62-1
1 # app/config/config.yml
2 parameters:
3
translator.class:
Acme\HelloBundle\Translation\Translator
Secondly, if the class is not available as a parameter, you want to make sure the class is always overridden
when your bundle is used, or you need to modify something beyond just the class name, you should use
a compiler pass:
Listing 62-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/FooBundle/DependencyInjection/Compiler/OverrideServiceCompilerPass.php
namespace Acme\DemoBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class OverrideServiceCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('original-service-id');
$definition->setClass('Acme\DemoBundle\YourService');
}
}
In this example we fetch the service definition of the original service, and set its class name to our own
class.
See How to work with Compiler Passes in Bundles for information on how to use compiler passes. If you
want to do something beyond just overriding the class - like adding a method call - you can only use the
compiler pass method.
Entities & Entity mapping
In progress...
Forms
In order to override a form type, it has to be registered as a service (meaning it is tagged as "form.type").
You can then override it as you would override any service as explained in Services & Configuration.
This, of course, will only work if the type is referred to by its alias rather than being instantiated, e.g.:
Listing 62-3
1 $builder->add('name', 'custom_type');
rather than:
Listing 62-4
1 $builder->add('name', new CustomType());
PDF brought to you by
generated on October 26, 2012
Chapter 62: How to Override any Part of a Bundle | 395
Validation metadata
In progress...
Translations
In progress...
PDF brought to you by
generated on October 26, 2012
Chapter 62: How to Override any Part of a Bundle | 396
Chapter 63
How to expose a Semantic Configuration for a
Bundle
If you open your application configuration file (usually app/config/config.yml), you'll see a number of
different configuration "namespaces", such as framework, twig, and doctrine. Each of these configures
a specific bundle, allowing you to configure things at a high level and then let the bundle make all the
low-level, complex changes that result.
For example, the following tells the FrameworkBundle to enable the form integration, which involves the
defining of quite a few services as well as integration of other related components:
Listing 63-1
1 framework:
2
# ...
3
form:
true
When you create a bundle, you have two choices on how to handle configuration:
1. Normal Service Configuration (easy):
You can specify your services in a configuration file (e.g. services.yml) that lives
in your bundle and then import it from your main application configuration. This is
really easy, quick and totally effective. If you make use of parameters, then you still
have the flexibility to customize your bundle from your application configuration. See
"Importing Configuration with imports" for more details.
2. Exposing Semantic Configuration (advanced):
This is the way configuration is done with the core bundles (as described above). The
basic idea is that, instead of having the user override individual parameters, you let
the user configure just a few, specifically created options. As the bundle developer,
you then parse through that configuration and load services inside an "Extension"
class. With this method, you won't need to import any configuration resources from
your main application configuration: the Extension class can handle all of this.
PDF brought to you by
generated on October 26, 2012
Chapter 63: How to expose a Semantic Configuration for a Bundle | 397
The second option - which you'll learn about in this article - is much more flexible, but also requires more
time to setup. If you're wondering which method you should use, it's probably a good idea to start with
method #1, and then change to #2 later if you need to.
The second method has several specific advantages:
• Much more powerful than simply defining parameters: a specific option value might trigger
the creation of many service definitions;
• Ability to have configuration hierarchy
• Smart merging when several configuration files (e.g. config_dev.yml and config.yml)
override each other's configuration;
• Configuration validation (if you use a Configuration Class);
• IDE auto-completion when you create an XSD and developers use XML.
Overriding bundle parameters
If a Bundle provides an Extension class, then you should generally not override any service
container parameters from that bundle. The idea is that if an Extension class is present, every
setting that should be configurable should be present in the configuration made available by that
class. In other words the extension class defines all the publicly supported configuration settings
for which backward compatibility will be maintained.
Creating an Extension Class
If you do choose to expose a semantic configuration for your bundle, you'll first need to create a new
"Extension" class, which will handle the process. This class should live in the DependencyInjection
directory of your bundle and its name should be constructed by replacing the Bundle suffix of the Bundle
class name with Extension. For example, the Extension class of AcmeHelloBundle would be called
AcmeHelloExtension:
Listing 63-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class AcmeHelloExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
// ... where all of the heavy logic is done
}
public function getXsdValidationBasePath()
{
return __DIR__.'/../Resources/config/';
}
public function getNamespace()
{
return 'http://www.example.com/symfony/schema/';
}
}
PDF brought to you by
generated on October 26, 2012
Chapter 63: How to expose a Semantic Configuration for a Bundle | 398
The getXsdValidationBasePath and getNamespace methods are only required if the bundle
provides optional XSD's for the configuration.
The presence of the previous class means that you can now define an acme_hello configuration
namespace in any configuration file. The namespace acme_hello is constructed from the extension's class
name by removing the word Extension and then lowercasing and underscoring the rest of the name. In
other words, AcmeHelloExtension becomes acme_hello.
You can begin specifying configuration under this namespace immediately:
Listing 63-3
1 # app/config/config.yml
2 acme_hello: ~
If you follow the naming conventions laid out above, then the load() method of your extension
code is always called as long as your bundle is registered in the Kernel. In other words, even if
the user does not provide any configuration (i.e. the acme_hello entry doesn't even appear), the
load() method will be called and passed an empty $configs array. You can still provide some
sensible defaults for your bundle if you want.
Parsing the $configs Array
Whenever a user includes the acme_hello namespace in a configuration file, the configuration under it
is added to an array of configurations and passed to the load() method of your extension (Symfony2
automatically converts XML and YAML to an array).
Take the following configuration:
Listing 63-4
1 # app/config/config.yml
2 acme_hello:
3
foo: fooValue
4
bar: barValue
The array passed to your load() method will look like this:
Listing 63-5
1 array(
2
array(
3
'foo' => 'fooValue',
4
'bar' => 'barValue',
5
)
6 )
Notice that this is an array of arrays, not just a single flat array of the configuration values. This is
intentional. For example, if acme_hello appears in another configuration file - say config_dev.yml with different values beneath it, then the incoming array might look like this:
Listing 63-6
1 array(
2
array(
3
'foo' => 'fooValue',
4
'bar' => 'barValue',
5
),
6
array(
PDF brought to you by
generated on October 26, 2012
Chapter 63: How to expose a Semantic Configuration for a Bundle | 399
7
8
9
10 )
'foo' => 'fooDevValue',
'baz' => 'newConfigEntry',
),
The order of the two arrays depends on which one is set first.
It's your job, then, to decide how these configurations should be merged together. You might, for
example, have later values override previous values or somehow merge them together.
Later, in the Configuration Class section, you'll learn of a truly robust way to handle this. But for now,
you might just merge them manually:
Listing 63-7
1 public function load(array $configs, ContainerBuilder $container)
2 {
3
$config = array();
4
foreach ($configs as $subConfig) {
5
$config = array_merge($config, $subConfig);
6
}
7
8
// ... now use the flat $config array
9 }
Make sure the above merging technique makes sense for your bundle. This is just an example, and
you should be careful to not use it blindly.
Using the load() Method
Within load(), the $container variable refers to a container that only knows about this namespace
configuration (i.e. it doesn't contain service information loaded from other bundles). The goal of the
load() method is to manipulate the container, adding and configuring any methods or services needed
by your bundle.
Loading External Configuration Resources
One common thing to do is to load an external configuration file that may contain the bulk of the services
needed by your bundle. For example, suppose you have a services.xml file that holds much of your
bundle's service configuration:
Listing 63-8
1
2
3
4
5
6
7
8
9
10
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\Config\FileLocator;
public function load(array $configs, ContainerBuilder $container)
{
// ... prepare your $config variable
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/
config'));
$loader->load('services.xml');
}
PDF brought to you by
generated on October 26, 2012
Chapter 63: How to expose a Semantic Configuration for a Bundle | 400
You might even do this conditionally, based on one of the configuration values. For example, suppose
you only want to load a set of services if an enabled option is passed and set to true:
Listing 63-9
1 public function load(array $configs, ContainerBuilder $container)
2 {
3
// ... prepare your $config variable
4
5
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/
6 config'));
7
8
if (isset($config['enabled']) && $config['enabled']) {
9
$loader->load('services.xml');
10
}
}
Configuring Services and Setting Parameters
Once you've loaded some service configuration, you may need to modify the configuration based on
some of the input values. For example, suppose you have a service whose first argument is some string
"type" that it will use internally. You'd like this to be easily configured by the bundle user, so in your
service configuration file (e.g. services.xml), you define this service and use a blank parameter acme_hello.my_service_type - as its first argument:
Listing 63-10
1 <!-- src/Acme/HelloBundle/Resources/config/services.xml -->
2 <container xmlns="http://symfony.com/schema/dic/services"
3
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/
5 dic/services/services-1.0.xsd">
6
7
<parameters>
8
<parameter key="acme_hello.my_service_type" />
9
</parameters>
10
11
<services>
12
<service id="acme_hello.my_service" class="Acme\HelloBundle\MyService">
13
<argument>%acme_hello.my_service_type%</argument>
14
</service>
15
</services>
</container>
But why would you define an empty parameter and then pass it to your service? The answer is that you'll
set this parameter in your extension class, based on the incoming configuration values. Suppose, for
example, that you want to allow the user to define this type option under a key called my_type. Add the
following to the load() method to do this:
Listing 63-11
1 public function load(array $configs, ContainerBuilder $container)
2 {
3
// ... prepare your $config variable
4
5
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/
6 config'));
7
$loader->load('services.xml');
8
9
if (!isset($config['my_type'])) {
10
throw new \InvalidArgumentException('The "my_type" option must be set');
11
}
PDF brought to you by
generated on October 26, 2012
Chapter 63: How to expose a Semantic Configuration for a Bundle | 401
12
13
$container->setParameter('acme_hello.my_service_type', $config['my_type']);
}
Now, the user can effectively configure the service by specifying the my_type configuration value:
Listing 63-12
1 # app/config/config.yml
2 acme_hello:
3
my_type: foo
4
# ...
Global Parameters
When you're configuring the container, be aware that you have the following global parameters available
to use:
•
•
•
•
•
•
•
•
•
kernel.name
kernel.environment
kernel.debug
kernel.root_dir
kernel.cache_dir
kernel.logs_dir
kernel.bundle_dirs
kernel.bundles
kernel.charset
All parameter and service names starting with a _ are reserved for the framework, and new ones
must not be defined by bundles.
Validation and Merging with a Configuration Class
So far, you've done the merging of your configuration arrays by hand and are checking for the presence
of config values manually using the isset() PHP function. An optional Configuration system is also
available which can help with merging, validation, default values, and format normalization.
Format normalization refers to the fact that certain formats - largely XML - result in slightly
different configuration arrays and that these arrays need to be "normalized" to match everything
else.
To take advantage of this system, you'll create a Configuration class and build a tree that defines your
configuration in that class:
Listing 63-13
1
2
3
4
5
6
7
// src/Acme/HelloBundle/DependencyInjection/Configuration.php
namespace Acme\HelloBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
PDF brought to you by
generated on October 26, 2012
Chapter 63: How to expose a Semantic Configuration for a Bundle | 402
8 {
9
10
11
12
13
14
15
16
17
18
19
20
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('acme_hello');
$rootNode
->children()
->scalarNode('my_type')->defaultValue('bar')->end()
->end();
return $treeBuilder;
}
This is a very simple example, but you can now use this class in your load() method to merge your
configuration and force validation. If any options other than my_type are passed, the user will be notified
with an exception that an unsupported option was passed:
Listing 63-14
1 public function load(array $configs, ContainerBuilder $container)
2 {
3
$configuration = new Configuration();
4
$config = $this->processConfiguration($configuration, $configs);
5
6
// ...
7 }
The processConfiguration() method uses the configuration tree you've defined in the Configuration
class to validate, normalize and merge all of the configuration arrays together.
The Configuration class can be much more complicated than shown here, supporting array nodes,
"prototype" nodes, advanced validation, XML-specific normalization and advanced merging. You can
read more about this in the Config Component documentation. You can also see it action by checking out
some of the core Configuration classes, such as the one from the FrameworkBundle Configuration1 or the
TwigBundle Configuration2.
Extension Conventions
When creating an extension, follow these simple conventions:
• The extension must be stored in the DependencyInjection sub-namespace;
• The extension must be named after the bundle name and suffixed with Extension
(AcmeHelloExtension for AcmeHelloBundle);
• The extension should provide an XSD schema.
If you follow these simple conventions, your extensions will be registered automatically by Symfony2. If
not, override the Bundle build()3 method in your bundle:
Listing 63-15
1 // ...
2 use Acme\HelloBundle\DependencyInjection\UnconventionalExtensionClass;
3
4 class AcmeHelloBundle extends Bundle
1. https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
2. https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php
3. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Bundle/Bundle.html#build()
PDF brought to you by
generated on October 26, 2012
Chapter 63: How to expose a Semantic Configuration for a Bundle | 403
5 {
6
7
8
9
10
11
12
13 }
public function build(ContainerBuilder $container)
{
parent::build($container);
// register extensions that do not follow the conventions manually
$container->registerExtension(new UnconventionalExtensionClass());
}
In this case, the extension class must also implement a getAlias() method and return a unique alias
named after the bundle (e.g. acme_hello). This is required because the class name doesn't follow the
standards by ending in Extension.
Additionally, the load() method of your extension will only be called if the user specifies the acme_hello
alias in at least one configuration file. Once again, this is because the Extension class doesn't follow the
standards set out above, so nothing happens automatically.
PDF brought to you by
generated on October 26, 2012
Chapter 63: How to expose a Semantic Configuration for a Bundle | 404
Chapter 64
How to send an Email
Sending emails is a classic task for any web application and one that has special complications and
potential pitfalls. Instead of recreating the wheel, one solution to send emails is to use the
SwiftmailerBundle, which leverages the power of the Swiftmailer1 library.
Don't forget to enable the bundle in your kernel before using it:
Listing 64-1
1 public function registerBundles()
2 {
3
$bundles = array(
4
...,
5
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
6
);
7
8
// ...
9 }
Configuration
Before using Swiftmailer, be sure to include its configuration. The only mandatory configuration
parameter is transport:
Listing 64-2
1 # app/config/config.yml
2 swiftmailer:
3
transport: smtp
4
encryption: ssl
5
auth_mode: login
6
host:
smtp.gmail.com
7
username:
your_username
8
password:
your_password
1. http://www.swiftmailer.org/
PDF brought to you by
generated on October 26, 2012
Chapter 64: How to send an Email | 405
The majority of the Swiftmailer configuration deals with how the messages themselves should be
delivered.
The following configuration attributes are available:
•
•
•
•
•
•
•
•
transport (smtp, mail, sendmail, or gmail)
username
password
host
port
encryption (tls, or ssl)
auth_mode (plain, login, or cram-md5)
spool
• type (how to queue the messages, only file is supported currently)
• path (where to store the messages)
• delivery_address (an email address where to send ALL emails)
• disable_delivery (set to true to disable delivery completely)
Sending Emails
The Swiftmailer library works by creating, configuring and then sending Swift_Message objects. The
"mailer" is responsible for the actual delivery of the message and is accessible via the mailer service.
Overall, sending an email is pretty straightforward:
Listing 64-3
1 public function indexAction($name)
2 {
3
$message = \Swift_Message::newInstance()
4
->setSubject('Hello Email')
5
->setFrom('send@example.com')
6
->setTo('recipient@example.com')
7
->setBody($this->renderView('HelloBundle:Hello:email.txt.twig', array('name' =>
8 $name)))
9
;
10
$this->get('mailer')->send($message);
11
12
return $this->render(...);
}
To keep things decoupled, the email body has been stored in a template and rendered with the
renderView() method.
The $message object supports many more options, such as including attachments, adding HTML
content, and much more. Fortunately, Swiftmailer covers the topic of Creating Messages2 in great detail
in its documentation.
Several other cookbook articles are available related to sending emails in Symfony2:
• How to use Gmail to send Emails
• How to Work with Emails During Development
• How to Spool Email
2. http://swiftmailer.org/docs/messages.html
PDF brought to you by
generated on October 26, 2012
Chapter 64: How to send an Email | 406
Chapter 65
How to use Gmail to send Emails
During development, instead of using a regular SMTP server to send emails, you might find using Gmail
easier and more practical. The Swiftmailer bundle makes it really easy.
Instead of using your regular Gmail account, it's of course recommended that you create a special
account.
In the development configuration file, change the transport setting to gmail and set the username and
password to the Google credentials:
Listing 65-1
1 # app/config/config_dev.yml
2 swiftmailer:
3
transport: gmail
4
username: your_gmail_username
5
password: your_gmail_password
You're done!
The gmail transport is simply a shortcut that uses the smtp transport and sets encryption,
auth_mode and host to work with Gmail.
PDF brought to you by
generated on October 26, 2012
Chapter 65: How to use Gmail to send Emails | 407
Chapter 66
How to Work with Emails During Development
When developing an application which sends email, you will often not want to actually send the email
to the specified recipient during development. If you are using the SwiftmailerBundle with Symfony2,
you can easily achieve this through configuration settings without having to make any changes to
your application's code at all. There are two main choices when it comes to handling email during
development: (a) disabling the sending of email altogether or (b) sending all email to a specific address.
Disabling Sending
You can disable sending email by setting the disable_delivery option to true. This is the default in the
test environment in the Standard distribution. If you do this in the test specific config then email will
not be sent when you run tests, but will continue to be sent in the prod and dev environments:
Listing 66-1
1 # app/config/config_test.yml
2 swiftmailer:
3
disable_delivery: true
If you'd also like to disable deliver in the dev environment, simply add this same configuration to the
config_dev.yml file.
Sending to a Specified Address
You can also choose to have all email sent to a specific address, instead of the address actually specified
when sending the message. This can be done via the delivery_address option:
Listing 66-2
1 # app/config/config_dev.yml
2 swiftmailer:
3
delivery_address: dev@example.com
Now, suppose you're sending an email to recipient@example.com.
Listing 66-3
PDF brought to you by
generated on October 26, 2012
Chapter 66: How to Work with Emails During Development | 408
1 public function indexAction($name)
2 {
3
$message = \Swift_Message::newInstance()
4
->setSubject('Hello Email')
5
->setFrom('send@example.com')
6
->setTo('recipient@example.com')
7
->setBody($this->renderView('HelloBundle:Hello:email.txt.twig', array('name' =>
8 $name)))
9
;
10
$this->get('mailer')->send($message);
11
12
return $this->render(...);
}
In the dev environment, the email will instead be sent to dev@example.com. Swiftmailer will add an extra
header to the email, X-Swift-To, containing the replaced address, so you can still see who it would have
been sent to.
In addition to the to addresses, this will also stop the email being sent to any CC and BCC addresses
set for it. Swiftmailer will add additional headers to the email with the overridden addresses in
them. These are X-Swift-Cc and X-Swift-Bcc for the CC and BCC addresses respectively.
Viewing from the Web Debug Toolbar
You can view any email sent during a single response when you are in the dev environment using the
Web Debug Toolbar. The email icon in the toolbar will show how many emails were sent. If you click it,
a report will open showing the details of the sent emails.
If you're sending an email and then immediately redirecting to another page, the web debug toolbar will
not display an email icon or a report on the next page.
Instead, you can set the intercept_redirects option to true in the config_dev.yml file, which will
cause the redirect to stop and allow you to open the report with details of the sent emails.
Alternatively, you can open the profiler after the redirect and search by the submit URL used
on previous request (e.g. /contact/handle). The profiler's search feature allows you to load the
profiler information for any past requests.
Listing 66-4
1 # app/config/config_dev.yml
2 web_profiler:
3
intercept_redirects: true
PDF brought to you by
generated on October 26, 2012
Chapter 66: How to Work with Emails During Development | 409
Chapter 67
How to Spool Email
When you are using the SwiftmailerBundle to send an email from a Symfony2 application, it will
default to sending the email immediately. You may, however, want to avoid the performance hit of the
communication between Swiftmailer and the email transport, which could cause the user to wait for
the next page to load while the email is sending. This can be avoided by choosing to "spool" the emails
instead of sending them directly. This means that Swiftmailer does not attempt to send the email but
instead saves the message to somewhere such as a file. Another process can then read from the spool and
take care of sending the emails in the spool. Currently only spooling to file is supported by Swiftmailer.
In order to use the spool, use the following configuration:
Listing 67-1
1 # app/config/config.yml
2 swiftmailer:
3
# ...
4
spool:
5
type: file
6
path: /path/to/spool
If you want to store the spool somewhere with your project directory, remember that you can use
the %kernel.root_dir% parameter to reference the project's root:
Listing 67-2
1 path: "%kernel.root_dir%/spool"
Now, when your app sends an email, it will not actually be sent but instead added to the spool. Sending
the messages from the spool is done separately. There is a console command to send the messages in the
spool:
Listing 67-3
1 $ php app/console swiftmailer:spool:send --env=prod
It has an option to limit the number of messages to be sent:
Listing 67-4
1 $ php app/console swiftmailer:spool:send --message-limit=10 --env=prod
PDF brought to you by
generated on October 26, 2012
Chapter 67: How to Spool Email | 410
You can also set the time limit in seconds:
Listing 67-5
1 $ php app/console swiftmailer:spool:send --time-limit=10 --env=prod
Of course you will not want to run this manually in reality. Instead, the console command should be
triggered by a cron job or scheduled task and run at a regular interval.
PDF brought to you by
generated on October 26, 2012
Chapter 67: How to Spool Email | 411
Chapter 68
How to simulate HTTP Authentication in a
Functional Test
If your application needs HTTP authentication, pass the username and password as server variables to
createClient():
Listing 68-1
1 $client = static::createClient(array(), array(
2
'PHP_AUTH_USER' => 'username',
3
'PHP_AUTH_PW'
=> 'pa$$word',
4 ));
You can also override it on a per request basis:
Listing 68-2
1 $client->request('DELETE', '/post/12', array(), array(), array(
2
'PHP_AUTH_USER' => 'username',
3
'PHP_AUTH_PW'
=> 'pa$$word',
4 ));
When your application is using a form_login, you can simplify your tests by allowing your test
configuration to make use of HTTP authentication. This way you can use the above to authenticate in
tests, but still have your users login via the normal form_login. The trick is to include the http_basic
key in your firewall, along with the form_login key:
Listing 68-3
1 # app/config/config_test.yml
2 security:
3
firewalls:
4
your_firewall_name:
5
http_basic:
PDF brought to you by
generated on October 26, 2012
Chapter 68: How to simulate HTTP Authentication in a Functional Test | 412
Chapter 69
How to test the Interaction of several Clients
If you need to simulate an interaction between different Clients (think of a chat for instance), create
several Clients:
Listing 69-1
1
2
3
4
5
6
7
8
$harry = static::createClient();
$sally = static::createClient();
$harry->request('POST', '/say/sally/Hello');
$sally->request('GET', '/messages');
$this->assertEquals(201, $harry->getResponse()->getStatusCode());
$this->assertRegExp('/Hello/', $sally->getResponse()->getContent());
This works except when your code maintains a global state or if it depends on a third-party library that
has some kind of global state. In such a case, you can insulate your clients:
Listing 69-2
1
2
3
4
5
6
7
8
9
10
11
$harry = static::createClient();
$sally = static::createClient();
$harry->insulate();
$sally->insulate();
$harry->request('POST', '/say/sally/Hello');
$sally->request('GET', '/messages');
$this->assertEquals(201, $harry->getResponse()->getStatusCode());
$this->assertRegExp('/Hello/', $sally->getResponse()->getContent());
Insulated clients transparently execute their requests in a dedicated and clean PHP process, thus avoiding
any side-effects.
As an insulated client is slower, you can keep one client in the main process, and insulate the other
ones.
PDF brought to you by
generated on October 26, 2012
Chapter 69: How to test the Interaction of several Clients | 413
Chapter 70
How to use the Profiler in a Functional Test
It's highly recommended that a functional test only tests the Response. But if you write functional tests
that monitor your production servers, you might want to write tests on the profiling data as it gives you
a great way to check various things and enforce some metrics.
The Symfony2 Profiler gathers a lot of data for each request. Use this data to check the number of
database calls, the time spent in the framework, ... But before writing assertions, always check that the
profiler is indeed available (it is enabled by default in the test environment):
Listing 70-1
1 class HelloControllerTest extends WebTestCase
2 {
3
public function testIndex()
4
{
5
$client = static::createClient();
6
$crawler = $client->request('GET', '/hello/Fabien');
7
8
// ... write some assertions about the Response
9
10
// Check that the profiler is enabled
11
if ($profile = $client->getProfile()) {
12
// check the number of requests
13
$this->assertLessThan(10, $profile->getCollector('db')->getQueryCount());
14
15
// check the time spent in the framework
16
$this->assertLessThan(0.5, $profile->getCollector('timer')->getTime());
17
}
18
}
19 }
If a test fails because of profiling data (too many DB queries for instance), you might want to use the Web
Profiler to analyze the request after the tests finish. It's easy to achieve if you embed the token in the error
message:
Listing 70-2
1 $this->assertLessThan(
2
30,
3
$profile->get('db')->getQueryCount(),
PDF brought to you by
generated on October 26, 2012
Chapter 70: How to use the Profiler in a Functional Test | 414
4
5 );
sprintf('Checks that query count is less than 30 (token %s)', $profile->getToken())
The profiler store can be different depending on the environment (especially if you use the SQLite
store, which is the default configured one).
The profiler information is available even if you insulate the client or if you use an HTTP layer for
your tests.
Read the API for built-in data collectors to learn more about their interfaces.
PDF brought to you by
generated on October 26, 2012
Chapter 70: How to use the Profiler in a Functional Test | 415
Chapter 71
How to test Doctrine Repositories
Unit testing Doctrine repositories in a Symfony project is not recommended. When you're dealing with
a repository, you're really dealing with something that's meant to be tested against a real database
connection.
Fortunately, you can easily test your queries against a real database, as described below.
Functional Testing
If you need to actually execute a query, you will need to boot the kernel to get a valid connection. In this
case, you'll extend the WebTestCase, which makes all of this quite easy:
Listing 71-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Acme/StoreBundle/Tests/Entity/ProductRepositoryFunctionalTest.php
namespace Acme\StoreBundle\Tests\Entity;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ProductRepositoryFunctionalTest extends WebTestCase
{
/**
* @var \Doctrine\ORM\EntityManager
*/
private $em;
public function setUp()
{
static::$kernel = static::createKernel();
static::$kernel->boot();
$this->em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
}
public function testSearchByCategoryName()
{
$products = $this->em
->getRepository('AcmeStoreBundle:Product')
PDF brought to you by
generated on October 26, 2012
Chapter 71: How to test Doctrine Repositories | 416
24
25
26
27
28
29 }
->searchByCategoryName('foo')
;
$this->assertCount(1, $products);
}
PDF brought to you by
generated on October 26, 2012
Chapter 71: How to test Doctrine Repositories | 417
Chapter 72
How to customize the Bootstrap Process before
running Tests
Sometimes when running tests, you need to do additional bootstrap work before running those tests. For
example, if you're running a functional test and have introduced a new translation resource, then you will
need to clear your cache before running those tests. This cookbook covers how to do that.
First, add the following file:
Listing 72-1
1
2
3
4
5
6
7
8
9
10
// app/tests.bootstrap.php
if (isset($_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'])) {
passthru(sprintf(
'php "%s/console" cache:clear --env=%s --no-warmup',
__DIR__,
$_ENV['BOOTSTRAP_CLEAR_CACHE_ENV']
));
}
require __DIR__.'/bootstrap.php.cache';
Replace the test bootstrap
tests.bootstrap.php:
Listing 72-2
file
bootstrap.php.cache
in
app/phpunit.xml.dist
with
1 <!-- app/phpunit.xml.dist -->
2 bootstrap = "tests.bootstrap.php"
Now, you can define in your phpunit.xml.dist file which environment you want the cache to be
cleared:
Listing 72-3
1 <!-- app/phpunit.xml.dist -->
2 <php>
3
<env name="BOOTSTRAP_CLEAR_CACHE_ENV" value="test"/>
4 </php>
PDF brought to you by
generated on October 26, 2012
Chapter 72: How to customize the Bootstrap Process before running Tests | 418
This now becomes an environment variable (i.e. $_ENV) that's available in the custom bootstrap file
(tests.bootstrap.php).
PDF brought to you by
generated on October 26, 2012
Chapter 72: How to customize the Bootstrap Process before running Tests | 419
Chapter 73
How to load Security Users from the Database
(the Entity Provider)
The security layer is one of the smartest tools of Symfony. It handles two things: the authentication
and the authorization processes. Although it may seem difficult to understand how it works internally,
the security system is very flexible and allows you to integrate your application with any authentication
backend, like Active Directory, an OAuth server or a database.
Introduction
This article focuses on how to authenticate users against a database table managed by a Doctrine entity
class. The content of this cookbook entry is split in three parts. The first part is about designing a
Doctrine User entity class and making it usable in the security layer of Symfony. The second part
describes how to easily authenticate a user with the Doctrine EntityUserProvider1 object bundled with
the framework and some configuration. Finally, the tutorial will demonstrate how to create a custom
EntityUserProvider2 object to retrieve users from a database with custom conditions.
This tutorial assumes there is a bootstrapped and loaded Acme\UserBundle bundle in the application
kernel.
The Data Model
For the purpose of this cookbook, the AcmeUserBundle bundle contains a User entity class with the
following fields: id, username, salt, password, email and isActive. The isActive field tells whether
or not the user account is active.
To make it shorter, the getter and setter methods for each have been removed to focus on the most
important methods that come from the UserInterface3.
1. http://api.symfony.com/2.0/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.html
2. http://api.symfony.com/2.0/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.html
3. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 420
Listing 73-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Acme\UserBundle\Entity\User
*
* @ORM\Table(name="acme_users")
* @ORM\Entity(repositoryClass="Acme\UserBundle\Entity\UserRepository")
*/
class User implements UserInterface
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", length=25, unique=true)
*/
private $username;
/**
* @ORM\Column(type="string", length=32)
*/
private $salt;
/**
* @ORM\Column(type="string", length=40)
*/
private $password;
/**
* @ORM\Column(type="string", length=60, unique=true)
*/
private $email;
/**
* @ORM\Column(name="is_active", type="boolean")
*/
private $isActive;
public function __construct()
{
$this->isActive = true;
$this->salt = md5(uniqid(null, true));
}
/**
* @inheritDoc
*/
public function getUsername()
{
return $this->username;
}
PDF brought to you by
generated on October 26, 2012
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 421
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99 }
/**
* @inheritDoc
*/
public function getSalt()
{
return $this->salt;
}
/**
* @inheritDoc
*/
public function getPassword()
{
return $this->password;
}
/**
* @inheritDoc
*/
public function getRoles()
{
return array('ROLE_USER');
}
/**
* @inheritDoc
*/
public function eraseCredentials()
{
}
/**
* @inheritDoc
*/
public function equals(UserInterface $user)
{
return $this->username === $user->getUsername();
}
In order to use an instance of the AcmeUserBundle:User class in the Symfony security layer, the entity
class must implement the UserInterface4. This interface forces the class to implement the six following
methods:
•
•
•
•
•
•
getUsername()
getSalt()
getPassword()
getRoles()
eraseCredentials()
equals()
For more details on each of these, see UserInterface5.
4. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserInterface.html
5. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 422
To keep it simple, the equals() method just compares the username field but it's also possible to do more
checks depending on the complexity of your data model. On the other hand, the eraseCredentials()
method remains empty as we don't care about it in this tutorial.
Below is an export of my User table from MySQL. For details on how to create user records and encode
their password, see Encoding the User's Password.
Listing 73-2
1
2
3
4
5
6
7
8
9
10
mysql> select * from user;
+----+----------+----------------------------------+------------------------------------------+-----------| id | username | salt
| password
| email
+----+----------+----------------------------------+------------------------------------------+-----------| 1 | hhamon | 7308e59b97f6957fb42d66f894793079 | 09610f61637408828a35d7debee5b38a8350eebe | hhamon@exam
| 2 | jsmith | ce617a6cca9126bf4036ca0c02e82dee | 8390105917f3a3d533815250ed7c64b4594d7ebf | jsmith@exam
| 3 | maxime | cd01749bb995dc658fa56ed45458d807 | 9764731e5f7fb944de5fd8efad4949b995b72a3c | maxime@exam
| 4 | donald | 6683c2bfd90c0426088402930cadd0f8 | 5c3bcec385f59edcc04490d1db95fdb8673bf612 | donald@exam
+----+----------+----------------------------------+------------------------------------------+-----------4 rows in set (0.00 sec)
The database now contains four users with different usernames, emails and statuses. The next part will
focus on how to authenticate one of these users thanks to the Doctrine entity user provider and a couple
of lines of configuration.
Authenticating Someone against a Database
Authenticating a Doctrine user against the database with the Symfony security layer is a piece of cake.
Everything resides in the configuration of the SecurityBundle stored in the app/config/security.yml
file.
Below is an example of configuration where the user will enter his/her username and password via
HTTP basic authentication. That information will then be checked against our User entity records in the
database:
Listing 73-3
1 # app/config/security.yml
2 security:
3
encoders:
4
Acme\UserBundle\Entity\User:
5
algorithm:
sha1
6
encode_as_base64: false
7
iterations:
1
8
9
role_hierarchy:
10
ROLE_ADMIN:
ROLE_USER
11
ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]
12
13
providers:
14
administrators:
15
entity: { class: AcmeUserBundle:User, property: username }
16
17
firewalls:
18
admin_area:
19
pattern:
^/admin
20
http_basic: ~
21
22
access_control:
23
- { path: ^/admin, roles: ROLE_ADMIN }
PDF brought to you by
generated on October 26, 2012
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 423
The encoders section associates the sha1 password encoder to the entity class. This means that Symfony
will expect the password that's stored in the database to be encoded using this algorithm. For details on
how to create a new User object with a properly encoded password, see the Encoding the User's Password
section of the security chapter.
The providers section defines an administrators user provider. A user provider is a "source" of where
users are loaded during authentication. In this case, the entity keyword means that Symfony will use
the Doctrine entity user provider to load User entity objects from the database by using the username
unique field. In other words, this tells Symfony how to fetch the user from the database before checking
the password validity.
This code and configuration works but it's not enough to secure the application for active users. As of
now, we still can authenticate with maxime. The next section explains how to forbid non active users.
Forbid non Active Users
The easiest way to exclude non active users is to implement the AdvancedUserInterface6 interface
that takes care of checking the user's account status. The AdvancedUserInterface7 extends the
UserInterface8 interface, so you just need to switch to the new interface in the AcmeUserBundle:User
entity class to benefit from simple and advanced authentication behaviors.
The AdvancedUserInterface9 interface adds four extra methods to validate the account status:
•
•
•
•
isAccountNonExpired() checks whether the user's account has expired,
isAccountNonLocked() checks whether the user is locked,
isCredentialsNonExpired() checks whether the user's credentials (password) has expired,
isEnabled() checks whether the user is enabled.
For this example, the first three methods will return true whereas the isEnabled() method will return
the boolean value in the isActive field.
Listing 73-4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Acme/UserBundle/Entity/User.php
namespace Acme\Bundle\UserBundle\Entity;
// ...
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
class User implements AdvancedUserInterface
{
// ...
public function isAccountNonExpired()
{
return true;
}
public function isAccountNonLocked()
{
return true;
}
public function isCredentialsNonExpired()
{
6. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/AdvancedUserInterface.html
7. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/AdvancedUserInterface.html
8. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserInterface.html
9. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/AdvancedUserInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 424
23
24
25
26
27
28
29
30 }
return true;
}
public function isEnabled()
{
return $this->isActive;
}
If we try to authenticate a maxime, the access is now forbidden as this user does not have an enabled
account. The next session will focus on how to write a custom entity provider to authenticate a user with
his username or his email address.
Authenticating Someone with a Custom Entity Provider
The next step is to allow a user to authenticate with his username or his email address as they are both
unique in the database. Unfortunately, the native entity provider is only able to handle a single property
to fetch the user from the database.
To accomplish this, create a custom entity provider that looks for a user whose username or email field
matches the submitted login username. The good news is that a Doctrine repository object can act as
an entity user provider if it implements the UserProviderInterface10. This interface comes with three
methods to implement: loadUserByUsername($username), refreshUser(UserInterface $user), and
supportsClass($class). For more details, see UserProviderInterface11.
The code below shows the implementation of the UserProviderInterface12 in the UserRepository
class:
Listing 73-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Acme/UserBundle/Entity/UserRepository.php
namespace Acme\UserBundle\Entity;
use
use
use
use
use
use
Symfony\Component\Security\Core\User\UserInterface;
Symfony\Component\Security\Core\User\UserProviderInterface;
Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
Symfony\Component\Security\Core\Exception\UnsupportedUserException;
Doctrine\ORM\EntityRepository;
Doctrine\ORM\NoResultException;
class UserRepository extends EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$q = $this
->createQueryBuilder('u')
->where('u.username = :username OR u.email = :email')
->setParameter('username', $username)
->setParameter('email', $username)
->getQuery()
;
try {
// The Query::getSingleResult() method throws an exception
10. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserProviderInterface.html
11. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserProviderInterface.html
12. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserProviderInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 425
25
// if there is no record matching the criteria.
26
$user = $q->getSingleResult();
27
} catch (NoResultException $e) {
28
throw new UsernameNotFoundException(sprintf('Unable to find an active admin
29 AcmeUserBundle:User object identified by "%s".', $username), null, 0, $e);
30
}
31
32
return $user;
33
}
34
35
public function refreshUser(UserInterface $user)
36
{
37
$class = get_class($user);
38
if (!$this->supportsClass($class)) {
39
throw new UnsupportedUserException(sprintf('Instances of "%s" are not
40 supported.', $class));
41
}
42
43
return $this->loadUserByUsername($user->getUsername());
44
}
45
46
public function supportsClass($class)
47
{
48
return $this->getEntityName() === $class || is_subclass_of($class,
$this->getEntityName());
}
}
To finish the implementation, the configuration of the security layer must be changed to tell Symfony to
use the new custom entity provider instead of the generic Doctrine entity provider. It's trival to achieve
by removing the property field in the security.providers.administrators.entity section of the
security.yml file.
Listing 73-6
1 # app/config/security.yml
2 security:
3
# ...
4
providers:
5
administrators:
6
entity: { class: AcmeUserBundle:User }
7
# ...
By doing this, the security layer will use an instance of UserRepository and call its
loadUserByUsername() method to fetch a user from the database whether he filled in his username or
email address.
Managing Roles in the Database
The end of this tutorial focuses on how to store and retrieve a list of roles from the database. As
mentioned previously, when your user is loaded, its getRoles() method returns the array of security
roles that should be assigned to the user. You can load this data from anywhere - a hardcoded list
used for all users (e.g. array('ROLE_USER')), a Doctrine array property called roles, or via a Doctrine
relationship, as we'll learn about in this section.
PDF brought to you by
generated on October 26, 2012
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 426
In a typical setup, you should always return at least 1 role from the getRoles() method. By
convention, a role called ROLE_USER is usually returned. If you fail to return any roles, it may appear
as if your user isn't authenticated at all.
In this example, the AcmeUserBundle:User entity class defines a many-to-many relationship with a
AcmeUserBundle:Group entity class. A user can be related to several groups and a group can be composed
of one or more users. As a group is also a role, the previous getRoles() method now returns the list of
related groups:
Listing 73-7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Acme/UserBundle/Entity/User.php
namespace Acme\Bundle\UserBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
// ...
class User implements AdvancedUserInterface
{
/**
* @ORM\ManyToMany(targetEntity="Group", inversedBy="users")
*
*/
private $groups;
public function __construct()
{
$this->groups = new ArrayCollection();
}
// ...
public function getRoles()
{
return $this->groups->toArray();
}
}
The AcmeUserBundle:Group entity class defines three table fields (id, name and role). The unique role
field contains the role name used by the Symfony security layer to secure parts of the application.
The most important thing to notice is that the AcmeUserBundle:Group entity class implements the
RoleInterface13 that forces it to have a getRole() method:
Listing 73-8
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Acme/Bundle/UserBundle/Entity/Group.php
namespace Acme\Bundle\UserBundle\Entity;
use Symfony\Component\Security\Core\Role\RoleInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="acme_groups")
* @ORM\Entity()
*/
class Group implements RoleInterface
{
13. http://api.symfony.com/2.0/Symfony/Component/Security/Core/Role/RoleInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 427
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 }
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="name", type="string", length=30)
*/
private $name;
/**
* @ORM\Column(name="role", type="string", length=20, unique=true)
*/
private $role;
/**
* @ORM\ManyToMany(targetEntity="User", mappedBy="groups")
*/
private $users;
public function __construct()
{
$this->users = new ArrayCollection();
}
// ... getters and setters for each property
/**
* @see RoleInterface
*/
public function getRole()
{
return $this->role;
}
To improve performances and avoid lazy loading of groups when retrieving a user from the custom
entity provider, the best solution is to join the groups relationship in the
UserRepository::loadUserByUsername() method. This will fetch the user and his associated roles /
groups with a single query:
Listing 73-9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/UserBundle/Entity/UserRepository.php
namespace Acme\Bundle\UserBundle\Entity;
// ...
class UserRepository extends EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$q = $this
->createQueryBuilder('u')
->select('u, g')
->leftJoin('u.groups', 'g')
->where('u.username = :username OR u.email = :email')
->setParameter('username', $username)
PDF brought to you by
generated on October 26, 2012
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 428
16
17
18
19
20
21
22
23 }
->setParameter('email', $username)
->getQuery();
// ...
}
// ...
The QueryBuilder::leftJoin() method joins and fetches related groups from
AcmeUserBundle:User model class when a user is retrieved with his email address or username.
PDF brought to you by
generated on October 26, 2012
the
Chapter 73: How to load Security Users from the Database (the Entity Provider) | 429
Chapter 74
How to add "Remember Me" Login
Functionality
Once a user is authenticated, their credentials are typically stored in the session. This means that when
the session ends they will be logged out and have to provide their login details again next time they wish
to access the application. You can allow users to choose to stay logged in for longer than the session lasts
using a cookie with the remember_me firewall option. The firewall needs to have a secret key configured,
which is used to encrypt the cookie's content. It also has several options with default values which are
shown here:
Listing 74-1
1 # app/config/security.yml
2 firewalls:
3
main:
4
remember_me:
5
key:
"%secret%"
6
lifetime: 31536000 # 365 days in seconds
7
path:
/
8
domain:
~ # Defaults to the current domain from $_SERVER
It's a good idea to provide the user with the option to use or not use the remember me functionality, as
it will not always be appropriate. The usual way of doing this is to add a checkbox to the login form. By
giving the checkbox the name _remember_me, the cookie will automatically be set when the checkbox is
checked and the user successfully logs in. So, your specific login form might ultimately look like this:
Listing 74-2
1
2
3
4
5
6
7
8
9
10
{# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
{% if error %}
<div>{{ error.message }}</div>
{% endif %}
<form action="{{ path('login_check') }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
PDF brought to you by
generated on October 26, 2012
Chapter 74: How to add "Remember Me" Login Functionality | 430
11
<input
12
13
<input
14
<label
15
16
<input
17 </form>
type="password" id="password" name="_password" />
type="checkbox" id="remember_me" name="_remember_me" checked />
for="remember_me">Keep me logged in</label>
type="submit" name="login" />
The user will then automatically be logged in on subsequent visits while the cookie remains valid.
Forcing the User to Re-authenticate before accessing certain Resources
When the user returns to your site, he/she is authenticated automatically based on the information stored
in the remember me cookie. This allows the user to access protected resources as if the user had actually
authenticated upon visiting the site.
In some cases, however, you may want to force the user to actually re-authenticate before accessing
certain resources. For example, you might allow "remember me" users to see basic account information,
but then require them to actually re-authenticate before modifying that information.
The security component provides an easy way to do this. In addition to roles explicitly assigned to them,
users are automatically given one of the following roles depending on how they are authenticated:
• IS_AUTHENTICATED_ANONYMOUSLY - automatically assigned to a user who is in a firewall
protected part of the site but who has not actually logged in. This is only possible if anonymous
access has been allowed.
• IS_AUTHENTICATED_REMEMBERED - automatically assigned to a user who was authenticated via
a remember me cookie.
• IS_AUTHENTICATED_FULLY - automatically assigned to a user that has provided their login
details during the current session.
You can use these to control access beyond the explicitly assigned roles.
If you have the IS_AUTHENTICATED_REMEMBERED role, then you also have the
IS_AUTHENTICATED_ANONYMOUSLY role. If you have the IS_AUTHENTICATED_FULLY role, then you
also have the other two roles. In other words, these roles represent three levels of increasing
"strength" of authentication.
You can use these additional roles for finer grained control over access to parts of a site. For example,
you may want your user to be able to view their account at /account when authenticated by cookie but
to have to provide their login details to be able to edit the account details. You can do this by securing
specific controller actions using these roles. The edit action in the controller could be secured using the
service context.
In the following example, the action is only allowed if the user has the IS_AUTHENTICATED_FULLY role.
Listing 74-3
1
2
3
4
5
6
7
8
9
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException
public function editAction()
{
if (false === $this->get('security.context')->isGranted(
'IS_AUTHENTICATED_FULLY'
)) {
throw new AccessDeniedException();
PDF brought to you by
generated on October 26, 2012
Chapter 74: How to add "Remember Me" Login Functionality | 431
10
11
12
13 }
}
// ...
You can also choose to install and use the optional JMSSecurityExtraBundle1, which can secure your
controller using annotations:
Listing 74-4
1
2
3
4
5
6
7
8
9
use JMS\SecurityExtraBundle\Annotation\Secure;
/**
* @Secure(roles="IS_AUTHENTICATED_FULLY")
*/
public function editAction($name)
{
// ...
}
If you also had an access control in your security configuration that required the user to have a
ROLE_USER role in order to access any of the account area, then you'd have the following situation:
• If a non-authenticated (or anonymously authenticated user) tries to access the account
area, the user will be asked to authenticate.
• Once the user has entered his username and password, assuming the user receives
the ROLE_USER role per your configuration, the user will have the
IS_AUTHENTICATED_FULLY role and be able to access any page in the account section,
including the editAction controller.
• If the user's session ends, when the user returns to the site, he will be able to access
every account page - except for the edit page - without being forced to re-authenticate.
However, when he tries to access the editAction controller, he will be forced to reauthenticate, since he is not, yet, fully authenticated.
For more information on securing services or methods in this way, see How to secure any Service or
Method in your Application.
1. https://github.com/schmittjoh/JMSSecurityExtraBundle
PDF brought to you by
generated on October 26, 2012
Chapter 74: How to add "Remember Me" Login Functionality | 432
Chapter 75
How to implement your own Voter to blacklist
IP Addresses
The Symfony2 security component provides several layers to authenticate users. One of the layers is
called a voter. A voter is a dedicated class that checks if the user has the rights to be connected to the
application. For instance, Symfony2 provides a layer that checks if the user is fully authenticated or if it
has some expected roles.
It is sometimes useful to create a custom voter to handle a specific case not handled by the framework.
In this section, you'll learn how to create a voter that will allow you to blacklist users by their IP.
The Voter Interface
A custom voter must implement VoterInterface1, which requires the following three methods:
Listing 75-1
1 interface VoterInterface
2 {
3
function supportsAttribute($attribute);
4
function supportsClass($class);
5
function vote(TokenInterface $token, $object, array $attributes);
6 }
The supportsAttribute() method is used to check if the voter supports the given user attribute (i.e: a
role, an acl, etc.).
The supportsClass() method is used to check if the voter supports the current user token class.
The vote() method must implement the business logic that verifies whether or not the user is granted
access. This method must return one of the following values:
• VoterInterface::ACCESS_GRANTED: The user is allowed to access the application
• VoterInterface::ACCESS_ABSTAIN: The voter cannot decide if the user is granted or not
• VoterInterface::ACCESS_DENIED: The user is not allowed to access the application
1. http://api.symfony.com/2.0/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 75: How to implement your own Voter to blacklist IP Addresses | 433
In this example, we will check if the user's IP address matches against a list of blacklisted addresses. If
the user's IP is blacklisted, we will return VoterInterface::ACCESS_DENIED, otherwise we will return
VoterInterface::ACCESS_ABSTAIN as this voter's purpose is only to deny access, not to grant access.
Creating a Custom Voter
To blacklist a user based on its IP, we can use the request service and compare the IP address against a
set of blacklisted IP addresses:
Listing 75-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/Acme/DemoBundle/Security/Authorization/Voter/ClientIpVoter.php
namespace Acme\DemoBundle\Security\Authorization\Voter;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class ClientIpVoter implements VoterInterface
{
public function __construct(ContainerInterface $container, array $blacklistedIp =
array())
{
$this->container
= $container;
$this->blacklistedIp = $blacklistedIp;
}
public function supportsAttribute($attribute)
{
// we won't check against a user attribute, so we return true
return true;
}
public function supportsClass($class)
{
// our voter supports all type of token classes, so we return true
return true;
}
function vote(TokenInterface $token, $object, array $attributes)
{
$request = $this->container->get('request');
if (in_array($request->getClientIp(), $this->blacklistedIp)) {
return VoterInterface::ACCESS_DENIED;
}
return VoterInterface::ACCESS_ABSTAIN;
}
}
That's it! The voter is done. The next step is to inject the voter into the security layer. This can be done
easily through the service container.
Declaring the Voter as a Service
To inject the voter into the security layer, we must declare it as a service, and tag it as a "security.voter":
Listing 75-3
PDF brought to you by
generated on October 26, 2012
Chapter 75: How to implement your own Voter to blacklist IP Addresses | 434
# src/Acme/AcmeBundle/Resources/config/services.yml
services:
security.access.blacklist_voter:
class:
Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter
arguments: [@service_container, [123.123.123.123, 171.171.171.171]]
public:
false
tags:
{ name: security.voter }
Be sure to import this configuration file from your main application configuration file (e.g. app/
config/config.yml). For more information see Importing Configuration with imports. To read
more about defining services in general, see the Service Container chapter.
Changing the Access Decision Strategy
In order for the new voter to take effect, we need to change the default access decision strategy, which,
by default, grants access if any voter grants access.
In our case, we will choose the unanimous strategy. Unlike the affirmative strategy (the default), with
the unanimous strategy, if only one voter denies access (e.g. the ClientIpVoter), access is not granted to
the end user.
To do that, override the default access_decision_manager section of your application configuration file
with the following code.
Listing 75-4
1 # app/config/security.yml
2 security:
3
access_decision_manager:
4
# Strategy can be: affirmative, unanimous or consensus
5
strategy: unanimous
That's it! Now, when deciding whether or not a user should have access, the new voter will deny access
to any user in the list of blacklisted IPs.
PDF brought to you by
generated on October 26, 2012
Chapter 75: How to implement your own Voter to blacklist IP Addresses | 435
Chapter 76
How to use Access Control Lists (ACLs)
In complex applications, you will often face the problem that access decisions cannot only be based
on the person (Token) who is requesting access, but also involve a domain object that access is being
requested for. This is where the ACL system comes in.
Imagine you are designing a blog system where your users can comment on your posts. Now, you want a
user to be able to edit his own comments, but not those of other users; besides, you yourself want to be
able to edit all comments. In this scenario, Comment would be our domain object that you want to restrict
access to. You could take several approaches to accomplish this using Symfony2, two basic approaches
are (non-exhaustive):
• Enforce security in your business methods: Basically, that means keeping a reference inside each
Comment to all users who have access, and then compare these users to the provided Token.
• Enforce security with roles: In this approach, you would add a role for each Comment object, i.e.
ROLE_COMMENT_1, ROLE_COMMENT_2, etc.
Both approaches are perfectly valid. However, they couple your authorization logic to your business code
which makes it less reusable elsewhere, and also increases the difficulty of unit testing. Besides, you could
run into performance issues if many users would have access to a single domain object.
Fortunately, there is a better way, which we will talk about now.
Bootstrapping
Now, before we finally can get into action, we need to do some bootstrapping. First, we need to configure
the connection the ACL system is supposed to use:
Listing 76-1
1 # app/config/security.yml
2 security:
3
acl:
4
connection: default
PDF brought to you by
generated on October 26, 2012
Chapter 76: How to use Access Control Lists (ACLs) | 436
The ACL system requires a connection from either Doctrine DBAL (usable by default) or Doctrine
MongoDB (usable with MongoDBAclBundle1). However, that does not mean that you have to use
Doctrine ORM or ODM for mapping your domain objects. You can use whatever mapper you like
for your objects, be it Doctrine ORM, MongoDB ODM, Propel, raw SQL, etc. The choice is yours.
After the connection is configured, we have to import the database structure. Fortunately, we have a task
for this. Simply run the following command:
Listing 76-2
1 $ php app/console init:acl
Getting Started
Coming back to our small example from the beginning, let's implement ACL for it.
Creating an ACL, and adding an ACE
Listing 76-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/Acme/DemoBundle/Controller/BlogController.php
namespace Acme\DemoBundle\Controller;
use
use
use
use
use
Symfony\Bundle\FrameworkBundle\Controller\Controller;
Symfony\Component\Security\Core\Exception\AccessDeniedException;
Symfony\Component\Security\Acl\Domain\ObjectIdentity;
Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
Symfony\Component\Security\Acl\Permission\MaskBuilder;
class BlogController
{
// ...
public function addCommentAction(Post $post)
{
$comment = new Comment();
// ... setup $form, and bind data
if ($form->isValid()) {
$entityManager = $this->get('doctrine.orm.default_entity_manager');
$entityManager->persist($comment);
$entityManager->flush();
// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$securityContext = $this->get('security.context');
$user = $securityContext->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// grant owner access
1. https://github.com/IamPersistent/MongoDBAclBundle
PDF brought to you by
generated on October 26, 2012
Chapter 76: How to use Access Control Lists (ACLs) | 437
36
37
38
39
40 }
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
}
}
There are a couple of important implementation decisions in this code snippet. For now, I only want to
highlight two:
First, you may have noticed that ->createAcl() does not accept domain objects directly, but only
implementations of the ObjectIdentityInterface. This additional step of indirection allows you to
work with ACLs even when you have no actual domain object instance at hand. This will be extremely
helpful if you want to check permissions for a large number of objects without actually hydrating these
objects.
The other interesting part is the ->insertObjectAce() call. In our example, we are granting the user who
is currently logged in owner access to the Comment. The MaskBuilder::MASK_OWNER is a pre-defined
integer bitmask; don't worry the mask builder will abstract away most of the technical details, but using
this technique we can store many different permissions in one database row which gives us a considerable
boost in performance.
The order in which ACEs are checked is significant. As a general rule, you should place more
specific entries at the beginning.
Checking Access
Listing 76-4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Acme/DemoBundle/Controller/BlogController.php
// ...
class BlogController
{
// ...
public function editCommentAction(Comment $comment)
{
$securityContext = $this->get('security.context');
// check for edit access
if (false === $securityContext->isGranted('EDIT', $comment))
{
throw new AccessDeniedException();
}
// ... retrieve actual comment object, and do your editing here
}
}
In this example, we check whether the user has the EDIT permission. Internally, Symfony2 maps the
permission to several integer bitmasks, and checks whether the user has any of them.
PDF brought to you by
generated on October 26, 2012
Chapter 76: How to use Access Control Lists (ACLs) | 438
You can define up to 32 base permissions (depending on your OS PHP might vary between 30 to
32). In addition, you can also define cumulative permissions.
Cumulative Permissions
In our first example above, we only granted the user the OWNER base permission. While this effectively
also allows the user to perform any operation such as view, edit, etc. on the domain object, there are
cases where we want to grant these permissions explicitly.
The MaskBuilder can be used for creating bit masks easily by combining several base permissions:
Listing 76-5
1
2
3
4
5
6
7
8
$builder = new MaskBuilder();
$builder
->add('view')
->add('edit')
->add('delete')
->add('undelete')
;
$mask = $builder->get(); // int(29)
This integer bitmask can then be used to grant a user the base permissions you added above:
Listing 76-6
1 $identity = new UserSecurityIdentity('johannes', 'Acme\UserBundle\Entity\User');
2 $acl->insertObjectAce($identity, $mask);
The user is now allowed to view, edit, delete, and un-delete objects.
PDF brought to you by
generated on October 26, 2012
Chapter 76: How to use Access Control Lists (ACLs) | 439
Chapter 77
How to use Advanced ACL Concepts
The aim of this chapter is to give a more in-depth view of the ACL system, and also explain some of the
design decisions behind it.
Design Concepts
Symfony2's object instance security capabilities are based on the concept of an Access Control List. Every
domain object instance has its own ACL. The ACL instance holds a detailed list of Access Control
Entries (ACEs) which are used to make access decisions. Symfony2's ACL system focuses on two main
objectives:
• providing a way to efficiently retrieve a large amount of ACLs/ACEs for your domain objects,
and to modify them;
• providing a way to easily make decisions of whether a person is allowed to perform an action
on a domain object or not.
As indicated by the first point, one of the main capabilities of Symfony2's ACL system is a highperformance way of retrieving ACLs/ACEs. This is extremely important since each ACL might have
several ACEs, and inherit from another ACL in a tree-like fashion. Therefore, we specifically do not
leverage any ORM, but the default implementation interacts with your connection directly using
Doctrine's DBAL.
Object Identities
The ACL system is completely decoupled from your domain objects. They don't even have to be stored
in the same database, or on the same server. In order to achieve this decoupling, in the ACL system your
objects are represented through object identity objects. Everytime you want to retrieve the ACL for a
domain object, the ACL system will first create an object identity from your domain object, and then pass
this object identity to the ACL provider for further processing.
Security Identities
This is analog to the object identity, but represents a user, or a role in your application. Each role, or user
has its own security identity.
PDF brought to you by
generated on October 26, 2012
Chapter 77: How to use Advanced ACL Concepts | 440
Database Table Structure
The default implementation uses five database tables as listed below. The tables are ordered from least
rows to most rows in a typical application:
• acl_security_identities: This table records all security identities (SID) which hold ACEs. The
default implementation ships with two security identities: RoleSecurityIdentity, and
UserSecurityIdentity
• acl_classes: This table maps class names to a unique id which can be referenced from other
tables.
• acl_object_identities: Each row in this table represents a single domain object instance.
• acl_object_identity_ancestors: This table allows us to determine all the ancestors of an ACL in
a very efficient way.
• acl_entries: This table contains all ACEs. This is typically the table with the most rows. It can
contain tens of millions without significantly impacting performance.
Scope of Access Control Entries
Access control entries can have different scopes in which they apply. In Symfony2, we have basically two
different scopes:
• Class-Scope: These entries apply to all objects with the same class.
• Object-Scope: This was the scope we solely used in the previous chapter, and it only applies to
one specific object.
Sometimes, you will find the need to apply an ACE only to a specific field of the object. Let's say you want
the ID only to be viewable by an administrator, but not by your customer service. To solve this common
problem, we have added two more sub-scopes:
• Class-Field-Scope: These entries apply to all objects with the same class, but only to a specific
field of the objects.
• Object-Field-Scope: These entries apply to a specific object, and only to a specific field of that
object.
Pre-Authorization Decisions
For pre-authorization decisions, that is decisions made before any secure method (or secure action)
is invoked, we rely on the proven AccessDecisionManager service that is also used for reaching
authorization decisions based on roles. Just like roles, the ACL system adds several new attributes which
may be used to check for different permissions.
Built-in Permission Map
Attribute
Intended Meaning
Integer Bitmasks
VIEW
Whether someone is allowed to
view the domain object.
VIEW, EDIT, OPERATOR,
MASTER, or OWNER
EDIT
Whether someone is allowed to
EDIT, OPERATOR, MASTER, or
make changes to the domain object. OWNER
CREATE
Whether someone is allowed to
create the domain object.
PDF brought to you by
generated on October 26, 2012
CREATE, OPERATOR, MASTER, or
OWNER
Chapter 77: How to use Advanced ACL Concepts | 441
Attribute
Intended Meaning
Integer Bitmasks
DELETE
Whether someone is allowed to
delete the domain object.
DELETE, OPERATOR, MASTER, or
OWNER
UNDELETE
Whether someone is allowed to
restore a previously deleted domain
object.
UNDELETE, OPERATOR,
MASTER, or OWNER
OPERATOR
Whether someone is allowed to
perform all of the above actions.
OPERATOR, MASTER, or OWNER
MASTER
Whether someone is allowed to
perform all of the above actions,
and in addition is allowed to grant
any of the above permissions to
others.
MASTER, or OWNER
OWNER
Whether someone owns the
domain object. An owner can
perform any of the above actions
and grant master and owner
permissions.
OWNER
Permission Attributes vs. Permission Bitmasks
Attributes are used by the AccessDecisionManager, just like roles. Often, these attributes represent in
fact an aggregate of integer bitmasks. Integer bitmasks on the other hand, are used by the ACL system
internally to efficiently store your users' permissions in the database, and perform access checks using
extremely fast bitmask operations.
Extensibility
The above permission map is by no means static, and theoretically could be completely replaced at will.
However, it should cover most problems you encounter, and for interoperability with other bundles, we
encourage you to stick to the meaning we have envisaged for them.
Post Authorization Decisions
Post authorization decisions are made after a secure method has been invoked, and typically involve the
domain object which is returned by such a method. After invocation providers also allow to modify, or
filter the domain object before it is returned.
Due to current limitations of the PHP language, there are no post-authorization capabilities build into the
core Security component. However, there is an experimental JMSSecurityExtraBundle1 which adds these
capabilities. See its documentation for further information on how this is accomplished.
Process for Reaching Authorization Decisions
The ACL class provides two methods for determining whether a security identity has the required
bitmasks, isGranted and isFieldGranted. When the ACL receives an authorization request through
one of these methods, it delegates this request to an implementation of PermissionGrantingStrategy. This
1. https://github.com/schmittjoh/JMSSecurityExtraBundle
PDF brought to you by
generated on October 26, 2012
Chapter 77: How to use Advanced ACL Concepts | 442
allows you to replace the way access decisions are reached without actually modifying the ACL class
itself.
The PermissionGrantingStrategy first checks all your object-scope ACEs if none is applicable, the classscope ACEs will be checked, if none is applicable, then the process will be repeated with the ACEs of the
parent ACL. If no parent ACL exists, an exception will be thrown.
PDF brought to you by
generated on October 26, 2012
Chapter 77: How to use Advanced ACL Concepts | 443
Chapter 78
How to force HTTPS or HTTP for Different URLs
You can force areas of your site to use the HTTPS protocol in the security config. This is done through the
access_control rules using the requires_channel option. For example, if you want to force all URLs
starting with /secure to use HTTPS then you could use the following configuration:
Listing 78-1
1 access_control:
2
- path: ^/secure
3
roles: ROLE_ADMIN
4
requires_channel: https
The login form itself needs to allow anonymous access, otherwise users will be unable to authenticate.
To force it to use HTTPS you can still use access_control rules by using the
IS_AUTHENTICATED_ANONYMOUSLY role:
Listing 78-2
1 access_control:
2
- path: ^/login
3
roles: IS_AUTHENTICATED_ANONYMOUSLY
4
requires_channel: https
It is also possible to specify using HTTPS in the routing configuration see How to force routes to always use
HTTPS or HTTP for more details.
PDF brought to you by
generated on October 26, 2012
Chapter 78: How to force HTTPS or HTTP for Different URLs | 444
Chapter 79
How to customize your Form Login
Using a form login for authentication is a common, and flexible, method for handling authentication in
Symfony2. Pretty much every aspect of the form login can be customized. The full, default configuration
is shown in the next section.
Form Login Configuration Reference
Listing 79-1
1 # app/config/security.yml
2 security:
3
firewalls:
4
main:
5
form_login:
6
# the user is redirected here when he/she needs to login
7
login_path:
/login
8
9
# if true, forward the user to the login form instead of redirecting
10
use_forward:
false
11
12
# submit the login form here
13
check_path:
/login_check
14
15
# by default, the login form *must* be a POST, not a GET
16
post_only:
true
17
18
# login success redirecting options (read further below)
19
always_use_default_target_path: false
20
default_target_path:
/
21
target_path_parameter:
_target_path
22
use_referer:
false
23
24
# login failure redirecting options (read further below)
25
failure_path:
null
26
failure_forward:
false
27
PDF brought to you by
generated on October 26, 2012
Chapter 79: How to customize your Form Login | 445
28
29
30
31
32
33
34
# field names for the username and password fields
username_parameter:
_username
password_parameter:
_password
# csrf token options
csrf_parameter:
intention:
_csrf_token
authenticate
Redirecting after Success
You can change where the login form redirects after a successful login using the various config options.
By default the form will redirect to the URL the user requested (i.e. the URL which triggered the login
form being shown). For example, if the user requested http://www.example.com/admin/post/18/edit,
then after she successfully logs in, she will eventually be sent back to http://www.example.com/admin/
post/18/edit. This is done by storing the requested URL in the session. If no URL is present in the
session (perhaps the user went directly to the login page), then the user is redirected to the default page,
which is / (i.e. the homepage) by default. You can change this behavior in several ways.
Changing the Default Page
First, the default page can be set (i.e. the page the user is redirected to if no previous page was stored in
the session). To set it to /admin use the following config:
Listing 79-2
1 # app/config/security.yml
2 security:
3
firewalls:
4
main:
5
form_login:
6
# ...
7
default_target_path: /admin
Now, when no URL is set in the session, users will be sent to /admin.
Always Redirect to the Default Page
You can make it so that users are always redirected to the default page regardless of what URL they had
requested previously by setting the always_use_default_target_path option to true:
Listing 79-3
1 # app/config/security.yml
2 security:
3
firewalls:
4
main:
5
form_login:
6
# ...
7
always_use_default_target_path: true
Using the Referring URL
In case no previous URL was stored in the session, you may wish to try using the HTTP_REFERER instead,
as this will often be the same. You can do this by setting use_referer to true (it defaults to false):
Listing 79-4
PDF brought to you by
generated on October 26, 2012
Chapter 79: How to customize your Form Login | 446
1 # app/config/security.yml
2 security:
3
firewalls:
4
main:
5
form_login:
6
# ...
7
use_referer:
true
Control the Redirect URL from inside the Form
You can also override where the user is redirected to via the form itself by including a hidden field with
the name _target_path. For example, to redirect to the URL defined by some account route, use the
following:
Listing 79-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
{% if error %}
<div>{{ error.message }}</div>
{% endif %}
<form action="{{ path('login_check') }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
<input type="hidden" name="_target_path" value="account" />
<input type="submit" name="login" />
</form>
Now, the user will be redirected to the value of the hidden form field. The value attribute can be a relative
path, absolute URL, or a route name. You can even change the name of the hidden form field by changing
the target_path_parameter option to another value.
Listing 79-6
1 # app/config/security.yml
2 security:
3
firewalls:
4
main:
5
form_login:
6
target_path_parameter: redirect_url
Redirecting on Login Failure
In addition to redirecting the user after a successful login, you can also set the URL that the user should
be redirected to after a failed login (e.g. an invalid username or password was submitted). By default,
the user is redirected back to the login form itself. You can set this to a different URL with the following
config:
Listing 79-7
1 # app/config/security.yml
2 security:
3
firewalls:
4
main:
5
form_login:
PDF brought to you by
generated on October 26, 2012
Chapter 79: How to customize your Form Login | 447
6
7
# ...
failure_path: /login_failure
PDF brought to you by
generated on October 26, 2012
Chapter 79: How to customize your Form Login | 448
Chapter 80
How to secure any Service or Method in your
Application
In the security chapter, you can see how to secure a controller by requesting the security.context
service from the Service Container and checking the current user's role:
Listing 80-1
1
2
3
4
5
6
7
8
9
10
11
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
public function helloAction($name)
{
if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
// ...
}
You can also secure any service in a similar way by injecting the security.context service into it. For
a general introduction to injecting dependencies into services see the Service Container chapter of the
book. For example, suppose you have a NewsletterManager class that sends out emails and you want to
restrict its use to only users who have some ROLE_NEWSLETTER_ADMIN role. Before you add security, the
class looks something like this:
Listing 80-2
1
2
3
4
5
6
7
8
9
10
// src/Acme/HelloBundle/Newsletter/NewsletterManager.php
namespace Acme\HelloBundle\Newsletter;
class NewsletterManager
{
public function sendNewsletter()
{
// ... where you actually do the work
}
PDF brought to you by
generated on October 26, 2012
Chapter 80: How to secure any Service or Method in your Application | 449
11
12
13 }
// ...
Your goal is to check the user's role when the sendNewsletter() method is called. The first step towards
this is to inject the security.context service into the object. Since it won't make sense not to perform
the security check, this is an ideal candidate for constructor injection, which guarantees that the security
context object will be available inside the NewsletterManager class:
Listing 80-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace Acme\HelloBundle\Newsletter;
use Symfony\Component\Security\Core\SecurityContextInterface;
class NewsletterManager
{
protected $securityContext;
public function __construct(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
// ...
}
Then in your service configuration, you can inject the service:
Listing 80-4
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager
services:
newsletter_manager:
class:
"%newsletter_manager.class%"
arguments: [@security.context]
The injected service can then be used to perform the security check when the sendNewsletter() method
is called:
Listing 80-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Acme\HelloBundle\Newsletter;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\SecurityContextInterface;
// ...
class NewsletterManager
{
protected $securityContext;
public function __construct(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
public function sendNewsletter()
{
if (false === $this->securityContext->isGranted('ROLE_NEWSLETTER_ADMIN')) {
PDF brought to you by
generated on October 26, 2012
Chapter 80: How to secure any Service or Method in your Application | 450
19
20
21
22
23
24
25
26 }
throw new AccessDeniedException();
}
// ...
}
// ...
If the current user does not have the ROLE_NEWSLETTER_ADMIN, they will be prompted to log in.
Securing Methods Using Annotations
You can also secure method calls in any service with annotations by using the optional
JMSSecurityExtraBundle1 bundle. This bundle is included in the Symfony2 Standard Distribution.
To enable the annotations functionality, tag the service you want to secure with the
security.secure_service tag (you can also automatically enable this functionality for all services, see
the sidebar below):
Listing 80-6
1 # src/Acme/HelloBundle/Resources/config/services.yml
2 # ...
3
4 services:
5
newsletter_manager:
6
# ...
7
tags:
8
- { name: security.secure_service }
You can then achieve the same results as above using an annotation:
Listing 80-7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Acme\HelloBundle\Newsletter;
use JMS\SecurityExtraBundle\Annotation\Secure;
// ...
class NewsletterManager
{
/**
* @Secure(roles="ROLE_NEWSLETTER_ADMIN")
*/
public function sendNewsletter()
{
// ...
}
// ...
}
1. https://github.com/schmittjoh/JMSSecurityExtraBundle
PDF brought to you by
generated on October 26, 2012
Chapter 80: How to secure any Service or Method in your Application | 451
The annotations work because a proxy class is created for your class which performs the security
checks. This means that, whilst you can use annotations on public and protected methods, you
cannot use them with private methods or methods marked final.
The JMSSecurityExtraBundle also allows you to secure the parameters and return values of methods.
For more information, see the JMSSecurityExtraBundle2 documentation.
Activating the Annotations Functionality for all Services
When securing the method of a service (as shown above), you can either tag each service
individually, or activate the functionality for all services at once. To do so, set the
secure_all_services configuration option to true:
Listing 80-8
1 # app/config/config.yml
2 jms_security_extra:
3
# ...
4
secure_all_services: true
The disadvantage of this method is that, if activated, the initial page load may be very slow
depending on how many services you have defined.
2. https://github.com/schmittjoh/JMSSecurityExtraBundle
PDF brought to you by
generated on October 26, 2012
Chapter 80: How to secure any Service or Method in your Application | 452
Chapter 81
How to create a custom User Provider
Part of Symfony's standard authentication process depends on "user providers". When a user submits a
username and password, the authentication layer asks the configured user provider to return a user object
for a given username. Symfony then checks whether the password of this user is correct and generates a
security token so the user stays authenticated during the current session. Out of the box, Symfony has
an "in_memory" and an "entity" user provider. In this entry we'll see how you can create your own user
provider, which could be useful if your users are accessed via a custom database, a file, or - as we show
in this example - a web service.
Create a User Class
First, regardless of where your user data is coming from, you'll need to create a User class that represents
that data. The User can look however you want and contain any data. The only requirement is that
the class implements UserInterface1. The methods in this interface should therefore be defined in
the custom user class: getRoles(), getPassword(), getSalt(), getUsername(), eraseCredentials(),
equals().
Let's see this in action:
Listing 81-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/WebserviceUserBundle/Security/User/WebserviceUser.php
namespace Acme\WebserviceUserBundle\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
class WebserviceUser implements UserInterface
{
private $username;
private $password;
private $salt;
private $roles;
public function __construct($username, $password, $salt, array $roles)
{
1. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 81: How to create a custom User Provider | 453
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 }
$this->username = $username;
$this->password = $password;
$this->salt = $salt;
$this->roles = $roles;
}
public function getRoles()
{
return $this->roles;
}
public function getPassword()
{
return $this->password;
}
public function getSalt()
{
return $this->salt;
}
public function getUsername()
{
return $this->username;
}
public function eraseCredentials()
{
}
public function equals(UserInterface $user)
{
if (!$user instanceof WebserviceUser) {
return false;
}
if ($this->password !== $user->getPassword()) {
return false;
}
if ($this->getSalt() !== $user->getSalt()) {
return false;
}
if ($this->username !== $user->getUsername()) {
return false;
}
return true;
}
If you have more information about your users - like a "first name" - then you can add a firstName field
to hold that data.
For more details on each of the methods, see UserInterface2.
2. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 81: How to create a custom User Provider | 454
Create a User Provider
Now that we have a User class, we'll create a user provider, which will grab user information from some
web service, create a WebserviceUser object, and populate it with data.
The user provider is just a plain PHP class that has to implement the UserProviderInterface3, which
requires three methods to be defined: loadUserByUsername($username), refreshUser(UserInterface
$user), and supportsClass($class). For more details, see UserProviderInterface4.
Here's an example of how this might look:
Listing 81-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// src/Acme/WebserviceUserBundle/Security/User/WebserviceUserProvider.php
namespace Acme\WebserviceUserBundle\Security\User;
use
use
use
use
Symfony\Component\Security\Core\User\UserProviderInterface;
Symfony\Component\Security\Core\User\UserInterface;
Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class WebserviceUserProvider implements UserProviderInterface
{
public function loadUserByUsername($username)
{
// make a call to your webservice here
$userData = ...
// pretend it returns an array on success, false if there is no user
if ($userData) {
$password = '...';
// ...
return new WebserviceUser($username, $password, $salt, $roles)
}
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.',
$username));
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof WebserviceUser) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not
supported.', get_class($user)));
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'Acme\WebserviceUserBundle\Security\User\WebserviceUser';
}
}
3. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserProviderInterface.html
4. http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserProviderInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 81: How to create a custom User Provider | 455
Create a Service for the User Provider
Now we make the user provider available as a service.
Listing 81-3
1
2
3
4
5
6
7
# src/Acme/WebserviceUserBundle/Resources/config/services.yml
parameters:
webservice_user_provider.class:
Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider
services:
webservice_user_provider:
class: "%webservice_user_provider.class%"
The real implementation of the user provider will probably have some dependencies or
configuration options or other services. Add these as arguments in the service definition.
Make sure the services file is being imported. See Importing Configuration with imports for details.
Modify security.yml
In /app/config/security.yml everything comes together. Add the user provider to the list of providers
in the "security" section. Choose a name for the user provider (e.g. "webservice") and mention the id of
the service you just defined.
Listing 81-4
1 security:
2
providers:
3
webservice:
4
id: webservice_user_provider
Symfony also needs to know how to encode passwords that are supplied by website users, e.g. by filling in
a login form. You can do this by adding a line to the "encoders" section in /app/config/security.yml.
Listing 81-5
1 security:
2
encoders:
3
Acme\WebserviceUserBundle\Security\User\WebserviceUser: sha512
The value here should correspond with however the passwords were originally encoded when creating
your users (however those users were created). When a user submits her password, the password
is appended to the salt value and then encoded using this algorithm before being compared to the
hashed password returned by your getPassword() method. Additionally, depending on your options,
the password may be encoded multiple times and encoded to base64.
PDF brought to you by
generated on October 26, 2012
Chapter 81: How to create a custom User Provider | 456
Specifics on how passwords are encoded
Symfony uses a specific method to combine the salt and encode the password before comparing it
to your encoded password. If getSalt() returns nothing, then the submitted password is simply
encoded using the algorithm you specify in security.yml. If a salt is specified, then the following
value is created and then hashed via the algorithm:
$password.'{'.$salt.'}';
If your external users have their passwords salted via a different method, then you'll need to do
a bit more work so that Symfony properly encodes the password. That is beyond the scope of
this entry, but would include sub-classing MessageDigestPasswordEncoder and overriding the
mergePasswordAndSalt method.
Additionally, the hash, by default, is encoded multiple times and encoded to base64. For specific
details, see MessageDigestPasswordEncoder5. To prevent this, configure it in security.yml:
Listing 81-6
1 security:
2
encoders:
3
Acme\WebserviceUserBundle\Security\User\WebserviceUser:
4
algorithm: sha512
5
encode_as_base64: false
6
iterations: 1
5. https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/MessageDigestPasswordEncoder.php
PDF brought to you by
generated on October 26, 2012
Chapter 81: How to create a custom User Provider | 457
Chapter 82
How to create a custom Authentication
Provider
If you have read the chapter on Security, you understand the distinction Symfony2 makes between
authentication and authorization in the implementation of security. This chapter discusses the core
classes involved in the authentication process, and how to implement a custom authentication provider.
Because authentication and authorization are separate concepts, this extension will be user-provider
agnostic, and will function with your application's user providers, may they be based in memory, a
database, or wherever else you choose to store them.
Meet WSSE
The following chapter demonstrates how to create a custom authentication provider for WSSE
authentication. The security protocol for WSSE provides several security benefits:
1. Username / Password encryption
2. Safe guarding against replay attacks
3. No web server configuration required
WSSE is very useful for the securing of web services, may they be SOAP or REST.
There is plenty of great documentation on WSSE1, but this article will focus not on the security protocol,
but rather the manner in which a custom protocol can be added to your Symfony2 application. The basis
of WSSE is that a request header is checked for encrypted credentials, verified using a timestamp and
nonce2, and authenticated for the requested user using a password digest.
WSSE also supports application key validation, which is useful for web services, but is outside the
scope of this chapter.
1. http://www.xml.com/pub/a/2003/12/17/dive.html
2. http://en.wikipedia.org/wiki/Cryptographic_nonce
PDF brought to you by
generated on October 26, 2012
Chapter 82: How to create a custom Authentication Provider | 458
The Token
The role of the token in the Symfony2 security context is an important one. A token represents the user
authentication data present in the request. Once a request is authenticated, the token retains the user's
data, and delivers this data across the security context. First, we will create our token class. This will
allow the passing of all relevant information to our authentication provider.
Listing 82-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Acme/DemoBundle/Security/Authentication/Token/WsseUserToken.php
namespace Acme\DemoBundle\Security\Authentication\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class WsseUserToken extends AbstractToken
{
public $created;
public $digest;
public $nonce;
public function __construct(array $roles = array())
{
parent::__construct($roles);
// If the user has roles, consider it authenticated
$this->setAuthenticated(count($roles) > 0);
}
public function getCredentials()
{
return '';
}
}
The WsseUserToken class extends the security component's AbstractToken3 class, which provides
basic token functionality. Implement the TokenInterface4 on any class to use as a token.
The Listener
Next, you need a listener to listen on the security context. The listener is responsible for fielding
requests to the firewall and calling the authentication provider. A listener must be an instance of
ListenerInterface5. A security listener should handle the GetResponseEvent6 event, and set an
authenticated token in the security context if successful.
Listing 82-2
1
2
3
4
5
6
// src/Acme/DemoBundle/Security/Firewall/WsseListener.php
namespace Acme\DemoBundle\Security\Firewall;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
3. http://api.symfony.com/2.0/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.html
4. http://api.symfony.com/2.0/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.html
5. http://api.symfony.com/2.0/Symfony/Component/Security/Http/Firewall/ListenerInterface.html
6. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Event/GetResponseEvent.html
PDF brought to you by
generated on October 26, 2012
Chapter 82: How to create a custom Authentication Provider | 459
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
use
use
use
use
Symfony\Component\Security\Core\Exception\AuthenticationException;
Symfony\Component\Security\Core\SecurityContextInterface;
Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
Acme\DemoBundle\Security\Authentication\Token\WsseUserToken;
class WsseListener implements ListenerInterface
{
protected $securityContext;
protected $authenticationManager;
public function __construct(SecurityContextInterface $securityContext,
AuthenticationManagerInterface $authenticationManager)
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)",
Nonce="([^"]+)", Created="([^"]+)"/';
if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex,
$request->headers->get('x-wsse'), $matches)) {
return;
}
$token = new WsseUserToken();
$token->setUser($matches[1]);
$token->digest
$token->nonce
$token->created
= $matches[2];
= $matches[3];
= $matches[4];
try {
$authToken = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($authToken);
} catch (AuthenticationException $failed) {
// ... you might log something here
// To deny the authentication clear the token. This will redirect to the login
page.
// $this->securityContext->setToken(null);
// return;
// Deny authentication with a '403 Forbidden' HTTP response
$response = new Response();
$response->setStatusCode(403);
$event->setResponse($response);
}
}
}
This listener checks the request for the expected X-WSSE header, matches the value returned for the
expected WSSE information, creates a token using that information, and passes the token on to the
PDF brought to you by
generated on October 26, 2012
Chapter 82: How to create a custom Authentication Provider | 460
authentication manager. If the proper information is not provided, or the authentication manager throws
an AuthenticationException7, a 403 Response is returned.
A class not used above, the AbstractAuthenticationListener8 class, is a very useful base class
which provides commonly needed functionality for security extensions. This includes maintaining
the token in the session, providing success / failure handlers, login form urls, and more. As WSSE
does not require maintaining authentication sessions or login forms, it won't be used for this
example.
The Authentication Provider
The authentication provider will do the verification of the WsseUserToken. Namely, the provider will
verify the Created header value is valid within five minutes, the Nonce header value is unique within five
minutes, and the PasswordDigest header value matches with the user's password.
Listing 82-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/Acme/DemoBundle/Security/Authentication/Provider/WsseProvider.php
namespace Acme\DemoBundle\Security\Authentication\Provider;
use
Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Acme\DemoBundle\Security\Authentication\Token\WsseUserToken;
class WsseProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cacheDir;
public function __construct(UserProviderInterface $userProvider, $cacheDir)
{
$this->userProvider = $userProvider;
$this->cacheDir
= $cacheDir;
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsername($token->getUsername());
if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created,
$user->getPassword())) {
$authenticatedToken = new WsseUserToken($user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The WSSE authentication failed.');
}
protected function validateDigest($digest, $nonce, $created, $secret)
7. http://api.symfony.com/2.0/Symfony/Component/Security/Core/Exception/AuthenticationException.html
8. http://api.symfony.com/2.0/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.html
PDF brought to you by
generated on October 26, 2012
Chapter 82: How to create a custom Authentication Provider | 461
39
{
40
// Expire timestamp after 5 minutes
41
if (time() - strtotime($created) > 300) {
42
return false;
43
}
44
45
// Validate nonce is unique within 5 minutes
46
if (file_exists($this->cacheDir.'/'.$nonce) &&
47 file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) {
48
throw new NonceExpiredException('Previously used nonce detected');
49
}
50
file_put_contents($this->cacheDir.'/'.$nonce, time());
51
52
// Validate Secret
53
$expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));
54
55
return $digest === $expected;
56
}
57
58
public function supports(TokenInterface $token)
59
{
return $token instanceof WsseUserToken;
}
}
The AuthenticationProviderInterface9 requires an authenticate method on the user token,
and a supports method, which tells the authentication manager whether or not to use this
provider for the given token. In the case of multiple providers, the authentication manager will
then move to the next provider in the list.
The Factory
You have created a custom token, custom listener, and custom provider. Now you need to tie them all
together. How do you make your provider available to your security configuration? The answer is by
using a factory. A factory is where you hook into the security component, telling it the name of your
provider and any configuration options available for it. First, you must create a class which implements
SecurityFactoryInterface10.
Listing 82-4
1
2
3
4
5
6
7
8
9
10
11
12
// src/Acme/DemoBundle/DependencyInjection/Security/Factory/WsseFactory.php
namespace Acme\DemoBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use
Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
class WsseFactory implements SecurityFactoryInterface
{
9. http://api.symfony.com/2.0/Symfony/Component/Security/Core/Authentication/Provider/AuthenticationProviderInterface.html
10. http://api.symfony.com/2.0/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 82: How to create a custom Authentication Provider | 462
13
public function create(ContainerBuilder $container, $id, $config, $userProvider,
14 $defaultEntryPoint)
15
{
16
$providerId = 'security.authentication.provider.wsse.'.$id;
17
$container
18
->setDefinition($providerId, new
19 DefinitionDecorator('wsse.security.authentication.provider'))
20
->replaceArgument(0, new Reference($userProvider))
21
;
22
23
$listenerId = 'security.authentication.listener.wsse.'.$id;
24
$listener = $container->setDefinition($listenerId, new
25 DefinitionDecorator('wsse.security.authentication.listener'));
26
27
return array($providerId, $listenerId, $defaultEntryPoint);
28
}
29
30
public function getPosition()
31
{
32
return 'pre_auth';
33
}
34
35
public function getKey()
36
{
37
return 'wsse';
38
}
39
public function addConfiguration(NodeDefinition $node)
{
}
}
The SecurityFactoryInterface11 requires the following methods:
• create method, which adds the listener and authentication provider to the DI container for
the appropriate security context;
• getPosition method, which must be of type pre_auth, form, http, and remember_me and
defines the position at which the provider is called;
• getKey method which defines the configuration key used to reference the provider;
• addConfiguration method, which is used to define the configuration options underneath the
configuration key in your security configuration. Setting configuration options are explained
later in this chapter.
A class not used in this example, AbstractFactory12, is a very useful base class which provides
commonly needed functionality for security factories. It may be useful when defining an
authentication provider of a different type.
Now that you have created a factory class, the wsse key can be used as a firewall in your security
configuration.
11. http://api.symfony.com/2.0/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.html
12. http://api.symfony.com/2.0/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.html
PDF brought to you by
generated on October 26, 2012
Chapter 82: How to create a custom Authentication Provider | 463
You may be wondering "why do we need a special factory class to add listeners and providers to
the dependency injection container?". This is a very good question. The reason is you can use your
firewall multiple times, to secure multiple parts of your application. Because of this, each time your
firewall is used, a new service is created in the DI container. The factory is what creates these new
services.
Configuration
It's time to see your authentication provider in action. You will need to do a few things in order to
make this work. The first thing is to add the services above to the DI container. Your factory class above
makes reference to service ids that do not exist yet: wsse.security.authentication.provider and
wsse.security.authentication.listener. It's time to define those services.
Listing 82-5
# src/Acme/DemoBundle/Resources/config/services.yml
services:
wsse.security.authentication.provider:
class: Acme\DemoBundle\Security\Authentication\Provider\WsseProvider
arguments: ['', %kernel.cache_dir%/security/nonces]
wsse.security.authentication.listener:
class: Acme\DemoBundle\Security\Firewall\WsseListener
arguments: [@security.context, @security.authentication.manager]
Now that your services are defined, tell your security context about your factory. Factories must be
included in an individual configuration file, at the time of this writing. So, start first by creating the file
with the factory service, tagged as security.listener.factory:
Listing 82-6
1 # src/Acme/DemoBundle/Resources/config/security_factories.yml
2 services:
3
security.authentication.factory.wsse:
4
class: Acme\DemoBundle\DependencyInjection\Security\Factory\WsseFactory
5
tags:
6
- { name: security.listener.factory }
Now, import the factory configuration via the the factories key in your security configuration:
Listing 82-7
1 # app/config/security.yml
2 security:
3
factories:
4
- "%kernel.root_dir%/../src/Acme/DemoBundle/Resources/config/security_factories.yml"
You are finished! You can now define parts of your app as under WSSE protection.
Listing 82-8
1 security:
2
firewalls:
3
wsse_secured:
4
pattern:
5
wsse:
/api/.*
true
Congratulations! You have written your very own custom security authentication provider!
PDF brought to you by
generated on October 26, 2012
Chapter 82: How to create a custom Authentication Provider | 464
A Little Extra
How about making your WSSE authentication provider a bit more exciting? The possibilities are endless.
Why don't you start by adding some sparkle to that shine?
Configuration
You can add custom options under the wsse key in your security configuration. For instance, the time
allowed before expiring the Created header item, by default, is 5 minutes. Make this configurable, so
different firewalls can have different timeout lengths.
You will first need to edit WsseFactory and define the new option in the addConfiguration method.
Listing 82-9
1 class WsseFactory implements SecurityFactoryInterface
2 {
3
// ...
4
5
public function addConfiguration(NodeDefinition $node)
6
{
7
$node
8
->children()
9
->scalarNode('lifetime')->defaultValue(300)
10
->end();
11
}
12 }
Now, in the create method of the factory, the $config argument will contain a 'lifetime' key, set
to 5 minutes (300 seconds) unless otherwise set in the configuration. Pass this argument to your
authentication provider in order to put it to use.
Listing 82-10
1 class WsseFactory implements SecurityFactoryInterface
2 {
3
public function create(ContainerBuilder $container, $id, $config, $userProvider,
4 $defaultEntryPoint)
5
{
6
$providerId = 'security.authentication.provider.wsse.'.$id;
7
$container
8
->setDefinition($providerId,
9
new DefinitionDecorator('wsse.security.authentication.provider'))
10
->replaceArgument(0, new Reference($userProvider))
11
->replaceArgument(2, $config['lifetime']);
12
// ...
13
}
14
15
// ...
}
You'll also need to add a third argument to the wsse.security.authentication.provider
service configuration, which can be blank, but will be filled in with the lifetime in the factory. The
WsseProvider class will also now need to accept a third constructor argument - the lifetime - which
it should use instead of the hard-coded 300 seconds. These two steps are not shown here.
The lifetime of each wsse request is now configurable, and can be set to any desirable value per firewall.
Listing 82-11
PDF brought to you by
generated on October 26, 2012
Chapter 82: How to create a custom Authentication Provider | 465
1 security:
2
firewalls:
3
wsse_secured:
4
pattern:
5
wsse:
/api/.*
{ lifetime: 30 }
The rest is up to you! Any relevant configuration items can be defined in the factory and consumed or
passed to the other classes in the container.
PDF brought to you by
generated on October 26, 2012
Chapter 82: How to create a custom Authentication Provider | 466
Chapter 83
How to use Varnish to speed up my Website
Because Symfony2's cache uses the standard HTTP cache headers, the Symfony2 Reverse Proxy can easily
be replaced with any other reverse proxy. Varnish is a powerful, open-source, HTTP accelerator capable
of serving cached content quickly and including support for Edge Side Includes.
Configuration
As seen previously, Symfony2 is smart enough to detect whether it talks to a reverse proxy that
understands ESI or not. It works out of the box when you use the Symfony2 reverse proxy, but you need a
special configuration to make it work with Varnish. Thankfully, Symfony2 relies on yet another standard
written by Akamaï (Edge Architecture1), so the configuration tips in this chapter can be useful even if you
don't use Symfony2.
Varnish only supports the src attribute for ESI tags (onerror and alt attributes are ignored).
First, configure Varnish so that it advertises its ESI support by adding a Surrogate-Capability header
to requests forwarded to the backend application:
Listing 83-1
1 sub vcl_recv {
2
set req.http.Surrogate-Capability = "abc=ESI/1.0";
3 }
Then, optimize Varnish so that it only parses the Response contents when there is at least one ESI tag by
checking the Surrogate-Control header that Symfony2 adds automatically:
Listing 83-2
1 sub vcl_fetch {
2
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
3
unset beresp.http.Surrogate-Control;
1. http://www.w3.org/TR/edge-arch
PDF brought to you by
generated on October 26, 2012
Chapter 83: How to use Varnish to speed up my Website | 467
4
5
6
7
8
9
10 }
// for Varnish >= 3.0
set beresp.do_esi = true;
// for Varnish < 3.0
// esi;
}
Compression with ESI was not supported in Varnish until version 3.0 (read GZIP and Varnish2). If
you're not using Varnish 3.0, put a web server in front of Varnish to perform the compression.
Cache Invalidation
You should never need to invalidate cached data because invalidation is already taken into account
natively in the HTTP cache models (see Cache Invalidation).
Still, Varnish can be configured to accept a special HTTP PURGE method that will invalidate the cache for
a given resource:
Listing 83-3
1
2
3
4
5
6
7
8
9
10
11
12
sub vcl_hit {
if (req.request == "PURGE") {
set obj.ttl = 0s;
error 200 "Purged";
}
}
sub vcl_miss {
if (req.request == "PURGE") {
error 404 "Not purged";
}
}
You must protect the PURGE HTTP method somehow to avoid random people purging your cached
data.
2. https://www.varnish-cache.org/docs/3.0/phk/gzip.html
PDF brought to you by
generated on October 26, 2012
Chapter 83: How to use Varnish to speed up my Website | 468
Chapter 84
How to Inject Variables into all Templates (i.e.
Global Variables)
Sometimes you want a variable to be accessible to all the templates you use. This is possible inside your
app/config/config.yml file:
Listing 84-1
1 # app/config/config.yml
2 twig:
3
# ...
4
globals:
5
ga_tracking: UA-xxxxx-x
Now, the variable ga_tracking is available in all Twig templates:
Listing 84-2
1 <p>Our google tracking code is: {{ ga_tracking }} </p>
It's that easy! You can also take advantage of the built-in Service Parameters system, which lets you isolate
or reuse the value:
Listing 84-3
; app/config/parameters.ini
[parameters]
ga_tracking: UA-xxxxx-x
Listing 84-4
1 # app/config/config.yml
2 twig:
3
globals:
4
ga_tracking: "%ga_tracking%"
The same variable is available exactly as before.
PDF brought to you by
generated on October 26, 2012
Chapter 84: How to Inject Variables into all Templates (i.e. Global Variables) | 469
More Complex Global Variables
If the global variable you want to set is more complicated - say an object - then you won't be able to use
the above method. Instead, you'll need to create a Twig Extension and return the global variable as one of
the entries in the getGlobals method.
PDF brought to you by
generated on October 26, 2012
Chapter 84: How to Inject Variables into all Templates (i.e. Global Variables) | 470
Chapter 85
How to use PHP instead of Twig for Templates
Even if Symfony2 defaults to Twig for its template engine, you can still use plain PHP code if you want.
Both templating engines are supported equally in Symfony2. Symfony2 adds some nice features on top of
PHP to make writing templates with PHP more powerful.
Rendering PHP Templates
If you want to use the PHP templating engine, first, make sure to enable it in your application
configuration file:
Listing 85-1
1 # app/config/config.yml
2 framework:
3
# ...
4
templating:
{ engines: ['twig', 'php'] }
You can now render a PHP template instead of a Twig one simply by using the .php extension in the
template name instead of .twig. The controller below renders the index.html.php template:
Listing 85-2
1
2
3
4
5
6
7
8
// src/Acme/HelloBundle/Controller/HelloController.php
// ...
public function indexAction($name)
{
return $this->render('AcmeHelloBundle:Hello:index.html.php', array('name' => $name));
}
Decorating Templates
More often than not, templates in a project share common elements, like the well-known header and
footer. In Symfony2, we like to think about this problem differently: a template can be decorated by
another one.
PDF brought to you by
generated on October 26, 2012
Chapter 85: How to use PHP instead of Twig for Templates | 471
The index.html.php template is decorated by layout.html.php, thanks to the extend() call:
Listing 85-3
1 <!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php -->
2 <?php $view->extend('AcmeHelloBundle::layout.html.php') ?>
3
4 Hello <?php echo $name ?>!
The AcmeHelloBundle::layout.html.php notation sounds familiar, doesn't it? It is the same notation
used to reference a template. The :: part simply means that the controller element is empty, so the
corresponding file is directly stored under views/.
Now, let's have a look at the layout.html.php file:
Listing 85-4
1
2
3
4
5
6
<!-- src/Acme/HelloBundle/Resources/views/layout.html.php -->
<?php $view->extend('::base.html.php') ?>
<h1>Hello Application</h1>
<?php $view['slots']->output('_content') ?>
The layout is itself decorated by another one (::base.html.php). Symfony2 supports multiple
decoration levels: a layout can itself be decorated by another one. When the bundle part of the template
name is empty, views are looked for in the app/Resources/views/ directory. This directory store global
views for your entire project:
Listing 85-5
1
2
3
4
5
6
7
8
9
10
11
<!-- app/Resources/views/base.html.php -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title><?php $view['slots']->output('title', 'Hello Application') ?></title>
</head>
<body>
<?php $view['slots']->output('_content') ?>
</body>
</html>
For both layouts, the $view['slots']->output('_content') expression is replaced by the content
of the child template, index.html.php and layout.html.php respectively (more on slots in the next
section).
As you can see, Symfony2 provides methods on a mysterious $view object. In a template, the $view
variable is always available and refers to a special object that provides a bunch of methods that makes the
template engine tick.
Working with Slots
A slot is a snippet of code, defined in a template, and reusable in any layout decorating the template. In
the index.html.php template, define a title slot:
Listing 85-6
1 <!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php -->
2 <?php $view->extend('AcmeHelloBundle::layout.html.php') ?>
3
4 <?php $view['slots']->set('title', 'Hello World Application') ?>
PDF brought to you by
generated on October 26, 2012
Chapter 85: How to use PHP instead of Twig for Templates | 472
5
6 Hello <?php echo $name ?>!
The base layout already has the code to output the title in the header:
Listing 85-7
1 <!-- app/Resources/views/base.html.php -->
2 <head>
3
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
4
<title><?php $view['slots']->output('title', 'Hello Application') ?></title>
5 </head>
The output() method inserts the content of a slot and optionally takes a default value if the slot is not
defined. And _content is just a special slot that contains the rendered child template.
For large slots, there is also an extended syntax:
Listing 85-8
1 <?php $view['slots']->start('title') ?>
2
Some large amount of HTML
3 <?php $view['slots']->stop() ?>
Including other Templates
The best way to share a snippet of template code is to define a template that can then be included into
other templates.
Create a hello.html.php template:
Listing 85-9
1 <!-- src/Acme/HelloBundle/Resources/views/Hello/hello.html.php -->
2 Hello <?php echo $name ?>!
And change the index.html.php template to include it:
Listing 85-10
1 <!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php -->
2 <?php $view->extend('AcmeHelloBundle::layout.html.php') ?>
3
4 <?php echo $view->render('AcmeHelloBundle:Hello:hello.html.php', array('name' => $name)) ?>
The render() method evaluates and returns the content of another template (this is the exact same
method as the one used in the controller).
Embedding other Controllers
And what if you want to embed the result of another controller in a template? That's very useful when
working with Ajax, or when the embedded template needs some variable not available in the main
template.
If you create a fancy action, and want to include it into the index.html.php template, simply use the
following code:
Listing 85-11
1 <!-- src/Acme/HelloBundle/Resources/views/Hello/index.html.php -->
2 <?php echo $view['actions']->render('AcmeHelloBundle:Hello:fancy', array('name' => $name,
'color' => 'green')) ?>
PDF brought to you by
generated on October 26, 2012
Chapter 85: How to use PHP instead of Twig for Templates | 473
Here, the AcmeHelloBundle:Hello:fancy string refers to the fancy action of the Hello controller:
Listing 85-12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/HelloBundle/Controller/HelloController.php
class HelloController extends Controller
{
public function fancyAction($name, $color)
{
// create some object, based on the $color variable
$object = ...;
return $this->render('AcmeHelloBundle:Hello:fancy.html.php', array('name' =>
$name, 'object' => $object));
}
// ...
}
But where is the $view['actions'] array element defined? Like $view['slots'], it's called a template
helper, and the next section tells you more about those.
Using Template Helpers
The Symfony2 templating system can be easily extended via helpers. Helpers are PHP objects that provide
features useful in a template context. actions and slots are two of the built-in Symfony2 helpers.
Creating Links between Pages
Speaking of web applications, creating links between pages is a must. Instead of hardcoding URLs in
templates, the router helper knows how to generate URLs based on the routing configuration. That way,
all your URLs can be easily updated by changing the configuration:
Listing 85-13
1 <a href="<?php echo $view['router']->generate('hello', array('name' => 'Thomas')) ?>">
2
Greet Thomas!
3 </a>
The generate() method takes the route name and an array of parameters as arguments. The route name
is the main key under which routes are referenced and the parameters are the values of the placeholders
defined in the route pattern:
Listing 85-14
1 # src/Acme/HelloBundle/Resources/config/routing.yml
2 hello: # The route name
3
pattern: /hello/{name}
4
defaults: { _controller: AcmeHelloBundle:Hello:index }
Using Assets: images, JavaScripts, and stylesheets
What would the Internet be without images, JavaScripts, and stylesheets? Symfony2 provides the assets
tag to deal with them easily:
Listing 85-15
1 <link href="<?php echo $view['assets']->getUrl('css/blog.css') ?>" rel="stylesheet"
2 type="text/css" />
3
PDF brought to you by
generated on October 26, 2012
Chapter 85: How to use PHP instead of Twig for Templates | 474
<img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>" />
The assets helper's main purpose is to make your application more portable. Thanks to this helper,
you can move the application root directory anywhere under your web root directory without changing
anything in your template's code.
Output Escaping
When using PHP templates, escape variables whenever they are displayed to the user:
Listing 85-16
1 <?php echo $view->escape($var) ?>
By default, the escape() method assumes that the variable is outputted within an HTML context. The
second argument lets you change the context. For instance, to output something in a JavaScript script,
use the js context:
Listing 85-17
1 <?php echo $view->escape($var, 'js') ?>
PDF brought to you by
generated on October 26, 2012
Chapter 85: How to use PHP instead of Twig for Templates | 475
Chapter 86
How to write a custom Twig Extension
The main motivation for writing an extension is to move often used code into a reusable class like adding
support for internationalization. An extension can define tags, filters, tests, operators, global variables,
functions, and node visitors.
Creating an extension also makes for a better separation of code that is executed at compilation time and
code needed at runtime. As such, it makes your code faster.
Before writing your own extensions, have a look at the Twig official extension repository1.
Create the Extension Class
To get your custom functionality you must first create a Twig Extension class. As an example we will
create a price filter to format a given number into price:
Listing 86-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Acme/DemoBundle/Twig/AcmeExtension.php
namespace Acme\DemoBundle\Twig;
class AcmeExtension extends \Twig_Extension
{
public function getFilters()
{
return array(
'price' => new \Twig_Filter_Method($this, 'priceFilter'),
);
}
public function priceFilter($number, $decimals = 0, $decPoint = '.', $thousandsSep =
',')
{
1. http://github.com/fabpot/Twig-extensions
PDF brought to you by
generated on October 26, 2012
Chapter 86: How to write a custom Twig Extension | 476
16
17
18
19
20
21
22
23
24
25
$price = number_format($number, $decimals, $decPoint, $thousandsSep);
$price = '$' . $price;
return $price;
}
public function getName()
{
return 'acme_extension';
}
}
Along with custom filters, you can also add custom functions and register global variables.
Register an Extension as a Service
Now you must let Service Container know about your newly created Twig Extension:
Listing 86-2
1 <!-- src/Acme/DemoBundle/Resources/config/services.xml -->
2 <services>
3
<service id="acme.twig.acme_extension" class="Acme\DemoBundle\Twig\AcmeExtension">
4
<tag name="twig.extension" />
5
</service>
6 </services>
Keep in mind that Twig Extensions are not lazily loaded. This means that there's a higher chance
that you'll get a CircularReferenceException or a ScopeWideningInjectionException if any
services (or your Twig Extension in this case) are dependent on the request service. For more
information take a look at How to work with Scopes.
Using the custom Extension
Using your newly created Twig Extension is no different than any other:
Listing 86-3
1 {# outputs $5,500.00 #}
2 {{ '5500'|price }}
Passing other arguments to your filter:
Listing 86-4
1 {# outputs $5500,2516 #}
2 {{ '5500.25155'|price(4, ',', '') }}
PDF brought to you by
generated on October 26, 2012
Chapter 86: How to write a custom Twig Extension | 477
Learning further
For a more in-depth look into Twig Extensions, please take a look at the Twig extensions documentation2.
2. http://twig.sensiolabs.org/doc/advanced.html#creating-an-extension
PDF brought to you by
generated on October 26, 2012
Chapter 86: How to write a custom Twig Extension | 478
Chapter 87
How to use Monolog to write Logs
Monolog1 is a logging library for PHP 5.3 used by Symfony2. It is inspired by the Python LogBook library.
Usage
In Monolog each logger defines a logging channel. Each channel has a stack of handlers to write the logs
(the handlers can be shared).
When injecting the logger in a service you can use a custom channel to see easily which part of the
application logged the message.
The basic handler is the StreamHandler which writes logs in a stream (by default in the app/logs/
prod.log in the prod environment and app/logs/dev.log in the dev environment).
Monolog comes also with a powerful built-in handler for the logging in prod environment:
FingersCrossedHandler. It allows you to store the messages in a buffer and to log them only if a message
reaches the action level (ERROR in the configuration provided in the standard edition) by forwarding the
messages to another handler.
To log a message simply get the logger service from the container in your controller:
Listing 87-1
1 $logger = $this->get('logger');
2 $logger->info('We just got the logger');
3 $logger->err('An error occurred');
Using only the methods of the LoggerInterface2 interface allows to change the logger
implementation without changing your code.
1. https://github.com/Seldaek/monolog
2. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/Log/LoggerInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 87: How to use Monolog to write Logs | 479
Using several handlers
The logger uses a stack of handlers which are called successively. This allows you to log the messages in
several ways easily.
Listing 87-2
1 monolog:
2
handlers:
3
syslog:
4
type: stream
5
path: /var/log/symfony.log
6
level: error
7
main:
8
type: fingers_crossed
9
action_level: warning
10
handler: file
11
file:
12
type: stream
13
level: debug
The above configuration defines a stack of handlers which will be called in the order where they are
defined.
The handler named "file" will not be included in the stack itself as it is used as a nested handler of
the fingers_crossed handler.
If you want to change the config of MonologBundle in another config file you need to redefine the
whole stack. It cannot be merged because the order matters and a merge does not allow to control
the order.
Changing the formatter
The handler uses a Formatter to format the record before logging it. All Monolog handlers use an
instance of Monolog\Formatter\LineFormatter by default but you can replace it easily. Your formatter
must implement Monolog\Formatter\FormatterInterface.
Listing 87-3
1 services:
2
my_formatter:
3
class: Monolog\Formatter\JsonFormatter
4 monolog:
5
handlers:
6
file:
7
type: stream
8
level: debug
9
formatter: my_formatter
Adding some extra data in the log messages
Monolog allows to process the record before logging it to add some extra data. A processor can be applied
for the whole handler stack or only for a specific handler.
A processor is simply a callable receiving the record as its first argument.
PDF brought to you by
generated on October 26, 2012
Chapter 87: How to use Monolog to write Logs | 480
Processors are configured using the monolog.processor DIC tag. See the reference about it.
Adding a Session/Request Token
Sometimes it is hard to tell which entries in the log belong to which session and/or request. The following
example will add a unique token for each request using a processor.
Listing 87-4
Listing 87-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace Acme\MyBundle;
use Symfony\Component\HttpFoundation\Session;
class SessionRequestProcessor
{
private $session;
private $token;
public function __construct(Session $session)
{
$this->session = $session;
}
public function processRecord(array $record)
{
if (null === $this->token) {
try {
$this->token = substr($this->session->getId(), 0, 8);
} catch (\RuntimeException $e) {
$this->token = '????????';
}
$this->token .= '-' . substr(uniqid(), -8);
}
$record['extra']['token'] = $this->token;
return $record;
}
}
services:
monolog.formatter.session_request:
class: Monolog\Formatter\LineFormatter
arguments:
- "[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%%\n"
monolog.processor.session_request:
class: Acme\MyBundle\SessionRequestProcessor
arguments: [ @session ]
tags:
- { name: monolog.processor, method: processRecord }
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
formatter: monolog.formatter.session_request
PDF brought to you by
generated on October 26, 2012
Chapter 87: How to use Monolog to write Logs | 481
If you use several handlers, you can also register the processor at the handler level instead of
globally.
PDF brought to you by
generated on October 26, 2012
Chapter 87: How to use Monolog to write Logs | 482
Chapter 88
How to Configure Monolog to Email Errors
Monolog1 can be configured to send an email when an error occurs with an application. The configuration
for this requires a few nested handlers in order to avoid receiving too many emails. This configuration
looks complicated at first but each handler is fairly straight forward when it is broken down.
Listing 88-1
1 # app/config/config.yml
2 monolog:
3
handlers:
4
mail:
5
type:
fingers_crossed
6
action_level: critical
7
handler:
buffered
8
buffered:
9
type:
buffer
10
handler: swift
11
swift:
12
type:
swift_mailer
13
from_email: error@example.com
14
to_email:
error@example.com
15
subject:
An Error Occurred!
16
level:
debug
The mail handler is a fingers_crossed handler which means that it is only triggered when the action
level, in this case critical is reached. It then logs everything including messages below the action level.
The critical level is only triggered for 5xx HTTP code errors. The handler setting means that the
output is then passed onto the buffered handler.
If you want both 400 level and 500 level errors to trigger an email, set the action_level to error
instead of critical.
The buffered handler simply keeps all the messages for a request and then passes them onto the nested
handler in one go. If you do not use this handler then each message will be emailed separately. This is
1. https://github.com/Seldaek/monolog
PDF brought to you by
generated on October 26, 2012
Chapter 88: How to Configure Monolog to Email Errors | 483
then passed to the swift handler. This is the handler that actually deals with emailing you the error. The
settings for this are straightforward, the to and from addresses and the subject.
You can combine these handlers with other handlers so that the errors still get logged on the server as
well as the emails being sent:
Listing 88-2
1 # app/config/config.yml
2 monolog:
3
handlers:
4
main:
5
type:
fingers_crossed
6
action_level: critical
7
handler:
grouped
8
grouped:
9
type:
group
10
members: [streamed, buffered]
11
streamed:
12
type: stream
13
path: "%kernel.logs_dir%/%kernel.environment%.log"
14
level: debug
15
buffered:
16
type:
buffer
17
handler: swift
18
swift:
19
type:
swift_mailer
20
from_email: error@example.com
21
to_email:
error@example.com
22
subject:
An Error Occurred!
23
level:
debug
This uses the group handler to send the messages to the two group members, the buffered and the
stream handlers. The messages will now be both written to the log file and emailed.
PDF brought to you by
generated on October 26, 2012
Chapter 88: How to Configure Monolog to Email Errors | 484
Chapter 89
How to create a Console Command
The Console page of the Components section (The Console Component) covers how to create a Console
command. This cookbook article covers the differences when creating Console commands within the
Symfony2 framework.
Automatically Registering Commands
To make the console commands available automatically with Symfony2, create a Command directory
inside your bundle and create a php file suffixed with Command.php for each command that you want
to provide. For example, if you want to extend the AcmeDemoBundle (available in the Symfony Standard
Edition) to greet us from the command line, create GreetCommand.php and add the following to it:
Listing 89-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Acme/DemoBundle/Command/GreetCommand.php
namespace Acme\DemoBundle\Command;
use
use
use
use
use
Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
Symfony\Component\Console\Input\InputArgument;
Symfony\Component\Console\Input\InputInterface;
Symfony\Component\Console\Input\InputOption;
Symfony\Component\Console\Output\OutputInterface;
class GreetCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('demo:greet')
->setDescription('Greet someone')
->addArgument('name', InputArgument::OPTIONAL, 'Who do you want to greet?')
->addOption('yell', null, InputOption::VALUE_NONE, 'If set, the task will yell
in uppercase letters')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
PDF brought to you by
generated on October 26, 2012
Chapter 89: How to create a Console Command | 485
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
$name = $input->getArgument('name');
if ($name) {
$text = 'Hello '.$name;
} else {
$text = 'Hello';
}
if ($input->getOption('yell')) {
$text = strtoupper($text);
}
$output->writeln($text);
}
}
This command will now automatically be available to run:
Listing 89-2
1 $ app/console demo:greet Fabien
Testing Commands
When testing commands used as part of the full framework Application1 should be used instead of
Application2:
Listing 89-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Acme\DemoBundle\Command\GreetCommand;
class ListCommandTest extends \PHPUnit_Framework_TestCase
{
public function testExecute()
{
// mock the Kernel or create one depending on your needs
$application = new Application($kernel);
$application->add(new GreetCommand());
$command = $application->find('demo:greet');
$commandTester = new CommandTester($command);
$commandTester->execute(array('command' => $command->getName()));
$this->assertRegExp('/.../', $commandTester->getDisplay());
// ...
}
}
1. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Console/Application.html
2. http://api.symfony.com/2.0/Symfony/Component/Console/Application.html
PDF brought to you by
generated on October 26, 2012
Chapter 89: How to create a Console Command | 486
Getting Services from the Service Container
By using ContainerAwareCommand3 as the base class for the command (instead of the more basic
Command4), you have access to the service container. In other words, you have access to any configured
service. For example, you could easily extend the task to be translatable:
Listing 89-4
1 protected function execute(InputInterface $input, OutputInterface $output)
2 {
3
$name = $input->getArgument('name');
4
$translator = $this->getContainer()->get('translator');
5
if ($name) {
6
$output->writeln($translator->trans('Hello %name%!', array('%name%' => $name)));
7
} else {
8
$output->writeln($translator->trans('Hello!'));
9
}
10 }
3. http://api.symfony.com/2.0/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.html
4. http://api.symfony.com/2.0/Symfony/Component/Console/Command/Command.html
PDF brought to you by
generated on October 26, 2012
Chapter 89: How to create a Console Command | 487
Chapter 90
How to use the Console
The Using Console Commands, Shortcuts and Built-in Commands page of the components documentation
looks at the global console options. When you use the console as part of the full stack framework, some
additional global options are available as well.
By default, console commands run in the dev environment and you may want to change this for
some commands. For example, you may want to run some commands in the prod environment for
performance reasons. Also, the result of some commands will be different depending on the environment.
for example, the cache:clear command will clear and warm the cache for the specified environment
only. To clear and warm the prod cache you need to run:
Listing 90-1
1 $ php app/console cache:clear --env=prod
or the equivalent:
Listing 90-2
1 $ php app/console cache:clear -e=prod
In addition to changing the environment, you can also choose to disable debug mode. This can be useful
where you want to run commands in the dev environment but avoid the performance hit of collecting
debug data:
Listing 90-3
1 $ php app/console list --no-debug
There is an interactive shell which allows you to enter commands without having to specify php app/
console each time, which is useful if you need to run several commands. To enter the shell run:
Listing 90-4
1 $ php app/console --shell
2 $ php app/console -s
You can now just run commands with the command name:
Listing 90-5
1 Symfony > list
When using the shell you can choose to run each command in a separate process:
PDF brought to you by
generated on October 26, 2012
Chapter 90: How to use the Console | 488
Listing 90-6
1 $ php app/console --shell --process-isolation
2 $ php app/console -s --process-isolation
When you do this, the output will not be colorized and interactivity is not supported so you will need to
pass all command params explicitly.
Unless you are using isolated processes, clearing the cache in the shell will not have an effect on
subsequent commands you run. This is because the original cached files are still being used.
PDF brought to you by
generated on October 26, 2012
Chapter 90: How to use the Console | 489
Chapter 91
How to generate URLs with a custom Host in
Console Commands
Unfortunately, the command line context does not know about your VirtualHost or domain name. This
means that if if you generate absolute URLs within a Console Command you'll probably end up with
something like http://localhost/foo/bar which is not very useful.
To fix this, you need to configure the "request context", which is a fancy way of saying that you need to
configure your environment so that it knows what URL it should use when generating URLs.
There are two ways of configuring the request context: at the application level and per Command.
Configuring the Request Context globally
To configure the Request Context - which is used by the URL Generator - you can redefine the
parameters it uses as default values to change the default host (localhost) and scheme (http). Note that
this does not impact URLs generated via normal web requests, since those will override the defaults.
Listing 91-1
1 # app/config/parameters.yml
2 parameters:
3
router.request_context.host: example.org
4
router.request_context.scheme: https
Configuring the Request Context per Command
To change it only in one command you can simply fetch the Request Context service and override its
settings:
Listing 91-2
1 // src/Acme/DemoBundle/Command/DemoCommand.php
2
3 // ...
PDF brought to you by
generated on October 26, 2012
Chapter 91: How to generate URLs with a custom Host in Console Commands | 490
4 class DemoCommand extends ContainerAwareCommand
5 {
6
protected function execute(InputInterface $input, OutputInterface $output)
7
{
8
$context = $this->getContainer()->get('router')->getContext();
9
$context->setHost('example.com');
10
$context->setScheme('https');
11
12
// ... your code here
13
}
14 }
PDF brought to you by
generated on October 26, 2012
Chapter 91: How to generate URLs with a custom Host in Console Commands | 491
Chapter 92
How to optimize your development
Environment for debugging
When you work on a Symfony project on your local machine, you should use the dev environment
(app_dev.php front controller). This environment configuration is optimized for two main purposes:
• Give the developer accurate feedback whenever something goes wrong (web debug toolbar,
nice exception pages, profiler, ...);
• Be as similar as possible as the production environment to avoid problems when deploying the
project.
Disabling the Bootstrap File and Class Caching
And to make the production environment as fast as possible, Symfony creates big PHP files in your
cache containing the aggregation of PHP classes your project needs for every request. However, this
behavior can confuse your IDE or your debugger. This recipe shows you how you can tweak this caching
mechanism to make it friendlier when you need to debug code that involves Symfony classes.
The app_dev.php front controller reads as follows by default:
Listing 92-1
1
2
3
4
5
6
7
8
9
10
// ...
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('dev', true);
$kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();
To make your debugger happier, disable all PHP class caches by removing the call to loadClassCache()
and by replacing the require statements like below:
PDF brought to you by
generated on October 26, 2012
Chapter 92: How to optimize your development Environment for debugging | 492
Listing 92-2
1
2
3
4
5
6
7
8
9
10
11
12
// ...
// require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../vendor/symfony/src/Symfony/Component/ClassLoader/
UniversalClassLoader.php';
require_once __DIR__.'/../app/autoload.php';
require_once __DIR__.'/../app/AppKernel.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('dev', true);
// $kernel->loadClassCache();
$kernel->handle(Request::createFromGlobals())->send();
If you disable the PHP caches, don't forget to revert after your debugging session.
Some IDEs do not like the fact that some classes are stored in different locations. To avoid problems, you
can either tell your IDE to ignore the PHP cache files, or you can change the extension used by Symfony
for these files:
Listing 92-3
1 $kernel->loadClassCache('classes', '.php.cache');
PDF brought to you by
generated on October 26, 2012
Chapter 92: How to optimize your development Environment for debugging | 493
Chapter 93
How to setup before and after Filters
It is quite common in web application development to need some logic to be executed just before or just
after your controller actions acting as filters or hooks.
In symfony1, this was achieved with the preExecute and postExecute methods. Most major frameworks
have similar methods but there is no such thing in Symfony2. The good news is that there is a much
better way to interfere with the Request -> Response process using the EventDispatcher component.
Token validation Example
Imagine that you need to develop an API where some controllers are public but some others are restricted
to one or some clients. For these private features, you might provide a token to your clients to identify
themselves.
So, before executing your controller action, you need to check if the action is restricted or not. If it is
restricted, you need to validate the provided token.
Please note that for simplicity in this recipe, tokens will be defined in config and neither database
setup nor authentication via the Security component will be used.
Before filters with the kernel.controller Event
First, store some basic token configuration using config.yml and the parameters key:
Listing 93-1
1 # app/config/config.yml
2 parameters:
3
tokens:
4
client1: pass1
5
client2: pass2
PDF brought to you by
generated on October 26, 2012
Chapter 93: How to setup before and after Filters | 494
Tag Controllers to be checked
A kernel.controller listener gets notified on every request, right before the controller is executed. So,
first, you need some way to identify if the controller that matches the request needs token validation.
A clean and easy way is to create an empty interface and make the controllers implement it:
Listing 93-2
1
2
3
4
5
6
namespace Acme\DemoBundle\Controller;
interface TokenAuthenticatedController
{
// ...
}
A controller that implements this interface simply looks like this:
Listing 93-3
1
2
3
4
5
6
7
8
9
10
11
12
13
namespace Acme\DemoBundle\Controller;
use Acme\DemoBundle\Controller\TokenAuthenticatedController;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller implements TokenAuthenticatedController
{
// An action that needs authentication
public function barAction()
{
// ...
}
}
Creating an Event Listener
Next, you'll need to create an event listener, which will hold the logic that you want executed before your
controllers. If you're not familiar with event listeners, you can learn more about them at How to create an
Event Listener:
Listing 93-4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Acme/DemoBundle/EventListener/TokenListener.php
namespace Acme\DemoBundle\EventListener;
use Acme\DemoBundle\Controller\TokenAuthenticatedController;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class TokenListener
{
private $tokens;
public function __construct($tokens)
{
$this->tokens = $tokens;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
/*
PDF brought to you by
generated on October 26, 2012
Chapter 93: How to setup before and after Filters | 495
22
* $controller passed can be either a class or a Closure. This is not usual in
23 Symfony2 but it may happen.
24
* If it is a class, it comes in array format
25
*/
26
if (!is_array($controller)) {
27
return;
28
}
29
30
if ($controller[0] instanceof TokenAuthenticatedController) {
31
$token = $event->getRequest()->query->get('token');
32
if (!in_array($token, $this->tokens)) {
33
throw new AccessDeniedHttpException('This action needs a valid token!');
34
}
35
}
36
}
}
Registering the Listener
Finally, register your listener as a service and tag it as an event listener. By listening on
kernel.controller, you're telling Symfony that you want your listener to be called just before any
controller is executed.
Listing 93-5
# app/config/config.yml (or inside your services.yml)
services:
demo.tokens.action_listener:
class: Acme\DemoBundle\EventListener\TokenListener
arguments: [ %tokens% ]
tags:
- { name: kernel.event_listener, event: kernel.controller, method:
onKernelController }
With this configuration, your TokenListener onKernelController method will be executed on each
request. If the controller that is about to be executed implements TokenAuthenticatedController,
token authentication is applied. This let's us have a "before" filter on any controller that you want.
After filters with the kernel.response Event
In addition to having a "hook" that's executed before your controller, you can also add a hook that's
executed after your controller. For this example, imagine that you want to add a sha1 hash (with a salt
using that token) to all responses that have passed this token authentication.
Another core Symfony event - called kernel.response - is notified on every request, but after the
controller returns a Response object. Creating an "after" listener is as easy as creating a listener class and
registering it as a service on this event.
For example, take the TokenListener from the previous example and first record the authentication
token inside the request attributes. This will serve as a basic flag that this request underwent token
authentication:
Listing 93-6
1 public function onKernelController(FilterControllerEvent $event)
2 {
3
// ...
4
5
if ($controller[0] instanceof TokenAuthenticatedController) {
PDF brought to you by
generated on October 26, 2012
Chapter 93: How to setup before and after Filters | 496
6
7
8
9
10
11
12
13
14 }
$token = $event->getRequest()->query->get('token');
if (!in_array($token, $this->tokens)) {
throw new AccessDeniedHttpException('This action needs a valid token!');
}
// mark the request as having passed token authentication
$event->getRequest()->attributes->set('auth_token', $token);
}
Now, add another method to this class - onKernelResponse - that looks for this flag on the request object
and sets a custom header on the response if it's found:
Listing 93-7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// add the new use statement at the top of your file
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
public function onKernelResponse(FilterResponseEvent $event)
{
// check to see if onKernelController marked this as a token "auth'ed" request
if (!$token = $event->getRequest()->attributes->get('auth_token')) {
return;
}
$response = $event->getResponse();
// create a hash and set it as a response header
$hash = sha1($response->getContent().$token);
$response->headers->set('X-CONTENT-HASH', $hash);
}
Finally, a second "tag" is needed on the service definition to notify Symfony that the onKernelResponse
event should be notified for the kernel.response event:
Listing 93-8
# app/config/config.yml (or inside your services.yml)
services:
demo.tokens.action_listener:
class: Acme\DemoBundle\EventListener\TokenListener
arguments: [ %tokens% ]
tags:
- { name: kernel.event_listener, event: kernel.controller, method:
onKernelController }
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
That's it! The TokenListener is now notified before every controller is executed (onKernelController)
and after every controller returns a response (onKernelResponse). By making specific controllers
implement the TokenAuthenticatedController interface, our listener knows which controllers it
should take action on. And by storing a value in the request's "attributes" bag, the onKernelResponse
method knows to add the extra header. Have fun!
PDF brought to you by
generated on October 26, 2012
Chapter 93: How to setup before and after Filters | 497
Chapter 94
How to extend a Class without using
Inheritance
To allow multiple classes to add methods to another one, you can define the magic __call() method in
the class you want to be extended like this:
Listing 94-1
1 class Foo
2 {
3
// ...
4
5
public function __call($method, $arguments)
6
{
7
// create an event named 'foo.method_is_not_found'
8
$event = new HandleUndefinedMethodEvent($this, $method, $arguments);
9
$this->dispatcher->dispatch('foo.method_is_not_found', $event);
10
11
// no listener was able to process the event? The method does not exist
12
if (!$event->isProcessed()) {
13
throw new \Exception(sprintf('Call to undefined method %s::%s.',
14 get_class($this), $method));
15
}
16
17
// return the listener returned value
18
return $event->getReturnValue();
19
}
}
This uses a special HandleUndefinedMethodEvent that should also be created. This is a generic class that
could be reused each time you need to use this pattern of class extension:
Listing 94-2
1 use Symfony\Component\EventDispatcher\Event;
2
3 class HandleUndefinedMethodEvent extends Event
4 {
5
protected $subject;
PDF brought to you by
generated on October 26, 2012
Chapter 94: How to extend a Class without using Inheritance | 498
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 }
protected
protected
protected
protected
$method;
$arguments;
$returnValue;
$isProcessed = false;
public function __construct($subject, $method, $arguments)
{
$this->subject = $subject;
$this->method = $method;
$this->arguments = $arguments;
}
public function getSubject()
{
return $this->subject;
}
public function getMethod()
{
return $this->method;
}
public function getArguments()
{
return $this->arguments;
}
/**
* Sets the value to return and stops other listeners from being notified
*/
public function setReturnValue($val)
{
$this->returnValue = $val;
$this->isProcessed = true;
$this->stopPropagation();
}
public function getReturnValue($val)
{
return $this->returnValue;
}
public function isProcessed()
{
return $this->isProcessed;
}
Next, create a class that will listen to the foo.method_is_not_found event and add the method bar():
Listing 94-3
1 class Bar
2 {
3
public function onFooMethodIsNotFound(HandleUndefinedMethodEvent $event)
4
{
5
// we only want to respond to the calls to the 'bar' method
6
if ('bar' != $event->getMethod()) {
7
// allow another listener to take care of this unknown method
8
return;
PDF brought to you by
generated on October 26, 2012
Chapter 94: How to extend a Class without using Inheritance | 499
9
10
11
12
13
14
15
16
17
18
19
20
21
22 }
}
// the subject object (the foo instance)
$foo = $event->getSubject();
// the bar method arguments
$arguments = $event->getArguments();
// ... do something
// set the return value
$event->setReturnValue($someValue);
}
Finally, add the new bar method to the Foo class by register an instance of Bar with the
foo.method_is_not_found event:
Listing 94-4
1 $bar = new Bar();
2 $dispatcher->addListener('foo.method_is_not_found', array($bar, 'onFooMethodIsNotFound'));
PDF brought to you by
generated on October 26, 2012
Chapter 94: How to extend a Class without using Inheritance | 500
Chapter 95
How to customize a Method Behavior without
using Inheritance
Doing something before or after a Method Call
If you want to do something just before, or just after a method is called, you can dispatch an event
respectively at the beginning or at the end of the method:
Listing 95-1
1 class Foo
2 {
3
// ...
4
5
public function send($foo, $bar)
6
{
7
// do something before the method
8
$event = new FilterBeforeSendEvent($foo, $bar);
9
$this->dispatcher->dispatch('foo.pre_send', $event);
10
11
// get $foo and $bar from the event, they may have been modified
12
$foo = $event->getFoo();
13
$bar = $event->getBar();
14
15
// the real method implementation is here
16
$ret = ...;
17
18
// do something after the method
19
$event = new FilterSendReturnValue($ret);
20
$this->dispatcher->dispatch('foo.post_send', $event);
21
22
return $event->getReturnValue();
23
}
24 }
PDF brought to you by
generated on October 26, 2012
Chapter 95: How to customize a Method Behavior without using Inheritance | 501
In this example, two events are thrown: foo.pre_send, before the method is executed, and
foo.post_send after the method is executed. Each uses a custom Event class to communicate
information to the listeners of the two events. These event classes would need to be created by you and
should allow, in this example, the variables $foo, $bar and $ret to be retrieved and set by the listeners.
For example, assuming the FilterSendReturnValue has a setReturnValue method, one listener might
look like this:
Listing 95-2
1 public function onFooPostSend(FilterSendReturnValue $event)
2 {
3
$ret = $event->getReturnValue();
4
// modify the original ``$ret`` value
5
6
$event->setReturnValue($ret);
7 }
PDF brought to you by
generated on October 26, 2012
Chapter 95: How to customize a Method Behavior without using Inheritance | 502
Chapter 96
How to register a new Request Format and
Mime Type
Every Request has a "format" (e.g. html, json), which is used to determine what type of content to
return in the Response. In fact, the request format, accessible via getRequestFormat()1, is used to
set the MIME type of the Content-Type header on the Response object. Internally, Symfony contains
a map of the most common formats (e.g. html, json) and their associated MIME types (e.g. text/
html, application/json). Of course, additional format-MIME type entries can easily be added. This
document will show how you can add the jsonp format and corresponding MIME type.
Create a kernel.request Listener
The key to defining a new MIME type is to create a class that will "listen" to the kernel.request event
dispatched by the Symfony kernel. The kernel.request event is dispatched early in Symfony's request
handling process and allows you to modify the request object.
Create the following class, replacing the path with a path to a bundle in your project:
Listing 96-1
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Acme/DemoBundle/RequestListener.php
namespace Acme\DemoBundle;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class RequestListener
{
public function onKernelRequest(GetResponseEvent $event)
{
$event->getRequest()->setFormat('jsonp', 'application/javascript');
}
}
1. http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Request.html#getRequestFormat()
PDF brought to you by
generated on October 26, 2012
Chapter 96: How to register a new Request Format and Mime Type | 503
Registering your Listener
As for any other listener, you need to add it in one of your configuration file and register it as a listener
by adding the kernel.event_listener tag:
Listing 96-2
1
2
3
4
5
6
7
8
9
10
11
12
<!-- app/config/config.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/
dic/services/services-1.0.xsd">
<services>
<service id="acme.demobundle.listener.request" class="Acme\DemoBundle\RequestListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest"
/>
</service>
</services>
</container>
At this point, the acme.demobundle.listener.request service has been configured and will be notified
when the Symfony kernel dispatches the kernel.request event.
You can also register the listener in a configuration extension class (see Importing Configuration via
Container Extensions for more information).
PDF brought to you by
generated on October 26, 2012
Chapter 96: How to register a new Request Format and Mime Type | 504
Chapter 97
How to create a custom Data Collector
The Symfony2 Profiler delegates data collecting to data collectors. Symfony2 comes bundled with a few
of them, but you can easily create your own.
Creating a Custom Data Collector
Creating a custom data collector is as simple as implementing the DataCollectorInterface1:
Listing 97-1
1 interface DataCollectorInterface
2 {
3
/**
4
* Collects data for the given Request and Response.
5
*
6
* @param Request
$request A Request instance
7
* @param Response $response A Response instance
8
* @param \Exception $exception An Exception instance
9
*/
10
function collect(Request $request, Response $response, \Exception $exception = null);
11
12
/**
13
* Returns the name of the collector.
14
*
15
* @return string The collector name
16
*/
17
function getName();
18 }
The getName() method must return a unique name. This is used to access the information later on (see
How to use the Profiler in a Functional Test for instance).
The collect() method is responsible for storing the data it wants to give access to in local properties.
1. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 97: How to create a custom Data Collector | 505
As the profiler serializes data collector instances, you should not store objects that cannot be
serialized (like PDO objects), or you need to provide your own serialize() method.
Most of the time, it is convenient to extend DataCollector2 and populate the $this->data property (it
takes care of serializing the $this->data property):
Listing 97-2
1 class MemoryDataCollector extends DataCollector
2 {
3
public function collect(Request $request, Response $response, \Exception $exception =
4 null)
5
{
6
$this->data = array(
7
'memory' => memory_get_peak_usage(true),
8
);
9
}
10
11
public function getMemory()
12
{
13
return $this->data['memory'];
14
}
15
16
public function getName()
17
{
18
return 'memory';
19
}
}
Enabling Custom Data Collectors
To enable a data collector, add it as a regular service in one of your configuration, and tag it with
data_collector:
Listing 97-3
1 services:
2
data_collector.your_collector_name:
3
class: Fully\Qualified\Collector\Class\Name
4
tags:
5
- { name: data_collector }
Adding Web Profiler Templates
When you want to display the data collected by your Data Collector in the web debug toolbar or the web
profiler, create a Twig template following this skeleton:
Listing 97-4
1 {% extends 'WebProfilerBundle:Profiler:layout.html.twig' %}
2
3 {% block toolbar %}
4
{# the web debug toolbar content #}
5 {% endblock %}
2. http://api.symfony.com/2.0/Symfony/Component/HttpKernel/DataCollector/DataCollector.html
PDF brought to you by
generated on October 26, 2012
Chapter 97: How to create a custom Data Collector | 506
6
7
8
9
10
11
12
13
14
15
16
17
{% block head %}
{# if the web profiler panel needs some specific JS or CSS files #}
{% endblock %}
{% block menu %}
{# the menu content #}
{% endblock %}
{% block panel %}
{# the panel content #}
{% endblock %}
Each block is optional. The toolbar block is used for the web debug toolbar and menu and panel are
used to add a panel to the web profiler.
All blocks have access to the collector object.
Built-in templates use a base64 encoded image for the toolbar (<img src="src="data:image/
png;base64,..."). You can easily calculate the base64 value for an image with this little script:
echo base64_encode(file_get_contents($_SERVER['argv'][1]));.
To enable the template, add a template attribute to the data_collector tag in your configuration. For
example, assuming your template is in some AcmeDebugBundle:
Listing 97-5
1 services:
2
data_collector.your_collector_name:
3
class: Acme\DebugBundle\Collector\Class\Name
4
tags:
5
- { name: data_collector, template: "AcmeDebugBundle:Collector:templatename",
id: "your_collector_name" }
PDF brought to you by
generated on October 26, 2012
Chapter 97: How to create a custom Data Collector | 507
Chapter 98
How to Create a SOAP Web Service in a
Symfony2 Controller
Setting up a controller to act as a SOAP server is simple with a couple tools. You must, of course, have
the PHP SOAP1 extension installed. As the PHP SOAP extension can not currently generate a WSDL, you
must either create one from scratch or use a 3rd party generator.
There are several SOAP server implementations available for use with PHP. Zend SOAP2 and
NuSOAP3 are two examples. Although we use the PHP SOAP extension in our examples, the
general idea should still be applicable to other implementations.
SOAP works by exposing the methods of a PHP object to an external entity (i.e. the person using the
SOAP service). To start, create a class - HelloService - which represents the functionality that you'll
expose in your SOAP service. In this case, the SOAP service will allow the client to call a method called
hello, which happens to send an email:
Listing 98-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/SoapBundle/Services/HelloService.php
namespace Acme\SoapBundle\Services;
class HelloService
{
private $mailer;
public function __construct(\Swift_Mailer $mailer)
{
$this->mailer = $mailer;
}
public function hello($name)
{
1. http://php.net/manual/en/book.soap.php
2. http://framework.zend.com/manual/en/zend.soap.server.html
3. http://sourceforge.net/projects/nusoap
PDF brought to you by
generated on October 26, 2012
Chapter 98: How to Create a SOAP Web Service in a Symfony2 Controller | 508
15
16
17
18
19
20
21
22
23
24
25
26 }
$message = \Swift_Message::newInstance()
->setTo('me@example.com')
->setSubject('Hello Service')
->setBody($name . ' says hi!');
$this->mailer->send($message);
return 'Hello, '.$name;
}
Next, you can train Symfony to be able to create an instance of this class. Since the class sends an e-mail,
it's been designed to accept a Swift_Mailer instance. Using the Service Container, we can configure
Symfony to construct a HelloService object properly:
Listing 98-2
# app/config/config.yml
services:
hello_service:
class: Acme\SoapBundle\Services\HelloService
arguments: [@mailer]
Below is an example of a controller that is capable of handling a SOAP request. If indexAction() is
accessible via the route /soap, then the WSDL document can be retrieved via /soap?wsdl.
Listing 98-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Acme\SoapBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloServiceController extends Controller
{
public function indexAction()
{
$server = new \SoapServer('/path/to/hello.wsdl');
$server->setObject($this->get('hello_service'));
$response = new Response();
$response->headers->set('Content-Type', 'text/xml; charset=ISO-8859-1');
ob_start();
$server->handle();
$response->setContent(ob_get_clean());
return $response;
}
}
Take note of the calls to ob_start() and ob_get_clean(). These methods control output buffering4
which allows you to "trap" the echoed output of $server->handle(). This is necessary because Symfony
expects your controller to return a Response object with the output as its "content". You must also
remember to set the "Content-Type" header to "text/xml", as this is what the client will expect. So, you
use ob_start() to start buffering the STDOUT and use ob_get_clean() to dump the echoed output
into the content of the Response and clear the output buffer. Finally, you're ready to return the Response.
4. http://php.net/manual/en/book.outcontrol.php
PDF brought to you by
generated on October 26, 2012
Chapter 98: How to Create a SOAP Web Service in a Symfony2 Controller | 509
Below is an example calling the service using NuSOAP5 client. This example assumes that the
indexAction in the controller above is accessible via the route /soap:
Listing 98-4
1 $client = new \Soapclient('http://example.com/app.php/soap?wsdl', true);
2
3 $result = $client->call('hello', array('name' => 'Scott'));
An example WSDL is below.
Listing 98-5
1 <?xml version="1.0" encoding="ISO-8859-1"?>
2 <definitions xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
3
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
4
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
6
xmlns:tns="urn:arnleadservicewsdl"
7
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
8
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
9
xmlns="http://schemas.xmlsoap.org/wsdl/"
10
targetNamespace="urn:helloservicewsdl">
11
<types>
12
<xsd:schema targetNamespace="urn:hellowsdl">
13
<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" />
14
<xsd:import namespace="http://schemas.xmlsoap.org/wsdl/" />
15
</xsd:schema>
16
</types>
17
<message name="helloRequest">
18
<part name="name" type="xsd:string" />
19
</message>
20
<message name="helloResponse">
21
<part name="return" type="xsd:string" />
22
</message>
23
<portType name="hellowsdlPortType">
24
<operation name="hello">
25
<documentation>Hello World</documentation>
26
<input message="tns:helloRequest"/>
27
<output message="tns:helloResponse"/>
28
</operation>
29
</portType>
30
<binding name="hellowsdlBinding" type="tns:hellowsdlPortType">
31
<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
32
<operation name="hello">
33
<soap:operation soapAction="urn:arnleadservicewsdl#hello" style="rpc"/>
34
<input>
35
<soap:body use="encoded" namespace="urn:hellowsdl"
36
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
37
</input>
38
<output>
39
<soap:body use="encoded" namespace="urn:hellowsdl"
40
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
41
</output>
42
</operation>
43 </binding>
44 <service name="hellowsdl">
45
<port name="hellowsdlPort" binding="tns:hellowsdlBinding">
46
<soap:address location="http://example.com/app.php/soap" />
47
</port>
5. http://sourceforge.net/projects/nusoap
PDF brought to you by
generated on October 26, 2012
Chapter 98: How to Create a SOAP Web Service in a Symfony2 Controller | 510
48 </service>
49 </definitions>
PDF brought to you by
generated on October 26, 2012
Chapter 98: How to Create a SOAP Web Service in a Symfony2 Controller | 511
Chapter 99
How Symfony2 differs from symfony1
The Symfony2 framework embodies a significant evolution when compared with the first version of the
framework. Fortunately, with the MVC architecture at its core, the skills used to master a symfony1
project continue to be very relevant when developing in Symfony2. Sure, app.yml is gone, but routing,
controllers and templates all remain.
In this chapter, we'll walk through the differences between symfony1 and Symfony2. As you'll see, many
tasks are tackled in a slightly different way. You'll come to appreciate these minor differences as they
promote stable, predictable, testable and decoupled code in your Symfony2 applications.
So, sit back and relax as we take you from "then" to "now".
Directory Structure
When looking at a Symfony2 project - for example, the Symfony2 Standard1 - you'll notice a very different
directory structure than in symfony1. The differences, however, are somewhat superficial.
The app/ Directory
In symfony1, your project has one or more applications, and each lives inside the apps/ directory
(e.g. apps/frontend). By default in Symfony2, you have just one application represented by the app/
directory. Like in symfony1, the app/ directory contains configuration specific to that application. It also
contains application-specific cache, log and template directories as well as a Kernel class (AppKernel),
which is the base object that represents the application.
Unlike symfony1, almost no PHP code lives in the app/ directory. This directory is not meant to house
modules or library files as it did in symfony1. Instead, it's simply the home of configuration and other
resources (templates, translation files).
The src/ Directory
Put simply, your actual code goes here. In Symfony2, all actual application-code lives inside a bundle
(roughly equivalent to a symfony1 plugin) and, by default, each bundle lives inside the src directory.
1. https://github.com/symfony/symfony-standard
PDF brought to you by
generated on October 26, 2012
Chapter 99: How Symfony2 differs from symfony1 | 512
In that way, the src directory is a bit like the plugins directory in symfony1, but much more flexible.
Additionally, while your bundles will live in the src/ directory, third-party bundles may live in the
vendor/bundles/ directory.
To get a better picture of the src/ directory, let's first think of a symfony1 application. First, part of your
code likely lives inside one or more applications. Most commonly these include modules, but could also
include any other PHP classes you put in your application. You may have also created a schema.yml file
in the config directory of your project and built several model files. Finally, to help with some common
functionality, you're using several third-party plugins that live in the plugins/ directory. In other words,
the code that drives your application lives in many different places.
In Symfony2, life is much simpler because all Symfony2 code must live in a bundle. In our pretend
symfony1 project, all the code could be moved into one or more plugins (which is a very good practice,
in fact). Assuming that all modules, PHP classes, schema, routing configuration, etc were moved into a
plugin, the symfony1 plugins/ directory would be very similar to the Symfony2 src/ directory.
Put simply again, the src/ directory is where your code, assets, templates and most anything else specific
to your project will live.
The vendor/ Directory
The vendor/ directory is basically equivalent to the lib/vendor/ directory in symfony1, which was
the conventional directory for all vendor libraries and bundles. By default, you'll find the Symfony2
library files in this directory, along with several other dependent libraries such as Doctrine2, Twig and
Swiftmailer. 3rd party Symfony2 bundles usually live in the vendor/bundles/.
The web/ Directory
Not much has changed in the web/ directory. The most noticeable difference is the absence of the
css/, js/ and images/ directories. This is intentional. Like with your PHP code, all assets should also
live inside a bundle. With the help of a console command, the Resources/public/ directory of each
bundle is copied or symbolically-linked to the web/bundles/ directory. This allows you to keep assets
organized inside your bundle, but still make them available to the public. To make sure that all bundles
are available, run the following command:
Listing 99-1
1 php app/console assets:install web
This command is the Symfony2 equivalent to the symfony1 plugin:publish-assets command.
Autoloading
One of the advantages of modern frameworks is never needing to worry about requiring files. By making
use of an autoloader, you can refer to any class in your project and trust that it's available. Autoloading
has changed in Symfony2 to be more universal, faster, and independent of needing to clear your cache.
In symfony1, autoloading was done by searching the entire project for the presence of PHP class files
and caching this information in a giant array. That array told symfony1 exactly which file contained each
class. In the production environment, this caused you to need to clear the cache when classes were added
or moved.
In Symfony2, a new class - UniversalClassLoader - handles this process. The idea behind the
autoloader is simple: the name of your class (including the namespace) must match up with the path to
PDF brought to you by
generated on October 26, 2012
Chapter 99: How Symfony2 differs from symfony1 | 513
the file containing that class. Take the FrameworkExtraBundle from the Symfony2 Standard Edition as
an example:
Listing 99-2
1
2
3
4
5
6
7
8
9
namespace Sensio\Bundle\FrameworkExtraBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
// ...
class SensioFrameworkExtraBundle extends Bundle
{
// ...
}
The
file
itself
lives
at
vendor/bundle/Sensio/Bundle/FrameworkExtraBundle/
SensioFrameworkExtraBundle.php. As you can see, the location of the file follows the namespace of
the class. Specifically, the namespace, Sensio\Bundle\FrameworkExtraBundle, spells out the directory
that the file should live in (vendor/bundle/Sensio/Bundle/FrameworkExtraBundle). This is because,
in the app/autoload.php file, you'll configure Symfony to look for the Sensio namespace in the vendor/
bundle directory:
Listing 99-3
1
2
3
4
5
6
7
// app/autoload.php
// ...
$loader->registerNamespaces(array(
...,
'Sensio'
=> __DIR__.'/../vendor/bundles',
));
If
the
file
did
not
live
at
this
exact
location,
you'd
receive
a
Class
"Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle" does not exist. error. In
Symfony2, a "class does not exist" means that the suspect class namespace and physical location do not
match. Basically, Symfony2 is looking in one exact location for that class, but that location doesn't exist
(or contains a different class). In order for a class to be autoloaded, you never need to clear your cache
in Symfony2.
As mentioned before, for the autoloader to work, it needs to know that the Sensio namespace lives
in the vendor/bundles directory and that, for example, the Doctrine namespace lives in the vendor/
doctrine/lib/ directory. This mapping is entirely controlled by you via the app/autoload.php file.
If you look at the HelloController from the Symfony2 Standard Edition you can see that it lives
in the Acme\DemoBundle\Controller namespace. Yet, the Acme namespace is not defined in the app/
autoload.php. By default you do not need to explicitly configure the location of bundles that live in
the src/ directory. The UniversalClassLoader is configured to fallback to the src/ directory using its
registerNamespaceFallbacks method:
Listing 99-4
1
2
3
4
5
6
// app/autoload.php
// ...
$loader->registerNamespaceFallbacks(array(
__DIR__.'/../src',
));
Using the Console
In symfony1, the console is in the root directory of your project and is called symfony:
PDF brought to you by
generated on October 26, 2012
Chapter 99: How Symfony2 differs from symfony1 | 514
Listing 99-5
1 php symfony
In Symfony2, the console is now in the app sub-directory and is called console:
Listing 99-6
1 php app/console
Applications
In a symfony1 project, it is common to have several applications: one for the frontend and one for the
backend for instance.
In a Symfony2 project, you only need to create one application (a blog application, an intranet
application, ...). Most of the time, if you want to create a second application, you might instead create
another project and share some bundles between them.
And if you need to separate the frontend and the backend features of some bundles, you can create
sub-namespaces for controllers, sub-directories for templates, different semantic configurations, separate
routing configurations, and so on.
Of course, there's nothing wrong with having multiple applications in your project, that's entirely up to
you. A second application would mean a new directory, e.g. my_app/, with the same basic setup as the
app/ directory.
Read the definition of a Project, an Application, and a Bundle in the glossary.
Bundles and Plugins
In a symfony1 project, a plugin could contain configuration, modules, PHP libraries, assets and anything
else related to your project. In Symfony2, the idea of a plugin is replaced by the "bundle". A bundle is
even more powerful than a plugin because the core Symfony2 framework is brought in via a series of
bundles. In Symfony2, bundles are first-class citizens that are so flexible that even core code itself is a
bundle.
In symfony1, a plugin must be enabled inside the ProjectConfiguration class:
Listing 99-7
1
2
3
4
5
// config/ProjectConfiguration.class.php
public function setup()
{
$this->enableAllPluginsExcept(array(... some plugins here));
}
In Symfony2, the bundles are activated inside the application kernel:
Listing 99-8
1 // app/AppKernel.php
2 public function registerBundles()
3 {
4
$bundles = array(
5
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
6
new Symfony\Bundle\TwigBundle\TwigBundle(),
PDF brought to you by
generated on October 26, 2012
Chapter 99: How Symfony2 differs from symfony1 | 515
7
8
9
10
11
12 }
...,
new Acme\DemoBundle\AcmeDemoBundle(),
);
return $bundles;
Routing (routing.yml) and Configuration (config.yml)
In symfony1, the routing.yml and app.yml configuration files were automatically loaded inside any
plugin. In Symfony2, routing and application configuration inside a bundle must be included manually.
For example, to include a routing resource from a bundle called AcmeDemoBundle, you can do the
following:
Listing 99-9
1 # app/config/routing.yml
2 _hello:
3
resource: "@AcmeDemoBundle/Resources/config/routing.yml"
This will load the routes found in the Resources/config/routing.yml file of the AcmeDemoBundle. The
special @AcmeDemoBundle is a shortcut syntax that, internally, resolves to the full path to that bundle.
You can use this same strategy to bring in configuration from a bundle:
Listing 99-10
1 # app/config/config.yml
2 imports:
3
- { resource: "@AcmeDemoBundle/Resources/config/config.yml" }
In Symfony2, configuration is a bit like app.yml in symfony1, except much more systematic. With
app.yml, you could simply create any keys you wanted. By default, these entries were meaningless and
depended entirely on how you used them in your application:
Listing 99-11
1 # some app.yml file from symfony1
2 all:
3
email:
4
from_address: foo.bar@example.com
In Symfony2, you can also create arbitrary entries under the parameters key of your configuration:
Listing 99-12
1 parameters:
2
email.from_address: foo.bar@example.com
You can now access this from a controller, for example:
Listing 99-13
1 public function helloAction($name)
2 {
3
$fromAddress = $this->container->getParameter('email.from_address');
4 }
In reality, the Symfony2 configuration is much more powerful and is used primarily to configure objects
that you can use. For more information, see the chapter titled "Service Container".
PDF brought to you by
generated on October 26, 2012
Chapter 99: How Symfony2 differs from symfony1 | 516
Part IV
The Components
Chapter 100
The ClassLoader Component
The ClassLoader Component loads your project classes automatically if they follow some
standard PHP conventions.
Whenever you use an undefined class, PHP uses the autoloading mechanism to delegate the loading of a
file defining the class. Symfony2 provides a "universal" autoloader, which is able to load classes from files
that implement one of the following conventions:
• The technical interoperability standards1 for PHP 5.3 namespaces and class names;
• The PEAR2 naming convention for classes.
If your classes and the third-party libraries you use for your project follow these standards, the Symfony2
autoloader is the only autoloader you will ever need.
Installation
You can install the component in many different ways:
• Use the official Git repository (https://github.com/symfony/ClassLoader3);
• Install it via PEAR ( pear.symfony.com/ClassLoader);
• Install it via Composer (symfony/class-loader on Packagist).
Usage
Registering the UniversalClassLoader4 autoloader is straightforward:
Listing 100-1
1. http://symfony.com/PSR0
2. http://pear.php.net/manual/en/standards.php
3. https://github.com/symfony/ClassLoader
4. http://api.symfony.com/2.0/Symfony/Component/ClassLoader/UniversalClassLoader.html
PDF brought to you by
generated on October 26, 2012
Chapter 100: The ClassLoader Component | 518
1
2
3
4
5
6
7
8
9
require_once '/path/to/src/Symfony/Component/ClassLoader/UniversalClassLoader.php';
use Symfony\Component\ClassLoader\UniversalClassLoader;
$loader = new UniversalClassLoader();
// ... register namespaces and prefixes here - see below
$loader->register();
For minor performance gains class paths can be cached in memory using APC by registering the
ApcUniversalClassLoader5:
Listing 100-2
1
2
3
4
5
6
7
require_once '/path/to/src/Symfony/Component/ClassLoader/UniversalClassLoader.php';
require_once '/path/to/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php';
use Symfony\Component\ClassLoader\ApcUniversalClassLoader;
$loader = new ApcUniversalClassLoader('apc.prefix.');
$loader->register();
The autoloader is useful only if you add some libraries to autoload.
The autoloader is automatically registered in a Symfony2 application (see app/autoload.php).
If the classes to autoload use namespaces, use the registerNamespace()6 or registerNamespaces()7
methods:
Listing 100-3
1
2
3
4
5
6
7
8
$loader->registerNamespace('Symfony', __DIR__.'/vendor/symfony/src');
$loader->registerNamespaces(array(
'Symfony' => __DIR__.'/../vendor/symfony/src',
'Monolog' => __DIR__.'/../vendor/monolog/src',
));
$loader->register();
For classes that follow the
registerPrefixes()9 methods:
Listing 100-4
PEAR
naming
convention,
use
the
registerPrefix()8 or
1 $loader->registerPrefix('Twig_', __DIR__.'/vendor/twig/lib');
2
3 $loader->registerPrefixes(array(
4
'Swift_' => __DIR__.'/vendor/swiftmailer/lib/classes',
5
'Twig_' => __DIR__.'/vendor/twig/lib',
6 ));
5. http://api.symfony.com/2.0/Symfony/Component/ClassLoader/ApcUniversalClassLoader.html
6. http://api.symfony.com/2.0/Symfony/Component/ClassLoader/UniversalClassLoader.html#registerNamespace()
7. http://api.symfony.com/2.0/Symfony/Component/ClassLoader/UniversalClassLoader.html#registerNamespaces()
8. http://api.symfony.com/2.0/Symfony/Component/ClassLoader/UniversalClassLoader.html#registerPrefix()
9. http://api.symfony.com/2.0/Symfony/Component/ClassLoader/UniversalClassLoader.html#registerPrefixes()
PDF brought to you by
generated on October 26, 2012
Chapter 100: The ClassLoader Component | 519
7
8 $loader->register();
Some libraries also require their root path be registered in the PHP include path
(set_include_path()).
Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be looked for in a location list to
ease the vendoring of a sub-set of classes for large projects:
Listing 100-5
1 $loader->registerNamespaces(array(
2
'Doctrine\\Common'
=> __DIR__.'/vendor/doctrine-common/lib',
3
'Doctrine\\DBAL\\Migrations' => __DIR__.'/vendor/doctrine-migrations/lib',
4
'Doctrine\\DBAL'
=> __DIR__.'/vendor/doctrine-dbal/lib',
5
'Doctrine'
=> __DIR__.'/vendor/doctrine/lib',
6 ));
7
8 $loader->register();
In this example, if you try to use a class in the Doctrine\Common namespace or one of its children, the
autoloader will first look for the class under the doctrine-common directory, and it will then fallback to
the default Doctrine directory (the last one configured) if not found, before giving up. The order of the
registrations is significant in this case.
PDF brought to you by
generated on October 26, 2012
Chapter 100: The ClassLoader Component | 520
Chapter 101
The Config Component
Introduction
The Config Component provides several classes to help you find, load, combine, autofill and validate
configuration values of any kind, whatever their source may be (Yaml, XML, INI files, or for instance a
database).
Installation
You can install the component in many different ways:
• Use the official Git repository (https://github.com/symfony/Config1);
• Install it via PEAR ( pear.symfony.com/Config);
• Install it via Composer (symfony/config on Packagist).
Sections
• Loading resources
• Caching based on resources
• Define and process configuration values
1. https://github.com/symfony/Config
PDF brought to you by
generated on October 26, 2012
Chapter 101: The Config Component | 521
Chapter 102
Loading resources
Locating resources
Loading the configuration normally starts with a search for resources – in most cases: files. This can be
done with the FileLocator1:
Listing 102-1
1
2
3
4
5
6
use Symfony\Component\Config\FileLocator;
$configDirectories = array(__DIR__.'/app/config');
$locator = new FileLocator($configDirectories);
$yamlUserFiles = $locator->locate('users.yml', null, false);
The locator receives a collection of locations where it should look for files. The first argument of
locate() is the name of the file to look for. The second argument may be the current path and when
supplied, the locator will look in this directory first. The third argument indicates whether or not the
locator should return the first file it has found, or an array containing all matches.
Resource loaders
For each type of resource (Yaml, XML, annotation, etc.) a loader must be defined. Each loader should
implement LoaderInterface2 or extend the abstract FileLoader3 class, which allows for recursively
importing other resources:
Listing 102-2
1 use Symfony\Component\Config\Loader\FileLoader;
2 use Symfony\Component\Yaml\Yaml;
3
1. http://api.symfony.com/2.0/Symfony/Component/Config/FileLocator.html
2. http://api.symfony.com/2.0/Symfony/Component/Config/Loader/LoaderInterface.html
3. http://api.symfony.com/2.0/Symfony/Component/Config/Loader/FileLoader.html
PDF brought to you by
generated on October 26, 2012
Chapter 102: Loading resources | 522
4 class YamlUserLoader extends FileLoader
5 {
6
public function load($resource, $type = null)
7
{
8
$configValues = Yaml::parse($resource);
9
10
// ... handle the config values
11
12
// maybe import some other resource:
13
14
// $this->import('extra_users.yml');
15
}
16
17
public function supports($resource, $type = null)
18
{
19
return is_string($resource) && 'yml' === pathinfo(
20
$resource,
21
PATHINFO_EXTENSION
22
);
23
}
24 }
Finding the right loader
The LoaderResolver4 receives as its first constructor argument a collection of loaders. When a resource
(for instance an XML file) should be loaded, it loops through this collection of loaders and returns the
loader which supports this particular resource type.
The DelegatingLoader5 makes use of the LoaderResolver6. When it is asked to load a resource, it
delegates this question to the LoaderResolver7. In case the resolver has found a suitable loader, this
loader will be asked to load the resource:
Listing 102-3
1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Config\Loader\DelegatingLoader;
$loaderResolver = new LoaderResolver(array(new YamlUserLoader($locator)));
$delegatingLoader = new DelegatingLoader($loaderResolver);
$delegatingLoader->load(__DIR__.'/users.yml');
/*
The YamlUserLoader will be used to load this resource,
since it supports files with a "yml" extension
*/
4. http://api.symfony.com/2.0/Symfony/Component/Config/Loader/LoaderResolver.html
5. http://api.symfony.com/2.0/Symfony/Component/Config/Loader/DelegatingLoader.html
6. http://api.symfony.com/2.0/Symfony/Component/Config/Loader/LoaderResolver.html
7. http://api.symfony.com/2.0/Symfony/Component/Config/Loader/LoaderResolver.html
PDF brought to you by
generated on October 26, 2012
Chapter 102: Loading resources | 523
Chapter 103
Caching based on resources
When all configuration resources are loaded, you may want to process the configuration values and
combine them all in one file. This file acts like a cache. Its contents don’t have to be regenerated every
time the application runs – only when the configuration resources are modified.
For example, the Symfony Routing component allows you to load all routes, and then dump a URL
matcher or a URL generator based on these routes. In this case, when one of the resources is modified
(and you are working in a development environment), the generated file should be invalidated and
regenerated. This can be accomplished by making use of the ConfigCache1 class.
The example below shows you how to collect resources, then generate some code based on the resources
that were loaded, and write this code to the cache. The cache also receives the collection of resources that
were used for generating the code. By looking at the "last modified" timestamp of these resources, the
cache can tell if it is still fresh or that its contents should be regenerated:
Listing 103-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Resource\FileResource;
$cachePath = __DIR__.'/cache/appUserMatcher.php';
// the second argument indicates whether or not you want to use debug mode
$userMatcherCache = new ConfigCache($cachePath, true);
if (!$userMatcherCache->isFresh()) {
// fill this with an array of 'users.yml' file paths
$yamlUserFiles = ...;
$resources = array();
foreach ($yamlUserFiles as $yamlUserFile) {
// see the previous article "Loading resources" to
// see where $delegatingLoader comes from
$delegatingLoader->load($yamlUserFile);
$resources[] = new FileResource($yamlUserFile);
}
1. http://api.symfony.com/2.0/Symfony/Component/Config/ConfigCache.html
PDF brought to you by
generated on October 26, 2012
Chapter 103: Caching based on resources | 524
21
22
// the code for the UserMatcher is generated elsewhere
23
$code = ...;
24
25
$userMatcherCache->write($code, $resources);
26 }
27
28 // you may want to require the cached code:
29 require $cachePath;
In debug mode, a .meta file will be created in the same directory as the cache file itself. This .meta file
contains the serialized resources, whose timestamps are used to determine if the cache is still fresh. When
not in debug mode, the cache is considered to be "fresh" as soon as it exists, and therefore no .meta file
will be generated.
PDF brought to you by
generated on October 26, 2012
Chapter 103: Caching based on resources | 525
Chapter 104
Define and process configuration values
Validate configuration values
After loading configuration values from all kinds of resources, the values and their structure can be
validated using the "Definition" part of the Config Component. Configuration values are usually
expected to show some kind of hierarchy. Also, values should be of a certain type, be restricted in number
or be one of a given set of values. For example, the following configuration (in Yaml) shows a clear
hierarchy and some validation rules that should be applied to it (like: "the value for auto_connect must
be a boolean value"):
Listing 104-1
1 auto_connect: true
2 default_connection: mysql
3 connections:
4
mysql:
5
host: localhost
6
driver: mysql
7
username: user
8
password: pass
9
sqlite:
10
host: localhost
11
driver: sqlite
12
memory: true
13
username: user
14
password: pass
When loading multiple configuration files, it should be possible to merge and overwrite some values.
Other values should not be merged and stay as they are when first encountered. Also, some keys are only
available when another key has a specific value (in the sample configuration above: the memory key only
makes sense when the driver is sqlite).
PDF brought to you by
generated on October 26, 2012
Chapter 104: Define and process configuration values | 526
Define a hierarchy of configuration values using the TreeBuilder
All the rules concerning configuration values can be defined using the TreeBuilder1.
A TreeBuilder2 instance should be returned from a custom Configuration class which implements the
ConfigurationInterface3:
Listing 104-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace Acme\DatabaseConfiguration;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class DatabaseConfiguration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('database');
// ... add node definitions to the root of the tree
return $treeBuilder;
}
}
Add node definitions to the tree
Variable nodes
A tree contains node definitions which can be laid out in a semantic way. This means, using indentation
and the fluent notation, it is possible to reflect the real structure of the configuration values:
Listing 104-3
1 $rootNode
2
->children()
3
->booleanNode('auto_connect')
4
->defaultTrue()
5
->end()
6
->scalarNode('default_connection')
7
->defaultValue('default')
8
->end()
9
->end()
10 ;
The root node itself is an array node, and has children, like the boolean node auto_connect and the
scalar node default_connection. In general: after defining a node, a call to end() takes you one step up
in the hierarchy.
Node type
It is possible to validate the type of a provided value by using the appropriate node definition. Node type
are available for:
1. http://api.symfony.com/2.0/Symfony/Component/Config/Definition/Builder/TreeBuilder.html
2. http://api.symfony.com/2.0/Symfony/Component/Config/Definition/Builder/TreeBuilder.html
3. http://api.symfony.com/2.0/Symfony/Component/Config/Definition/ConfigurationInterface.html
PDF brought to you by
generated on October 26, 2012
Chapter 104: Define and process configuration values | 527
•
•
•
•
scalar
boolean
array
variable (no validation)
and are created with node($name, $type) or their associated shortcut xxxxNode($name) method.
Array nodes
It is possible to add a deeper level to the hierarchy, by adding an array node. The array node itself, may
have a pre-defined set of variable nodes:
Listing 104-4
1 $rootNode
2
->arrayNode('connection')
3
->scalarNode('driver')->end()
4
->scalarNode('host')->end()
5
->scalarNode('username')->end()
6
->scalarNode('password')->end()
7
->end()
8 ;
Or you may define a prototype for each node inside an array node:
Listing 104-5
1 $rootNode
2
->arrayNode('connections')
3
->prototype('array')
4
->children()
5
->scalarNode('driver')->end()
6
->scalarNode('host')->end()
7
->scalarNode('username')->end()
8
->scalarNode('password')->end()
9
->end()
10
->end()
11
->end()
12 ;
A prototype can be used to add a definition which may be repeated many times inside the current node.
According to the prototype definition in the example above, it is possible to have multiple connection
arrays (containing a driver, host, etc.).
Array node options
Before defining the children of an array node, you can provide options like:
useAttributeAsKey()
Provide the name of a child node, whose value should be used as the key in the resulting array
requiresAtLeastOneElement()
There should be at least one element in the array (works only when isRequired() is also called).
An example of this:
Listing 104-6
1 $rootNode
2
->arrayNode('parameters')
3
->isRequired()
4
->requiresAtLeastOneElement()
5
->useAttributeAsKey('name')
PDF brought to you by
generated on October 26, 2012
Chapter 104: Define and process configuration values | 528
6
7
8
9
10
11
12
13 ;
->prototype('array')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('value')->isRequired()->end()
->end()
->end()
->end()
Default and required values
For all node types, it is possible to define default values and replacement values in case a node has a
certain value:
defaultValue()
Set a default value
isRequired()
Must be defined (but may be empty)
cannotBeEmpty()
May not contain an empty value
default*()
(null, true, false), shortcut for defaultValue()
treat*Like()
(null, true, false), provide a replacement value in case the value is *.
Listing 104-7
1 $rootNode
2
->arrayNode('connection')
3
->children()
4
->scalarNode('driver')
5
->isRequired()
6
->cannotBeEmpty()
7
->end()
8
->scalarNode('host')
9
->defaultValue('localhost')
10
->end()
11
->scalarNode('username')->end()
12
->scalarNode('password')->end()
13
->booleanNode('memory')
14
->defaultFalse()
15
->end()
16
->end()
17
->end()
18 ;
Merging options
Extra options concerning the merge process may be provided. For arrays:
PDF brought to you by
generated on October 26, 2012
Chapter 104: Define and process configuration values | 529
performNoDeepMerging()
When the value is also defined in a second configuration array, don’t try to merge an array, but
overwrite it entirely
For all nodes:
cannotBeOverwritten()
don’t let other configuration arrays overwrite an existing value for this node
Appending sections
If you have a complex configuration to validate then the tree can grow to be large and you may want to
split it up into sections. You can do this by making a section a separate node and then appending it into
the main tree with append():
Listing 104-8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('database');
$rootNode
->arrayNode('connection')
->children()
->scalarNode('driver')
->isRequired()
->cannotBeEmpty()
->end()
->scalarNode('host')
->defaultValue('localhost')
->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->booleanNode('memory')
->defaultFalse()
->end()
->end()
->append($this->addParametersNode())
->end()
;
return $treeBuilder;
}
public function addParametersNode()
{
$builder = new TreeBuilder();
$node = $builder->root('parameters');
$node
->isRequired()
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
->prototype('array')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('value')->isRequired()->end()
->end()
->end()
PDF brought to you by
generated on October 26, 2012
Chapter 104: Define and process configuration values | 530
44
45
46
47 }
;
return $node;
This is also useful to help you avoid repeating yourself if you have sections of the config that are repeated
in different places.
Normalization
When the config files are processed they are first normalized, then merged and finally the tree is used
to validate the resulting array. The normalization process is used to remove some of the differences that
result from different configuration formats, mainly the differences between Yaml and XML.
The separator used in keys is typically _ in Yaml and - in XML. For example, auto_connect in Yaml and
auto-connect. The normalization would make both of these auto_connect.
Another difference between Yaml and XML is in the way arrays of values may be represented. In Yaml
you may have:
1 twig:
2
extensions: ['twig.extension.foo', 'twig.extension.bar']
Listing 104-9
and in XML:
Listing 104-10 1
<twig:config>
2
<twig:extension>twig.extension.foo</twig:extension>
3
<twig:extension>twig.extension.bar</twig:extension>
4 </twig:config>
This difference can be removed in normalization by pluralizing the key used in XML. You can specify
that you want a key to be pluralized in this way with fixXmlConfig():
Listing 104-11 1
$rootNode
2
->fixXmlConfig('extension')
3
->children()
4
->arrayNode('extensions')
5
->prototype('scalar')->end()
6
->end()
7
->end()
8 ;
If it is an irregular pluralization you can specify the plural to use as a second argument:
Listing 104-12 1
$rootNode
2
->fixXmlConfig('child', 'children')
3
->children()
4
->arrayNode('children')
5
->end()
6 ;
As well as fixing this, fixXmlConfig ensures that single xml elements are still turned into an array. So
you may have:
Listing 104-13
PDF brought to you by
generated on October 26, 2012
Chapter 104: Define and process configuration values | 531
1 <connection>default</connection>
2 <connection>extra</connection>
and sometimes only:
Listing 104-14 1
<connection>default</connection>
By default connection would be an array in the first case and a string in the second making it difficult to
validate. You can ensure it is always an array with with fixXmlConfig.
You can further control the normalization process if you need to. For example, you may want to allow a
string to be set and used as a particular key or several keys to be set explicitly. So that, if everything apart
from id is optional in this config:
Listing 104-15 1
2
3
4
5
6
connection:
name: my_mysql_connection
host: localhost
driver: mysql
username: user
password: pass
you can allow the following as well:
Listing 104-16 1
connection: my_mysql_connection
By changing a string value into an associative array with name as the key:
Listing 104-17
1 $rootNode
2
->arrayNode('connection')
3
->beforeNormalization()
4
->ifString()
5
->then(function($v) { return array('name'=> $v); })
6
->end()
7
->scalarValue('name')->isRequired()
8
// ...
9
->end()
10 ;
Validation rules
More advanced validation rules can be provided using the ExprBuilder4. This builder implements a
fluent interface for a well-known control structure. The builder is used for adding advanced validation
rules to node definitions, like:
Listing 104-18
1 $rootNode
2
->arrayNode('connection')
3
->children()
4
->scalarNode('driver')
5
->isRequired()
6
->validate()
7
->ifNotInArray(array('mysql', 'sqlite', 'mssql'))
4. http://api.symfony.com/2.0/Symfony/Component/Config/Definition/Builder/ExprBuilder.html
PDF brought to you by
generated on October 26, 2012
Chapter 104: Define and process configuration values | 532
8
9
10
11
12
13 ;
->thenInvalid('Invalid database driver "%s"')
->end()
->end()
->end()
->end()
A validation rule always has an "if" part. You can specify this part in the following ways:
•
•
•
•
•
•
•
ifTrue()
ifString()
ifNull()
ifArray()
ifInArray()
ifNotInArray()
always()
A validation rule also requires a "then" part:
•
•
•
•
then()
thenEmptyArray()
thenInvalid()
thenUnset()
Usually, "then" is a closure. Its return value will be used as a new value for the node, instead of the node's
original value.
Processing configuration values
The Processor5 uses the tree as it was built using the TreeBuilder6 to process multiple arrays of
configuration values that should be merged. If any value is not of the expected type, is mandatory and
yet undefined, or could not be validated in some other way, an exception will be thrown. Otherwise the
result is a clean array of configuration values:
Listing 104-19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Config\Definition\Processor;
use Acme\DatabaseConfiguration;
$config1 = Yaml::parse(__DIR__.'/src/Matthias/config/config.yml');
$config2 = Yaml::parse(__DIR__.'/src/Matthias/config/config_extra.yml');
$configs = array($config1, $config2);
$processor = new Processor();
$configuration = new DatabaseConfiguration;
$processedConfiguration = $processor->processConfiguration(
$configuration,
$configs)
;
5. http://api.symfony.com/2.0/Symfony/Component/Config/Definition/Processor.html
6. http://api.symfony.com/2.0/Symfony/Component/Config/Definition/Builder/TreeBuilder.html
PDF brought to you by
generated on October 26, 2012
Chapter 104: Define and process configuration values | 533
Chapter 105
The Console Component
The Console component eases the creation of beautiful and testable command line interfaces.
The Console component allows you to create command-line commands. Your console commands can be
used for any recurring task, such as cronjobs, imports, or other batch jobs.
Installation
You can install the component in many different ways:
• Use the official Git repository (https://github.com/symfony/Console1);
• Install it via PEAR ( pear.symfony.com/Console);
• Install it via Composer (symfony/console on Packagist).
Creating a basic Command
To make a console command to greet us from the command line, create GreetCommand.php and add the
following to it:
Listing 105-1
1
2
3
4
5
6
7
8
9
10
11
namespace Acme\DemoBundle\Command;
use
use
use
use
use
Symfony\Component\Console\Command\Command;
Symfony\Component\Console\Input\InputArgument;
Symfony\Component\Console\Input\InputInterface;
Symfony\Component\Console\Input\InputOption;
Symfony\Component\Console\Output\OutputInterface;
class GreetCommand extends Command
{
protected function configure()
1. https://github.com/symfony/Console
PDF brought to you by
generated on October 26, 2012
Chapter 105: The Console Component | 534
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 }
{
$this
->setName('demo:greet')
->setDescription('Greet someone')
->addArgument(
'name',
InputArgument::OPTIONAL,
'Who do you want to greet?'
)
->addOption(
'yell',
null,
InputOption::VALUE_NONE,
'If set, the task will yell in uppercase letters'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$name = $input->getArgument('name');
if ($name) {
$text = 'Hello '.$name;
} else {
$text = 'Hello';
}
if ($input->getOption('yell')) {
$text = strtoupper($text);
}
$output->writeln($text);
}
You also need to create the file to run at the command line which creates an Application and adds
commands to it:
Listing 105-2
1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env php
# app/console
<?php
use Acme\DemoBundle\Command\GreetCommand;
use Symfony\Component\Console\Application;
$application = new Application();
$application->add(new GreetCommand);
$application->run();
Test the new console command by running the following
Listing 105-3
1 $ app/console demo:greet Fabien
This will print the following to the command line:
Listing 105-4
1 Hello Fabien
PDF brought to you by
generated on October 26, 2012
Chapter 105: The Console Component | 535
You can also use the --yell option to make everything uppercase:
Listing 105-5
1 $ app/console demo:greet Fabien --yell
This prints:
Listing 105-6
1 HELLO FABIEN
Coloring the Output
Whenever you output text, you can surround the text with tags to color its output. For example:
Listing 105-7
1
2
3
4
5
6
7
8
9
10
11
// green text
$output->writeln('<info>foo</info>');
// yellow text
$output->writeln('<comment>foo</comment>');
// black text on a cyan background
$output->writeln('<question>foo</question>');
// white text on a red background
$output->writeln('<error>foo</error>');
It is possible to define your own styles using the class OutputFormatterStyle2:
Listing 105-8
1 $style = new OutputFormatterStyle('red', 'yellow', array('bold', 'blink'));
2 $output->getFormatter()->setStyle('fire', $style);
3 $output->writeln('<fire>foo</fire>');
Available foreground and background colors are: black, red, green, yellow, blue, magenta, cyan and
white.
And available options are: bold, underscore, blink, reverse and conceal.
Using Command Arguments
The most interesting part of the commands are the arguments and options that you can make available.
Arguments are the strings - separated by spaces - that come after the command name itself. They are
ordered, and can be optional or required. For example, add an optional last_name argument to the
command and make the name argument required:
Listing 105-9
1 $this
2
// ...
3
->addArgument(
4
'name',
5
InputArgument::REQUIRED,
6
'Who do you want to greet?'
7
)
8
->addArgument(
9
'last_name',
10
InputArgument::OPTIONAL,
2. http://api.symfony.com/2.0/Symfony/Component/Console/Formatter/OutputFormatterStyle.html
PDF brought to you by
generated on October 26, 2012
Chapter 105: The Console Component | 536
11
12
'Your last name?'
);
You now have access to a last_name argument in your command:
Listing 105-10 1
if ($lastName = $input->getArgument('last_name')) {
2
$text .= ' '.$lastName;
3 }
The command can now be used in either of the following ways:
Listing 105-11 1
$ app/console demo:greet Fabien
2 $ app/console demo:greet Fabien Potencier
Using Command Options
Unlike arguments, options are not ordered (meaning you can specify them in any order) and are specified
with two dashes (e.g. --yell - you can also declare a one-letter shortcut that you can call with a single
dash like -y). Options are always optional, and can be setup to accept a value (e.g. dir=src) or simply as
a boolean flag without a value (e.g. yell).
It is also possible to make an option optionally accept a value (so that --yell or yell=loud work).
Options can also be configured to accept an array of values.
For example, add a new option to the command that can be used to specify how many times in a row the
message should be printed:
Listing 105-12 1
2
3
4
5
6
7
8
9
$this
// ...
->addOption(
'iterations',
null,
InputOption::VALUE_REQUIRED,
'How many times should the message be printed?',
1
);
Next, use this in the command to print the message multiple times:
Listing 105-13 1
for ($i = 0; $i < $input->getOption('iterations'); $i++) {
2
$output->writeln($text);
3 }
Now, when you run the task, you can optionally specify a --iterations flag:
Listing 105-14 1
$ app/console demo:greet Fabien
2 $ app/console demo:greet Fabien --iterations=5
The first example will only print once, since iterations is empty and defaults to 1 (the last argument of
addOption). The second example will print five times.
PDF brought to you by
generated on October 26, 2012
Chapter 105: The Console Component | 537
Recall that options don't care about their order. So, either of the following will work:
Listing 105-15 1
$ app/console demo:greet Fabien --iterations=5 --yell
2 $ app/console demo:greet Fabien --yell --iterations=5
There are 4 option variants you can use:
Option
Value
InputOption::VALUE_IS_ARRAY
This option accepts multiple values (e.g. --dir=/foo --dir=/
bar)
InputOption::VALUE_NONE
Do not accept input for this option (e.g. --yell)
InputOption::VALUE_REQUIRED This value is required (e.g. --iterations=5), the option itself
is still optional
InputOption::VALUE_OPTIONAL This option may or may not have a value (e.g. yell or
yell=loud)
You can combine VALUE_IS_ARRAY with VALUE_REQUIRED or VALUE_OPTIONAL like this:
Listing 105-16 1
2
3
4
5
6
7
8
9
$this
// ...
->addOption(
'iterations',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'How many times should the message be printed?',
1
);
Asking the User for Information
When creating commands, you have the ability to collect more information from the user by asking him/
her questions. For example, suppose you want to confirm an action before actually executing it. Add the
following to your command:
Listing 105-17 1
$dialog = $this->getHelperSet()->get('dialog');
2 if (!$dialog->askConfirmation(
3
$output,
4
'<question>Continue with this action?</question>',
5
false
6
)) {
7
return;
8 }
In this case, the user will be asked "Continue with this action", and unless they answer with y, the task
will stop running. The third argument to askConfirmation is the default value to return if the user
doesn't enter any input.
You can also ask questions with more than a simple yes/no answer. For example, if you needed to know
the name of something, you might do the following:
Listing 105-18
PDF brought to you by
generated on October 26, 2012
Chapter 105: The Console Component | 538
1 $dialog = $this->getHelperSet()->get('dialog');
2 $name = $dialog->ask(
3
$output,
4
'Please enter the name of the widget',
5
'foo'
6 );
Testing Commands
Symfony2 provides several tools to help you test your commands. The most useful one is the
CommandTester3 class. It uses special input and output classes to ease testing without a real console:
Listing 105-19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Acme\DemoBundle\Command\GreetCommand;
class ListCommandTest extends \PHPUnit_Framework_TestCase
{
public function testExecute()
{
$application = new Application();
$application->add(new GreetCommand());
$command = $application->find('demo:greet');
$commandTester = new CommandTester($command);
$commandTester->execute(array('command' => $command->getName()));
$this->assertRegExp('/.../', $commandTester->getDisplay());
// ...
}
}
The getDisplay()4 method returns what would have been displayed during a normal call from the
console.
You can test sending arguments and options to the command by passing them as an array to the
execute()5 method:
Listing 105-20
1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Acme\DemoBundle\Command\GreetCommand;
class ListCommandTest extends \PHPUnit_Framework_TestCase
{
// ...
public function testNameIsOutput()
{
$application = new Application();
$application->add(new GreetCommand());
3. http://api.symfony.com/2.0/Symfony/Component/Console/Tester/CommandTester.html
4. http://api.symfony.com/2.0/Symfony/Component/Console/Tester/CommandTester.html#getDisplay()
5. http://api.symfony.com/2.0/Symfony/Component/Console/Tester/CommandTester.html#execute()
PDF brought to you by
generated on October 26, 2012
Chapter 105: The Console Component | 539
13
14
15
16
17
18
19
20
21
22 }
$command = $application->find('demo:greet');
$commandTester = new CommandTester($command);
$commandTester->execute(
array('command' => $command->getName(), 'name' => 'Fabien')
);
$this->assertRegExp('/Fabien/', $commandTester->getDisplay());
}
You can also test a whole console application by using ApplicationTester6.
Calling an existing Command
If a command depends on another one being run before it, instead of asking the user to remember the
order of execution, you can call it directly yourself. This is also useful if you want to create a "meta"
command that just runs a bunch of other commands (for instance, all commands that need to be run
when the project's code has changed on the production servers: clearing the cache, generating Doctrine2
proxies, dumping Assetic assets, ...).
Calling a command from another one is straightforward:
Listing 105-21
1 protected function execute(InputInterface $input, OutputInterface $output)
2 {
3
$command = $this->getApplication()->find('demo:greet');
4
5
$arguments = array(
6
'command' => 'demo:greet',
7
'name'
=> 'Fabien',
8
'--yell' => true,
9
);
10
11
$input = new ArrayInput($arguments);
12
$returnCode = $command->run($input, $output);
13
14
// ...
15 }
First, you find()7 the command you want to execute by passing the command name.
Then, you need to create a new ArrayInput8 with the arguments and options you want to pass to the
command.
Eventually, calling the run() method actually executes the command and returns the returned code from
the command (return value from command's execute() method).
6. http://api.symfony.com/2.0/Symfony/Component/Console/Tester/ApplicationTester.html
7. http://api.symfony.com/2.0/Symfony/Component/Console/Application.html#find()
8. http://api.symfony.com/2.0/Symfony/Component/Console/Input/ArrayInput.html
PDF brought to you by
generated on October 26, 2012
Chapter 105: The Console Component | 540
Most of the time, calling a command from code that is not executed on the command line is not
a good idea for several reasons. First, the command's output is optimized for the console. But
more important, you can think of a command as being like a controller; it should use the model to
do something and display feedback to the user. So, instead of calling a command from the Web,
refactor your code and move the logic to a new class.
Learn More!
• Using Console Commands, Shortcuts and Built-in Commands
• How to build an Application that is a single Command
PDF brought to you by
generated on October 26, 2012
Chapter 105: The Console Component | 541
Chapter 106
Using Console Commands, Shortcuts and Builtin Commands
In addition to the options you specify for your commands, there are some built-in options as well as a
couple of built-in commands for the console component.
These examples assume you have added a file app/conso