Symfony2 - use of Delete form in CRUD operation - php

the adutomatic crud operation generated by symfony and also the symfony demo application has the following code structure for the delete action
/**
* Deletes a testing entity.
*
* #Route("/{id}", name="testing_delete")
* #Method("DELETE")
*/
public function deleteAction(Request $request, testing $testing)
{
$form = $this->createDeleteForm($testing);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->remove($testing);
$em->flush();
}
return $this->redirectToRoute('testing_index');
}
/**
* Creates a form to delete a testing entity.
*
* #param testing $testing The testing entity
*
* #return \Symfony\Component\Form\Form The form
*/
private function createDeleteForm(testing $testing)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('testing_delete', array('id' => $testing->getId())))
->setMethod('DELETE')
->getForm()
;
}
my question is why do we need a form to delete? cant we just have a link in the twig with an id parameter set accordingly, cant we just do the following, why do we need to check if the entity isValid() inside a form before deleteing it?
/**
* test delete
* #Route("/{id}", name="testing_delete")
* #Method("DELETE")
*/
public function deleteAction(testing $testing) {
$em = $this->getDoctrine()->getManager();
$em->remove($testing);
$em->flush();
return $this->redirectToRoute('testing_showall');
}

If you used link for delete with id, it's possible to robot can delete you data with looping.
In Symfony action check "DELETE" method as well as if your crsf token verify with method isValid "$form->isValid()"
That's security reason it's create form and validate

Not using a simple link to delete data denotes to the concept of safe methods in HTTP (if you had just a simple link, you would have to send a GET request to the URL):
Some of the methods (for example, HEAD, GET, OPTIONS and TRACE) are, by convention, defined as safe, which means they are intended only for information retrieval and should not change the state of the server. In other words, they should not have side effects [...]

I think it's important to write a word about CSRF.
By using a Symfony form, it creates a CSRF token that ensure the user who deletes the entity is the same user who wanted it.
If there was no form and only a link /{id}, it would be possible by using a bad link in a mail, or an XSS attack, to make someone else sending the request to delete an entity.
If Bob uses an XSS breach or something else to make Alice (the admin) sending a request for deleting an entity, the request is sent by Alice, event if it's an attack from Bob. So, Bob hasn't the rights for this request but he used the session of Alice, who has the rights. The entity is deleted.
To protect against CSRF attacks, using a CSRF token is really important. Symfony's Form includes it automatically, and check if in isValid().

Related

How to safely use UniqueEntity (on sites with more than one simultaneous user)

