├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── cache.html.twig ├── controller.html.twig ├── doctrine.html.twig ├── feedbackEmail.html.twig ├── forms.html.twig ├── index.html.twig ├── routing.html.twig ├── security.html.twig ├── services.html.twig ├── templating.html.twig ├── testing.html.twig ├── translations.html.twig └── validation.html.twig /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 David Perez Vicens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | symfony2cheatsheet 2 | ================== 3 | 4 | A practical symfony2 cheatsheet 5 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /cache.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

For the purposes of learning how to cache with Symfony2, we'll cover the subject in four steps:

3 | 4 | 10 |

Caching with a Gateway Cache

11 | 12 |

To enable caching, modify the code of a front controller to use the caching kernel:

13 | 14 | {% raw %}
// web/app.php
 15 | require_once __DIR__.'/../app/bootstrap.php.cache';
 16 | require_once __DIR__.'/../app/AppKernel.php';
 17 | require_once __DIR__.'/../app/AppCache.php';
 18 | 
 19 | use Symfony\Component\HttpFoundation\Request;
 20 | 
 21 | $kernel = new AppKernel('prod', false);
 22 | $kernel->loadClassCache();
 23 | // wrap the default AppKernel with the AppCache one
 24 | $kernel = new AppCache($kernel);
 25 | $request = Request::createFromGlobals();
 26 | $response = $kernel->handle($request);
 27 | $response->send();
 28 | $kernel->terminate($request, $response);
 29 | 
{% endraw %} 30 | 31 |

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:

32 | 33 | {% raw %}
error_log($kernel->getLog());
 34 | 
{% endraw %} 35 | 36 |

Introduction to HTTP Caching

37 | 38 |

HTTP specifies four response cache headers that we're concerned with:

39 | 40 | 46 |

The Cache-Control Header

47 | 48 |

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:

49 | 50 | {% raw %}
Cache-Control: private, max-age=0, must-revalidate
 51 | Cache-Control: max-age=3600, must-revalidate
 52 | 
{% endraw %} 53 | 54 |

Symfony provides abstraction layer:

55 | 56 | {% raw %}
$response = new Response();
 57 | 
 58 | // mark the response as either public or private
 59 | $response->setPublic();
 60 | $response->setPrivate();
 61 | 
 62 | // set the private or shared max age
 63 | $response->setMaxAge(600);
 64 | $response->setSharedMaxAge(600);
 65 | 
 66 | // set a custom Cache-Control directive
 67 | $response->headers->addCacheControlDirective('must-revalidate', true);
 68 | 
{% endraw %} 69 | 70 |

Public vs Private Responses

71 | 72 |

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 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!

73 | 74 | {% raw %}
public: Indicates that the response may be cached by both private and shared caches;
 75 | 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.
 76 | 
{% endraw %} 77 | 78 |

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).

79 | 80 |

Caching Rules and Defaults

81 | 82 |

Symfony2 automatically sets a sensible and conservative Cache-Control header when none is set by the developer by following these rules:

83 | 84 | 89 |

Expiration with Expires Header

90 | 91 | {% raw %}
$date = new DateTime();
 92 | $date->modify('+600 seconds');
 93 | $response->setExpires($date);
 94 | 
{% endraw %} 95 | 96 |

Expiration with the Cache-Control Header

97 | 98 | {% raw %}
/ Sets the number of seconds after which the response
 99 | // should no longer be considered fresh
100 | $response->setMaxAge(600);
101 | 
102 | // Same as above but only for shared caches
103 | $response->setSharedMaxAge(600);
104 | 
{% endraw %} 105 | 106 |

Validation the the ETag Header

107 | 108 |

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.

109 | 110 | {% raw %}
public function indexAction()
111 | {
112 |     $response = $this->render('MyBundle:Main:index.html.twig');
113 |     $response->setETag(md5($response->getContent()));
114 |     $response->setPublic(); // make sure the response is public/cacheable
115 |     $response->isNotModified($this->getRequest());
116 | 
117 |     return $response;
118 | }
119 | 
{% endraw %} 120 | 121 |

Validation with the Last-Modified Header

122 | 123 |

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."

124 | 125 | {% raw %}
public function showAction($articleSlug)
126 | {
127 |     // ...
128 | 
129 |     $articleDate = new \DateTime($article->getUpdatedAt());
130 |     $authorDate = new \DateTime($author->getUpdatedAt());
131 | 
132 |     $date = $authorDate > $articleDate ? $authorDate : $articleDate;
133 | 
134 |     $response->setLastModified($date);
135 |     // Set response as public. Otherwise it will be private by default.
136 |     $response->setPublic();
137 | 
138 |     if ($response->isNotModified($this->getRequest())) {
139 |         return $response;
140 |     }
141 | 
142 |     // do more work to populate the response will the full content
143 | 
144 |     return $response;
145 | }
146 | 
{% endraw %} 147 | 148 |

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:

149 | 150 | {% raw %}
public function showAction($articleSlug)
151 | {
152 |     // Get the minimum information to compute
153 |     // the ETag or the Last-Modified value
154 |     // (based on the Request, data is retrieved from
155 |     // a database or a key-value store for instance)
156 |     $article = ...;
157 | 
158 |     // create a Response with a ETag and/or a Last-Modified header
159 |     $response = new Response();
160 |     $response->setETag($article->computeETag());
161 |     $response->setLastModified($article->getPublishedAt());
162 | 
163 |     // Set response as public. Otherwise it will be private by default.
164 |     $response->setPublic();
165 | 
166 |     // Check that the Response is not modified for the given Request
167 |     if ($response->isNotModified($this->getRequest())) {
168 |     // return the 304 Response immediately
169 |         return $response;
170 |     } else {
171 |         // do more work here - like retrieving more data
172 |         $comments = ...;
173 | 
174 |         // or render a template with the $response you've already started
175 |         return $this->render(
176 |             'MyBundle:MyController:article.html.twig',
177 |             array('article' => $article, 'comments' => $comments),
178 |             $response
179 |         );
180 |     }
181 | }
182 | 
{% endraw %} 183 | 184 |

Varying the Response

185 | 186 |

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. 187 | 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.

188 | 189 | {% raw %}
// set one vary header
190 | $response->setVary('Accept-Encoding');
191 | 
192 | // set multiple vary headers
193 | $response->setVary(array('Accept-Encoding', 'User-Agent'));
194 | 
195 | // Marks the Response stale
196 | $response->expire();
197 | 
198 | // Force the response to return a proper 304 response with no content
199 | $response->setNotModified();
200 | 
{% endraw %} 201 | 202 |

Using Edge Side Includes

203 | 204 |

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 ESI, 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.

205 | 206 | {% raw %}
# app/config/config.yml
207 | framework:
208 |     # ...
209 |     esi: { enabled: true }
210 | 
{% endraw %} 211 | 212 |

Let's suppose that we hace an static page with a dynamic tickets section:

213 | 214 | {% raw %}
public function indexAction()
215 | {
216 |     $response = $this->render('MyBundle:MyController:index.html.twig');
217 |     // set the shared max age - which also marks the response as public
218 |     $response->setSharedMaxAge(600);
219 |     return $response;
220 | }
221 | 
{% endraw %} 222 | 223 |

Now, let's embedd the ticket content using twig render's tag.

224 | 225 | {% raw %}
{% render '...:news' with {}, {'standalone': true} %}
226 | 
{% endraw %} 227 | 228 |

Using the 229 | standalone true tells symfony to use ESI tags. The embedded action can now specify its own caching rules, entirely independent of the master page. 230 |

231 | 232 | {% raw %}
public function newsAction()
233 | {
234 | // ...
235 | $response->setSharedMaxAge(60);
236 | }
237 | 
{% endraw %} 238 | 239 |

For the ESI include tag to work properly, you must define the _internal route:

240 | 241 | {% raw %}
# app/config/routing.yml
242 | _internal:
243 |     resource: "@FrameworkBundle/Resources/config/routing/internal.xml"
244 |     prefix: /_internal
245 | 
{% endraw %} 246 | 247 |

Learn more 248 | How to use Varnish to speed up my Website 249 |

-------------------------------------------------------------------------------- /controller.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

Here are some useful shortcuts and functions regarding the request in Symfony 2 controllers.

3 | 4 |

REQUEST and RESPONSE objects

5 | 6 |
7 | 8 |
9 | 10 | {% raw %} 11 |
$request->query->get('foo'); //gets foo GET var.
 12 | $request->request->get('bar'); //gets POST var.
 13 | $request->getMethod();
 14 | 
 15 | $request->server->get('HTTP_HOST'); //server variables.
 16 | $request->getPathInfo(); //gets the URI.
 17 | $request->files->get('file'); //files posted in a form.
 18 | 
 19 | $request->headers->get('content-type');
 20 | $request->cookies->get('PHPSESSID'); //cookies
 21 | 
 22 | $request->getLanguages();
 23 | $request->getPreferedLanguage(array('es','fr'));
 24 | $request->isXmlHttpRequest();
 25 | 
{% endraw %} 26 |
27 | 28 |
29 | 30 |

Redirecting in a controller:

31 | 32 | {% raw %} 33 |
$this->redirect($this->generateUrl("homepage"));
 34 | // 2.6 and above
 35 | $this->redirectToRoute('homepage');
 36 | 
37 | {% endraw %} 38 | 39 |

Rendering text from a controller:

40 | 41 | {% raw %} 42 |
return new Response('<html>…</html>');
 43 | 
{% endraw %} 44 | 45 |

Forwarding:

46 | 47 | {% raw %} 48 |
return $this->forward('Bundle:Controller:Action');
 49 | 
{% endraw %} 50 | 51 |

Redirect to 404 not found:

52 | 53 | {% raw %} 54 |
throw $this->createNotFoundException(message);
 55 | 
{% endraw %} 56 |
57 | 58 |
59 | 60 |
61 | 62 |
63 | 64 |

Working with the session

65 | 66 |

You can manage session attributes with:

67 | 68 | {% raw %} 69 |
$session = $this->getRequest()->getSession();
 70 | 
{% endraw %} 71 | 72 |

or the shortcut version

73 | 74 | {% raw %} 75 |
$this->get('session');
 76 | 
{% endraw %} 77 | 78 |

and to work with the data:

79 | 80 | {% raw %} 81 |
$session->get('foo','default value');
 82 | $session->set('foo','bar');
 83 | 
{% endraw %} 84 |
85 | 86 |
87 | 88 |

Flash messages

89 | 90 |

Flash messages only last one request and they are stored in a FlashBag:

91 | 92 | {% raw %} 93 |
$this->addFlash('notice','message');
 94 | 
{% endraw %} 95 | 96 |

To iterate trough all flash messages in a template you can use:

