diff --git a/best_practices/configuration.rst b/best_practices/configuration.rst index df9779b121e..ff1d8d05428 100644 --- a/best_practices/configuration.rst +++ b/best_practices/configuration.rst @@ -42,6 +42,8 @@ they have nothing to do with the application's behavior. In other words, your application doesn't care about the location of your database or the credentials to access to it, as long as the database is correctly configured. +.. _best-practices-canonical-parameters: + Canonical Parameters ~~~~~~~~~~~~~~~~~~~~ @@ -101,8 +103,8 @@ to control the number of posts to display on the blog homepage: parameters: homepage.num_items: 10 -If you ask yourself when the last time was that you changed the value of -*any* option like this, odds are that you *never* have. Creating a configuration +If you've done something like this in the past, it's likely that you've in fact +*never* actually needed to change that value. Creating a configuration option for a value that you are never going to configure just isn't necessary. Our recommendation is to define these values as constants in your application. You could, for example, define a ``NUM_ITEMS`` constant in the ``Post`` entity: diff --git a/best_practices/i18n.rst b/best_practices/i18n.rst index 9da3fad3271..5190af0a4ba 100644 --- a/best_practices/i18n.rst +++ b/best_practices/i18n.rst @@ -11,7 +11,7 @@ following ``translator`` configuration option and set your application locale: # app/config/config.yml framework: # ... - translator: { fallback: "%locale%" } + translator: { fallbacks: ["%locale%"] } # app/config/parameters.yml parameters: diff --git a/best_practices/templates.rst b/best_practices/templates.rst index 801646587b2..7e96b2d5bfb 100644 --- a/best_practices/templates.rst +++ b/best_practices/templates.rst @@ -51,6 +51,10 @@ Another advantage is that centralizing your templates simplifies the work of your designers. They don't need to look for templates in lots of directories scattered through lots of bundles. +.. best-practice:: + + Use lowercased snake_case for directory and template names. + Twig Extensions --------------- diff --git a/book/controller.rst b/book/controller.rst index e79af5c803c..cc484c0cd3f 100644 --- a/book/controller.rst +++ b/book/controller.rst @@ -471,14 +471,14 @@ If you're serving HTML, you'll want to render a template. The ``render()`` method renders a template **and** puts that content into a ``Response`` object for you:: - // renders app/Resources/views/Hello/index.html.twig - return $this->render('Hello/index.html.twig', array('name' => $name)); + // renders app/Resources/views/hello/index.html.twig + return $this->render('hello/index.html.twig', array('name' => $name)); You can also put templates in deeper sub-directories. Just try to avoid creating unnecessarily deep structures:: - // renders app/Resources/views/Hello/Greetings/index.html.twig - return $this->render('Hello/Greetings/index.html.twig', array('name' => $name)); + // renders app/Resources/views/hello/greetings/index.html.twig + return $this->render('hello/greetings/index.html.twig', array('name' => $name)); The Symfony templating engine is explained in great detail in the :doc:`Templating ` chapter. diff --git a/book/forms.rst b/book/forms.rst index 4ccbcd1e7e4..90a074ac838 100644 --- a/book/forms.rst +++ b/book/forms.rst @@ -96,7 +96,7 @@ from inside a controller:: ->add('save', 'submit', array('label' => 'Create Task')) ->getForm(); - return $this->render('Default/new.html.twig', array( + return $this->render('default/new.html.twig', array( 'form' => $form->createView(), )); } @@ -144,14 +144,14 @@ helper functions: .. code-block:: html+jinja - {# app/Resources/views/Default/new.html.twig #} + {# app/Resources/views/default/new.html.twig #} {{ form_start(form) }} {{ form_widget(form) }} {{ form_end(form) }} .. code-block:: html+php - + start($form) ?> widget($form) ?> end($form) ?> @@ -442,12 +442,12 @@ corresponding errors printed out with the form. .. code-block:: html+jinja - {# app/Resources/views/Default/new.html.twig #} + {# app/Resources/views/default/new.html.twig #} {{ form(form, {'attr': {'novalidate': 'novalidate'}}) }} .. code-block:: html+php - + form($form, array( 'attr' => array('novalidate' => 'novalidate'), )) ?> @@ -784,7 +784,7 @@ of code. Of course, you'll usually need much more flexibility when rendering: .. code-block:: html+jinja - {# app/Resources/views/Default/new.html.twig #} + {# app/Resources/views/default/new.html.twig #} {{ form_start(form) }} {{ form_errors(form) }} @@ -794,7 +794,7 @@ of code. Of course, you'll usually need much more flexibility when rendering: .. code-block:: html+php - + start($form) ?> errors($form) ?> @@ -1002,12 +1002,12 @@ to the ``form()`` or the ``form_start()`` helper: .. code-block:: html+jinja - {# app/Resources/views/Default/new.html.twig #} + {# app/Resources/views/default/new.html.twig #} {{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }} .. code-block:: html+php - + start($form, array( 'action' => $view['router']->generate('target_route'), 'method' => 'GET', @@ -1466,7 +1466,7 @@ renders the form: .. code-block:: html+jinja - {# app/Resources/views/Default/new.html.twig #} + {# app/Resources/views/default/new.html.twig #} {% form_theme form 'form/fields.html.twig' %} {% form_theme form 'form/fields.html.twig' 'form/fields2.html.twig' %} @@ -1475,10 +1475,10 @@ renders the form: .. code-block:: html+php - - setTheme($form, array('Form')) ?> + + setTheme($form, array('form')) ?> - setTheme($form, array('Form', 'Form2')) ?> + setTheme($form, array('form', 'form2')) ?> diff --git a/book/from_flat_php_to_symfony2.rst b/book/from_flat_php_to_symfony2.rst index 8df11d360ba..200ecc6b3ba 100644 --- a/book/from_flat_php_to_symfony2.rst +++ b/book/from_flat_php_to_symfony2.rst @@ -710,7 +710,7 @@ for example, the list template written in Twig: .. code-block:: html+jinja - {# app/Resources/views/Blog/list.html.twig #} + {# app/Resources/views/blog/list.html.twig #} {% extends "layout.html.twig" %} {% block title %}List of Posts{% endblock %} diff --git a/book/http_cache.rst b/book/http_cache.rst index 5cae7a710e2..1c1e5f64fa8 100644 --- a/book/http_cache.rst +++ b/book/http_cache.rst @@ -155,9 +155,12 @@ kernel:: $kernel->loadClassCache(); // wrap the default AppKernel with the AppCache one $kernel = new AppCache($kernel); + $request = Request::createFromGlobals(); + $response = $kernel->handle($request); $response->send(); + $kernel->terminate($request, $response); The caching kernel will immediately act as a reverse proxy - caching responses @@ -576,16 +579,22 @@ each ``ETag`` must be unique across all representations of the same resource. To see a simple implementation, generate the ETag as the md5 of the content:: + // src/AppBundle/Controller/DefaultController.php + namespace AppBundle\Controller; + use Symfony\Component\HttpFoundation\Request; - public function indexAction(Request $request) + class DefaultController extends Controller { - $response = $this->render('MyBundle:Main:index.html.twig'); - $response->setETag(md5($response->getContent())); - $response->setPublic(); // make sure the response is public/cacheable - $response->isNotModified($request); + public function homepageAction(Request $request) + { + $response = $this->render('static/homepage.html.twig'); + $response->setETag(md5($response->getContent())); + $response->setPublic(); // make sure the response is public/cacheable + $response->isNotModified($request); - return $response; + return $response; + } } The :method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified` @@ -632,28 +641,36 @@ 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:: + // src/AppBundle/Controller/ArticleController.php + namespace AppBundle\Controller; + + // ... use Symfony\Component\HttpFoundation\Request; + use AppBundle\Entity\Article; - public function showAction($articleSlug, Request $request) + class ArticleController extends Controller { - // ... + public function showAction(Article $article, Request $request) + { + $author = $article->getAuthor(); - $articleDate = new \DateTime($article->getUpdatedAt()); - $authorDate = new \DateTime($author->getUpdatedAt()); + $articleDate = new \DateTime($article->getUpdatedAt()); + $authorDate = new \DateTime($author->getUpdatedAt()); - $date = $authorDate > $articleDate ? $authorDate : $articleDate; + $date = $authorDate > $articleDate ? $authorDate : $articleDate; - $response->setLastModified($date); - // Set response as public. Otherwise it will be private by default. - $response->setPublic(); + $response->setLastModified($date); + // Set response as public. Otherwise it will be private by default. + $response->setPublic(); - if ($response->isNotModified($request)) { - return $response; - } + if ($response->isNotModified($request)) { + return $response; + } - // ... do more work to populate the response with the full content + // ... do more work to populate the response with the full content - return $response; + return $response; + } } The :method:`Symfony\\Component\\HttpFoundation\\Response::isNotModified` @@ -682,40 +699,46 @@ 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:: + // src/AppBundle/Controller/ArticleController.php + namespace AppBundle\Controller; + + // ... use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; - public function showAction($articleSlug, Request $request) + class ArticleController extends Controller { - // Get the minimum information to compute - // the ETag or the Last-Modified value - // (based on the Request, data is retrieved from - // a database or a key-value store for instance) - $article = ...; - - // create a Response with an ETag and/or a Last-Modified header - $response = new Response(); - $response->setETag($article->computeETag()); - $response->setLastModified($article->getPublishedAt()); - - // Set response as public. Otherwise it will be private by default. - $response->setPublic(); - - // Check that the Response is not modified for the given Request - if ($response->isNotModified($request)) { - // return the 304 Response immediately - return $response; - } + public function showAction($articleSlug, Request $request) + { + // Get the minimum information to compute + // the ETag or the Last-Modified value + // (based on the Request, data is retrieved from + // a database or a key-value store for instance) + $article = ...; + + // create a Response with an ETag and/or a Last-Modified header + $response = new Response(); + $response->setETag($article->computeETag()); + $response->setLastModified($article->getPublishedAt()); + + // Set response as public. Otherwise it will be private by default. + $response->setPublic(); + + // Check that the Response is not modified for the given Request + if ($response->isNotModified($request)) { + // return the 304 Response immediately + return $response; + } - // do more work here - like retrieving more data - $comments = ...; + // do more work here - like retrieving more data + $comments = ...; - // or render a template with the $response you've already started - return $this->render( - 'MyBundle:MyController:article.html.twig', - array('article' => $article, 'comments' => $comments), - $response - ); + // or render a template with the $response you've already started + return $this->render('article/show.html.twig', array( + 'article' => $article, + 'comments' => $comments + ), $response); + } } When the ``Response`` is not modified, the ``isNotModified()`` automatically sets @@ -865,10 +888,10 @@ Here is how you can configure the Symfony reverse proxy to support the // app/AppCache.php - // ... use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; + // ... class AppCache extends HttpCache { @@ -930,7 +953,7 @@ 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, Symfony provides a solution for these cases, based on a technology called `ESI`_, or Edge Side Includes. Akamai wrote this specification -almost 10 years ago, and it allows specific parts of a page to have a different +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 @@ -1017,13 +1040,19 @@ independent of the rest of the page. .. code-block:: php - public function indexAction() + // src/AppBundle/Controller/DefaultController.php + + // ... + class DefaultController extends Controller { - $response = $this->render('MyBundle:MyController:index.html.twig'); - // set the shared max age - which also marks the response as public - $response->setSharedMaxAge(600); + public function aboutAction() + { + $response = $this->render('static/about.html.twig'); + // set the shared max age - which also marks the response as public + $response->setSharedMaxAge(600); - return $response; + return $response; + } } In this example, the full-page cache has a lifetime of ten minutes. @@ -1038,21 +1067,36 @@ matter), Symfony uses the standard ``render`` helper to configure ESI tags: .. code-block:: jinja + {# app/Resources/views/static/about.html.twig #} + {# you can use a controller reference #} - {{ render_esi(controller('...:news', { 'maxPerPage': 5 })) }} + {{ render_esi(controller('AppBundle:News:latest', { 'maxPerPage': 5 })) }} {# ... or a URL #} {{ render_esi(url('latest_news', { 'maxPerPage': 5 })) }} .. code-block:: html+php + + + // you can use a controller reference + use Symfony\Component\HttpKernel\Controller\ControllerReference; render( - new \Symfony\Component\HttpKernel\Controller\ControllerReference('...:news', array('maxPerPage' => 5)), - array('strategy' => 'esi')) - ?> + new ControllerReference( + 'AppBundle:News:latest', + array('maxPerPage' => 5) + ), + array('strategy' => 'esi') + ) ?> + // ... or a URL + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; render( - $view['router']->generate('latest_news', array('maxPerPage' => 5), true), + $view['router']->generate( + 'latest_news', + array('maxPerPage' => 5), + UrlGeneratorInterface::ABSOLUTE_URL + ), array('strategy' => 'esi'), ) ?> @@ -1072,7 +1116,7 @@ if there is no gateway cache installed. When using the default ``render`` function (or setting the renderer to ``inline``), Symfony merges the included page content into the main one before sending the response to the client. But if you use the ``esi`` renderer -(i.e. call ``render_esi``), *and* if Symfony detects that it's talking to a +(i.e. call ``render_esi``) *and* if Symfony 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, Symfony will just merge the included page content within the main one as it would have done if you had @@ -1089,11 +1133,19 @@ of the master page. .. code-block:: php - public function newsAction($maxPerPage) + // src/AppBundle/Controller/NewsController.php + namespace AppBundle\Controller; + + // ... + class NewsController extends Controller { - // ... + public function latestAction($maxPerPage) + { + // ... + $response->setSharedMaxAge(60); - $response->setSharedMaxAge(60); + return $response; + } } With ESI, the full page cache will be valid for 600 seconds, but the news diff --git a/book/propel.rst b/book/propel.rst index 9ff589afb04..d1b812f4653 100644 --- a/book/propel.rst +++ b/book/propel.rst @@ -15,15 +15,6 @@ 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. -.. sidebar:: Code along with the Example - - If you want to follow along with the example in this chapter, create an - AcmeStoreBundle via: - - .. code-block:: bash - - $ php app/console generate:bundle --namespace=Acme/StoreBundle - Configuring the Database ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -42,12 +33,6 @@ information. By convention, this information is usually configured in an database_password: password database_charset: UTF8 -.. note:: - - Defining the configuration via ``parameters.yml`` is just a convention. The - parameters defined in that file are referenced by the main configuration - file when setting up Propel: - These parameters defined in ``parameters.yml`` can now be included in the configuration file (``config.yml``): @@ -60,7 +45,13 @@ configuration file (``config.yml``): password: "%database_password%" dsn: "%database_driver%:host=%database_host%;dbname=%database_name%;charset=%database_charset%" -Now that Propel knows about your database, Symfony can create the database for +.. note:: + + Defining the configuration via ``parameters.yml`` is a + :ref:`Symfony Framework Best Practice `, + feel free to do it differently if that suits your application better. + +Now that Propel knows about your database, it can create the database for you: .. code-block:: bash @@ -70,8 +61,8 @@ you: .. note:: In this example, you have one configured connection, named ``default``. If - you want to configure more than one connection, read the `PropelBundle - configuration section`_. + you want to configure more than one connection, read the + `PropelBundle configuration section`_. Creating a Model Class ~~~~~~~~~~~~~~~~~~~~~~ @@ -86,14 +77,15 @@ generated by Propel contain some business logic. 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: +of your AppBundle: .. code-block:: xml + @@ -129,7 +121,7 @@ After creating your ``schema.xml``, generate your model from it by running: $ php app/console propel:model:build This generates each model class to quickly develop your application in the -``Model/`` directory of the AcmeStoreBundle bundle. +``Model/`` directory of the AppBundle bundle. Creating the Database Tables/Schema ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -150,32 +142,39 @@ match the schema you've specified. .. tip:: You can run the last three commands combined by using the following - command: ``php app/console propel:build --insert-sql``. + command: + + .. code-block:: bash + + $ 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 +is pretty easy. Add the following method to the ``ProductController`` of the bundle:: - // src/Acme/StoreBundle/Controller/DefaultController.php + // src/AppBundle/Controller/ProductController.php // ... - use Acme\StoreBundle\Model\Product; + use AppBundle\Model\Product; use Symfony\Component\HttpFoundation\Response; - public function createAction() + class ProductController extends Controller { - $product = new Product(); - $product->setName('A Foo Bar'); - $product->setPrice(19.99); - $product->setDescription('Lorem ipsum dolor'); + public function createAction() + { + $product = new Product(); + $product->setName('A Foo Bar'); + $product->setPrice(19.99); + $product->setDescription('Lorem ipsum dolor'); - $product->save(); + $product->save(); - return new Response('Created product id '.$product->getId()); + return new Response('Created product id '.$product->getId()); + } } In this piece of code, you instantiate and work with the ``$product`` object. @@ -194,21 +193,27 @@ 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:: + // src/AppBundle/Controller/ProductController.php + // ... - use Acme\StoreBundle\Model\ProductQuery; + use AppBundle\Model\ProductQuery; - public function showAction($id) + class ProductController extends Controller { - $product = ProductQuery::create() - ->findPk($id); + // ... - if (!$product) { - throw $this->createNotFoundException( - 'No product found for id '.$id - ); - } + 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 + // ... do something, like pass the $product object into a template + } } Updating an Object @@ -217,31 +222,37 @@ 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:: + // src/AppBundle/Controller/ProductController.php + // ... - use Acme\StoreBundle\Model\ProductQuery; + use AppBundle\Model\ProductQuery; - public function updateAction($id) + class ProductController extends Controller { - $product = ProductQuery::create() - ->findPk($id); + // ... - if (!$product) { - throw $this->createNotFoundException( - 'No product found for id '.$id - ); - } + public function updateAction($id) + { + $product = ProductQuery::create()->findPk($id); - $product->setName('New product name!'); - $product->save(); + if (!$product) { + throw $this->createNotFoundException( + 'No product found for id '.$id + ); + } - return $this->redirect($this->generateUrl('homepage')); + $product->setName('New product name!'); + $product->save(); + + return $this->redirect($this->generateUrl('homepage')); + } } Updating an object involves just three steps: -#. fetching the object from Propel (line 6 - 13); -#. modifying the object (line 15); -#. saving it (line 16). +#. fetching the object from Propel (line 12 - 18); +#. modifying the object (line 20); +#. saving it (line 21). Deleting an Object ~~~~~~~~~~~~~~~~~~ @@ -257,16 +268,22 @@ Querying for Objects Propel provides generated ``Query`` classes to run both basic and complex queries without any work:: - \Acme\StoreBundle\Model\ProductQuery::create()->findPk($id); + use AppBundle\Model\ProductQuery; + // ... + + ProductQuery::create()->findPk($id); - \Acme\StoreBundle\Model\ProductQuery::create() + ProductQuery::create() ->filterByName('Foo') ->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:: - $products = \Acme\StoreBundle\Model\ProductQuery::create() + use AppBundle\Model\ProductQuery; + // ... + + $products = ProductQuery::create() ->filterByPrice(array('min' => 19.99)) ->orderByPrice() ->find(); @@ -279,20 +296,26 @@ abstraction layer. If you want to reuse some queries, you can add your own methods to the ``ProductQuery`` class:: - // src/Acme/StoreBundle/Model/ProductQuery.php + // src/AppBundle/Model/ProductQuery.php + + // ... class ProductQuery extends BaseProductQuery { public function filterByExpensivePrice() { - return $this - ->filterByPrice(array('min' => 1000)); + return $this->filterByPrice(array( + 'min' => 1000, + )); } } -But note that Propel generates a lot of methods for you and a simple +However, note that Propel generates a lot of methods for you and a simple ``findAllOrderedByName()`` can be written without any effort:: - \Acme\StoreBundle\Model\ProductQuery::create() + use AppBundle\Model\ProductQuery; + // ... + + ProductQuery::create() ->orderByName() ->find(); @@ -310,7 +333,7 @@ Start by adding the ``category`` definition in your ``schema.xml``:
@@ -382,12 +405,14 @@ Saving Related Objects Now, try the code in action. Imagine you're inside a controller:: + // src/AppBundle/Controller/ProductController.php + // ... - use Acme\StoreBundle\Model\Category; - use Acme\StoreBundle\Model\Product; + use AppBundle\Model\Category; + use AppBundle\Model\Product; use Symfony\Component\HttpFoundation\Response; - class DefaultController extends Controller + class ProductController extends Controller { public function createProductAction() { @@ -418,21 +443,25 @@ 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``:: +before: Fetch a ``$product`` object and then access its related ``Category``:: + + // src/AppBundle/Controller/ProductController.php // ... - use Acme\StoreBundle\Model\ProductQuery; + use AppBundle\Model\ProductQuery; - public function showAction($id) + class ProductController extends Controller { - $product = ProductQuery::create() - ->joinWithCategory() - ->findPk($id); + public function showAction($id) + { + $product = ProductQuery::create() + ->joinWithCategory() + ->findPk($id); - $categoryName = $product->getCategory()->getName(); + $categoryName = $product->getCategory()->getName(); - // ... + // ... + } } Note, in the above example, only one query was made. @@ -454,14 +483,14 @@ inserted, updated, deleted, etc). To add a hook, just add a new method to the object class:: - // src/Acme/StoreBundle/Model/Product.php + // src/AppBundle/Model/Product.php // ... class Product extends BaseProduct { public function preInsert(\PropelPDO $con = null) { - // do something before the object is inserted + // ... do something before the object is inserted } } @@ -488,8 +517,8 @@ Behaviors --------- All bundled behaviors in Propel are working with Symfony. To get more -information about how to use Propel behaviors, look at the `Behaviors reference -section`_. +information about how to use Propel behaviors, look at the +`Behaviors reference section`_. Commands -------- diff --git a/book/templating.rst b/book/templating.rst index d8a4fba2cf0..feaa3cdef4d 100644 --- a/book/templating.rst +++ b/book/templating.rst @@ -263,7 +263,7 @@ A child template might look like this: .. code-block:: html+jinja - {# app/Resources/views/Blog/index.html.twig #} + {# app/Resources/views/blog/index.html.twig #} {% extends 'base.html.twig' %} {% block title %}My cool blog posts{% endblock %} @@ -277,7 +277,7 @@ A child template might look like this: .. code-block:: html+php - + extend('base.html.php') ?> set('title', 'My cool blog posts') ?> @@ -394,8 +394,8 @@ Most of the templates you'll use live in the ``app/Resources/views/`` directory. The path you'll use will be relative to this directory. For example, to render/extend ``app/Resources/views/base.html.twig``, you'll use the ``base.html.twig`` path and to render/extend -``app/Resources/views/Blog/index.html.twig``, you'll use the -``Blog/index.html.twig`` path. +``app/Resources/views/blog/index.html.twig``, you'll use the +``blog/index.html.twig`` path. .. _template-referencing-in-bundle: @@ -448,9 +448,9 @@ Every template name also has two extensions that specify the *format* and ======================== ====== ====== Filename Format Engine ======================== ====== ====== -``Blog/index.html.twig`` HTML Twig -``Blog/index.html.php`` HTML PHP -``Blog/index.css.twig`` CSS Twig +``blog/index.html.twig`` HTML Twig +``blog/index.html.php`` HTML PHP +``blog/index.css.twig`` CSS Twig ======================== ====== ====== By default, any Symfony template can be written in either Twig or PHP, and @@ -513,7 +513,7 @@ template. First, create the template that you'll need to reuse. .. code-block:: html+jinja - {# app/Resources/views/Article/articleDetails.html.twig #} + {# app/Resources/views/article/article_details.html.twig #}