Can someone smart can share the design pattern they use to avoid this basic and common concurrency problem in Doctrine\Symfony?
Scenario: Each User must have a unique username.
Failed Solution:
Add a UniqueEntity constraint to the User entity.
Follow the pattern suggested in Symfony's docs: Use the Form component to validate a potential new User. If it's valid, persist it.
Why It Fails: Between validating and persisting the User, the username may be taken by another User. If so, Doctrine throws a UniqueConstraintViolationException when it tries to persist the newest User.
Here is what my following answer does:
It displays errors gracefully to the user if a constraint violation occurs, like if the validator handled it,
It prevents database updates that are not "protected" to break your controller logic (for example with an UPDATE statement or a form submission with "unprotected" controllers),
It is a database-independant solution.
Here is the code, with explanations on comments:
<?php
// ...
use Doctrine\DBAL\Exception\ConstraintViolationException;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
// ...
public function indexAction(Request $request)
{
$task = new Task();
$form = $this->createFormBuilder($task)
->add('name', TextType::class)
->add('save', SubmitType::class, array('label' => 'Create Task'))
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$task = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($task);
try {
$em->flush();
// Everything went well, do whatever you're supposed to.
return $this->redirectToRoute('task_success');
} catch (ConstraintViolationException $e) {
// Reopen the entity manager so the validator can do jobs
// that needs to be performed with the database (in example:
// unique constraint checks)
$em = $em->create($em->getConnection(), $em->getConfiguration());
// Revalidate the form to see if the validator knows what
// has thrown this constraint violation exception.
$violations = $this->get('validator')->validate($form);
if (empty($violations)) {
// The validator didn't see anything wrong...
// It can happens if you have a constraint on your table,
// but didn't add a similar validation constraint.
// Add an error at the root of the form.
$form->add(new FormError('Unexpected error, please retry.'));
} else {
// Add errors to the form with the ViolationMapper.
// The ViolationMapper will links error with its
// corresponding field on the form.
// So errors are not displayed at the root of the form,
// just like if the form was validated natively.
$violationMapper = new ViolationMapper();
foreach ($violations as $violation) {
$violationMapper->mapViolation($violation, $form);
}
}
}
}
return $this->render('default/new.html.twig', array(
'form' => $form->createView(),
));
}
One way to achieve what you want is by locking with the symfony LockHandler.
Here is a simple example, using the pattern you are referring in your question:
<?php
// ...
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Filesystem\LockHandler;
use Symfony\Component\Form\FormError;
public function newAction(Request $request)
{
$task = new Task();
$form = $this->createFormBuilder($task)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class, array('label' => 'Create Task'))
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// locking here
$lock = new LockHandler('task_validator.lock');
$lock->lock();
// since entity is validated when the form is submitted, you
// have to call the validator manually
$validator = $this->get('validator');
if (empty($validator->validate($task))) {
$task = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($task);
$em->flush();
// lock is released by garbage collector
return $this->redirectToRoute('task_success');
}
$form->addError(new FormError('An error occured, please retry'));
// explicit release here to avoid keeping the Lock too much time.
$lock->release();
}
return $this->render('default/new.html.twig', array(
'form' => $form->createView(),
));
}
NB: This won't work if you run your application on multi hosts, from the documentation:
The lock handler only works if you're using just one server. If you have several hosts, you must not use this helper.
You could also override the EntityManager to create a new function like validateAndFlush($entity) that manage the LockHandler and the validation process itself.
Could you not set the unique constraint on database level.
You can also check the Doctrine2 documentation on how to do this:
/**
* #Entity
* #Table(name="user",
* uniqueConstraints={#UniqueConstraint(name="username_unique", columns={"username"})},
* )
*/
class User {
//...
/**
* #var string
* #Column(type="string", name="username", nullable=false)
*/
protected $username;
//...
}
Now you have a unique restriction on database level (so the same username can never be inserted in the user table twice).
When you perform your insert operation you will get an exception thrown in case the username already exists (a UniqueConstraintViolationException). You can catch the exception and return a valid response to the client where you communicate that this username was already used (was in your database).
If I understand the question correctly, you've set a very high bar for yourself. It's clearly impossible for your persistence layer to see the future. Thus, it's impossible to support a validator that will guarantee that the insert will succeed (and not throw a UniqueConstraintViolationException) using only your domain entities. You will need to maintain additional state somewhere.
If you want some incremental improvement, you'll need some way to reserve the username at validation time. That's easy enough, of course -- you just create a list somewhere to track "in-flight" usernames, and check that list in addition to checking your persistence layer during validation.
Where it gets tricky is designing a sane way to prune that list and release usernames that are submitted for validation, but never used in a successful registration.
That's an implementation detail, and you'll need to consider how long a username-reservation sticks around.
A simple implementation off the top of my head: Maintain a table in your database with (username, session_id, reserved_at) and have some process regularly delete all rows where reserved_at < :datetime.
You'll need to track the session_id, since you're reserving the username for a particular user. Since the user hasn't created an account yet, the only way to identify them is via their session identifier.

Symfony CRUD controller - how does it works?