97 | 98 | {% raw %} 99 |
{% for flashMessage in app.session.flashbag.get('notice') %}
100 |     <div class="flash notice">
101 |         {{ flashMessage }}
102 |     </div>
103 | {% endfor %}
104 | 
{% endraw %} 105 |
106 | 107 |
108 | 109 |

Finally, here is an example of a controller class with Request and Response object in use.

110 | 111 | 112 | {% raw %}
namespace Symfony\CheatSheetBundle\Controller;
113 | 
114 | use Symfony\Bundle\FrameworkBundle\Controller\Controller;
115 | use Symfony\Component\HttpFoundation\Request;
116 | use Symfony\Component\HttpFoundation\Response;
117 | 
118 | class DefaultController extends Controller
119 | {
120 |     public function indexAction()
121 |     {
122 |         return $this->render('SymfonyCheatSheetBundle:Default:index.html.twig');
123 |     }
124 | 
125 |     public function contactAction(Request $request)
126 |     {
127 |         //get request variables.
128 |         //do something, call service, go to database, create form, send emails, etc...
129 |         return $this->render('SymfonyCheatSheetBundle:Default:feedback.html.twig', array([template vars]));
130 |     }
131 | }
132 | 
{% endraw %} 133 | -------------------------------------------------------------------------------- /doctrine.html.twig: -------------------------------------------------------------------------------- 1 |

Skipper & Doctrine ORM

2 |

Symfony comes with Doctrine ORM framework. Doctrine has extensive documentation, but you can start using it right away without a need to dive too deep. Skipper can make your life with Doctrine and other ORM frameworks much easier.

3 |

Skipper is a multiplatform tool for modeling ORM in a very comfortable way. You can continuously export your model and even edit exported classes without losing the changes on consequent exports. What’s also great about Skipper is that it can construct a model from an existing project. And it can do so in a very colourful and well-arranged way :-).

4 |
5 |

Start by downloading Skipper and installing on your computer. Skipper works natively on Mac OS X, Windows and Linux.

6 | 30 |

Working with the model

31 |

Your first model is now exported. Click Export to ORM button whenever you want to update your schema definitions.

32 |

Create new Entities

33 |

Select tool Entity from the top ribbon (or press Ctrl + T) and click in your model to place the new entity. Add required fields. You can navigate with keyboard shortcuts:

34 |

41 |
42 |

Create new Association

43 |

Select tool Association from the top ribbon (or press Ctrl + R) and select the entities you want to create association between. By filling either one or both aliases you can set the Association as an unidirectional or bidirectional.

44 |
45 |

Create new Many-Many Association

46 |

Select tool ManyToMany from the top ribbon (or press Ctrl + M) and select the entities you want to create association between. Again you need to fill aliases and additionally also the MN Entity name. If you don’t want to type it manually, you can use the naming tool (red arrow) which follows the usual naming conventions.

47 |
48 |

Set Doctrine ORM properties

49 |

You can set ORM properties using the property window in the lower bottom corner of Skipper. Property window shows you available properties based on the object type (Entity/Relation/Field).

50 |
51 |

Generate doctrine schema

52 |

If you have selected XML or YML format you need to generate the PHP classes generate:doctrine:entities [bundleNamespace]. This will automatically create all entity and repository classes for us. Easy, isn’t it? If you have been exporting to PHP annotations you’re ready to go.

53 |

Wide possibilities for customization

54 |

Another great thing about Skipper is a wide range of areas where it is possible to customize the behaviour of the application. For example you can configure list of external commands and executed it directly from the app:

55 |
56 |

Also you can extend shipped ORM properties with your own ones, you can add new datatypes, structures, etc. These configurations can be shared between all projects or add only to specific one. This topic is nicely described in Skipper documentation. 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |

Persisting object into the database

69 |

Now that you have a mapped Product entity and corresponding product table, you're ready to persist 70 | data to the database. From inside a controller, this is pretty easy. Add the following method to the 71 | DefaultController of the bundle:

72 |
// src/Acme/StoreBundle/Controller/DefaultController.php
73 | 
74 |         use Acme\StoreBundle\Entity\Product;
75 |         use Symfony\Component\HttpFoundation\Response;
76 | 
77 |         public function createAction()
78 |         {
79 |         $product = new Product();
80 |         $product->setName('A Foo Bar');
81 |         $product->setPrice('19.99');
82 |         $product->setDescription('Lorem ipsum dolor');
83 | 
84 |         $em = $this->getDoctrine()->getManager();
85 |         $em->persist($product); //marks object to be saved in the next transaction.
86 |         $em->flush(); //performs all saves and transactions.
87 | 
88 |         return new Response('Created product id '.$product->getId());
89 |         }
90 |     
91 | -------------------------------------------------------------------------------- /feedbackEmail.html.twig: -------------------------------------------------------------------------------- 1 | Message from {{ app.request.get('name') }}: 2 | 3 | {{ app.request.get('message') }} 4 | 5 | Name: {{ app.request.get('name') }} 6 | E-mail: {{ app.request.get('email') }} 7 | 8 | Sending IP: {{ remote }} 9 | Sending Script: {{ host }} {{ self }} -------------------------------------------------------------------------------- /forms.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

Creating simple forms

3 | 4 | {% raw %}
// src/Acme/TaskBundle/Controller/DefaultController.php
  5 | namespace Acme\TaskBundle\Controller;
  6 | 
  7 | use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  8 | use Acme\TaskBundle\Entity\Task;
  9 | use Symfony\Component\HttpFoundation\Request;
 10 | 
 11 | class DefaultController extends Controller
 12 | {
 13 |     public function newAction(Request $request)
 14 |     {
 15 |     // create a task and give it some dummy data for this example
 16 |     $task = $this->createForm(new Task());
 17 |     $task->setTask('Write a blog post');
 18 |     $task->setDueDate(new \DateTime('tomorrow'));
 19 | 
 20 |     $form = $this->createFormBuilder($task)
 21 |         ->add('task', 'text')
 22 |         ->add('dueDate', 'date')
 23 |         ->getForm();
 24 | 
 25 |     return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
 26 |         'form' => $form->createView()
 27 |     ));
 28 | 
 29 |     }
 30 | }
 31 | 
{% endraw %} 32 | 33 |

Creating form classes and embedding subform

34 | 35 | {% raw %}
// src/Acme/TaskBundle/Form/Type/TaskType.php
 36 | namespace Acme\TaskBundle\Form\Type;
 37 | 
 38 | use Symfony\Component\Form\AbstractType;
 39 | use Symfony\Component\Form\FormBuilderInterface;
 40 | use Symfony\Component\OptionsResolver\OptionsResolverInterface;
 41 | 
 42 | class TaskType extends AbstractType
 43 | {
 44 |     public function buildForm(FormBuilderInterface $builder, array $options)
 45 |     {
 46 |         $builder->add('task');
 47 |         $builder->add('dueDate', null, array('widget' => 'single_text'));
 48 | 
 49 |         //Any extra field not mapped to the object must define property_path.
 50 |         $builder->add('agree','checkbox', array('property_path' => false));
 51 | 
 52 |         //Embedding one form, you need to create first the categoryType class as usual.
 53 |         $builder->add('category', new CategoryType());
 54 | 
 55 |         //Embedding a collection of TAGS forms. You already have a tagType form.
 56 |         $builder->add('tags', 'collection', array('type' => new TagType()));
 57 | 
 58 |     }
 59 | 
 60 |     public function getName()
 61 |     {
 62 |         return 'task'; //must be unique.
 63 |     }
 64 | 
 65 |     //Symfony can guess the type but it is a good practice to always set de data_class because embedding forms is necessary.
 66 |     public function setDefaultOptions(OptionsResolverInterface $resolver)
 67 |     {
 68 |         $resolver->setDefaults(array(
 69 |             'data_class' => 'Acme\TaskBundle\Entity\Task',
 70 |             'cascade_validation' => true, //needed to validate embeed forms.
 71 |             'validation_groups' => array('registration'), //use of validation groups.
 72 |             'csrf_protection' => true,
 73 |             'csrf_field_name' => '_token',
 74 |             // a unique key to help generate the secret token
 75 |             'intention' => 'task_item',
 76 |         ));
 77 |     }
 78 | 
 79 | }
 80 | 
{% endraw %} 81 | 82 |

Field Type Options

83 | 84 | {% raw %}
->add('dueDate', 'date', array(
 85 |     'widget' => 'single_text',
 86 |     'label' => 'Due Date'
 87 | ))
 88 | 
{% endraw %} 89 | 90 |

The field data can be accessed in a controller with:

91 | 92 | {% raw %}
$form->get('dueDate')->getData();
 93 | 
{% endraw %} 94 | 95 |

If, for some reason, you don't have access to your original $task object, you can fetch it from the form:

96 | 97 | {% raw %}
$task = $form->getData();
 98 | 
{% endraw %} 99 | 100 |

Using form classes in a controller:

101 | 102 | {% raw %}
$form = $this->createForm(new TaskType(), new Task());
103 | 
104 | // process the form on POST
105 | if ($request->isMethod('POST')) {
106 | 
107 | //you can access POST variables directly
108 | $this->get('request')->request->get('name');
109 | 
110 | $form->bind($request);
111 | 
112 | if ($form->isValid()) {
113 |     $em = $this->getDoctrine()->getManager();
114 |     $em->persist($task);
115 |     $em->flush();
116 | }
117 | }
118 | 
119 | return $this->render('AcmeTaskBundle:Task:new.html.twig', array(
120 |     'form' => $form->createView()
121 | ));
122 | 
{% endraw %} 123 | 124 |

Groups based on Submitted Data

125 | 126 |

The ability to specify a callback or Closure in validation_groups is new to version 2.1

127 | 128 | {% raw %}
public function setDefaultOptions(OptionsResolverInterface $resolver)
129 | {
130 |     $resolver->setDefaults(array(
131 |         'validation_groups' => array('Acme\\AcmeBundle\\Entity\\Client', 'determineValidationGroups'),
132 |     ));
133 | }
134 | 
{% endraw %} 135 | 136 |

This will call the static method determineValidationGroups() on the Client class after the form is bound, but before validation is executed. 137 | The Form object is passed as an argument to that method (see next example). You can also define whole logic inline by using a Closure:

138 | 139 |

You can use inline post validation also:

140 | 141 | {% raw %}
public function setDefaultOptions(OptionsResolverInterface $resolver)
142 | {
143 |     $resolver->setDefaults(array(
144 |         'validation_groups' => function(FormInterface $form) {
145 |             $data = $form->getData();
146 |             if (Entity\Client::TYPE_PERSON == $data->getType()) {
147 |                 return array('person');
148 |             } else {
149 |                 return array('company');
150 |             }
151 |         },
152 |     ));
153 | }
154 | 
{% endraw %} 155 | 156 |