{{ article.title }}

@@ -523,7 +523,7 @@ template. First, create the template that you'll need to reuse. .. code-block:: html+php - +

getTitle() ?>

@@ -537,20 +537,20 @@ Including this template from any other template is simple: .. code-block:: html+jinja - {# app/Resources/views/Article/list.html.twig #} + {# app/Resources/views/article/list.html.twig #} {% extends 'layout.html.twig' %} {% block body %}

Recent Articles

{% for article in articles %} - {{ include('Article/articleDetails.html.twig', { 'article': article }) }} + {{ include('article/article_details.html.twig', { 'article': article }) }} {% endfor %} {% endblock %} .. code-block:: html+php - + extend('layout.html.php') ?> start('body') ?> @@ -558,17 +558,17 @@ Including this template from any other template is simple: render( - 'Article/articleDetails.html.php', + 'Article/article_details.html.php', array('article' => $article) ) ?> stop() ?> The template is included using the ``{{ include() }}`` function. Notice that the -template name follows the same typical convention. The ``articleDetails.html.twig`` +template name follows the same typical convention. The ``article_details.html.twig`` template uses an ``article`` variable, which we pass to it. In this case, you could avoid doing this entirely, as all of the variables available in -``list.html.twig`` are also available in ``articleDetails.html.twig`` (unless +``list.html.twig`` are also available in ``article_details.html.twig`` (unless you set `with_context`_ to false). .. tip:: @@ -608,7 +608,7 @@ articles:: $articles = ...; return $this->render( - 'Article/recentList.html.twig', + 'article/recent_list.html.twig', array('articles' => $articles) ); } @@ -620,7 +620,7 @@ The ``recentList`` template is perfectly straightforward: .. code-block:: html+jinja - {# app/Resources/views/Article/recentList.html.twig #} + {# app/Resources/views/article/recent_list.html.twig #} {% for article in articles %} {{ article.title }} @@ -629,7 +629,7 @@ The ``recentList`` template is perfectly straightforward: .. code-block:: html+php - + getTitle() ?> @@ -797,7 +797,7 @@ any global default template that is defined): .. code-block:: jinja {{ render_hinclude(controller('...'), { - 'default': 'Default/content.html.twig' + 'default': 'default/content.html.twig' }) }} .. code-block:: php @@ -806,7 +806,7 @@ any global default template that is defined): new ControllerReference('...'), array( 'renderer' => 'hinclude', - 'default' => 'Default/content.html.twig', + 'default' => 'default/content.html.twig', ) ) ?> @@ -942,7 +942,7 @@ correctly: .. code-block:: html+jinja - {# app/Resources/views/Article/recentList.html.twig #} + {# app/Resources/views/article/recent_list.html.twig #} {% for article in articles %} {{ article.title }} @@ -951,7 +951,7 @@ correctly: .. code-block:: html+php - + getRequestFormat(); - return $this->render('Blog/index.'.$format.'.twig'); + return $this->render('article/index.'.$format.'.twig'); } The ``getRequestFormat`` on the ``Request`` object defaults to ``html``, @@ -1671,7 +1671,7 @@ their use is not mandatory. The ``Response`` object returned by a controller can be created with or without the use of a template:: // creates a Response object whose content is the rendered template - $response = $this->render('Article/index.html.twig'); + $response = $this->render('article/index.html.twig'); // creates a Response object whose content is simple text $response = new Response('response content'); diff --git a/book/testing.rst b/book/testing.rst index e9231577b13..2faa734a68e 100644 --- a/book/testing.rst +++ b/book/testing.rst @@ -36,7 +36,8 @@ file. .. tip:: - Code coverage can be generated with the ``--coverage-html`` option. + Code coverage can be generated with the ``--coverage-*`` options, see the + help information that is shown when using ``--help`` for more information. .. index:: single: Tests; Unit tests @@ -44,15 +45,16 @@ file. 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`_. +A unit test is a test against a single PHP class, also called a *unit*. If you +want to test the overall behavior of your application, see the section about +`Functional Tests`_. Writing Symfony unit tests is no different from writing standard PHPUnit unit tests. Suppose, for example, that you have an *incredibly* simple class -called ``Calculator`` in the ``Utility/`` directory of your bundle:: +called ``Calculator`` in the ``Util/`` directory of the app bundle:: - // src/Acme/DemoBundle/Utility/Calculator.php - namespace Acme\DemoBundle\Utility; + // src/AppBundle/Util/Calculator.php + namespace AppBundle\Util; class Calculator { @@ -62,13 +64,13 @@ called ``Calculator`` in the ``Utility/`` directory of your bundle:: } } -To test this, create a ``CalculatorTest`` file in the ``Tests/Utility`` directory +To test this, create a ``CalculatorTest`` file in the ``Tests/Util`` directory of your bundle:: - // src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php - namespace Acme\DemoBundle\Tests\Utility; + // src/AppBundle/Tests/Util/CalculatorTest.php + namespace AppBundle\Tests\Util; - use Acme\DemoBundle\Utility\Calculator; + use AppBundle\Util\Calculator; class CalculatorTest extends \PHPUnit_Framework_TestCase { @@ -85,8 +87,9 @@ of your bundle:: .. note:: 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. + of your bundle for unit tests. So, if you're testing a class in your + bundle's ``Util/`` directory, put the test in the ``Tests/Util/`` + directory. Just like in your real application - autoloading is automatically enabled via the ``bootstrap.php.cache`` file (as configured by default in the @@ -96,14 +99,17 @@ Running tests for a given file or directory is also very easy: .. code-block:: bash - # run all tests in the Utility directory - $ phpunit -c app src/Acme/DemoBundle/Tests/Utility/ + # run all tests of the application + $ phpunit -c app + + # run all tests in the Util directory + $ phpunit -c app src/AppBundle/Tests/Util # run tests for the Calculator class - $ phpunit -c app src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php + $ phpunit -c app src/AppBundle/Tests/Util/CalculatorTest.php # run all tests for the entire Bundle - $ phpunit -c app src/Acme/DemoBundle/ + $ phpunit -c app src/AppBundle/ .. index:: single: Tests; Functional tests @@ -126,28 +132,27 @@ 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`` +``PostController`` class, start by creating a new ``PostControllerTest.php`` file that extends a special ``WebTestCase`` class. -For example, the Symfony Standard Edition provides a simple functional test -for its ``DemoController`` (`DemoControllerTest`_) that reads as follows:: +As an example, a test could look like this:: - // src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php - namespace Acme\DemoBundle\Tests\Controller; + // src/AppBundle/Tests/Controller/PostControllerTest.php + namespace AppBundle\Tests\Controller; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - class DemoControllerTest extends WebTestCase + class PostControllerTest extends WebTestCase { - public function testIndex() + public function testShowPost() { $client = static::createClient(); - $crawler = $client->request('GET', '/demo/hello/Fabien'); + $crawler = $client->request('GET', '/post/hello-world'); $this->assertGreaterThan( 0, - $crawler->filter('html:contains("Hello Fabien")')->count() + $crawler->filter('html:contains("Hello World")')->count() ); } } @@ -157,13 +162,13 @@ for its ``DemoController`` (`DemoControllerTest`_) that reads as follows:: 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: + to modify your ``phpunit.xml.dist`` file to set the ``KERNEL_DIR`` + environment variable to the directory of your kernel: .. code-block:: xml + - @@ -173,28 +178,31 @@ for its ``DemoController`` (`DemoControllerTest`_) that reads as follows:: The ``createClient()`` method returns a client, which is like a browser that you'll use to crawl your site:: - $crawler = $client->request('GET', '/demo/hello/Fabien'); + $crawler = $client->request('GET', '/post/hello-world'); -The ``request()`` method (see :ref:`more about the request method `) +The ``request()`` method (read +:ref:`more about the request method `) returns a :class:`Symfony\\Component\\DomCrawler\\Crawler` object which can -be used to select elements in the Response, click on links, and submit forms. +be used to select elements in the response, click on links and submit forms. .. tip:: - The Crawler only works when the response is an XML or an HTML document. + 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:: +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:: - $link = $crawler->filter('a:contains("Greet")')->eq(1)->link(); + $link = $crawler + ->filter('a:contains("Greet")') // find all links with the text "Greet" + ->eq(1) // select the second link in the list + ->link() // and click it + ; $crawler = $client->click($link); -Submitting a form is very similar; select a form button, optionally override -some form values, and submit the corresponding form:: +Submitting a form is very similar: select a form button, optionally override +some form values and submit the corresponding form:: $form = $crawler->selectButton('submit')->form(); @@ -218,48 +226,15 @@ on the DOM:: // Assert that the response matches a given CSS selector. $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 +Or test against the response content directly if you just want to assert that +the content contains some text or in case that the response is not an XML/HTML document:: - $this->assertRegExp( - '/Hello Fabien/', + $this->assertContains( + 'Hello World', $client->getResponse()->getContent() ); -.. _book-testing-request-method-sidebar: - -.. sidebar:: More about the ``request()`` Method: - - The full signature of the ``request()`` method is:: - - request( - $method, - $uri, - array $parameters = array(), - array $files = array(), - array $server = array(), - $content = null, - $changeHistory = true - ) - - The ``server`` array is the raw values that you'd expect to normally - find in the PHP `$_SERVER`_ superglobal. For example, to set the ``Content-Type``, - ``Referer`` and ``X-Requested-With`` HTTP headers, you'd pass the following (mind - the ``HTTP_`` prefix for non standard headers):: - - $client->request( - 'GET', - '/demo/hello/Fabien', - array(), - array(), - array( - 'CONTENT_TYPE' => 'application/json', - 'HTTP_REFERER' => '/foo/bar', - 'HTTP_X-Requested-With' => 'XMLHttpRequest', - ) - ); - .. index:: single: Tests; Assertions @@ -290,8 +265,10 @@ document:: ) ); - // Assert that the response content matches a regexp. - $this->assertRegExp('/foo/', $client->getResponse()->getContent()); + // Assert that the response content contains a string + $this->assertContains('foo', $client->getResponse()->getContent()); + // ...or matches a regex + $this->assertRegExp('/foo(bar)?/', $client->getResponse()->getContent()); // Assert that the response status code is 2xx $this->assertTrue($client->getResponse()->isSuccessful()); @@ -299,7 +276,7 @@ document:: $this->assertTrue($client->getResponse()->isNotFound()); // Assert a specific 200 status code $this->assertEquals( - Response::HTTP_OK, + 200, // or Symfony\Component\HttpFoundation\Response::HTTP_OK $client->getResponse()->getStatusCode() ); @@ -307,7 +284,7 @@ document:: $this->assertTrue( $client->getResponse()->isRedirect('/demo/contact') ); - // or simply check that the response is a redirect to any URL + // ...or simply check that the response is a redirect to any URL $this->assertTrue($client->getResponse()->isRedirect()); .. versionadded:: 2.4 @@ -317,12 +294,12 @@ document:: single: Tests; Client Working with the Test Client ------------------------------ +---------------------------- -The Test Client simulates an HTTP client like a browser and makes requests +The test client simulates an HTTP client like a browser and makes requests into your Symfony application:: - $crawler = $client->request('GET', '/hello/Fabien'); + $crawler = $client->request('GET', '/post/hello-world'); The ``request()`` method takes the HTTP method and a URL as arguments and returns a ``Crawler`` instance. @@ -333,7 +310,40 @@ returns a ``Crawler`` instance. test generates URLs using the Symfony router, it won't detect any change made to the application URLs which may impact the end users. -Use the Crawler to find DOM elements in the Response. These elements can then +.. _book-testing-request-method-sidebar: + +.. sidebar:: More about the ``request()`` Method: + + The full signature of the ``request()`` method is:: + + request( + $method, + $uri, + array $parameters = array(), + array $files = array(), + array $server = array(), + $content = null, + $changeHistory = true + ) + + The ``server`` array is the raw values that you'd expect to normally + find in the PHP `$_SERVER`_ superglobal. For example, to set the ``Content-Type``, + ``Referer`` and ``X-Requested-With`` HTTP headers, you'd pass the following (mind + the ``HTTP_`` prefix for non standard headers):: + + $client->request( + 'GET', + '/post/hello-world', + array(), + array(), + array( + 'CONTENT_TYPE' => 'application/json', + 'HTTP_REFERER' => '/foo/bar', + 'HTTP_X-Requested-With' => 'XMLHttpRequest', + ) + ); + +Use the crawler to find DOM elements in the response. These elements can then be used to click on links and submit forms:: $link = $crawler->selectLink('Go elsewhere...')->link(); @@ -353,7 +363,7 @@ giving you a nice API for uploading files. :ref:`Crawler ` section below. The ``request`` method can also be used to simulate form submissions directly -or perform more complex requests:: +or perform more complex requests. Some useful examples:: // Directly submit a form (but using the Crawler is easier!) $client->request('POST', '/submit', array('name' => 'Fabien')); @@ -384,7 +394,7 @@ or perform more complex requests:: array('photo' => $photo) ); - // Perform a DELETE requests, and pass HTTP headers + // Perform a DELETE requests and pass HTTP headers $client->request( 'DELETE', '/post/12', @@ -411,7 +421,7 @@ The Client supports many operations that can be done in a real browser:: // Clears all cookies and the history $client->restart(); -Accessing internal Objects +Accessing Internal Objects ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 2.3 @@ -422,16 +432,16 @@ Accessing internal Objects If you use the client to test your application, you might want to access the client's internal objects:: - $history = $client->getHistory(); + $history = $client->getHistory(); $cookieJar = $client->getCookieJar(); You can also get the objects related to the latest request:: // the HttpKernel request instance - $request = $client->getRequest(); + $request = $client->getRequest(); // the BrowserKit request instance - $request = $client->getInternalRequest(); + $request = $client->getInternalRequest(); // the HttpKernel response instance $response = $client->getResponse(); @@ -439,13 +449,13 @@ You can also get the objects related to the latest request:: // the BrowserKit response instance $response = $client->getInternalResponse(); - $crawler = $client->getCrawler(); + $crawler = $client->getCrawler(); If your requests are not insulated, you can also access the ``Container`` and the ``Kernel``:: $container = $client->getContainer(); - $kernel = $client->getKernel(); + $kernel = $client->getKernel(); Accessing the Container ~~~~~~~~~~~~~~~~~~~~~~~ @@ -637,9 +647,7 @@ The ``selectButton()`` method can select ``button`` tags and submit ``input`` tags. It uses several 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 @@ -863,6 +871,7 @@ section: Learn more ---------- +* The :doc:`chapter about tests in the Symfony Framework Best Practices ` * :doc:`/components/dom_crawler` * :doc:`/components/css_selector` * :doc:`/cookbook/testing/http_authentication` @@ -870,6 +879,5 @@ Learn more * :doc:`/cookbook/testing/profiling` * :doc:`/cookbook/testing/bootstrap` -.. _`DemoControllerTest`: https://github.com/sensiolabs/SensioDistributionBundle/blob/master/Resources/skeleton/acme-demo-bundle/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php .. _`$_SERVER`: http://php.net/manual/en/reserved.variables.server.php .. _`documentation`: http://phpunit.de/manual/current/en/ diff --git a/book/translation.rst b/book/translation.rst index e2170afaaf3..3f7969e5af1 100644 --- a/book/translation.rst +++ b/book/translation.rst @@ -59,7 +59,7 @@ enable the ``translator`` in your configuration: # app/config/config.yml framework: - translator: { fallback: en } + translator: { fallbacks: [en] } .. code-block:: xml @@ -74,7 +74,9 @@ enable the ``translator`` in your configuration: http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + + en + @@ -82,10 +84,10 @@ enable the ``translator`` in your configuration: // app/config/config.php $container->loadFromExtension('framework', array( - 'translator' => array('fallback' => 'en'), + 'translator' => array('fallbacks' => array('en')), )); -See :ref:`book-translation-fallback` for details on the ``fallback`` key +See :ref:`book-translation-fallback` for details on the ``fallbacks`` key and what Symfony does when it doesn't find a translation. The locale used in translations is the one stored on the request. This is @@ -400,7 +402,7 @@ checks translation resources for several locales: #. If it wasn't found, Symfony looks for the translation in a ``fr`` translation resource (e.g. ``messages.fr.xliff``); -#. If the translation still isn't found, Symfony uses the ``fallback`` configuration +#. If the translation still isn't found, Symfony uses the ``fallbacks`` configuration parameter, which defaults to ``en`` (see `Configuration`_). .. versionadded:: 2.6 diff --git a/book/validation.rst b/book/validation.rst index a7a6a3c1d75..d6037406fff 100644 --- a/book/validation.rst +++ b/book/validation.rst @@ -22,8 +22,8 @@ The best way to understand validation 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:: - // src/Acme/BlogBundle/Entity/Author.php - namespace Acme\BlogBundle\Entity; + // src/AppBundle/Entity/Author.php + namespace AppBundle\Entity; class Author { @@ -42,17 +42,9 @@ following: .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - name: - - NotBlank: ~ - .. code-block:: php-annotations - // src/Acme/BlogBundle/Entity/Author.php + // src/AppBundle/Entity/Author.php // ... use Symfony\Component\Validator\Constraints as Assert; @@ -65,15 +57,23 @@ following: public $name; } + .. code-block:: yaml + + # src/AppBundle/Resources/config/validation.yml + AppBundle\Entity\Author: + properties: + name: + - NotBlank: ~ + .. code-block:: xml - + - + @@ -82,7 +82,7 @@ following: .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php + // src/AppBundle/Entity/Author.php // ... use Symfony\Component\Validator\Mapping\ClassMetadata; @@ -119,11 +119,13 @@ returned. Take this simple example from inside a controller:: // ... use Symfony\Component\HttpFoundation\Response; - use Acme\BlogBundle\Entity\Author; + use AppBundle\Entity\Author; - public function indexAction() + // ... + public function authorAction() { $author = new Author(); + // ... do something to the $author object $validator = $this->get('validator'); @@ -133,7 +135,7 @@ returned. Take this simple example from inside a controller:: /* * Uses a __toString method on the $errors variable which is a * ConstraintViolationList object. This gives us a nice string - * for debugging + * for debugging. */ $errorsString = (string) $errors; @@ -148,7 +150,7 @@ message: .. code-block:: text - Acme\BlogBundle\Author.name: + AppBundle\Author.name: This value should not be blank If you insert a value into the ``name`` property, the happy success message @@ -161,12 +163,10 @@ will appear. you'll use validation indirectly when handling submitted form data. For more information, see the :ref:`book-validation-forms`. -You could also pass the collection of errors into a template. - -.. code-block:: php +You could also pass the collection of errors into a template:: if (count($errors) > 0) { - return $this->render('AcmeBlogBundle:Author:validate.html.twig', array( + return $this->render('author/validation.html.twig', array( 'errors' => $errors, )); } @@ -177,7 +177,7 @@ Inside the template, you can output the list of errors exactly as needed: .. code-block:: html+jinja - {# src/Acme/BlogBundle/Resources/views/Author/validate.html.twig #} + {# app/Resources/views/author/validation.html.twig #}

The author has the following errors

    {% for error in errors %} @@ -187,7 +187,7 @@ Inside the template, you can output the list of errors exactly as needed: .. code-block:: html+php - +

    The author has the following errors

      @@ -217,10 +217,11 @@ objects that can easily be displayed with your form. The typical form submission workflow looks like the following from inside a controller:: // ... - use Acme\BlogBundle\Entity\Author; - use Acme\BlogBundle\Form\AuthorType; + use AppBundle\Entity\Author; + use AppBundle\Form\AuthorType; use Symfony\Component\HttpFoundation\Request; + // ... public function updateAction(Request $request) { $author = new Author(); @@ -234,7 +235,7 @@ workflow looks like the following from inside a controller:: return $this->redirect($this->generateUrl(...)); } - return $this->render('BlogBundle:Author:form.html.twig', array( + return $this->render('author/form.html.twig', array( 'form' => $form->createView(), )); } @@ -327,22 +328,16 @@ Constraint Configuration Some constraints, like :doc:`NotBlank `, are simple whereas others, like the :doc:`Choice ` constraint, have several configuration options available. Suppose that the -``Author`` class has another property, ``gender`` that can be set to either +``Author`` class has another property called ``gender`` that can be set to either "male" or "female": .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - gender: - - Choice: { choices: [male, female], message: Choose a valid gender. } - .. code-block:: php-annotations - // src/Acme/BlogBundle/Entity/Author.php + // src/AppBundle/Entity/Author.php + + // ... use Symfony\Component\Validator\Constraints as Assert; class Author @@ -354,17 +349,28 @@ constraint, have several configuration options available. Suppose that the * ) */ public $gender; + + // ... } + .. code-block:: yaml + + # src/AppBundle/Resources/config/validation.yml + AppBundle\Entity\Author: + properties: + gender: + - Choice: { choices: [male, female], message: Choose a valid gender. } + # ... + .. code-block:: xml - + - + + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php + // src/AppBundle/Entity/Author.php // ... use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\Choice; + use Symfony\Component\Validator\Constraints as Assert; class Author { public $gender; + // ... + public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addPropertyConstraint('gender', new Choice(array( + // ... + + $metadata->addPropertyConstraint('gender', new Assert\Choice(array( 'choices' => array('male', 'female'), 'message' => 'Choose a valid gender.', ))); @@ -407,17 +419,9 @@ options can be specified in this way. .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - gender: - - Choice: [male, female] - .. code-block:: php-annotations - // src/Acme/BlogBundle/Entity/Author.php + // src/AppBundle/Entity/Author.php // ... use Symfony\Component\Validator\Constraints as Assert; @@ -428,33 +432,46 @@ options can be specified in this way. * @Assert\Choice({"male", "female"}) */ protected $gender; + + // ... } + .. code-block:: yaml + + # src/AppBundle/Resources/config/validation.yml + AppBundle\Entity\Author: + properties: + gender: + - Choice: [male, female] + # ... + .. code-block:: xml - + - + male female + + .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php + // src/AppBundle/Entity/Author.php // ... use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\Choice; + use Symfony\Component\Validator\Constraints as Assert; class Author { @@ -462,9 +479,11 @@ options can be specified in this way. public static function loadValidatorMetadata(ClassMetadata $metadata) { + // ... + $metadata->addPropertyConstraint( 'gender', - new Choice(array('male', 'female')) + new Assert\Choice(array('male', 'female')) ); } } @@ -509,19 +528,9 @@ class to have at least 3 characters. .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - properties: - firstName: - - NotBlank: ~ - - Length: - min: 3 - .. code-block:: php-annotations - // Acme/BlogBundle/Entity/Author.php + // AppBundle/Entity/Author.php // ... use Symfony\Component\Validator\Constraints as Assert; @@ -530,20 +539,30 @@ class to have at least 3 characters. { /** * @Assert\NotBlank() - * @Assert\Length(min = "3") + * @Assert\Length(min=3) */ private $firstName; } + .. code-block:: yaml + + # src/AppBundle/Resources/config/validation.yml + AppBundle\Entity\Author: + properties: + firstName: + - NotBlank: ~ + - Length: + min: 3 + .. code-block:: xml - + - + @@ -555,12 +574,11 @@ class to have at least 3 characters. .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php + // src/AppBundle/Entity/Author.php // ... use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\NotBlank; - use Symfony\Component\Validator\Constraints\Length; + use Symfony\Component\Validator\Constraints as Assert; class Author { @@ -568,10 +586,11 @@ class to have at least 3 characters. public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addPropertyConstraint('firstName', new NotBlank()); + $metadata->addPropertyConstraint('firstName', new Assert\NotBlank()); $metadata->addPropertyConstraint( 'firstName', - new Length(array("min" => 3))); + new Assert\Length(array("min" => 3)) + ); } } @@ -597,17 +616,9 @@ this method must return ``true``: .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\Author: - getters: - passwordLegal: - - "True": { message: "The password cannot match your first name" } - .. code-block:: php-annotations - // src/Acme/BlogBundle/Entity/Author.php + // src/AppBundle/Entity/Author.php // ... use Symfony\Component\Validator\Constraints as Assert; @@ -619,19 +630,27 @@ this method must return ``true``: */ public function isPasswordLegal() { - // return true or false + // ... return true or false } } + .. code-block:: yaml + + # src/AppBundle/Resources/config/validation.yml + AppBundle\Entity\Author: + getters: + passwordLegal: + - "True": { message: "The password cannot match your first name" } + .. code-block:: xml - + - + @@ -642,27 +661,27 @@ this method must return ``true``: .. code-block:: php - // src/Acme/BlogBundle/Entity/Author.php + // src/AppBundle/Entity/Author.php // ... use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\True; + use Symfony\Component\Validator\Constraints as Assert; class Author { public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addGetterConstraint('passwordLegal', new True(array( + $metadata->addGetterConstraint('passwordLegal', new Assert\True(array( 'message' => 'The password cannot match your first name', ))); } } -Now, create the ``isPasswordLegal()`` method, and include the logic you need:: +Now, create the ``isPasswordLegal()`` method and include the logic you need:: public function isPasswordLegal() { - return $this->firstName != $this->password; + return $this->firstName !== $this->password; } .. note:: @@ -700,24 +719,10 @@ user registers and when a user updates their contact information later: .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\User: - properties: - email: - - Email: { groups: [registration] } - password: - - NotBlank: { groups: [registration] } - - Length: { min: 7, groups: [registration] } - city: - - Length: - min: 2 - .. code-block:: php-annotations - // src/Acme/BlogBundle/Entity/User.php - namespace Acme\BlogBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Validator\Constraints as Assert; @@ -736,20 +741,37 @@ user registers and when a user updates their contact information later: private $password; /** - * @Assert\Length(min = "2") + * @Assert\Length(min=2) */ private $city; } + .. code-block:: yaml + + # src/AppBundle/Resources/config/validation.yml + AppBundle\Entity\User: + properties: + email: + - Email: { groups: [registration] } + password: + - NotBlank: { groups: [registration] } + - Length: { min: 7, groups: [registration] } + city: + - Length: + min: 2 + .. code-block:: xml - + + xsi:schemaLocation=" + http://symfony.com/schema/dic/constraint-mapping + http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd + "> - + + + @@ -780,33 +804,31 @@ user registers and when a user updates their contact information later: .. code-block:: php - // src/Acme/BlogBundle/Entity/User.php - namespace Acme\BlogBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; use Symfony\Component\Validator\Mapping\ClassMetadata; - use Symfony\Component\Validator\Constraints\Email; - use Symfony\Component\Validator\Constraints\NotBlank; - use Symfony\Component\Validator\Constraints\Length; + use Symfony\Component\Validator\Constraints as Assert; class User { public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addPropertyConstraint('email', new Email(array( + $metadata->addPropertyConstraint('email', new Assert\Email(array( 'groups' => array('registration'), ))); - $metadata->addPropertyConstraint('password', new NotBlank(array( + $metadata->addPropertyConstraint('password', new Assert\NotBlank(array( 'groups' => array('registration'), ))); - $metadata->addPropertyConstraint('password', new Length(array( - 'min' => 7, - 'groups' => array('registration') + $metadata->addPropertyConstraint('password', new Assert\Length(array( + 'min' => 7, + 'groups' => array('registration'), ))); - $metadata->addPropertyConstraint( - 'city', - Length(array("min" => 3))); + $metadata->addPropertyConstraint('city', new Assert\Length(array( + "min" => 3, + ))); } } @@ -824,31 +846,34 @@ With this configuration, there are three validation groups: ``registration`` Contains the constraints on the ``email`` and ``password`` fields only. -Constraints in the ``Default`` group of a class are the constraints that have either no -explicit group configured or that are configured to a group equal to the class name or -the string ``Default``. +Constraints in the ``Default`` group of a class are the constraints that have +either no explicit group configured or that are configured to a group equal to +the class name or the string ``Default``. .. caution:: - When validating *just* the User object, there is no difference between the ``Default`` group - and the ``User`` group. But, there is a difference if ``User`` has embedded objects. For example, - imagine ``User`` has an ``address`` property that contains some ``Address`` object and that - you've added the :doc:`/reference/constraints/Valid` constraint to this property so that it's - validated when you validate the ``User`` object. + When validating *just* the User object, there is no difference between the + ``Default`` group and the ``User`` group. But, there is a difference if + ``User`` has embedded objects. For example, imagine ``User`` has an + ``address`` property that contains some ``Address`` object and that you've + added the :doc:`/reference/constraints/Valid` constraint to this property + so that it's validated when you validate the ``User`` object. - If you validate ``User`` using the ``Default`` group, then any constraints on the ``Address`` - class that are in the ``Default`` group *will* be used. But, if you validate ``User`` using the - ``User`` validation group, then only constraints on the ``Address`` class with the ``User`` - group will be validated. + If you validate ``User`` using the ``Default`` group, then any constraints + on the ``Address`` class that are in the ``Default`` group *will* be used. + But, if you validate ``User`` using the ``User`` validation group, then + only constraints on the ``Address`` class with the ``User`` group will be + validated. - In other words, the ``Default`` group and the class name group (e.g. ``User``) are identical, - except when the class is embedded in another object that's actually the one being validated. + In other words, the ``Default`` group and the class name group (e.g. + ``User``) are identical, except when the class is embedded in another + object that's actually the one being validated. If you have inheritance (e.g. ``User extends BaseUser``) and you validate with the class name of the subclass (i.e. ``User``), then all constraints - in the ``User`` and ``BaseUser`` will be validated. However, if you validate - using the base class (i.e. ``BaseUser``), then only the default constraints in - the ``BaseUser`` class will be validated. + in the ``User`` and ``BaseUser`` will be validated. However, if you + validate using the base class (i.e. ``BaseUser``), then only the default + constraints in the ``BaseUser`` class will be validated. To tell the validator to use a specific group, pass one or more group names as the third argument to the ``validate()`` method:: @@ -884,28 +909,10 @@ username and the password are different only if all other validation passes .. configuration-block:: - .. code-block:: yaml - - # src/Acme/BlogBundle/Resources/config/validation.yml - Acme\BlogBundle\Entity\User: - group_sequence: - - User - - Strict - getters: - passwordLegal: - - "True": - message: "The password cannot match your username" - groups: [Strict] - properties: - username: - - NotBlank: ~ - password: - - NotBlank: ~ - .. code-block:: php-annotations - // src/Acme/BlogBundle/Entity/User.php - namespace Acme\BlogBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Validator\Constraints as Assert; @@ -934,21 +941,41 @@ username and the password are different only if all other validation passes } } + .. code-block:: yaml + + # src/AppBundle/Resources/config/validation.yml + AppBundle\Entity\User: + group_sequence: + - User + - Strict + getters: + passwordLegal: + - "True": + message: "The password cannot match your username" + groups: [Strict] + properties: + username: + - NotBlank: ~ + password: + - NotBlank: ~ + .. code-block:: xml - + - + + + @@ -957,6 +984,7 @@ username and the password are different only if all other validation passes + User Strict @@ -966,8 +994,8 @@ username and the password are different only if all other validation passes .. code-block:: php - // src/Acme/BlogBundle/Entity/User.php - namespace Acme\BlogBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints as Assert; @@ -976,22 +1004,13 @@ username and the password are different only if all other validation passes { public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addPropertyConstraint( - 'username', - new Assert\NotBlank() - ); - $metadata->addPropertyConstraint( - 'password', - new Assert\NotBlank() - ); + $metadata->addPropertyConstraint('username', new Assert\NotBlank()); + $metadata->addPropertyConstraint('password', new Assert\NotBlank()); - $metadata->addGetterConstraint( - 'passwordLegal', - new Assert\True(array( - 'message' => 'The password cannot match your first name', - 'groups' => array('Strict'), - )) - ); + $metadata->addGetterConstraint('passwordLegal', new Assert\True(array( + 'message' => 'The password cannot match your first name', + 'groups' => array('Strict'), + ))); $metadata->setGroupSequence(array('User', 'Strict')); } @@ -1026,29 +1045,15 @@ entity and a new constraint group called ``Premium``: .. configuration-block:: - .. code-block:: yaml - - # src/Acme/DemoBundle/Resources/config/validation.yml - Acme\DemoBundle\Entity\User: - properties: - name: - - NotBlank: ~ - creditCard: - - CardScheme: - schemes: [VISA] - groups: [Premium] - .. code-block:: php-annotations - // src/Acme/DemoBundle/Entity/User.php - namespace Acme\DemoBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; use Symfony\Component\Validator\Constraints as Assert; class User { - // ... - /** * @Assert\NotBlank() */ @@ -1061,17 +1066,31 @@ entity and a new constraint group called ``Premium``: * ) */ private $creditCard; + + // ... } + .. code-block:: yaml + + # src/AppBundle/Resources/config/validation.yml + AppBundle\Entity\User: + properties: + name: + - NotBlank: ~ + creditCard: + - CardScheme: + schemes: [VISA] + groups: [Premium] + .. code-block:: xml - + - + @@ -1086,13 +1105,15 @@ entity and a new constraint group called ``Premium``: + + .. code-block:: php - // src/Acme/DemoBundle/Entity/User.php - namespace Acme\DemoBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; @@ -1118,10 +1139,10 @@ Now, change the ``User`` class to implement :class:`Symfony\\Component\\Validator\\GroupSequenceProviderInterface` and add the :method:`Symfony\\Component\\Validator\\GroupSequenceProviderInterface::getGroupSequence`, -which should return an array of groups to use:: +method, which should return an array of groups to use:: - // src/Acme/DemoBundle/Entity/User.php - namespace Acme\DemoBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; // ... use Symfony\Component\Validator\GroupSequenceProviderInterface; @@ -1147,16 +1168,10 @@ provides a sequence of groups to be validated: .. configuration-block:: - .. code-block:: yaml - - # src/Acme/DemoBundle/Resources/config/validation.yml - Acme\DemoBundle\Entity\User: - group_sequence_provider: true - .. code-block:: php-annotations - // src/Acme/DemoBundle/Entity/User.php - namespace Acme\DemoBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; // ... @@ -1168,16 +1183,22 @@ provides a sequence of groups to be validated: // ... } + .. code-block:: yaml + + # src/AppBundle/Resources/config/validation.yml + AppBundle\Entity\User: + group_sequence_provider: true + .. code-block:: xml - + - + @@ -1185,8 +1206,8 @@ provides a sequence of groups to be validated: .. code-block:: php - // src/Acme/DemoBundle/Entity/User.php - namespace Acme\DemoBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; // ... use Symfony\Component\Validator\Mapping\ClassMetadata; @@ -1212,12 +1233,13 @@ 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:: - use Symfony\Component\Validator\Constraints\Email; // ... + use Symfony\Component\Validator\Constraints as Assert; + // ... public function addEmailAction($email) { - $emailConstraint = new Email(); + $emailConstraint = new Assert\Email(); // all constraint "options" can be set this way $emailConstraint->message = 'Invalid email address'; @@ -1236,8 +1258,8 @@ it looks like this:: ); */ - if (count($errorList) == 0) { - // this IS a valid email address, do something + if (0 === count($errorList)) { + // ... this IS a valid email address, do something } else { // this is *not* a valid email address $errorMessage = $errorList[0]->getMessage(); diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index eeedf1e00dd..1c95b1ea457 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -11,6 +11,7 @@ reStructuredText reStructuredText is a plaintext markup syntax similar to Markdown, but much stricter with its syntax. If you are new to reStructuredText, take some time to familiarize with this format by reading the existing `Symfony documentation`_ +source code. If you want to learn more about this format, check out the `reStructuredText Primer`_ tutorial and the `reStructuredText Reference`_. @@ -182,7 +183,7 @@ Symfony, you should precede your description of the change with a You can also ask a question and hide the response. This is particularly [...] If you're documenting a behavior change, it may be helpful to *briefly* describe -how the behavior has changed. +how the behavior has changed: .. code-block:: rst diff --git a/contributing/documentation/overview.rst b/contributing/documentation/overview.rst index e610df1b994..f7cd0608926 100644 --- a/contributing/documentation/overview.rst +++ b/contributing/documentation/overview.rst @@ -79,19 +79,19 @@ even remove any content, but make sure that you comply with the **Step 7.** Everything is now ready to initiate a **pull request**. Go to your forked repository at ``https//github.com//symfony-docs`` -and click on the ``Pull Requests`` link located in the sidebar. +and click on the **Pull Requests** link located in the sidebar. -Then, click on the big ``New pull request`` button. As GitHub cannot guess the +Then, click on the big **New pull request** button. As GitHub cannot guess the exact changes that you want to propose, select the appropriate branches where -changes should be applied:ยบ +changes should be applied: .. image:: /images/contributing/docs-pull-request-change-base.png :align: center -In this example, the **base repository** should be ``symfony/symfony-docs`` and -the **base branch** should be the ``2.3``, which is the branch that you selected -to base your changes on. The **compare repository** should be your forked copy -of ``symfony-docs`` and the **compare branch** should be ``improve_install_chapter``, +In this example, the **base fork** should be ``symfony/symfony-docs`` and +the **base** branch should be the ``2.3``, which is the branch that you selected +to base your changes on. The **head fork** should be your forked copy +of ``symfony-docs`` and the **compare** branch should be ``improve_install_chapter``, which is the name of the branch you created and where you made your changes. .. _pull-request-format: @@ -188,7 +188,7 @@ section: # submit the changes to your forked repository $ git add xxx.rst # (optional) only if this is a new content $ git commit xxx.rst - $ git push + $ git push origin my_changes # go to GitHub and create the Pull Request # @@ -230,7 +230,7 @@ steps to contribute to the Symfony documentation, which you can use as a # add and commit your changes $ git add xxx.rst # (optional) only if this is a new content $ git commit xxx.rst - $ git push + $ git push origin my_changes # go to GitHub and create the Pull Request # @@ -248,6 +248,21 @@ steps to contribute to the Symfony documentation, which you can use as a You guessed right: after all this hard work, it's **time to celebrate again!** +Minor Changes (e.g. Typos) +-------------------------- + +You may find just a typo and want to fix it. Due to GitHub's functional +frontend, it is quite simple to create Pull Requests right in your +browser while reading the docs on symfony.com. To do this, just click +the **edit this page** button on the upper right corner. Beforehand, +please switch to the right branch as mentioned before. Now you are able +to edit the content and describe your changes within the GitHub +frontend. When your work is done, click **Propose file change** to +create a commit and, in case it is your first contribution, also your +fork. A new branch is created automatically in order to provide a base +for your Pull Request. Then fill out the form to create the Pull Request +as described above. + Frequently Asked Questions -------------------------- diff --git a/contributing/documentation/standards.rst b/contributing/documentation/standards.rst index 39d073ec0da..2a1b10e28f5 100644 --- a/contributing/documentation/standards.rst +++ b/contributing/documentation/standards.rst @@ -58,8 +58,10 @@ Code Examples * When you fold a part of a line, e.g. a variable value, put ``...`` (without comment) at the place of the fold; * Description of the folded code: (optional) - If you fold several lines: the description of the fold can be placed after the ``...`` - If you fold only part of a line: the description can be placed before the line; + + * If you fold several lines: the description of the fold can be placed after the ``...``; + * If you fold only part of a line: the description can be placed before the line; + * If useful to the reader, a PHP code example should start with the namespace declaration; * When referencing classes, be sure to show the ``use`` statements at the @@ -77,8 +79,9 @@ Configuration examples should show all supported formats using :ref:`configuration blocks `. The supported formats (and their orders) are: -* **Configuration** (including services and routing): YAML, XML, PHP -* **Validation**: YAML, Annotations, XML, PHP +* **Configuration** (including services): YAML, XML, PHP +* **Routing**: Annotations, YAML, XML, PHP +* **Validation**: Annotations, YAML, XML, PHP * **Doctrine Mapping**: Annotations, YAML, XML, PHP * **Translation**: XML, YAML, PHP @@ -151,6 +154,7 @@ English Language Standards * **Gender-neutral language**: when referencing a hypothetical person, such as *"a user with a session cookie"*, use gender-neutral pronouns (they/their/them). For example, instead of: + * he or she, use they * him or her, use them * his or her, use their diff --git a/contributing/documentation/translations.rst b/contributing/documentation/translations.rst index 54c5002ff54..f6951d228c4 100644 --- a/contributing/documentation/translations.rst +++ b/contributing/documentation/translations.rst @@ -6,7 +6,7 @@ in the translation process. .. note:: - Symfony Project officially discourages starting new translations for the + Symfony Project officially discourages from starting new translations for the documentation. As a matter of fact, there is `an ongoing discussion`_ in the community about the benefits and drawbacks of community driven translations. diff --git a/cookbook/form/form_collections.rst b/cookbook/form/form_collections.rst index 638a1dc2f49..8ff4b3422eb 100644 --- a/cookbook/form/form_collections.rst +++ b/cookbook/form/form_collections.rst @@ -246,7 +246,7 @@ great, your user can't actually add any new tags yet. .. caution:: In this entry, you embed only one collection, but you are not limited - to this. You can also embed nested collection as many level down as you + to this. You can also embed nested collection as many levels 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 @@ -459,7 +459,7 @@ is added to the ``Task`` class by calling the ``addTag`` method. Before this change, they were added internally by the form by calling ``$task->getTags()->add($tag)``. That was just fine, but forcing the use of the "adder" method makes handling these new ``Tag`` objects easier (especially if you're using Doctrine, which -we talk about next!). +you will learn about next!). .. caution:: diff --git a/cookbook/logging/monolog.rst b/cookbook/logging/monolog.rst index 4f060e5988f..3a1f93c3f5c 100644 --- a/cookbook/logging/monolog.rst +++ b/cookbook/logging/monolog.rst @@ -228,8 +228,14 @@ Monolog allows you 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. +.. tip:: + Beware that log file sizes can grow very rapidly, leading to disk space exhaustion. + This is specially true in the ``dev`` environment, where a simple request can + generate hundreds of log lines. Consider using tools like the `logrotate`_ + Linux command to rotate log files before they become a problem. + +A processor is simply a callable receiving the record as its first argument. Processors are configured using the ``monolog.processor`` DIC tag. See the :ref:`reference about it `. @@ -474,3 +480,4 @@ the ``monolog.processor`` tag: .. _Monolog: https://github.com/Seldaek/monolog .. _LoggerInterface: https://github.com/php-fig/log/blob/master/Psr/Log/LoggerInterface.php +.. _`logrotate`: https://fedorahosted.org/logrotate/ diff --git a/cookbook/session/locale_sticky_session.rst b/cookbook/session/locale_sticky_session.rst index 2aa61eed497..3946ed85a87 100644 --- a/cookbook/session/locale_sticky_session.rst +++ b/cookbook/session/locale_sticky_session.rst @@ -106,3 +106,110 @@ method:: { $locale = $request->getLocale(); } + +Setting the Locale Based on the User's Preferences +-------------------------------------------------- + +You might want to improve this technique even further and define the locale based on +the user entity of the logged in user. However, since the ``LocaleListener`` is called +before the ``FirewallListener``, which is responsible for handling authentication and +setting the user token on the ``TokenStorage``, you have no access to the user +which is logged in. + +Let's pretend you have defined a ``locale"`` property on your ``User`` entity +and you want to use this as the locale for the given user. To accomplish this, +you can hook into the login process and update the user's session with the +this locale value before they are redirected to their first page. + +To do this, you need an event listener for the ``security.interactive_login`` +event: + +.. code-block:: php + + // src/AppBundle/EventListener/UserLocaleListener.php + namespace AppBundle\EventListener; + + use Symfony\Component\HttpFoundation\Session\Session; + use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; + + /** + * Stores the locale of the user in the session after the + * login. This can be used by the LocaleListener afterwards. + */ + class UserLocaleListener + { + /** + * @var Session + */ + private $session; + + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * @param InteractiveLoginEvent $event + */ + public function onInteractiveLogin(InteractiveLoginEvent $event) + { + $user = $event->getAuthenticationToken()->getUser(); + + if (null !== $user->getLocale()) { + $this->session->set('_locale', $user->getLocale()); + } + } + } + +Then register the listener: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + app.user_locale_listener: + class: AppBundle\EventListener\UserLocaleListener + arguments: [@session] + tags: + - { name: kernel.event_listener, event: security.interactive_login, method: onInteractiveLogin } + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/services.php + $container + ->register('app.user_locale_listener', 'AppBundle\EventListener\UserLocaleListener') + ->addArgument('session') + ->addTag( + 'kernel.event_listener', + array('event' => 'security.interactive_login', 'method' => 'onInteractiveLogin' + ); + +.. caution:: + + In order to update the language immediately after a user has changed + their language preferences, you need to update the session after an update + to the ``User`` entity. diff --git a/quick_tour/the_architecture.rst b/quick_tour/the_architecture.rst index 536210c6529..db52f13dbd2 100644 --- a/quick_tour/the_architecture.rst +++ b/quick_tour/the_architecture.rst @@ -145,7 +145,7 @@ PHP. Have a look at this sample of the default Symfony configuration: framework: #esi: ~ - #translator: { fallback: "%locale%" } + #translator: { fallbacks: ["%locale%"] } secret: "%secret%" router: resource: "%kernel.root_dir%/config/routing.yml" diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index c4a9db9f76f..92a8cf7c1eb 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -51,7 +51,7 @@ Configuration * :ref:`enabled ` * `translator`_ * :ref:`enabled ` - * `fallback`_ + * `fallbacks`_ * `logging`_ * `property_accessor`_ * `magic_call`_ @@ -577,10 +577,19 @@ enabled Whether or not to enable the ``translator`` service in the service container. -fallback -........ +.. _fallback: -**type**: ``string`` **default**: ``en`` +fallbacks +......... + +**type**: ``string|array`` **default**: ``array('en')`` + +.. versionadded:: 2.3.25 + The ``fallbacks`` option was introduced in Symfony 2.3.25. Prior + to Symfony 2.3.25, it was called ``fallback`` and only allowed one fallback + language defined as a string. + Please note that you can still use the old ``fallback`` option if you want + define only one fallback. This option is used when the translation key for the current locale wasn't found. @@ -812,7 +821,7 @@ Full default Configuration # translator configuration translator: enabled: false - fallback: en + fallbacks: [en] logging: "%kernel.debug%" # validation configuration