diff --git a/best_practices/forms.rst b/best_practices/forms.rst index 86b57e633d6..e942a40854c 100644 --- a/best_practices/forms.rst +++ b/best_practices/forms.rst @@ -22,6 +22,9 @@ form in its own PHP class:: use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; + use Symfony\Component\Form\Extension\Core\Type\TextareaType; + use Symfony\Component\Form\Extension\Core\Type\EmailType; + use Symfony\Component\Form\Extension\Core\Type\DateTimeType; class PostType extends AbstractType { @@ -29,10 +32,10 @@ form in its own PHP class:: { $builder ->add('title') - ->add('summary', 'textarea') - ->add('content', 'textarea') - ->add('authorEmail', 'email') - ->add('publishedAt', 'datetime') + ->add('summary', TextareaType::class) + ->add('content', TextareaType::class) + ->add('authorEmail', EmailType::class) + ->add('publishedAt', DateTimeType::class) ; } @@ -42,14 +45,9 @@ form in its own PHP class:: 'data_class' => 'AppBundle\Entity\Post' )); } - - public function getName() - { - return 'post'; - } } -To use the class, use ``createForm`` and instantiate the new class:: +To use the class, use ``createForm`` and pass the fully qualified class name:: use AppBundle\Form\PostType; // ... @@ -57,7 +55,7 @@ To use the class, use ``createForm`` and instantiate the new class:: public function newAction(Request $request) { $post = new Post(); - $form = $this->createForm(new PostType(), $post); + $form = $this->createForm(PostType::class, $post); // ... } @@ -97,7 +95,7 @@ directly in your form class, this would effectively limit the scope of that form { $builder // ... - ->add('save', 'submit', array('label' => 'Create Post')) + ->add('save', SubmitType::class, array('label' => 'Create Post')) ; } @@ -112,6 +110,7 @@ some developers configure form buttons in the controller:: use Symfony\Component\HttpFoundation\Request; use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Component\Form\Extension\Core\Type\SubmitType; use AppBundle\Entity\Post; use AppBundle\Form\PostType; @@ -122,8 +121,8 @@ some developers configure form buttons in the controller:: public function newAction(Request $request) { $post = new Post(); - $form = $this->createForm(new PostType(), $post); - $form->add('submit', 'submit', array( + $form = $this->createForm(PostType::class, $post); + $form->add('submit', SubmitType::class, array( 'label' => 'Create', 'attr' => array('class' => 'btn btn-default pull-right') )); @@ -207,21 +206,3 @@ Second, we recommend using ``$form->isSubmitted()`` in the ``if`` statement for clarity. This isn't technically needed, since ``isValid()`` first calls ``isSubmitted()``. But without this, the flow doesn't read well as it *looks* like the form is *always* processed (even on the GET request). - -Custom Form Field Types ------------------------ - -.. best-practice:: - - Add the ``app_`` prefix to your custom form field types to avoid collisions. - -Custom form field types inherit from the ``AbstractType`` class, which defines the -``getName()`` method to configure the name of that form type. These names must -be unique in the application. - -If a custom form type uses the same name as any of the Symfony's built-in form -types, it will override it. The same happens when the custom form type matches -any of the types defined by the third-party bundles installed in your application. - -Add the ``app_`` prefix to your custom form field types to avoid name collisions -that can lead to hard to debug errors. diff --git a/best_practices/security.rst b/best_practices/security.rst index 207499aea07..6b25a7f54b5 100644 --- a/best_practices/security.rst +++ b/best_practices/security.rst @@ -264,37 +264,66 @@ the same ``getAuthorEmail`` logic you used above: namespace AppBundle\Security; - use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; + use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\User\UserInterface; + use AppBundle\Entity\Post; - // AbstractVoter class requires Symfony 2.6 or higher version - class PostVoter extends AbstractVoter + // Voter class requires Symfony 2.8 or higher version + class PostVoter extends Voter { const CREATE = 'create'; const EDIT = 'edit'; - protected function getSupportedAttributes() + /** + * @var AccessDecisionManagerInterface + */ + private $decisionManager; + + public function __construct(AccessDecisionManagerInterface $decisionManager) { - return array(self::CREATE, self::EDIT); + $this->decisionManager = $decisionManager; } - protected function getSupportedClasses() + protected function supports($attribute, $subject) { - return array('AppBundle\Entity\Post'); + if (!in_array($attribute, array(self::CREATE, self::EDIT))) { + return false; + } + + if (!$subject instanceof Post) { + return false; + } + + return true; } - protected function isGranted($attribute, $post, $user = null) + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) { + $user = $token->getUser(); + /** @var Post */ + $post = $subject; // $subject must be a Post instance, thanks to the supports method + if (!$user instanceof UserInterface) { return false; } - if ($attribute === self::CREATE && in_array('ROLE_ADMIN', $user->getRoles(), true)) { - return true; - } - - if ($attribute === self::EDIT && $user->getEmail() === $post->getAuthorEmail()) { - return true; + switch ($attribute) { + case self::CREATE: + // if the user is an admin, allow them to create new posts + if ($this->decisionManager->decide($token, array('ROLE_ADMIN'))) { + return true; + } + + break; + case self::EDIT: + // if the user is the author of the post, allow them to edit the posts + if ($user->getEmail() === $post->getAuthorEmail()) { + return true; + } + + break; } return false; @@ -310,6 +339,7 @@ To enable the security voter in the application, define a new service: # ... post_voter: class: AppBundle\Security\PostVoter + arguments: ['@security.access.decision_manager'] public: false tags: - { name: security.voter } @@ -337,7 +367,7 @@ via the even easier shortcut in a controller: */ public function editAction($id) { - $post = // query for the post ... + $post = ...; // query for the post $this->denyAccessUnlessGranted('edit', $post); diff --git a/book/forms.rst b/book/forms.rst index 63c0d37651e..599ef7dae91 100644 --- a/book/forms.rst +++ b/book/forms.rst @@ -80,6 +80,9 @@ from inside a controller:: use AppBundle\Entity\Task; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Form\Extension\Core\Type\TextType; + use Symfony\Component\Form\Extension\Core\Type\DateType; + use Symfony\Component\Form\Extension\Core\Type\SubmitType; class DefaultController extends Controller { @@ -91,9 +94,11 @@ from inside a controller:: $task->setDueDate(new \DateTime('tomorrow')); $form = $this->createFormBuilder($task) - ->add('task', 'text') - ->add('dueDate', 'date') - ->add('save', 'submit', array('label' => 'Create Task')) + ->add('task', TextType::class) + // If you use PHP 5.3 or 5.4 you must use + // ->add('task', 'Symfony\Component\Form\Extension\Core\Type\TextType') + ->add('dueDate', DateType::class) + ->add('save', SubmitType::class, array('label' => 'Create Task')) ->getForm(); return $this->render('default/new.html.twig', array( @@ -116,8 +121,16 @@ building the form. In this example, you've added two fields to your form - ``task`` and ``dueDate`` - corresponding to the ``task`` and ``dueDate`` properties of the ``Task`` class. -You've also assigned each a "type" (e.g. ``text``, ``date``), which, among -other things, determines which HTML form tag(s) is rendered for that field. +You've also assigned each a "type" (e.g. ``TextType`` and ``DateType``), +represented by its fully qualified class name. Among other things, it determines +which HTML form tag(s) is rendered for that field. + +.. versionadded:: 2.8 + To denote the form type, you have to use the fully qualified class name - like + ``TextType::class`` in PHP 5.5+ or ``Symfony\Component\Form\Extension\Core\Type\TextType``. + Before Symfony 2.8, you could use an alias for each type like ``text`` or + ``date``. The old alias syntax will still work until Symfony 3.0. For more details, + see the `2.8 UPGRADE Log`_. Finally, you added a submit button with a custom label for submitting the form to the server. @@ -224,9 +237,9 @@ controller:: $task = new Task(); $form = $this->createFormBuilder($task) - ->add('task', 'text') - ->add('dueDate', 'date') - ->add('save', 'submit', array('label' => 'Create Task')) + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->add('save', SubmitType::class, array('label' => 'Create Task')) ->getForm(); $form->handleRequest($request); @@ -241,7 +254,7 @@ controller:: 'form' => $form->createView(), )); } - + .. caution:: Be aware that the ``createView()`` method should be called *after* ``handleRequest`` @@ -310,10 +323,10 @@ which of the buttons was clicked to adapt the program flow in your controller. To do this, add a second button with the caption "Save and add" to your form:: $form = $this->createFormBuilder($task) - ->add('task', 'text') - ->add('dueDate', 'date') - ->add('save', 'submit', array('label' => 'Create Task')) - ->add('saveAndAdd', 'submit', array('label' => 'Save and Add')) + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->add('save', SubmitType::class, array('label' => 'Create Task')) + ->add('saveAndAdd', SubmitType::class, array('label' => 'Save and Add')) ->getForm(); In your controller, use the button's @@ -622,8 +635,8 @@ First, we need to add the two buttons to the form:: $form = $this->createFormBuilder($task) // ... - ->add('nextStep', 'submit') - ->add('previousStep', 'submit') + ->add('nextStep', SubmitType::class) + ->add('previousStep', SubmitType::class) ->getForm(); Then, we configure the button for returning to the previous step to run @@ -632,7 +645,7 @@ so we set its ``validation_groups`` option to false:: $form = $this->createFormBuilder($task) // ... - ->add('previousStep', 'submit', array( + ->add('previousStep', SubmitType::class, array( 'validation_groups' => false, )) ->getForm(); @@ -669,7 +682,7 @@ boxes. However, the :doc:`date field ` can be configured to be rendered as a single text box (where the user would enter the date as a string in the box):: - ->add('dueDate', 'date', array('widget' => 'single_text')) + ->add('dueDate', DateType::class, array('widget' => 'single_text')) .. image:: /images/book/form-simple2.png :align: center @@ -701,7 +714,7 @@ the documentation for each type. The label for the form field can be set using the ``label`` option, which can be applied to any field:: - ->add('dueDate', 'date', array( + ->add('dueDate', DateType::class, array( 'widget' => 'single_text', 'label' => 'Due Date', )) @@ -722,7 +735,7 @@ Now that you've added validation metadata to the ``Task`` class, Symfony already knows a bit about your fields. If you allow it, Symfony can "guess" the type of your field and set it up for you. In this example, Symfony can guess from the validation rules that both the ``task`` field is a normal -``text`` field and the ``dueDate`` field is a ``date`` field:: +``TextType`` field and the ``dueDate`` field is a ``DateType`` field:: public function newAction() { @@ -731,7 +744,7 @@ guess from the validation rules that both the ``task`` field is a normal $form = $this->createFormBuilder($task) ->add('task') ->add('dueDate', null, array('widget' => 'single_text')) - ->add('save', 'submit') + ->add('save', SubmitType::class) ->getForm(); } @@ -992,9 +1005,9 @@ ways. If you build your form in the controller, you can use ``setAction()`` and $form = $this->createFormBuilder($task) ->setAction($this->generateUrl('target_route')) ->setMethod('GET') - ->add('task', 'text') - ->add('dueDate', 'date') - ->add('save', 'submit') + ->add('task', TextType::class) + ->add('dueDate', DateType::class) + ->add('save', SubmitType::class) ->getForm(); .. note:: @@ -1006,7 +1019,10 @@ In :ref:`book-form-creating-form-classes` you will learn how to move the form building code into separate classes. When using an external form class in the controller, you can pass the action and method as form options:: - $form = $this->createForm(new TaskType(), $task, array( + use AppBundle\Form\Type\TaskType; + // ... + + $form = $this->createForm(TaskType::class, $task, array( 'action' => $this->generateUrl('target_route'), 'method' => 'GET', )); @@ -1025,7 +1041,9 @@ to the ``form()`` or the ``form_start()`` helper: start($form, array( - 'action' => $view['router']->generate('target_route'), + // The path() method was introduced in Symfony 2.8. Prior to 2.8, + // you had to use generate(). + 'action' => $view['router']->path('target_route'), 'method' => 'GET', )) ?> @@ -1056,6 +1074,7 @@ that will house the logic for building the task form:: use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\Form\Extension\Core\Type\SubmitType; class TaskType extends AbstractType { @@ -1064,36 +1083,21 @@ that will house the logic for building the task form:: $builder ->add('task') ->add('dueDate', null, array('widget' => 'single_text')) - ->add('save', 'submit') + ->add('save', SubmitType::class) ; } - - public function getName() - { - return 'task'; - } } -.. caution:: - - The ``getName()`` method returns the identifier of this form "type". These - identifiers must be unique in the application. Unless you want to override - a built-in type, they should be different from the default Symfony types - and from any type defined by a third-party bundle installed in your application. - Consider prefixing your types with ``app_`` to avoid identifier collisions. - This new class contains all the directions needed to create the task form. It can be used to quickly build a form object in the controller:: // src/AppBundle/Controller/DefaultController.php - - // add this new use statement at the top of the class use AppBundle\Form\Type\TaskType; public function newAction() { $task = ...; - $form = $this->createForm(new TaskType(), $task); + $form = $this->createForm(TaskType::class, $task); // ... } @@ -1140,7 +1144,7 @@ the choice is ultimately up to you. $builder ->add('task') ->add('dueDate', null, array('mapped' => false)) - ->add('save', 'submit') + ->add('save', SubmitType::class) ; } @@ -1160,8 +1164,8 @@ the choice is ultimately up to you. Defining your Forms as Services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Defining your form type as a service is a good practice and makes it really -easy to use in your application. +Your form type might have some external dependencies. You can define your form +type as a service, and inject inject all dependencies you need. .. note:: @@ -1169,6 +1173,39 @@ easy to use in your application. :doc:`later on in this book `. Things will be more clear after reading that chapter. +You might want to use a service defined as ``app.my_service`` in your form +type. Create a constructor to your form type to receive the service:: + + // src/AppBundle/Form/Type/TaskType.php + namespace AppBundle\Form\Type; + + use App\Utility\MyService; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\Form\Extension\Core\Type\SubmitType; + + class TaskType extends AbstractType + { + private $myService; + + public function __construct(MyService $mySevice) + { + $this->myService = $myService; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + // You can now use myService. + $builder + ->add('task') + ->add('dueDate', null, array('widget' => 'single_text')) + ->add('save', SubmitType::class) + ; + } + } + +Define your form type as a service. + .. configuration-block:: .. code-block:: yaml @@ -1177,8 +1214,9 @@ easy to use in your application. services: app.form.type.task: class: AppBundle\Form\Type\TaskType + arguments: ["@app.my_service"] tags: - - { name: form.type, alias: task } + - { name: form.type } .. code-block:: xml @@ -1190,7 +1228,8 @@ easy to use in your application. - + + @@ -1198,43 +1237,11 @@ easy to use in your application. .. code-block:: php // src/AppBundle/Resources/config/services.php - $container - ->register( - 'app.form.type.task', - 'AppBundle\Form\Type\TaskType' - ) - ->addTag('form.type', array( - 'alias' => 'task', - )) - ; - -That's it! Now you can use your form type directly in a controller:: + use ; - // src/AppBundle/Controller/DefaultController.php - // ... - - public function newAction() - { - $task = ...; - $form = $this->createForm('task', $task); - - // ... - } - -or even use from within the form type of another form:: - - // src/AppBundle/Form/Type/ListType.php - // ... - - class ListType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - // ... - - $builder->add('someTask', 'task'); - } - } + $container->register('app.form.type.task', 'AppBundle\Form\Type\TaskType') + ->addArgument(new Reference('app.my_service')) + ->addTag('form.type') Read :ref:`form-cookbook-form-field-service` for more information. @@ -1360,11 +1367,6 @@ create a form class so that a ``Category`` object can be modified by the user:: 'data_class' => 'AppBundle\Entity\Category', )); } - - public function getName() - { - return 'category'; - } } The end goal is to allow the ``Category`` of a ``Task`` to be modified right @@ -1375,12 +1377,13 @@ class: .. code-block:: php use Symfony\Component\Form\FormBuilderInterface; + use AppBundle\Form\Type\CategoryType; public function buildForm(FormBuilderInterface $builder, array $options) { // ... - $builder->add('category', new CategoryType()); + $builder->add('category', CategoryType::class); } The fields from ``CategoryType`` can now be rendered alongside those from @@ -1851,10 +1854,10 @@ an array of the submitted data. This is actually really easy:: { $defaultData = array('message' => 'Type your message here'); $form = $this->createFormBuilder($defaultData) - ->add('name', 'text') - ->add('email', 'email') - ->add('message', 'textarea') - ->add('send', 'submit') + ->add('name', TextType::class) + ->add('email', EmailType::class) + ->add('message', TextareaType::class) + ->add('send', SubmitType::class) ->getForm(); $form->handleRequest($request); @@ -1915,12 +1918,13 @@ but here's a short example: use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; + use Symfony\Component\Form\Extension\Core\Type\TextType; $builder - ->add('firstName', 'text', array( + ->add('firstName', TextType::class, array( 'constraints' => new Length(array('min' => 3)), )) - ->add('lastName', 'text', array( + ->add('lastName', TextType::class, array( 'constraints' => array( new NotBlank(), new Length(array('min' => 3)), @@ -1974,3 +1978,4 @@ Learn more from the Cookbook .. _`form_div_layout.html.twig`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig .. _`Cross-site request forgery`: http://en.wikipedia.org/wiki/Cross-site_request_forgery .. _`view on GitHub`: https://github.com/symfony/symfony/tree/master/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form +.. _`2.8 UPGRADE Log`: https://github.com/symfony/symfony/blob/2.8/UPGRADE-2.8.md#form \ No newline at end of file diff --git a/book/from_flat_php_to_symfony2.rst b/book/from_flat_php_to_symfony2.rst index d243b390434..5420a31ea1b 100644 --- a/book/from_flat_php_to_symfony2.rst +++ b/book/from_flat_php_to_symfony2.rst @@ -598,7 +598,7 @@ database and the Templating component to render a template and return a