Rendering forms in TWIG

157 | 158 |

First of all check all Form Type References

159 | 160 |

161 | http://symfony.com/doc/current/reference/forms/types.html 162 |

163 | 164 |
165 | 166 |
167 |

Text field

168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 |
WidgetWidget
texttextarea
textareainteger
emailmoney
numberpassword
percentsearch
195 | 196 |

Choice fields

197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 |
WidgetWidget
choiceentity
countrylanguage
localetimezone
216 | 217 |
218 | 219 |
220 | 221 |

Date and datetime fields

222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 |
WidgetWidget
datedatetime
timebirthday
237 | 238 | 239 | 240 |

Other fields

241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 |
WidgetWidget
checkboxfile
radio
256 |

Field groups

257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 |
WidgetWidget
collectionrepeated
268 |

Hidden fields

269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 |
WidgetWidget
hiddencsrf
280 |
281 |
282 | 283 | 284 | 285 |

Simple and fast

286 | 287 | {% raw %}
{{ form_start(form) }}
288 | {{ form_widget(form) }}
289 | {{ form_end(form) }}
290 | 
{% endraw %} 291 | 292 |

Rows

293 | 294 | {% raw %}
<form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
295 | {{ form_errors(form) }}
296 | 
297 | {{ form_row(form.task) }}
298 | {{ form_row(form.dueDate) }}
299 | 
300 | {{ form_rest(form) }}
301 | 
302 | <input type="submit" />
303 | </form>
304 | 
{% endraw %} 305 | 306 |

Manual