I'm trying to understand how is Symfoy CRUD controllers works, i've googled a lot and can't find any answers.
So the question is, how does controller knows, which entity is passed to route?
For example:
In this index route, we are calling doctrine manager and then pulling all the comments from database.
/**
* Lists all Comment entities.
*
* #Route("/", name="admin_comment_index")
* #Method("GET")
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$comments = $em->getRepository('AppBundle:Comment')->findAll();
return $this->render('comment/index.html.twig', array(
'comments' => $comments,
));
}
but at next "new" action we are not calling any doctrine instances.Controller seems alredy knows which entity is operating.
/**
* Creates a new Comment entity.
*
* #Route("/new", name="admin_comment_new")
* #Method({"GET", "POST"})
*/
public function newAction(Request $request)
{
$comment = new Comment();
$form = $this->createForm('AppBundle\Form\CommentType', $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
return $this->redirectToRoute('admin_comment_show', array('id' => $comment->getId()));
}
return $this->render('comment/new.html.twig', array(
'comment' => $comment,
'form' => $form->createView(),
));
}
I guess it's because second route gets "Request" object, is entity stored in it? I'd like to have some deeper explanation.
UPDATE: "new" action seems clear to me now,it was a bad example of what i'm trying to figure, but let's see the "edit" action:
public function editAction(Request $request, Comment $comment)
{
$deleteForm = $this->createDeleteForm($comment);
$editForm = $this->createForm('AppBundle\Form\CommentType', $comment);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
return $this->redirectToRoute('admin_comment_edit', array('id' => $comment->getId()));
}
return $this->render('comment/edit.html.twig', array(
'comment' => $comment,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
}
This time, form is already rendered with data in it, but we are only passing 'id" in request
edit
From where data comes this time? Seems like from comment object,which is passed into controller, but i dont see where it come from.
Sorry for my noobish questions and bad english!
I think you are making it complicated assuming controller knows what to do.
It's actually what code is written inside a controller defines what controller does. For example :
indexAction supposed to get you list of comments from database, hence you need to get EntityManager first to fetch the data. this controller doesn't deal with form as its not required.
newAction supposed to create a new instance of the Comment entity and generate a form to be filled up by user, when submitted Request parameter catches all data and saves to the database. hence, unless you get data from a submitted form, you don't need entity manager to deal with database.
Also, don't assume these are limited to what controller can do, You can customise any controller as per you requirement.
Hope it make sense.
newAction does get an EntityManager instance inside the if statement.
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
//...
And then it uses this manager to persist the object and flush.
When you load the newAction page, it created a new comment object, and sends that to the formbuilder. After that, it makes sure that the form data is placed onto the new Comment object, allowing it to be persisted.
In newAction function:
First, your form is mapped with your entity :
$comment = new Comment();
$form = $this->createForm('AppBundle\Form\CommentType', $comment);
So Symfony knows that you deal with Comment objects in your form
Then when you submit the form, handleRequest() recognizes this and immediately writes the submitted data back into the Comment object
Finally, if object is valid, you just have to save it in the database thanks to entityManager
So between the request and your formType Symfony knows what he wants
I was wondering the same when I created the first controller via generate:doctrine:crud.
As I'm new to symfony as well, what I state is still based on some assumptions. If I'm wrong, I'm thankful for any correction as it helps me learning.
The Kernel seems to recognises what your controller function accepts based on type hinting. By this, it determines whether your controller function accepts the Request object or not.
Further more, the arguments from the route are resolved and parsed with respect to the hinted types. This should
The magic is then done via Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter Class.
By type hinting your desired object Comment, the id parameter is converted to the associated object and loaded from your database.
By the way, what I couldn't figure out so far: is it possible to use a different order of parameters in my route than in my function?
e.g. is it possible to do
/user/{user_id}/{comment_id}
with
public function funWithUserComment( Request $request, Comment $comment, User $user)
{
//..
}
Hope, it helps you though.

Cakephp 3 - How to retrieve current logged user in a 'Table' class during validation process?

I'am using CakePhp3 for my website and I have to inject some custom validation logic based on the current user Id when I'am creating or modifying an entity.
The basic case is "Is the user allow to change this field to this new value" ? If' not, I want to raise a validation error (or an unauthorized exception).
In cakephp, for what I'am understanding, most of the application and businness rules must be placed on Models or 'ModelsTable'of the ORM. But, in this classes, the AuthComponent or the current session is not available.
I don't want to call manually a method on the entity from the controller each time I need to check. I would like to use a validator, something like :
$validator->add('protected_data', 'valid', [
'rule' => 'canChangeProtectedData',
'message' => __('You're not able to change this data !'),
'provider' => 'table',
]);
Method on ModelTable :
public function canChangeProtectedData($value, array $context)
{
\Cake\Log\Log::debug("canChangeProtectedData");
// Find logged user, look at the new value, check if he is authorized to do that, return true/false
return false;
}
I cakephp < 3, the AuthComponent have a static method 'AuthComponent::user()' that is not available anymore. So, how Can I do that in CakePhp 3 ?
Thank you for any response.
EDIT - Adding more details
So here are more details. In case of an REST API. I have an edit function of an entity. The "Article" Entity.
This Article has an owner with a foreign key on the column named "user_id" (nothing special here). My users are organized in groups with a leader on the group. Leaders of groups can change article's owner but "basics" users can't do it (but they can edit their own articles). Admin users can edit everything.
So the edit method must be available for any authenticated user, but changing the "user_id" of the entity must be allowed and checked depending the case (if I'am admin yes, if I'am leader yes only if the new Id is one of my group and if I'am basic user no).
I can do this check on the controller but if I want this rule to be checked everywhere in my code where an Article is modified (in another method than the "Edit" of ArticlesController). So for me the Model seems the good place to put it no?
Authentication vs Authorisation
Authentication means identifying an user by credentials, which most of the time boils down to "Is a user logged in".
Authorisation means to check if an user is allowed to do a specific action
So don't mix these two.
You don't want validation you want application rules
Taken from the book:
Validation vs. Application Rules
The CakePHP ORM is unique in that it uses a two-layered approach to
validation.
The first layer is validation. Validation rules are intended to
operate in a stateless way. They are best leveraged to ensure that the
shape, data types and format of data is correct.
The second layer is application rules. Application rules are best
leveraged to check stateful properties of your entities. For example,
validation rules could ensure that an email address is valid, while an
application rule could ensure that the email address is unique.
What you want to implement is complex application logic and more than just a simple validation, so the best way to implement this is as an application rule.
I'm taking a code snippet from one of my articles that explains a similar case. I had to check for a limitation of languages (translations) that can be associated to a model. You can read the whole article here http://florian-kraemer.net/2016/08/complex-application-rules-in-cakephp3/
<?php
namespace App\Model\Rule;
use Cake\Datasource\EntityInterface;
use Cake\ORM\TableRegistry;
use RuntimeException;
class ProfileLanguageLimitRule {
/**
* Performs the check
*
* #link http://php.net/manual/en/language.oop5.magic.php
* #param \Cake\Datasource\EntityInterface $entity Entity.
* #param array $options Options.
* #return bool
*/
public function __invoke(EntityInterface $entity, array $options) {
if (!isset($entity->profile_constraint->amount_of_languages)) {
if (!isset($entity->profile_constraint_id)) {
throw new RuntimeException('Profile Constraint ID is missing!');
}
$languageLimit = $this->_getConstraintFromDB($entity);
} else {
$languageLimit = $entity->profile_constraint->amount_of_languages;
}
// Unlimited languages are represented by -1
if ($languageLimit === -1) {
return true;
}
// -1 Here because the language_id of the profiles table already counts as one language
// So it's always -1 of the constraint value
$count = count($entity->languages);
return $count <= ($languageLimit - 1);
}
/**
* Gets the limitation from the ProfileConstraints Table object.
*
* #param \Cake\Datasource\EntityInterface $entity Entity.
* #return int
*/
protected function _getConstraintFromDB(EntityInterface $entity) {
$constraintsTable = TableRegistry::get('ProfileConstraints');
$constraint = $constraintsTable->find()
->where([
'id' => $entity['profile_constraint_id']
])
->select([
'amount_of_languages'
])
->firstOrFail();
return $constraint->amount_of_languages;
}
}
I think it is pretty self-explaining. Make sure your entities user_id field is not accessible for the "public". Before saving the data, just after the patching add it:
$entity->set('user_id', $this->Auth->user('id'));
If you alter the above snippet and change the profile_constraint_id to user_id or whatever else you have there this should do the job for you.
What you really want is row / field level based authorisation
Guess you can use ACL for that, but I've never ever had the need for field based ACL yet. So I can't give you much input on that, but it was (Cake2) and still is (Cake3) possible. For Cake3 the ACL stuff was moved to a plugin. Technically it is possible to check against anything, DB fields, rows, anything.
You could write a behavior that uses the Model.beforeMarshal event and checks if user_id (or role, or whatever) is present and not empty and then run a check on all fields you want for the given user id or user role using ACL.
You could probably use this method PermissionsTable::check() or you can write a more dedicated method does checks on multiple objects (fields) at the same time. Like I said, you'll spend some time to figure the best way out using ACL if you go for it.
UX and yet another cheap solution
First I would not show fields at all an user is not allowed to change or enter as inputs. If you need to show them, fine, disable the form input or just show it as text. Then use a regular set of validation rules that requires the field to be empty (or not present) or empty a list of fields based on your users role. If you don't show the fields the user would have to temper the form and then fail the CSRF check as well (if used).
I don't think you need to validate in the table. I just thought of a way to do it in the controller.
In my Users/Add method in the controller for instance:
public function add()
{
$user = $this->Users->newEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->data);
//check if user is logged in and is a certain user
if ($this->request->session()->read('Auth.User.id') === 1) {
//allow adding/editing role or whatever
$user->role = $this->request->data('role');
} else {
$user->role = 4;//or whatever the correct data is for your problem.
}
if ($this->Users->save($user)) {
$this->Flash->success(__('You have been added.'));
} else {
$this->Flash->error(__('You could not be added. Please, try again.'));
}
}
$this->set(compact('user'));
$this->set('_serialize', ['user']);
}

Forms with Symfony2 - Doctrine Entity with some immutable constructor parameters and OneToMany association

I have a OneToMany association between a Server entity and Client entities in the database. One server can have many clients. I want to make a form where the user can choose a server from a dropdown, fill in some details for a new client, and submit it.
Goal
To create a form where a user can input data into fields for the Client, choose a Server from the dropdown, then click submit and have this data (and the association) persisted via Doctrine.
Simple, right? Hell no. We'll get to that. Here's the pretty form as it stands:
Things of note:
Server is populated from the Server entities (EntityRepository::findAll())
Client is a dropdown with hardcoded values
Port, endpoint, username and password are all text fields
Client Entity
In my infinite wisdom I have declared that my Client entity has the following constructor signature:
class Client
{
/** -- SNIP -- **/
public function __construct($type, $port, $endPoint, $authPassword, $authUsername);
/** -- SNIP -- **/
}
This will not change. To create a valid Client object, the above constructor parameters exist. They are not optional, and this object cannot be created without the above parameters being given upon object instantiation.
Potential Problems:
The type property is immutable. Once you've created a client, you cannot change the type.
I do not have a setter for type. It is a constructor parameter only. This is because once a client is created, you cannot change the type. Therefore I am enforcing this at the entity level. As a result, there is no setType() or changeType() method.
I do not have the standard setObject naming convention. I state that to change the port, for example, the method name is changePort() not setPort(). This is how I require my object API to function, before the use of an ORM.
Server Entity
I'm using __toString() to concatenate the name and ipAddress members to display in the form dropdown:
class Server
{
/** -- SNIP -- **/
public function __toString()
{
return sprintf('%s - %s', $this->name, $this->ipAddress);
}
/** -- SNIP -- **/
}
Custom Form Type
I used Building Forms with Entities as a baseline for my code.
Here is the ClientType I created to build the form for me:
class ClientType extends AbstractType
{
/**
* #var UrlGenerator
*/
protected $urlGenerator;
/**
* #constructor
*
* #param UrlGenerator $urlGenerator
*/
public function __construct(UrlGenerator $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** Dropdown box containing the server name **/
$builder->add('server', 'entity', [
'class' => 'App\Model\Entity\Server',
'query_builder' => function(ServerRepository $serverRepository) {
return $serverRepository->createQueryBuilder('s');
},
'empty_data' => '--- NO SERVERS ---'
]);
/** Dropdown box containing the client names **/
$builder->add('client', 'choice', [
'choices' => [
'transmission' => 'transmission',
'deluge' => 'deluge'
],
'mapped' => false
]);
/** The rest of the form elements **/
$builder->add('port')
->add('authUsername')
->add('authPassword')
->add('endPoint')
->add('addClient', 'submit');
$builder->setAction($this->urlGenerator->generate('admin_servers_add_client'))->setMethod('POST');
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'data_class' => 'App\Model\Entity\Client',
'empty_data' => function(FormInterface $form) {
return new Client(
$form->getData()['client'],
$form->getData()['port'],
$form->getData()['endPoint'],
$form->getData()['authPassword'],
$form->getData()['authUsername']
);
}
]);
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'client';
}
}
The above code is what actually generates the form to be used client-side (via twig).
The Problems
First and foremost, with the above code, submitting the form gives me:
NoSuchPropertyException in PropertyAccessor.php line 456:
Neither the property "port" nor one of the methods "addPort()"/"removePort()", "setPort()", "port()", "__set()" or "__call()" exist and have public access in class "App\Model\Entity\Client".
So it can't find the port method. That's because it's changePort() as I explained earlier. How do I tell it that it should use changePort() instead? According to the docs I would have to use the entity type for port, endPoint etc. But they're just text fields. How do I go about this the right way?
I have tried:
Setting ['mapped' => false] on port, authUsername etc. This gives me null for all the client fields, but it does seem to have the relevant server details with it. Regardless, $form->isValid() return false. Here's what var_dump() shows me:
A combination of other things involving setting each on field to "entity", and more..
Basically, "it's not working". But this is as far as I've got. What am I doing wrong? I am reading the manual over and over but everything is so far apart that I don't know if I should be using a DataTransformer, the Entity Field Type, or otherwise. I'm close to scrapping Symfony/Forms altogether and just writing this myself in a tenth of the time.
Could someone please give me a solid answer on how to get where I want to be? Also this may help future users :-)
There are a few problems with the above solution, so here's how I got it working!
Nulls
It turns out that in setDefaultOptions(), the code: $form->getData['key'] was returning null, hence all the nulls in the screenshot. This needed to be changed to $form->get('key')->getData()
return new Client(
$form->get('client')->getData(),
$form->get('port')->getData(),
$form->get('endPoint')->getData(),
$form->get('authPassword')->getData(),
$form->get('authUsername')->getData()
);
As a result, the data came through as expected, with all the values intact (apart from the id).
Twig Csrf
According to the documentation you can set csrf_protection => false in your form options. If you don't do this, you will need to render the hidden csrf field in your form:
{{ form_rest(form) }}
This renders the rest of the form fields for you, including the hidden _token one:
Symfony2 has a mechanism that helps to prevent cross-site scripting: they generate a CSRF token that have to be used for form validation. Here, in your example, you're not displaying (so not submitting) it with form_rest(form). Basically form_rest(form) will "render" every field that you didn't render before but that is contained into the form object that you've passed to your view. CSRF token is one of those values.
Silex
Here's the error I was getting after solving the above issue:
The CSRF token is invalid. Please try to resubmit the form.
I'm using Silex, and when registering the FormServiceProvider, I had the following:
$app->register(new FormServiceProvider, [
'form.secret' => uniqid(rand(), true)
]);
This Post shows how Silex is giving you some deprecated CsrfProvider code:
Turned out it was not due to my ajax, but because Silex gives you a deprecated DefaultCsrfProvider which uses the session ID itself as part of the token, and I change the ID randomly for security. Instead, explicitly telling it to use the new CsrfTokenManager fixes it, since that one generates a token and stores it in the session, such that the session ID can change without affecting the validity of the token.
As a result, I had to remove the form.secret option and also add the following to my application bootstrap, before registering the form provider:
/** Use a CSRF provider that does not depend on the session ID being constant. We change the session ID randomly */
$app['form.csrf_provider'] = $app->share(function ($app) {
$storage = new Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage($app['session']);
return new Symfony\Component\Security\Csrf\CsrfTokenManager(null, $storage);
});
With the above modifications, the form now posts and the data is persisted in the database correctly, including the doctrine association!

