From c49f75d22437ef201b0870cd4c0039950e614f88 Mon Sep 17 00:00:00 2001 From: WouterJ Date: Mon, 5 Jan 2015 23:39:13 +0100 Subject: [PATCH 1/5] Made validation chapter best-practices-compatible --- book/validation.rst | 462 +++++++++++++++++++++++--------------------- 1 file changed, 242 insertions(+), 220 deletions(-) diff --git a/book/validation.rst b/book/validation.rst index b365a84b057..656a2600978 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)) + ); } } @@ -594,17 +613,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; @@ -616,19 +627,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 - + - + @@ -639,27 +658,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:: @@ -697,24 +716,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; @@ -733,20 +738,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 + "> - + + + @@ -777,33 +801,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', Assert\Length(array( + "min" => 3, + ))); } } @@ -821,31 +843,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 second argument to the ``validate()`` method:: @@ -877,28 +902,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; @@ -927,21 +934,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 - + - + + + @@ -950,6 +977,7 @@ username and the password are different only if all other validation passes + User Strict @@ -959,8 +987,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; @@ -969,22 +997,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')); } @@ -1019,29 +1038,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() */ @@ -1054,17 +1059,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 - + - + @@ -1079,13 +1098,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; @@ -1111,10 +1132,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; @@ -1140,16 +1161,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; // ... @@ -1161,16 +1176,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 - + - + @@ -1178,8 +1199,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; @@ -1205,12 +1226,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'; @@ -1220,8 +1242,8 @@ it looks like this:: $emailConstraint ); - 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(); From a272f99fa1c2c097cd9fc6a5ba4b79e989a1eb22 Mon Sep 17 00:00:00 2001 From: WouterJ Date: Tue, 6 Jan 2015 00:01:40 +0100 Subject: [PATCH 2/5] Made testing chapter best-practices-compatible and lots of other fixes --- book/testing.rst | 198 ++++++++++++++++++++++++----------------------- 1 file changed, 103 insertions(+), 95 deletions(-) diff --git a/book/testing.rst b/book/testing.rst index 19d6443d83d..e7e0a8c139b 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\Utility; 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 @@ -286,8 +261,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()); @@ -295,7 +272,7 @@ document:: $this->assertTrue($client->getResponse()->isNotFound()); // Assert a specific 200 status code $this->assertEquals( - 200, + 200, // or Symfony\Component\HttpFoundation\Response::HTTP_OK $client->getResponse()->getStatusCode() ); @@ -303,19 +280,19 @@ 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()); .. index:: 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. @@ -326,7 +303,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(); @@ -346,7 +356,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 usefull examples:: // Directly submit a form (but using the Crawler is easier!) $client->request('POST', '/submit', array('name' => 'Fabien')); @@ -377,7 +387,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', @@ -404,7 +414,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 @@ -415,16 +425,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(); @@ -432,13 +442,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 ~~~~~~~~~~~~~~~~~~~~~~~ @@ -626,9 +636,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 @@ -847,6 +855,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` @@ -854,6 +863,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/ From 484076873ee2bf64b743c770259b897d33f56db4 Mon Sep 17 00:00:00 2001 From: WouterJ Date: Tue, 6 Jan 2015 00:14:49 +0100 Subject: [PATCH 3/5] Made propel chapter best-practices-compatible and lots of other fixes --- best_practices/configuration.rst | 2 + book/propel.rst | 193 ++++++++++++++++++------------- 2 files changed, 113 insertions(+), 82 deletions(-) diff --git a/best_practices/configuration.rst b/best_practices/configuration.rst index df9779b121e..649c97a197a 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 ~~~~~~~~~~~~~~~~~~~~ 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 -------- From c6ff013ca4726fea73b77db30df96a87ab950e84 Mon Sep 17 00:00:00 2001 From: WouterJ Date: Tue, 6 Jan 2015 01:28:11 +0100 Subject: [PATCH 4/5] Made http cache chapter best-practices-compatible and lots of other fixes --- book/http_cache.rst | 179 ++++++++++++++++++++++++++++---------------- 1 file changed, 116 insertions(+), 63 deletions(-) diff --git a/book/http_cache.rst b/book/http_cache.rst index 5cae7a710e2..ea6bda84153 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('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 @@ -996,6 +1019,7 @@ First, to use ESI, be sure to enable it in your application configuration: http://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + "> @@ -1017,13 +1041,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('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 +1068,36 @@ matter), Symfony uses the standard ``render`` helper to configure ESI tags: .. code-block:: jinja + {# app/Resources/views/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 +1117,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 +1134,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 From 3ab53a6ecfa816bba1025240a8f4dcd0b3140234 Mon Sep 17 00:00:00 2001 From: WouterJ Date: Sat, 17 Jan 2015 01:23:35 +0100 Subject: [PATCH 5/5] Fixed some of the comments --- book/http_cache.rst | 9 ++++----- book/testing.rst | 2 +- book/validation.rst | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/book/http_cache.rst b/book/http_cache.rst index ea6bda84153..07398463073 100644 --- a/book/http_cache.rst +++ b/book/http_cache.rst @@ -588,7 +588,7 @@ To see a simple implementation, generate the ETag as the md5 of the content:: { public function homepageAction(Request $request) { - $response = $this->render('homepage.html.twig'); + $response = $this->render('static/homepage.html.twig'); $response->setETag(md5($response->getContent())); $response->setPublic(); // make sure the response is public/cacheable $response->isNotModified($request); @@ -1019,7 +1019,6 @@ First, to use ESI, be sure to enable it in your application configuration: http://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - "> @@ -1048,7 +1047,7 @@ independent of the rest of the page. { public function aboutAction() { - $response = $this->render('about.html.twig'); + $response = $this->render('static/about.html.twig'); // set the shared max age - which also marks the response as public $response->setSharedMaxAge(600); @@ -1068,7 +1067,7 @@ matter), Symfony uses the standard ``render`` helper to configure ESI tags: .. code-block:: jinja - {# app/Resources/views/about.html.twig #} + {# app/Resources/views/static/about.html.twig #} {# you can use a controller reference #} {{ render_esi(controller('AppBundle:News:latest', { 'maxPerPage': 5 })) }} @@ -1078,7 +1077,7 @@ matter), Symfony uses the standard ``render`` helper to configure ESI tags: .. code-block:: html+php - + // you can use a controller reference use Symfony\Component\HttpKernel\Controller\ControllerReference; diff --git a/book/testing.rst b/book/testing.rst index e7e0a8c139b..8955ba79301 100644 --- a/book/testing.rst +++ b/book/testing.rst @@ -54,7 +54,7 @@ unit tests. Suppose, for example, that you have an *incredibly* simple class called ``Calculator`` in the ``Util/`` directory of the app bundle:: // src/AppBundle/Util/Calculator.php - namespace AppBundle\Utility; + namespace AppBundle\Util; class Calculator { diff --git a/book/validation.rst b/book/validation.rst index 656a2600978..3eacf0f2c3f 100644 --- a/book/validation.rst +++ b/book/validation.rst @@ -539,7 +539,7 @@ class to have at least 3 characters. { /** * @Assert\NotBlank() - * @Assert\Length(min="3") + * @Assert\Length(min=3) */ private $firstName; } @@ -823,7 +823,7 @@ user registers and when a user updates their contact information later: 'groups' => array('registration'), ))); - $metadata->addPropertyConstraint('city', Assert\Length(array( + $metadata->addPropertyConstraint('city', new Assert\Length(array( "min" => 3, ))); }