307 | 308 | {% raw %} 309 |
<form action="{{ path('task_new') }}" method="post" {{ form_enctype(form) }}>
310 | {{ form_errors(form) }}
311 | <div>
312 |     {{ form_label(form.task,'custom label') }}
313 |     {{ form_errors(form.task) }}
314 |     {{ form_widget(form.task, { 'attr': {'class': 'span3'} })) }}
315 | </div>
316 | <div>
317 |     {{ form_label(form.dueDate) }}
318 |     {{ form_errors(form.dueDate) }}
319 |     {{ form_widget(form.dueDate) }}
320 | </div>
321 | 
322 | {# Render one embedded form #}
323 | <h3>Category</h3>
324 | <div class="category">
325 |     {{ form_row(form.category.name) }}
326 | </div>
327 | 
328 | {# Render multiple embedded forms #}
329 | <h3>Tags</h3>
330 | <ul class="tags">
331 |     {% for tag in form.tags %}
332 |     <li>{{ form_row(tag.name) }}</li>
333 |     {% endfor %}
334 | </ul>
335 | 
336 | {{ form_rest(form) }}
337 | 
338 | </form>
339 | 
{% endraw %} 340 | 341 |

Access "name" and "id" attributes

342 | 343 | {% raw %}
{{ form.task.vars.id }}
344 | {{ form.task.vars.full_name }}
345 | 
{% endraw %} 346 | 347 |

You can access current data of your form via:

348 | 349 | {% raw %}
{{ form.vars.value.task }}
350 | 
{% endraw %} 351 | 352 |

Form Theming

353 | 354 |

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.

355 | 356 |

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:

357 | 358 | {% raw %}
{# src/Acme/TaskBundle/Resources/views/Form/fields.html.twig #}
359 | {% block form_row %}
360 |     {% spaceless %}
361 |     <div class="form_row">
362 |         {{ form_label(form) }}
363 |         {{ form_errors(form) }}
364 |         {{ form_widget(form) }}
365 |     </div>
366 |     {% endspaceless %}
367 | {% endblock form_row %}
368 | 
{% endraw %} 369 | 370 |

To tell the form component to use your new form_row fragment defined above, add the following to the top of the template that renders the form:

371 | 372 | {% raw %}
{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
373 | {% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' %}
374 | {% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' 'AcmeTaskBundle:Form:fields2.html.twig' %}
375 | <form ...>
376 | 
{% endraw %} 377 | 378 |

New in version 2.1: An alternate Twig syntax for form_theme has been introduced in 2.1. It accepts any valid Twig expression (the most noticeable difference is using an array when using multiple themes).

379 | 380 | {% raw %} 381 |
{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}
382 | {% form_theme form with 'AcmeTaskBundle:Form:fields.html.twig' %}
383 | {% form_theme form with ['AcmeTaskBundle:Form:fields.html.twig', 'AcmeTaskBundle:Form:fields2.html.twig'] %}
384 | 
{% endraw %} 385 | 386 |

You can see all form fragments from twig followin this link: 387 | https://github.com/symfony/symfony/tree/master/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form 388 |

389 | 390 |

If your form customizations live inside an external template, you can reference the base block by using the parent() Twig function:

391 | 392 | {% raw %}
{# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
393 | {% extends 'form_div_layout.html.twig' %}
394 | 
395 | {% block integer_widget %}
396 | <div class="integer_widget">
397 |     {{ parent() }}
398 | </div>
399 | {% endblock %}
400 | 
{% endraw %} 401 | 402 |

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:

403 | 404 | {% raw %}
# app/config/config.yml
405 |     twig:
406 |         form:
407 |             resources:
408 |                 - 'AcmeDemoBundle:Form:fields.html.twig'
409 | 
{% endraw %} 410 | 411 |

See more in: 412 | http://symfony.com/doc/current/cookbook/form/form_customization.html 413 |

414 | -------------------------------------------------------------------------------- /index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "SymfonyCheatSheetBundle::layout.html.twig" %} 2 | 3 | {% block content %} 4 | 5 |
6 | 7 |
8 |
9 |
Symfony2cheatsheet
10 |

Finally, a Symfony2 guide to make your work easier.

11 | v 2.* 12 |
13 | 14 | {#
#} 15 | {#Download PDF#} 16 | {#
#} 17 | 18 | 46 |
47 | 48 |
49 |
50 |
51 |

CONTROLLER back to top

52 | {% include "SymfonyCheatSheetBundle:Default:controller.html.twig" %} 53 | 54 |
55 |

ROUTING back to top

56 | {% include "SymfonyCheatSheetBundle:Default:routing.html.twig" %} 57 | 58 |
59 |

TEMPLATING and TWIG back to top

60 | {% include "SymfonyCheatSheetBundle:Default:templating.html.twig" %} 61 | 62 |
63 |

DOCTRINE back to top

64 | {% include "SymfonyCheatSheetBundle:Default:doctrine.html.twig" %} 65 | 66 |
67 |

PHP UNIT TESTING back to top

68 | {% include "SymfonyCheatSheetBundle:Default:testing.html.twig" %} 69 | 70 |
71 |

VALIDATION back to top

72 | {% include "SymfonyCheatSheetBundle:Default:validation.html.twig" %} 73 | 74 |
75 |

FORMS back to top

76 | {% include "SymfonyCheatSheetBundle:Default:forms.html.twig" %} 77 | 78 |
79 |

SECURITY back to top

80 | {% include "SymfonyCheatSheetBundle:Default:security.html.twig" %} 81 | 82 |
83 |

HTTP Cache back to top

84 | {% include "SymfonyCheatSheetBundle:Default:cache.html.twig" %} 85 | 86 |
87 |

Translation back to top

88 | {% include "SymfonyCheatSheetBundle:Default:translations.html.twig" %} 89 | 90 |
91 |

Service Container back to top

92 | {% include "SymfonyCheatSheetBundle:Default:services.html.twig" %} 93 | 94 |
95 |
96 |
97 | 98 | {% endblock %} 99 | 100 | 101 | -------------------------------------------------------------------------------- /routing.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

Routing in Symfony 2 is even easier than in Symfony 1.x. Here is an example of the most complex routing you can get in Symfony 2.

3 | 4 | {% raw %} 5 |
article_show:
 6 |     pattern: /{_locale}/article-details/{page}.{_format}
 7 |     defaults: {_controller:Bundle:Controller:Action, _format: html, page:1}
 8 |     requirements:
 9 |         _locale: en|fr
10 |         _format: html|rss
11 |         page: \d+
12 |         _scheme: http|https
13 | 
{% endraw %} 14 |

Also you can prefix imported routes and give a group of routes a prepend text:

15 | 16 | {% raw %} 17 |
#app/config/routing.yml
18 |     acme_hello:
19 |     resource: "@AcmeHelloBundle/Resources/config/routing.yml"
20 |     prefix: /admin
21 | 
{% endraw %} 22 | 23 |

Working with annotations

24 |

You can use annotations in your controller by enabling annotations in your routing.yml and in your config.yml

25 | 26 |
#config.yml
27 | sensio_framework_extra:
28 |     router:  { annotations: true }
29 |     request: { converters: true }
30 |     view:    { annotations: true }
31 |     cache:   { annotations: true }
32 | 
33 | 34 |
#routing.yml
35 | acmedemo_main:
36 |     resource: "@AcmeDemoWebBundle/Controller"
37 |     prefix:   /
38 |     type: annotation
39 | 
40 | 41 |

In your controller:

42 |
/**
43 | * @Route("/{_locale}/", name="localizedHomepage")
44 | * @Method("POST")
45 | * @param $name
46 | * @Template("AcmeDemoWebBundle:Web:index.html.twig")
47 | * @Cache(expires="+30 days")
48 | */
49 | public function localizedHomepageAction($name)
50 | {
51 |     return array('name' => $name);
52 | }
53 | 
-------------------------------------------------------------------------------- /security.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

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.

3 | 4 |

Authentication

5 | 6 | 13 |

Authorization

14 | 15 | 20 |

Basic Example: HTTP Authentication

21 | 22 | {% raw %}
# app/config/security.yml
 23 | security:
 24 |     firewalls:
 25 |         secured_area:
 26 |             pattern:    ^/
 27 |             anonymous: ~
 28 |             http_basic:
 29 |                 realm: "Secured Demo Area"
 30 | 
 31 |     access_control:
 32 |         - { path: ^/admin, roles: ROLE_ADMIN }
 33 | 
 34 |     providers:
 35 |         in_memory:
 36 |             memory:
 37 |                 users:
 38 |                     ryan:  { password: ryanpass, roles: 'ROLE_USER' }
 39 |                     admin: { password: kitten, roles: 'ROLE_ADMIN' }
 40 | 
 41 |     encoders:
 42 |         Symfony\Component\Security\Core\User\User: plaintext
 43 | 
{% endraw %} 44 | 45 |

Using a Traditional Login Form

46 | 47 | {% raw %}
# app/config/security.yml
 48 | security:
 49 |     firewalls:
 50 |         secured_area:
 51 |             pattern:    ^/
 52 |             anonymous: ~
 53 |             form_login:
 54 |                 login_path:  /login
 55 |                 check_path:  /login_check
 56 | 
{% endraw %} 57 | 58 |

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:

59 | 60 | {% raw %}
form_login: ~
 61 | 
{% endraw %} 62 | 63 |

Now we need to create the login routes:

64 | 65 | {% raw %}
# app/config/routing.yml
 66 | login:
 67 |     pattern: /login
 68 |     defaults: { _controller: AcmeSecurityBundle:Security:login }
 69 | 
 70 | login_check:
 71 |     pattern: /login_check
 72 | 
{% endraw %} 73 | 74 |

Next, create the controller that will display the login form:

75 | 76 | {% raw %} 77 |
// src/Acme/SecurityBundle/Controller/SecurityController.php;
 78 | namespace Acme\SecurityBundle\Controller;
 79 | 
 80 | use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 81 | use Symfony\Component\Security\Core\SecurityContext;
 82 | 
 83 | class SecurityController extends Controller
 84 | {
 85 |    public function loginAction()
 86 |    {
 87 |        $request = $this->getRequest();
 88 |        $session = $request->getSession();
 89 | 
 90 |        // get the login error if there is one
 91 |        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
 92 |            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
 93 |        } else {
 94 |            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
 95 |            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
 96 |        }
 97 | 
 98 |        return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
 99 |            // last username entered by the user
100 |            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
101 |            'error'         => $error,
102 |        ));
103 |    }
104 | }
105 | 
{% endraw %} 106 | 107 | Upate Since 2.6 108 | {% raw %} 109 |

110 | // Symfony 2.6
111 | public function loginAction()
112 | {
113 |     $helper = $this->get('security.authentication_utils');
114 | 
115 |     return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
116 |         'last_username' => $helper->getLastUsername(),
117 |         'error'         => $helper->getLastAuthenticationError(),
118 |     ));
119 | }
120 | 
{% endraw %} 121 | 122 | 123 |

Finally, create the corresponding template:

124 | 125 |
{% filter escape %}{% raw %}{# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
126 | {% if error %}
127 |     
{{ error.message }}
128 | {% endif %} 129 | 130 |
131 | 132 | 133 | 134 | 135 | 136 | 137 | {#If you want to control the URL the user is redirected to on success (more details below) #} 138 | 139 | 140 | 141 |
{% endraw %} 142 | {% endfilter %} 143 |
144 | 145 |

Securing Specific URL Patterns

146 | 147 |

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.

148 | 149 | {% raw %}
# app/config/security.yml
150 | security:
151 |     # ...
152 |     access_control:
153 |         - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
154 |         - { path: ^/admin, roles: ROLE_ADMIN }
155 | 
{% endraw %} 156 | 157 |

Securing by IP

158 | 159 |

Here is an example of how you might secure this route from outside access:

160 | 161 | {% raw %}
# app/config/security.yml
162 | security:
163 | # ...
164 | access_control:
165 |     - { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
166 | 
{% endraw %} 167 | 168 |

Securing by Channel

169 | 170 | {% raw %}
# app/config/security.yml
171 | security:
172 | # ...
173 | access_control:
174 |     - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
175 | 
{% endraw %} 176 | 177 |

Securing a Controller

178 | 179 | {% raw %}
/ ...
180 | use Symfony\Component\Security\Core\Exception\AccessDeniedException;
181 | 
182 | public function helloAction($name)
183 | {
184 |     if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
185 |         throw new AccessDeniedException();
186 |     }
187 |     //Since 2.6
188 |     if (false === $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
189 |         throw new AccessDeniedException();
190 |     }
191 | }
192 | 
{% endraw %} 193 | 194 |

You can also choose to install and use the optional JMSSecurityExtraBundle, which can secure your controller using annotations:

195 | 196 | {% raw %}
// ...
197 | use JMS\SecurityExtraBundle\Annotation\Secure;
198 | 
199 | /**
200 | * @Secure(roles="ROLE_ADMIN")
201 | */
202 | public function helloAction($name)
203 | {
204 | // ...
205 | }
206 | 
{% endraw %} 207 | 208 |

Users

209 | 210 |

User Providers

211 | 212 | {% raw %} 213 |
# app/config/security.yml
214 | security:
215 |     # ...
216 |     providers:
217 |         default_provider:
218 |             memory:
219 |                 users:
220 |                     ryan: { password: ryanpass, roles: 'ROLE_USER' }
221 |                     admin: { password: kitten, roles: 'ROLE_ADMIN' }
222 | 
{% endraw %} 223 | 224 |

Loading Users from the Database

225 | 226 |

Next, configure an entity user provider, and point it to your User class:

227 | 228 | {% raw %}
# app/config/security.yml
229 | security:
230 | providers:
231 |     main:
232 |         entity: { class: Acme\UserBundle\Entity\User, property: username }
233 | 
{% endraw %} 234 | 235 |

Encoding the User's Password

236 | 237 | {% raw %} 238 |
# app/config/security.yml
239 | security:
240 |     # ...
241 |     providers:
242 |         in_memory:
243 |             memory:
244 |                 users:
245 |                     ryan:  { password: bb87a29949f3a1ee0559f8a57357487151281386, roles: 'ROLE_USER' }
246 |                     admin: { password: 74913f5cd5f61ec0bcfdb775414c2fb3d161b620, roles: 'ROLE_ADMIN' }
247 | 
248 |     encoders:
249 |         Symfony\Component\Security\Core\User\User:
250 |             algorithm:   sha1
251 |             iterations: 1
252 |             encode_as_base64: false
253 |         Acme\UserBundle\Entity\User: sha512 //user comes from database
254 | 
{% endraw %} 255 | 256 |

To encode the password you can use some online functions such as 257 | functions-online.com

258 | 259 |

Determine the hasing password in a controller

260 | 261 | {% raw %}
$factory = $this->get('security.encoder_factory');
262 | $user = new Acme\UserBundle\Entity\User();
263 | 
264 | $encoder = $factory->getEncoder($user);
265 | $password = $encoder->encodePassword('ryanpass', $user->getSalt());
266 | $user->setPassword($password);
267 | 
268 | //Since 2.6
269 | $user = new Acme\UserBundle\Entity\User();
270 | $encoder = $this->container->get('security.password_encoder');
271 | $password = $encoder->encodePassword($user, $plainTextPassword);
272 | 
{% endraw %} 273 | 274 |

Retrieving the User Object

275 | 276 |

After authentication, the User object of the current user can be accessed via the security.context service.

277 | 278 | {% raw %}
public function indexAction()
279 | {
280 |     $user = $this->get('security.context')->getToken()->getUser();
281 |     //Since 2.6
282 |     $user = $this->get('security.token_storage')->getToken()->getUser();
283 |     //or
284 |     $user = $this->getUser();
285 |     
286 |     
287 | }
288 | 
{% endraw %} 289 | 290 |

You can also retrieve current user in a twig template by:

291 | 292 | {% raw %}
<p>Username: {{ app.user.username }}</p>
293 | 
{% endraw %} 294 | 295 |

Using Multiple User Providers

296 | 297 | {% raw %}
# app/config/security.yml
298 | security:
299 |     providers:
300 |         chain_provider:
301 |             chain:
302 |                 providers: [in_memory, user_db]
303 |         in_memory:
304 |             memory:
305 |                 users:
306 |                     foo: { password: test }
307 |         user_db:
308 |             entity: { class: Acme\UserBundle\Entity\User, property: username }
309 | 
{% endraw %} 310 | 311 |

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:

312 | 313 | {% raw %}
# app/config/security.yml
314 | security:
315 |     firewalls:
316 |         secured_area:
317 |             # ...
318 |             provider: user_db
319 |             http_basic:
320 |                 realm: "Secured Demo Area"
321 |                 provider: in_memory
322 |             form_login: ~
323 | 
{% endraw %} 324 | 325 |

For more information about user provider and firewall configuration, see the 326 | Security Configuration Reference. 327 |

328 | 329 |

Roles

330 | 331 | {% raw %}
# app/config/security.yml
332 | security:
333 |     role_hierarchy:
334 |         ROLE_ADMIN:       ROLE_USER
335 |         ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
336 | 
{% endraw %} 337 | 338 |

Logging Out

339 | 340 | {% raw %}
# app/config/security.yml
341 | security:
342 |     firewalls:
343 |         secured_area:
344 |             # ...
345 |             logout:
346 |                 path:   /logout
347 |                 target: /
348 | 
{% endraw %} 349 | 350 |

and define the route:

351 | 352 | {% raw %}
# app/config/routing.yml
353 | logout:
354 |     pattern: /logout
355 | 
{% endraw %} 356 | 357 |

Access Control in Templates

358 | 359 |

Twig

360 | 361 | {% raw %}
{% if is_granted('ROLE_ADMIN') %}
362 |     <a href="...">Delete</a>
363 | {% endif %}
364 | 
{% endraw %} 365 | 366 |

Access Control in Controllers

367 | 368 | {% raw %} 369 |
public function indexAction()
370 | {
371 |     // show different content to admin users
372 |     if ($this->get('security.context')->isGranted('ROLE_ADMIN')) {
373 |         // Load admin content here
374 |     }
375 |     // load other regular content here
376 | }
377 | 
{% endraw %} 378 | 379 |

Switching users

380 | 381 |

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:

382 | 383 | {% raw %}
# app/config/security.yml
384 | security:
385 |     firewalls:
386 |         main:
387 |             # ...
388 |             switch_user: true
389 | 
{% endraw %} 390 | 391 |

Switch user by:

392 | 393 | {% raw %}
http://example.com/somewhere?_switch_user=thomas
394 | 
{% endraw %} 395 | 396 |

and back to normal user:

397 | 398 | {% raw %}
http://example.com/somewhere?_switch_user=_exit
399 | 
{% endraw %} 400 | 401 |

Secure this behaviour:

402 | 403 | {% raw %}
# app/config/security.yml
404 | security:
405 |     firewalls:
406 |         main:
407 |             # ...
408 |             switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user }
409 | 
{% endraw %} 410 | -------------------------------------------------------------------------------- /services.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

A Service Container (or dependency injection container) is simply a PHP object that manages the instantiation of services (i.e. objects).

3 | 4 | {% raw %}
# src/Acme/HelloBundle/Resources/config/services.yml
 5 | parameters:
 6 |     newsletter_manager.class: Acme\HelloBundle\Newsletter\NewsletterManager
 7 |     my_mailer.transport: sendmail
 8 |     my_mailer.gateways:
 9 |         - mail1
10 |         - mail2
11 |         - mail3
12 | 
13 | services:
14 |     my_mailer:
15 |         class: "%my_mailer.class%"
16 |         arguments: [%my_mailer.transport%]
17 |     newsletter_manager:
18 |         class: "%newsletter_manager.class%"
19 |         arguments: [@my_mailer] //required contructor args. Use @ to refer another service.
20 |         calls:
21 |         - [ setMailer, [ @my_mailer ] ] //Optional dependencies.
22 |         tags:
23 |         - { name: twig.extension } //Twig finds all services tagged with twig.extension and automatically registers them as extensions.
24 | 
{% endraw %} 25 | 26 |

Now we can set our class to be a real service:

27 | 28 | {% raw %}
namespace Acme\HelloBundle\Newsletter;
29 | 
30 | use Symfony\Component\Templating\EngineInterface;
31 | 
32 | class NewsletterManager
33 | {
34 |     protected $mailer;
35 |     protected $templating;
36 | 
37 |     public function __construct(\Swift_Mailer $mailer, EngineInterface $templating)
38 |     {
39 |         $this->mailer = $mailer;
40 |         $this->templating = $templating;
41 |     }
42 | 
43 |     // ...
44 | }
45 | 
{% endraw %} 46 | 47 |

And for this particual service the corresponding services.yml would be:

48 | 49 | {% raw %}
 services:
50 |     newsletter_manager:
51 |         class: "%newsletter_manager.class%"
52 |         arguments: [@mailer, @templating]
53 | 
{% endraw %} 54 | 55 |

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

56 | 57 |

Making References Optional

58 | 59 | {% raw %}
services:
60 |     newsletter_manager:
61 |         class: "%newsletter_manager.class%"
62 |         arguments: [@?my_mailer]
63 | 
{% endraw %} 64 | 65 |

Debugging services

66 | 67 |

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:

68 | 69 | {% raw %}
$ php app/console container:debug
70 | $ php app/console container:debug --show-private
71 | $ php app/console container:debug my_mailer
72 | 
{% endraw %} 73 | 74 |

See also:

75 | 76 | -------------------------------------------------------------------------------- /templating.html.twig: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |

Including partials and components

6 | 7 | {% raw %} 8 |
{% include "Bundle:Controller:action" %}
  9 | in Symfony 2.1:
 10 | {% render "Bundle:Controller:action" with {"max" : 3} %}
 11 | in Symfony 2.2:
 12 | {{ render(controller("Bundle:Controller:action", {max :3})) }}
 13 | 
{% endraw %} 14 | 15 |

Links

16 | 17 | {% raw %} 18 |
<a href="{{ path('homepage') }}">Home<a/> //relative
 19 | <a href="{{ url('homepage') }}">Home<a/> //absolute
 20 | <a href="{{ path('show', {'id':article.id}) }}">Home</a>
 21 | 
{% endraw %} 22 | 23 |

Assets

24 | 25 | {% raw %} 26 |
<img src="{{ 'uploads/'~foto.url }}"/>
 27 | 
{% endraw %} 28 | 29 |
30 | 31 |
32 | 33 |

Debug variables in a template

34 | 35 | {% raw %} 36 |
{{ dump(article) }}
 37 |             
{% endraw %} 38 | 39 |

Global TWIG variables

40 | 41 | {% raw %} 42 |
app.security
 43 | app.user
 44 | app.request
 45 | app.request.get('foo') //get
 46 | app.request.request.get('foo') //post
 47 | app.session
 48 | app.environment
 49 | app.debug
 50 | 
{% endraw %} 51 |
52 | 53 |
54 | 55 |

TWIG TAGS

56 | 57 |
58 | 59 |
60 |

block

61 | 62 |

When a template uses inheritance and if you want to print a block multiple times, use the block function:

63 | 64 | {% raw %} 65 |
<title>{% block title %}{% endblock %}</title>
 66 | <h1>{{ block('title') }}</h1>
 67 | {% block body %}{% endblock %}
 68 | 
{% endraw %} 69 | 70 |

parent

71 | 72 |

When a template uses inheritance, it's possible to render the contents of the parent block when overriding a block by using the parent function:

73 | 74 | {% raw %} 75 |
{% extends "base.html" %}
 76 | 
 77 | {% block sidebar %}
 78 | <h3>Table Of Contents</h3>
 79 | ...
 80 | {{ parent() }}
 81 | {% endblock %}
 82 | 
{% endraw %} 83 | 84 |

for

85 | 86 |

Loop over each item in a sequence. For example, to display a list of users provided in a variable called users:

87 | 88 | {% raw %} 89 |
<h1>Members</h1>
 90 | <ul>
 91 | {% for user in users %}
 92 | <li>{{ user.username|e }}</li>
 93 | {% endfor %}
 94 | </ul>
 95 | 
{% endraw %} 96 | 97 |
The loop variable
98 | 99 |

Inside of a for loop block you can access some special variables:

100 | 101 | {% raw %} 102 |
Variable Description
103 | loop.index The current iteration of the loop. (1 indexed)
104 | loop.index0 The current iteration of the loop. (0 indexed)
105 | loop.revindex The number of iterations from the end of the loop (1 indexed)
106 | loop.revindex0 The number of iterations from the end of the loop (0 indexed)
107 | loop.first True if first iteration
108 | loop.last True if last iteration
109 | loop.length The number of items in the sequence
110 | loop.parent The parent context
111 | 
{% endraw %} 112 | 113 | {% raw %} 114 |
{% for user in users %}
115 | {{ loop.index }} - {{ user.username }}
116 | {% endfor %}
117 | 
{% endraw %} 118 | 119 |

if

120 | 121 |

The if statement in Twig is comparable with the if statements of PHP.

122 | 123 |

In the simplest form you can use it to test if an expression evaluates to true:

124 | 125 | {% raw %} 126 |
{% if online == false %}
127 | <p>Our website is in maintenance mode. Please, come back later.</p>
128 | {% endif %}
129 | 
{% endraw %} 130 | 131 |

raw

132 | 133 |

Everything inside raw tags won't be parsed.

134 | 135 | {% raw %} 136 |
{% raw %}
137 | This variable {{foo}} won't be parsed as twig var.
138 | {% endraw. %}
{% endraw %} 139 | 140 |
141 | 142 |
143 |

set

144 | 145 |

Inside code blocks you can also assign values to variables. Assignments use the set tag and can have multiple targets:

146 | 147 | {% raw %} 148 |
{% set foo = 'foo' %}
149 | {% set foo = [1, 2] %}
150 | {% set foo = {'foo': 'bar'} %}
151 | {% set foo = 'foo' ~ 'bar' %}
152 | {% set foo, bar = 'foo', 'bar' %}
153 | 
{% endraw %} 154 | 155 |

filter

156 | 157 |

Filter sections allow you to apply regular Twig filters on a block of template data. Just wrap the code in the special filter section:

158 | 159 | {% raw %} 160 |
{% filter upper %}
161 | This text becomes uppercase
162 | {% endfilter %}
163 | 
{% endraw %} 164 | 165 |

You can also chain filters:

166 | 167 | {% raw %} 168 |
{% filter lower|escape %}
169 | <strong>SOME TEXT</strong>
170 | {% endfilter %}
171 | 
{% endraw %} 172 | 173 |

macro

174 | 175 |

Macros are comparable with functions in regular programming languages. They are useful to put often used HTML idioms into reusable elements to not repeat yourself.

176 | 177 |

Here is a small example of a macro that renders a form element:

178 | 179 | {% raw %} 180 |
{% macro input(name, value, type, size) %}
181 | <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
182 | {% endmacro %}
183 | 
{% endraw %} 184 | 185 |

Macros differs from native PHP functions in a few ways:

186 | 187 | 191 |

But as PHP functions, macros don't have access to the current template variables.

192 | 193 |

Macros can be defined in any template, and need to be "imported" before being used (see the documentation for the import tag for more information):

194 | 195 | {% raw %} 196 |
{% import "forms.html" as forms %}
197 | 
{% endraw %} 198 | 199 |

The above import call imports the "forms.html" file (which can contain only macros, or a template and some macros), and import the functions as items of the forms variable.

200 | 201 |

The macro can then be called at will:

202 | 203 | {% raw %} 204 |
<p>{{ forms.input('username') }}</p>
205 | <p>{{ forms.input('password', null, 'password') }}</p>
206 | 
{% endraw %} 207 | 208 |

If macros are defined and used in the same template, you can use the special _self variable to import them:

209 | 210 | {% raw %} 211 |
{% import _self as forms %}
212 | <p>{{ forms.input('username') }}</p>
213 | 
{% endraw %} 214 | 215 |
216 | 217 |
218 | 219 |

TWIG FILTERS

220 | 221 |
222 | 223 |
224 | 225 |

date

226 | 227 | {% raw %} 228 |
{{ post.published_at|date("m/d/Y") }}
229 | {{ post.published_at|date("m/d/Y", "Europe/Paris") }}
230 | 
{% endraw %} 231 | 232 |

date_modify

233 | 234 | {% raw %} 235 |
{{ post.published_at|date_modify("+1 day")|date("m/d/Y") }}
236 | 
{% endraw %} 237 | 238 |

format

239 | 240 | {% raw %} 241 |
{{ "I like %s and %s."|format(foo, "bar") }}
242 | 
{% endraw %} 243 | 244 |

replace

245 | 246 | {% raw %} 247 |
{{ "I like %this% and %that%."|replace({'%this%': foo, '%that%': "bar"}
248 | 
{% endraw %} 249 | 250 |

number_format

251 | 252 | {% raw %} 253 |
{{ 200.35|number_format }}
254 | {{ 9800.333|number_format(2, '.', ',') }}
255 | 
{% endraw %} 256 | 257 |

url_encode

258 | 259 | {% raw %} 260 |
{{ data|url_encode() }}
261 | 
{% endraw %} 262 | 263 |

json_encode

264 | 265 | {% raw %} 266 |
{{ data|json_encode() }}
267 | 
{% endraw %} 268 | 269 |

convert_encoding

270 | 271 | {% raw %} 272 |
{{ data|convert_encoding('UTF-8', 'iso-2022-jp') }}
273 | 
{% endraw %} 274 | 275 |

title

276 | 277 | {% raw %} 278 |
{{ 'my first car'|title }}
279 | {# outputs 'My First Car' #}
280 | 
{% endraw %} 281 | 282 |

capitalize

283 | 284 | {% raw %} 285 |
{{ 'my first car'|capitalize }}
286 | 
{% endraw %} 287 | 288 |

nl2br

289 | 290 | {% raw %} 291 |
{{ "I like Twig.\nYou will like it too."|nl2br }}
292 |     {# outputs
293 | 
294 |     I like Twig.<br />
295 |     You will like it too.
296 | 
297 |     #}
298 | 
{% endraw %} 299 | 300 |

raw

301 | 302 | {% raw %} 303 |
{{ var|raw }} {# var won't be escaped #}
304 | 
{% endraw %} 305 | 306 |

trim

307 | 308 | {% raw %} 309 |
{{ ' I like Twig.'|trim('.') }}
310 | 
{% endraw %} 311 |
312 | 313 |
314 |

upper

315 | 316 | {% raw %} 317 |
{{ 'welcome'|upper }}
318 | 
{% endraw %} 319 | 320 |

lower

321 | 322 | {% raw %} 323 |
{{ 'WELCOME'|lower }}
324 | 
{% endraw %} 325 | 326 |

striptags

327 | 328 | {% raw %} 329 |
{% some_html|striptags %}
330 | 
{% endraw %} 331 | 332 |

join

333 | 334 | {% raw %} 335 |
{{ [1, 2, 3]|join('|') }}
336 | {# returns 1|2|3 #}
337 | 
{% endraw %} 338 | 339 |

split

340 | 341 | {% raw %} 342 |
{{ "one,two,three"|split(',') }}
343 | {# returns ['one', 'two', 'three'] #}
344 | 
{% endraw %} 345 | 346 |

reverse

347 | 348 | {% raw %} 349 |
{% for use in users|reverse %}
350 |     ...
351 | {% endfor %}
352 | 
{% endraw %} 353 | 354 |

abs

355 | 356 | {% raw %} 357 |
{{ number|abs }}
358 | 
{% endraw %} 359 | 360 |

length

361 | 362 | {% raw %} 363 |
{% if users|length > 10 %}
364 |     ...
365 | {% endif %}
366 | 
{% endraw %} 367 | 368 |

sort

369 | 370 | {% raw %} 371 |
{% for use in users|sort %}
372 |     ...
373 | {% endfor %}
374 | 
{% endraw %} 375 | 376 |

default

377 | 378 | {% raw %} 379 |
{{ var|default('var is not defined') }}
380 | 
{% endraw %} 381 | 382 |

keys

383 | 384 | {% raw %} 385 |
{% for key in array|keys %}
386 |     ...
387 | {% endfor %}
388 | 
{% endraw %} 389 | 390 |

escape

391 | 392 | {% raw %} 393 |
{{ user.username|e }}
394 | {# is equivalent to #}
395 | {{ user.username|e('html') }}
396 | {{ user.username|e('css') }}
397 | {{ user.username|e('js') }}
398 | 
{% endraw %} 399 | 400 |
401 | 402 |
403 | -------------------------------------------------------------------------------- /testing.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

Symfony2 works with PHPUnit 3.5.11 or later, though version 3.6.4 is needed to test the Symfony 3 | core code itself.

4 | 5 | {% raw %}
# specify the configuration directory on the command line
  6 |     $ phpunit -c app/
  7 | 
{% endraw %} 8 | 9 |

Unit tests

10 | 11 |

Writing Symfony2 unit tests is no different than writing standard PHPUnit unit tests. Suppose, for 12 | example, that you have an incredibly simple class called Calculator in the Utility/ directory of your 13 | bundle:

14 | 15 | {% raw %}
// src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
 16 | namespace Acme\DemoBundle\Tests\Utility;
 17 | 
 18 | use Acme\DemoBundle\Utility\Calculator;
 19 | 
 20 | class CalculatorTest extends \PHPUnit_Framework_TestCase
 21 | {
 22 |     public function testAdd()
 23 |     {
 24 |         $calc = new Calculator();
 25 |         $result = $calc->add(30, 12);
 26 | 
 27 |         // assert that our calculator added the numbers correctly!
 28 |         $this->assertEquals(42, $result);
 29 |     }
 30 | }
 31 | 
{% endraw %} 32 | 33 |

By convention, the Tests/ sub-directory should replicate the directory of your bundle. So, if you're 34 | testing a class in your bundle's Utility/ directory, put the test in the Tests/Utility/ directory.

35 | 36 |

Running tests for a given file or directory is also very easy:

37 | 38 | {% raw %}
# run all tests in the Utility directory
 39 | $ phpunit -c app src/Acme/DemoBundle/Tests/Utility/
 40 | 
 41 | # run tests for the Calculator class
 42 | $ phpunit -c app src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
 43 | 
 44 | # run all tests for the entire Bundle
 45 | $ phpunit -c app src/Acme/DemoBundle/
 46 | 
{% endraw %} 47 | 48 |

Functional Tests

49 | 50 |

They are no different from unit tests as far as PHPUnit is concerned, but they have a very specific 51 | workflow:

52 | 53 | 60 |

Symfony 2 provides a simple functional test for its DemoController as follows:

61 | 62 | {% raw %}
// src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
 63 | namespace Acme\DemoBundle\Tests\Controller;
 64 | 
 65 | use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 66 | 
 67 | class DemoControllerTest extends WebTestCase
 68 | {
 69 |     public function testIndex()
 70 |     {
 71 |         $client = static::createClient();
 72 |         $crawler = $client->request('GET', '/demo/hello/Fabien');
 73 |         $this->assertGreaterThan(0, $crawler->filter('html:contains("Hello Fabien")')->count());
 74 |     }
 75 | }
 76 | 
{% endraw %} 77 | 78 | 79 |
80 | 81 |
82 |

The createClient() method returns a client, which is like a browser that you'll use to crawl your site:

83 | 84 |

The request() method returns a Crawler object which can be used to select elements in the Response, click on links, and submit forms.

85 | 86 |
95 | 96 |
97 |
  • 98 |

    Submit a form:

    99 | 100 | {% raw %}
    $form = $crawler->selectButton('submit')->form();
    101 | 
    102 | // set some values
    103 | $form['name'] = 'Lucas';
    104 | $form['form_name[subject]'] = 'Hey there!';
    105 | 
    106 | // submit the form
    107 | $crawler = $client->submit($form);
    108 | 
    {% endraw %} 109 |
  • 110 |
  • 111 | 112 |
  • 113 | 114 |
    115 | 116 |

    Assertions:

    117 | 118 | 119 |
    120 | 121 |
    122 |

    Assert that the response matches a given CSS selector.

    123 | 124 | {% raw %}
     $this->assertGreaterThan(0, $crawler->filter('h1')->count());
    125 | 
    {% endraw %} 126 | 127 |

    Check content text

    128 | 129 | {% raw %}
     $this->assertRegExp('/Hello Fabien/', $client->getResponse()->getContent());
    130 | 
    {% endraw %} 131 | 132 |

    Assert that there is more than one h2 tag with the class "subtitle"

    133 | 134 | {% raw %}
     $this->assertGreaterThan(0, $crawler->filter('h2.subtitle')->count());
    135 | 
    {% endraw %} 136 | 137 |

    Assert that there are exactly 4 h2 tags on the page

    138 | 139 | {% raw %}
     $this->assertCount(4, $crawler->filter('h2'));
    140 | 
    {% endraw %} 141 | 142 |

    Assert that the "Content-Type" header is "application/json

    143 | 144 | {% raw %}
     $this->assertTrue($client->getResponse()->headers->contains('Content-Type','application/json'));
    145 | 
    {% endraw %} 146 | 147 |

    Assert that the response content matches a regexp

    148 | 149 | {% raw %}
    $this->assertRegExp('/foo/', $client->getResponse()->getContent());
    150 | 
    {% endraw %} 151 | 152 |

    Assert that the response status code is 2xx

    153 | 154 | {% raw %}
    $this->assertTrue($client->getResponse()->isSuccessful());
    155 | 
    {% endraw %} 156 | 157 |

    Assert that the response status code is 404

    158 | 159 | {% raw %}
    $this->assertTrue($client->getResponse()->isNotFound());
    160 | 
    {% endraw %} 161 | 162 |

    Assert a specific 200 status code

    163 | 164 | {% raw %}
    $this->assertEquals(200, $client->getResponse()->getStatusCode());
    165 | 
    {% endraw %} 166 | 167 |

    Assert that the response is a redirect to /demo/contact

    168 | 169 | {% raw %}
    $this->assertTrue($client->getResponse()->isRedirect('/demo/contact'));
    170 | 
    {% endraw %} 171 | 172 |

    or simply check that the response is a redirect to any URL

    173 | 174 | {% raw %}
    $this->assertTrue($client->getResponse()->isRedirect());
    175 | 
    {% endraw %} 176 | 177 |

    Directly submit a form (but using the Crawler is easier!)

    178 | 179 | {% raw %}
    $client->request('POST', '/submit', array('name' => 'Fabien'));
    180 | 
    {% endraw %} 181 |
    182 | 183 |
    184 | 185 | 186 |

    Form submission with a file upload

    187 | 188 | {% raw %}
    use Symfony\Component\HttpFoundation\File\UploadedFile;
    189 | 
    190 |     $photo = new UploadedFile(
    191 |     '/path/to/photo.jpg',
    192 |     'photo.jpg',
    193 |     'image/jpeg',
    194 |     123
    195 |     );
    196 |     // or
    197 |     $photo = array(
    198 |     'tmp_name' => '/path/to/photo.jpg',
    199 |     'name' => 'photo.jpg',
    200 |     'type' => 'image/jpeg',
    201 |     'size' => 123,
    202 |     'error' => UPLOAD_ERR_OK
    203 |     );
    204 |     $client->request(
    205 |     'POST',
    206 |     '/submit',
    207 |     array('name' => 'Fabien'),
    208 |     array('photo' => $photo)
    209 |     );
    210 | 
    {% endraw %} 211 | 212 |

    Perform a DELETE requests, and pass HTTP headers

    213 | 214 | {% raw %}
    $client->request(
    215 |     'DELETE',
    216 |     '/post/12',
    217 |     array(),
    218 |     array(),
    219 |     array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word')
    220 |     );
    221 | 
    {% endraw %} 222 | 223 |

    browsing

    224 | 225 | {% raw %}
    $client->back();
    226 | $client->forward();
    227 | $client->reload();
    228 | 
    {% endraw %} 229 | 230 |

    Clears all cookies and the history

    231 | 232 | {% raw %}
    $client->restart();
    233 | 
    {% endraw %} 234 | 235 |

    redirecting

    236 | 237 | {% raw %}
    $crawler = $client->followRedirect();
    238 | $client->followRedirects();
    239 | 
    {% endraw %} 240 | 241 |
    242 | 243 |
    244 | 245 | 246 | 247 | 248 |

    The Request() method:

    249 | 250 |
    251 | 252 |
    253 | 254 | {% raw %}
    request(
    255 |     $method,
    256 |     $uri,
    257 |     array $parameters = array(),
    258 |     array $files = array(),
    259 |     array $server = array(),
    260 |     $content = null,
    261 |     $changeHistory = true
    262 | )
    263 | 
    {% endraw %} 264 |

    The server array is the raw values that you'd expect to normally find in the PHP $_SERVER4 265 | superglobal. For example, to set the Content-Type and Referer HTTP headers, you'd pass the 266 | following:

    267 |
    268 | 269 |
    270 | 271 | 272 | {% raw %}
    $client->request(
    273 |     'GET',
    274 |     '/demo/hello/Fabien',
    275 |     array(),
    276 |     array(),
    277 |     array(
    278 |         'CONTENT_TYPE' => 'application/json',
    279 |         'HTTP_REFERER' => '/foo/bar',
    280 |         )
    281 |     );
    282 | 
    {% endraw %} 283 |
    284 | 285 |
    286 | 287 | 288 |

    Accessing Internal Objects

    289 | 290 |

    If you use the client to test your application, you might want to access the client's internal objects:

    291 | 292 | {% raw %}
    $history = $client->getHistory();
    293 | $cookieJar = $client->getCookieJar();
    294 | 
    {% endraw %} 295 | 296 |

    You can also get the objects related to the latest request:

    297 | 298 | {% raw %}
    $request = $client->getRequest();
    299 | $response = $client->getResponse();
    300 | $crawler = $client->getCrawler();
    301 | 
    {% endraw %} 302 | 303 |

    If your requests are not insulated, you can also access the Container and the Kernel:

    304 | 305 | {% raw %}
    $container = $client->getContainer();
    306 | $kernel = $client->getKernel();
    307 | $profile = $client->getProfile();
    308 | 
    {% endraw %} 309 | 310 |

    The Crawler

    311 | 312 |
    313 | 314 |
    315 |

    A Crawler instance is returned each time you make a request with the Client. It allows you to traverse 316 | HTML documents, select nodes, find links and forms.

    317 | 318 |

    Like jQuery, the Crawler has methods to traverse the DOM of an HTML/XML document.

    319 | 320 | {% raw %}
    $newCrawler = $crawler->filter('input[type=submit]')
    321 |     ->last()
    322 |     ->parents()
    323 |     ->first();
    324 | 
    {% endraw %} 325 | 326 |

    Many other methods are also available:

    327 | 328 | 341 | 342 |
    343 | 344 |
    345 |

    Extracting information

    346 | 347 | {% raw %} 348 |
    // Returns the attribute value for the first node
    349 | $crawler->attr('class');
    350 | 
    351 | // Returns the node value for the first node
    352 | $crawler->text();
    353 | 
    354 | // Extracts an array of attributes for all nodes (_text returns the node value)
    355 | // returns an array for each element in crawler, each with the value and href
    356 | $info = $crawler->extract(array('_text', 'href'));
    357 | 
    358 | // Executes a lambda for each node and return an array of results
    359 | $data = $crawler->each(function ($node, $i)
    360 | {
    361 |     return $node->attr('href');
    362 | });
    363 | 
    364 | //Selecting links
    365 | $crawler->selectLink('Click here');
    366 | $link = $crawler->selectLink('Click here')->link();
    367 | $client->click($link);
    368 | 
    {% endraw %} 369 | 370 |
    371 | 372 |
    373 | 374 | 375 | 376 |
    377 | 378 |
    379 |

    Forms

    380 | {% raw%}
    // Selecting buttons.
    381 | $buttonCrawlerNode = $crawler->selectButton('submit');
    382 | 
    383 | // You can override values by:
    384 | 
    385 | $form = $buttonCrawlerNode->form(array(
    386 |     'name' => 'Fabien',
    387 |     'my_form[subject]' => 'Symfony rocks!',
    388 | ));
    389 | 
    390 | //Simulate methods
    391 | $form = $buttonCrawlerNode->form(array(), 'DELETE');
    392 | 
    393 | //Submit form.
    394 | $client->submit($form);
    395 | 
    396 | //Submit with arguments.
    397 | $client->submit($form, array(
    398 |     'name' => 'Fabien',
    399 |     'my_form[subject]' => 'Symfony rocks!',
    400 | ));
    401 | 
    402 | // Using arrays.
    403 | $form['name'] = 'Fabien';
    404 | $form['my_form[subject]'] = 'Symfony rocks!';
    405 | 
    406 | // Select an option or a radio
    407 | $form['country']->select('France');
    408 | 
    409 | // Tick a checkbox
    410 | $form['like_symfony']->tick();
    411 | 
    412 | // Upload a file
    413 | $form['photo']->upload('/path/to/lucas.jpg');
    414 | 
    {% endraw %} 415 | 416 |
    417 | 418 |
    419 | 420 |

    Test environment configuration

    421 | 422 |

    The swiftmailer is configured to not actually deliver emails in the test 423 | environment. You can see this under the swiftmailer configuration option:

    424 | 425 | {% raw %} 426 |
    # app/config/config_test.yml
    427 | swiftmailer:
    428 |     disable_delivery: true
    429 | 
    {% endraw %} 430 | 431 |

    You can also use a different environment entirely, or override the default debug mode (true) by passing 432 | each as options to the createClient() method:

    433 | 434 |

    custom environment

    435 | 436 | {% raw %}
    $client = static::createClient(array(
    437 |     'environment' => 'my_test_env',
    438 |     'debug' => false,
    439 |     ));
    440 | 
    {% endraw %} 441 | 442 |

    custom user agent

    443 | 444 | {% raw %}
    $client = static::createClient(array(), array(
    445 |     'HTTP_HOST' => 'en.example.com',
    446 |     'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
    447 | ));
    448 | 
    {% endraw %} 449 | 450 |

    override HTTP headers

    451 | 452 | {% raw %}
    $client->request('GET', '/', array(), array(), array(
    453 |     'HTTP_HOST' => 'en.example.com',
    454 |     'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
    455 | ));
    456 | 
    {% endraw %} 457 | 458 | 459 |
    460 | 461 |
    462 | 463 | -------------------------------------------------------------------------------- /translations.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

    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:

    3 | 4 | {% raw %}
    # app/config/config.yml
      5 | framework:
      6 |     translator: { fallback: en }
      7 |     default_locale: en
      8 | 
    {% endraw %} 9 | 10 |

    Basic translation

    11 | 12 | {% raw %}
    $t = $this->get('translator')->trans('Symfony2 is great');
     13 | $t = $this->get('translator')->trans('Hello %name%', array('%name%' => $name));
     14 | 
    {% endraw %} 15 | 16 |

    When this code is executed, Symfony2 will attempt to translate the message "Symfony2 is great" based on the locale of the user.

    17 | 18 | {% raw %}
    <!-- messages.fr.xliff -->
     19 | <?xml version="1.0"?>
     20 | <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
     21 |     <file source-language="en" datatype="plaintext" original="file.ext">
     22 |         <body>
     23 |             <trans-unit id="1">
     24 |             <source>Symfony2 is great</source>
     25 |             <target>J'aime Symfony2</target>
     26 |             </trans-unit>
     27 |             <trans-unit id="2">
     28 |             <source>Hello %name%</source>
     29 |             <target>Bonjour %name%</target>
     30 |             </trans-unit>
     31 |         </body>
     32 |     </file>
     33 | </xliff>
     34 | 
    {% endraw %} 35 | 36 |

    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:

    37 | 38 |

    Using Real or Keyword Messages

    39 | 40 | {% raw %}
    $t = $translator->trans('Symfony2 is great');
     41 | $t = $translator->trans('symfony2.great');
     42 | 
    {% endraw %} 43 | 44 |

    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. 45 | 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).

    46 | 47 | {% raw %}
    symfony2.is.great: Symfony2 is great
     48 | symfony2.is.amazing: Symfony2 is amazing
     49 | symfony2.has.bundles: Symfony2 has bundles
     50 | user.login: Login
     51 | 
    {% endraw %} 52 | 53 |

    Using Message Domains

    54 | 55 |

    When translating strings that are not in the default domain (messages), you must specify the domain as the third argument of trans():

    56 | 57 | {% raw %}
    * messages.fr.xliff
     58 | * admin.fr.xliff
     59 | * navigation.fr.xliff
     60 | 
     61 | $this->get('translator')->trans('Symfony2 is great', array(), 'admin');
     62 | 
    {% endraw %} 63 | 64 |

    Pluralization

    65 | 66 |

    To translate pluralized messages, use the transChoice() method:

    67 | 68 | {% raw %}
    $t = $this->get('translator')->transChoice(
     69 |     'There is one apple|There are %count% apples',
     70 |     10,
     71 |     array('%count%' => 10)
     72 | );
     73 | 
    {% endraw %} 74 | 75 |

    Translations in Templates

    76 | 77 |

    Translating in Twig templates example:

    78 | 79 | {% raw %}
    //you can set de translation domain for entire twig temples
     80 | {% trans_default_domain "app" %}
     81 | 
     82 | {% trans %}Hello %name%{% endtrans %}
     83 | 
     84 | {% transchoice count %}
     85 |     {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
     86 | {% endtranschoice %}
     87 | 
     88 | //variables traduction
     89 | {{ message|trans }}
     90 | 
     91 | {{ message|transchoice(5) }}
     92 | 
     93 | {{ message|trans({'%name%': 'Fabien'}, "app") }}
     94 | 
     95 | {{ message|transchoice(5, {'%name%': 'Fabien'}, 'app') }}
     96 | 
    {% endraw %} 97 | 98 |

    If you need to use the percent character (%) in a string, escape it by doubling it: {% raw %}{% trans %}Percent: %percent%%%{% endtrans %}{% endraw %}

    99 | 100 |

    Translating Database Content

    101 | 102 |

    The translation of database content should be handled by Doctrine through the 103 | Translatable Extension

    104 | 105 |

    Translating constraint messages

    106 | 107 | {% raw %}
    # src/Acme/BlogBundle/Resources/config/validation.yml
    108 | Acme\BlogBundle\Entity\Author:
    109 |     properties:
    110 |         name:
    111 |             - NotBlank: { message: "author.name.not_blank" }
    112 | 
    {% endraw %} 113 | 114 |

    Create a translation file under the validators catalog:

    115 | 116 | {% raw %}
    <!-- validators.en.xliff -->
    117 | <?xml version="1.0"?>
    118 | <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    119 |     <file source-language="en" datatype="plaintext" original="file.ext">
    120 |         <body>
    121 |             <trans-unit id="1">
    122 |             <source>author.name.not_blank</source>
    123 |             <target>Please enter an author name.</target>
    124 |             </trans-unit>
    125 |         </body>
    126 |     </file>
    127 | </xliff>
    128 | 
    {% endraw %} 129 | 130 | -------------------------------------------------------------------------------- /validation.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

    Symfony2 ships with a Validator component that makes this task easy and transparent.

    3 | 4 | {% raw %}
    # src/Acme/BlogBundle/Resources/config/validation.yml
      5 | Acme\BlogBundle\Entity\Author:
      6 | properties:
      7 |     name:
      8 |         - NotBlank: ~
      9 | 
    {% endraw %} 10 | 11 |

    Protected and private properties can also be validated, as well as "getter" methods (see validatorconstraint- 12 | targets).

    13 | 14 |

    Using the validator Service

    15 | 16 | {% raw %}
    // ...
     17 | use Symfony\Component\HttpFoundation\Response;
     18 | use Acme\BlogBundle\Entity\Author;
     19 | 
     20 | public function indexAction()
     21 | {
     22 |     $author = new Author();
     23 | 
     24 |     // ... do something to the $author object
     25 |     $validator = $this->get('validator');
     26 |     $errors = $validator->validate($author);
     27 | 
     28 |     if (count($errors) > 0) {
     29 |         return new Response(print_r($errors, true));
     30 |     } else {
     31 |         return new Response('The author is valid! Yes!');
     32 |     }
     33 | }
     34 | 
    {% endraw %} 35 | 36 |

    Inside the template, you can output the list of errors exactly as needed:

    37 | 38 | {% raw %}
    {# src/Acme/BlogBundle/Resources/views/Author/validate.html.twig #}
     39 | <h3>The author has the following errors</h3>
     40 | <ul>
     41 | {% for error in errors %}
     42 | <li>{{ error.message }}</li>
     43 | {% endfor %}
     44 | </ul>
     45 | 
    {% endraw %} 46 | 47 |

    Validation and Forms

    48 | 49 |

    The Symfony2 validator is enabled by default, but you must explicitly enable annotations if you're using 50 | the annotation method to specify your constraints:

    51 | 52 | {% raw %}
    # app/config/config.yml
     53 | framework:
     54 |     validation: { enable_annotations: true }
     55 | 
    {% endraw %} 56 | 57 |

    The good thing about annotations is that you write down all your entities validation in the entities .php in each entity PHPDOC so everything is in the same place.

    58 | 59 |

    Constraints

    60 | 61 |

    The validator is designed to validate objects against constraints (i.e. rules). In order to validate an 62 | object, simply map one or more constraints to its class and then pass it to the validator service.

    63 | 64 |

    Basic

    65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 82 | 83 | 84 | 85 | 88 | 89 | 90 | 91 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 106 | 107 | 108 | 109 | 112 | 113 |
    YAMLAnnotation
    NotBlank 74 | @Assert\NotBlank() 75 |
    Blank 80 | @Assert\Blank() 81 |
    NotNull 86 | @Assert\NotNull() 87 |
    Null 92 | @Assert\Null() 93 |
    True 98 | @Assert\True(message = "The token is invalid") 99 |
    False 104 | @Assert\False() 105 |
    Type 110 | @Assert\Type(type="integer", message="The value {% raw %}{{ value }}{% endraw %} is not a valid {% raw %}{{ type }}{% endraw %}.") 111 |
    114 |

    String

    115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 136 | 137 | 138 | 139 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |
    YAMLAnnotation
    Email 124 | @Assert\Email(message = "The email '{% raw %}{{ value }}{% endraw %}' is not a valid email.", checkMX = true, checkHost = true) 125 |
    MinLengthAssert\MinLength(limit=3, message="Your name must have at least {% raw %}{{ limit }}{% endraw %} characters.")
    MaxLength 134 | @Assert\MaxLength(100) 135 |
    Length 140 | @Assert\Length( min = "2",max = "50", minMessage = "msg", maxMessage = "msg" ) 141 |
    Url@Assert\Url(message="msg1", protocolos=array('http','https')
    Regex@Assert\Regex("/^\w+/") => options (pattern, match, message)
    Ip@Assert\Ip
    156 |

    Number

    157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 |
    YAMLAnnotation
    Max@Assert\Max(limit=5, message="msg1")
    Min@Assert\Min(limit=5, message="msg1")
    Range@Assert\Range(min = "120", max = "180",minMessage = "msg",maxMessage = "msg")
    176 |

    Date

    177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 |
    YAMLAnnotation
    Date@Assert\Date()
    DateTime@Assert\DateTime()
    Time@Assert\Time()
    196 |

    Collection

    197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 |
    YAMLAnnotation
    Choice@Assert\Choice(choices = {"male", "female"}, message = "Choose a valid gender.")
    Collection 210 | http://symfony.com/doc/current/reference/constraints/Collection.html 211 |
    Count@Assert\Count(min = "1", max = "5", minMessage = "msg", maxMessage = "msg" )
    UniqueEntity@ORM\Column(name="email", type="string", length=255, unique=true) (Suppose you have an AcmeUserBundle bundle with a User entity that has an email field. You can use the UniqueEntity constraint to guarantee that the email field remains unique between all of the constraints in your user table)
    Language@Assert\Language (Validates that it is a valid language code)
    Locale@Assert\Locale (Validates a valid Locale code (ej : ISO639-1)
    Country@Assert\Country (Valid two letter country code)
    234 |

    File

    235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 246 | 247 | 248 | 249 | 252 | 253 |
    YAMLAnnotation
    FileAssert\File(maxSize = "1024k",mimeTypes = {"application/pdf", "application/x-pdf"},mimeeTypesMessage = "msg") 244 | http://symfony.com/doc/current/reference/constraints/File.html 245 |
    Image@Assert\Image(minWidth = 200, maxWidth = 400, minHeight = 200, maxHeight = 400) 250 | http://symfony.com/doc/current/reference/constraints/Image.html 251 |
    254 |

    Other

    255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 278 | 279 |
    YAMLAnnotation
    Callback@Assert\Callback(methods={"isAuthorValid"})
    All@Assert\All({ @Assert\NotBlank @Assert\MinLength(5),}) (Aplies all constraints to each element of the Transversable object)
    UserPassword@SecurityAssert\UserPassword(message = "Wrong password") (This validates that an input value is equal to the current authenticated user's password.)
    ValidThis constraint is used to enable validation on objects that are embedded as properties on an object being validated. This allows you to validate an object and all sub-objects associated with it. 276 | http://symfony.com/doc/current/reference/constraints/Valid.html 277 |
    280 |

    Callback validations

    281 | 282 | {% raw %}
    // src/Acme/BlogBundle/Entity/Author.php
    283 | use Symfony\Component\Validator\Constraints as Assert;
    284 | 
    285 | /**
    286 | * @Assert\Callback(methods={"isAuthorValid"})
    287 | */
    288 | class Author
    289 | {
    290 | }
    291 | 
    {% endraw %} 292 | 293 |

    If the name of a method is a simple string (e.g. isAuthorValid), that method will be called on the same object that's being validated and the ExecutionContext will be the only argument (see the above example).

    294 | 295 | {% raw %}
    use Symfony\Component\Validator\ExecutionContext;
    296 | 
    297 | class Author
    298 | {
    299 |     // ...
    300 |     private $firstName;
    301 | 
    302 |     public function isAuthorValid(ExecutionContext $context)
    303 |     {
    304 |         // somehow you have an array of "fake names"
    305 |         $fakeNames = array();
    306 | 
    307 |         // check if the name is actually a fake name
    308 |         if (in_array($this->getFirstName(), $fakeNames)) {
    309 |             $context->addViolationAtSubPath('firstname', 'This name sounds totally fake!', array(), null);
    310 |         }
    311 |     }
    312 | }
    313 | 
    {% endraw %} 314 | 315 |

    You can define more complex validation in the repository class of the entity:

    316 | 317 | {% raw %}
    // src/Acme/BlogBundle/Entity/Author.php
    318 | use Symfony\Component\Validator\Constraints as Assert;
    319 | 
    320 | /**
    321 | * @Assert\Callback(methods={
    322 | * { "Acme\BlogBundle\MyStaticValidatorClass", "isAuthorValid"}
    323 | * })
    324 | */
    325 | class Author
    326 | {
    327 | }
    328 | 
    {% endraw %} 329 | 330 |

    In this case, the static method isAuthorValid will be called on the Acme\BlogBundle\MyStaticValidatorClass class. It's passed both the original object being validated (e.g. Author) as well as the ExecutionContext:

    331 | 332 | {% raw %} 333 |
    namespace Acme\BlogBundle;
    334 | 
    335 | use Symfony\Component\Validator\ExecutionContext;
    336 | use Acme\BlogBundle\Entity\Author;
    337 | 
    338 | class MyStaticValidatorClass
    339 | {
    340 |     static public function isAuthorValid(Author $author, ExecutionContext $context)
    341 |     {
    342 |     // ...
    343 |     }
    344 | }
    345 | 
    {% endraw %} 346 | 347 |

    Translating constraint messages

    348 | 349 |

    Create a translation file under the validators catalog for the constraint messages, typically in the Resources/translations/ directory of the bundle.

    350 | 351 | {% raw %}
     <!-- validators.es.xliff -->
    352 | <?xml version="1.0"?>
    353 | <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    354 | <file source-language="en" datatype="plaintext" original="file.ext">
    355 | <body>
    356 | <trans-unit id="1">
    357 | <source>author.gender.choice</source>
    358 | <target>Escoge un género válido.</target>
    359 | </trans-unit>
    360 | </body>
    361 | </file>
    362 | </xliff>
    363 | 
    {% endraw %} 364 | 365 |

    Validation groups

    366 | 367 | {% raw %}
    # src/Acme/BlogBundle/Resources/config/validation.yml
    368 | Acme\BlogBundle\Entity\User:
    369 |     properties:
    370 |         email:
    371 |             - Email: { groups: [registration] }
    372 |     password:
    373 |         - NotBlank: { groups: [registration] }
    374 |         - MinLength: { limit: 7, groups: [registration] }
    375 |     city:
    376 |         - MinLength: 2
    377 | 
    {% endraw %} 378 | 379 |

    With this configuration, there are two validation groups:

    380 | 381 | 391 |

    Validating Values and Arrays

    392 | 393 |

    Sometimes, you just want to validate a simple value - like to verify that a string is a valid email address.

    394 | 395 | {% raw %}
    // add this to the top of your class
    396 | use Symfony\Component\Validator\Constraints\Email;
    397 | 
    398 | public function addEmailAction($email)
    399 | {
    400 |     $emailConstraint = new Email();
    401 | 
    402 |     // all constraint "options" can be set this way
    403 |     $emailConstraint->message = 'Invalid email address';
    404 | 
    405 |     // use the validator to validate the value
    406 |     $errorList = $this->get('validator')->validateValue($email, $emailConstraint);
    407 | 
    408 |     if (count($errorList) == 0) {
    409 |         // this IS a valid email address, do something
    410 |     } else {
    411 |         // this is *not* a valid email address
    412 |         $errorMessage = $errorList[0]->getMessage()
    413 |         // ... do something with the error
    414 |     }
    415 |     // ...
    416 | }
    417 | 
    {% endraw %} --------------------------------------------------------------------------------