Symfony2 Form pre-fill fields with data

Assume for a moment that this form utilizes an imaginary Animal document object class from a ZooCollection that has only two properties ("name" and "color") in symfony2.
I'm looking for a working simple stupid solution, to pre-fill the form fields with the given object auto-magically (eg. for updates ?).
Acme/DemoBundle/Controller/CustomController:
public function updateAnimalAction(Request $request)
{
...
// Create the form and handle the request
$form = $this->createForm(AnimalType(), $animal);
// Set the data again << doesn't work ?
$form->setData($form->getData());
$form->handleRequest($request);
...
}
You should load the animal object, which you want to update. createForm() will use the loaded object for filling up the field in your form.
Assuming you are using annotations to define your routes:
/**
* #Route("/animal/{animal}")
* #Method("PUT")
*/
public function updateAnimalAction(Request $request, Animal $animal) {
$form = $this->createForm(AnimalType(), $animal, array(
'method' => 'PUT', // You have to specify the method, if you are using PUT
// method otherwise handleRequest() can't
// process your request.
));
$form->handleRequest($request);
if ($form->isValid()) {
...
}
...
}
I think its always a good idea to learn from the code generated by Symfony and doctrine console commands (doctrine:generate:crud). You can learn the idea and the way you should handle this type of requests.
Creating your form using the object is the best approach (see #dtengeri's answer). But you could also use $form->setData() with an associative array, and that sounds like what you were asking for. This is helpful when not using an ORM, or if you just need to change a subset of the form's data.
http://api.symfony.com/2.8/Symfony/Component/Form/Form.html#method_setData
The massive gotcha is that any default values in your form builder will not be overridden by setData(). This is counter-intuitive, but it's how Symfony works. Discussion:
https://github.com/symfony/symfony/issues/7141

Categories