Add error to Symfony2 Form - symfony

I am trying to add error to form using FormError. Error must be displayed when user tries to create collection with existing name. But this code doesn't work, and I can't understand why
public function submitInObjectAction(Request $request)
{
$collection = new Collection();
$user = $this->getUser();
$form = $this->createForm(
new CollectionType(),
$collection
);
$form->handleRequest($request);
if ($form->isValid() && $form->isSubmitted()) {
$colname = $form["name"]->getData();
$existing = $this->getDoctrine()->getRepository('CollectionBundle:Collection')
->findBy(['name' => $colname, 'user' => $user]);
if ($existing != NULL) {
$error = new FormError("You already have collection with such name");
$form->get('name')->addError($error);
}
$em = $this->getDoctrine()->getManager();
$collection->setUser($user);
$em->persist($collection);
$em->flush();
return new JsonResponse([
'id' => $collection->getId(),
'name' => $collection->getName()
]);
}
}
I cannot use annotation on name field in Collection entity, because names must be unique only for particular user

I think it is too late in the chain. Form validation happens when you call $form->handleRequest() and by the time $form->isValid() is called your validation should be complete. It is better to add validation constraints further up the chain. See the Symfony guide on form validation and if necessary the validation component, for more info.
I would use annotations to set a unique constraint on the name field of the Collection entity in the CollectionBundle.
This not only validates this user input form, but any other form or component or bundle which uses CollectionBundle - and Doctrine will even prevent storage depending on the constraint leaving your database tidy!
EDIT: Another option for more advanced validation is writing a custom form event listener. Three events are dispatched when Form::handleRequest() or Form::submit() are called: FormEvents::PRE_SUBMIT, FormEvents::SUBMIT, FormEvents::POST_SUBMIT. This example also shows how to access the form itself.
$form = $formFactory->createBuilder()
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$user = $event->getData();
$form = $event->getForm();
// .. validation here
})
->getForm();

Related

Silverstripe - FormSchema with submissions and validation

I'm trying to make use of the new FormSchema class in Silverstripe 4 but I'm having a tough time with the workflow for submitting the form. I've been able to return the schema and state, but when submitting the form back to the controller is where I run in to issues. Here is some example code:
class TestController extends Controller {
private static $allowed_actions = [
'schema',
'TestForm'
];
public function schema(HTTPRequest $request) {
$schema = new FormSchema();
return json_encode($schema->getMultipartSchema(['schema', 'state', 'errors'], "FormID", $this->TestForm()));
}
public function TestForm() {
$fields = FieldList::create(
TextField::create('Name', 'Name'),
EmailField::create('Email', 'Email')
);
$actions = FieldList::create(
FormAction::create('doTestSubmit', 'Submit')
);
$required = RequiredFields::create(['Name', 'Email']);
return Form::create($this, 'TestForm', $fields, $actions, $required);
}
public function doTestSubmit($data, $form) {
return json_encode(array('response' => 'The form validated and was submitted.'));
}
}
So in this scenario hitting /schema returns TestForm schema in json then the front end renders the form. Submitting the form sends the data back to /TestForm where it is validated. If the submission is valid it'll continue to doTestSubmit and return the response. That's great! But, if the submission is not valid then TestForm attempts to return the form and not the schema with the validation messages.
I first though about using a condition in TestForm() like if($this->request->isPOST()) or if($form->validationResult()->isValid()) but it doesn't seem like the proper way to handle it.
Any input or a simple code sample would be great.
I think you cannot use the standard Form > Form Post > Form Action way, you have to create an own Form Handler that validates the form. Like this (untested code):
public function doTestSubmit($request) {
$form = $this->TestForm()
$form->loadDataFrom($request->postVars);
if(!$form->validationResult()->isValid()) {
$formSchema = new FormSchema();
$state = $formSchema->getState($form);
return json_encode($state);
}
//Valid, continue
}

How to create a new FOSUserBundle user programatically using the same validation as on web form?

I am running a Symfony 2.8 based web app using FOSUserBundle to manage users. Creating new users with a web form is absolutely no problem.
Now I would like to add a feature to create new users with a REST api. Of course submitting username, password, email, etc. to a controller is no big deal:
public function registerAction(Request $request) {
$requestJson = json_decode($request->getContent(), true);
$username = $requestJson[JSON_KEY_USERNAME];
$email = $requestJson[JSON_KEY_MAIL];
$password = $requestJson[JSON_KEY_PASSWORD];
...
$this->registerUser($username, $email, $password);
...
}
private function registerUser($username, $email, $password, $locale, $timezone, $currency) {
$userManager = $this->get('fos_user.user_manager');
$emailExist = $userManager->findUserByEmail($email);
$userNameExists = $userManager->findUserByUsername($username);
if ($emailExist || $userNameExists)
return false;
$user = $userManager->createUser();
$user->setUsername($username);
$user->setEmail($email);
$user->setPlainPassword($password);
...
$user->setLocked(0);
$user->setEnabled(0);
$userManager->updateUser($user);
return true;
}
However, this performs no validation at all. If for example the username is empty an NotNullConstraintViolationException is thrown when persisting the user object.
Of course I could manually re-implement the same validation process which is used by the RegistrationForm (username not empty, not taken, no invalid characters, password min length, e-mail format, etc.) and pass back the same error messages but this would mean to reinvent the wheel.
Is it somehow possible to run the exact same validation which is used by the RegistrationForm?
Symfony validator can work independently. In a controller you can use validator service like this:
$violations = $this->get('validator')->validate($user, null, ['your_groups_here']);
// Or inject Symfony\Component\Validator\Validator\ValidatorInterface into a service.
It will return a ConstraintViolationListInterface, you can loop trough this object.
You can check FOSUserBundle validation groups here: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/config/validation.xml

Update Entity using Doctrine in Symfony

I want to update values in my Entity using PATCH method, but when I try to do that, doctrine create new record in table, but do not update. For example I want to update name attribute, so I send this JSON array (in url I send id of the record: api.test/item/{id}):
{
"name": "newname"
}
My Controller:
public function updateItemAction(Request $request, $id)
{
$serializer = $this->get('jms_serializer');
$content = $request->getContent();
$item = $serializer->deserialize($content,Item::class,'json');
$em = $this->getDoctrine()->getManager();
$em->getRepository(Item::class)->find($id);
$em->persist($item);
$em->flush();
return new View("updated!",Response::HTTP_OK);
}
You must withdraw the persist method from your code and it will work better.
Have you tried using the merge() function? This allows you to merge the entity into the database, updating the existing entity. Call this function instead of persist. Here is a guide
https://www.vivait.co.uk/labs/updating-entities-when-an-insert-has-a-duplicate-key-in-doctrine

Symfony 2: Form Permissions Authorization Strategy

I have a Symfony application with five different entities (what they are doesn't really matter).
For each of these entities, a registered user must either have NONE, READ, EDIT, DELETE permissions. The sticky part for me to grasp is that each user can have different permissions for each entity; User A can edit Entity A, but can only view Entity B, etc.
Now on each user's options page, an admin should be able to see his permissions for each form. Radio buttons should be displayed with the four options for each form. Something like:
Entity A: O NONE O READ X EDIT O DELETE
Entity B: O NONE X READ O EDIT O DELETE
...
I know my choices are basically between creating some type of Voter system or an Access Control List.
At first I just started by listing all of the roles currently in the system within my UserType:
$builder
...
->add('roles', 'choice', array(
'choices' => $this->roles,
'choices_as_values' => true,
'label' => 'Roles',
'expanded' => true,
'multiple' => true,
'mapped' => true,
))
;
but I'm feeling like this isn't going to be very effective in the long run. And either way, this also displays other system roles that have nothing to do with access control to specific entities (such as ROLE_USER, ROLE_ADMIN, etc.)
I'm not looking for a complete solution or anything like that, I'm just having a really hard time getting started and seeing the big picture on how to make this happen. (And yes, I am aware of the Symfony documentation...sometimes that stuff just doesn't make a ton of sense at first).
PROGRESS UPDATE
I decided on Access Control List.
First, when a new entity is created, I use the standard ACL creation strategy as mentioned in the Symfony Documentation:
public function postAvrequestAction(Request $request){
$entity = new AvRequest();
$form = $this->get('form.factory')->createNamed('', new AvRequestType(), $entity);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
$serializer = $this->get('serializer');
$serialized = $serializer->serialize($entity, 'json');
// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($entity);
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$tokenStorage = $this->get('security.token_storage');
$users = $em->getRepository('AppBundle:User')->findAll();
//$tokenStorage->getToken()->getUser();
foreach($users as $user){
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// grant owner access based on owner's overall permissions for this type of entity
$acl->insertObjectAce($securityIdentity, 0);
$aclProvider->updateAcl($acl);
}
return new Response($serialized, 201);
}
return new JsonResponse(array(
'errors' => $this->getFormErrors($form)
));
}
Next, I created a service with all of the necessary dependencies to update a user's permissions for each entity:
#services.yml
services:
user_service:
class: AppBundle\Resources\Services\UserService
arguments: [ #doctrine.orm.entity_manager, #service_container, #security.authorization_checker, #security.acl.provider ]
The service has the function:
/**
* ACLs grant user permission on every instance of each entity.
* In order to edit permissions across all of these entites for each user,
* first iterate over all entities.
* For each entity, update the permission for the specified user.
*
* #param \AppBundle\Entity\User $user The user object whose permissions should be updated
* #param String $entity The entity whose permissions should be updated (e.g. 'AppBundle:AvRequest')
* #param int $permission The bitmask value of the permission level (e.g. MaskBuilder::MASK_VIEW (=4))
*
* #return null
*/
public function editPermission(User $user, $entity, $permission){
$allEntities = $this->em->getRepository($entity)->findAll();
foreach($allEntities as $oneEntity){
// locate the ACL
$objectIdentity = ObjectIdentity::fromDomainObject($oneEntity);
$acl = $this->aclProvider->findAcl($objectIdentity);
// update user access
$objectAces = $acl->getObjectAces();
foreach($objectAces as $i => $ace) {
$acl->updateObjectAce($i, $permission);
}
}
}
This function goes through every instance of the entity and gives it the same permission level for the specified user.
The next step that I haven't quite figured out yet is setting a master permission level for a user on an entity as described up top with my radio buttons. I need to be able to go to the user's profile page, see a radio list of the user's permissions for each entity type, submit the radio button value and then run the editPermission() function on save.
You are looking for Access Control Lists. It is easy to set permission by user or group of users.
Add access level by user:
$builder = new MaskBuilder();
$builder
->add('view')
->add('edit')
->add('delete')
->add('undelete')
;
$mask = $builder->get(); // int(29)
$identity = new UserSecurityIdentity('johannes', 'AppBundle\Entity\User');
$acl->insertObjectAce($identity, $mask);
Specify min access level by entity:
public function addCommentAction(Post $post)
{
$comment = new Comment();
// ... setup $form, and submit data
if ($form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($comment);
$entityManager->flush();
// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$tokenStorage = $this->get('security.token_storage');
$user = $tokenStorage->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
}
}
public function editCommentAction(Comment $comment)
{
$authorizationChecker = $this->get('security.authorization_checker');
// check for edit access
if (false === $authorizationChecker->isGranted('EDIT', $comment)) {
throw new AccessDeniedException();
}
// ... retrieve actual comment object, and do your editing here
}

Symfony2 Doctrine - Flushing in kernel.response listener flushs bad data

In order to do some logging for my Symfony2 app, I created a service that logs any connection, here is the method called on kernel.response :
public function log(FilterResponseEvent $event)
{
$log = new Log();
$request = $event->getRequest();
$response = $event->getResponse();
//fill the Log entity with stuff from request & response data
$manager = $this->container->get('doctrine.orm.entity_manager');
$manager->persist($log);
$manager->flush();
}
All of this seems fine, however when I execute a test like this one (patch with empty data to trigger a failure):
$this->client->request(
'PATCH',
'/users/testificate',
array(
'firstName' => '',
)
);
Which calls this action :
protected function processForm($item, $method = 'PATCH')
{
$form = $this->createForm(new $this->form(), $item, array('method' => $method));
$form->handleRequest($this->getRequest());
if ($form->isValid()) {
$response = new Response();
// Set the `Location` header only when creating new resources
if ($method == 'POST') {
$response->setStatusCode(201);
$response->headers->set('Location',
$this->generateUrl(
'get_' . strtolower($class), array('slug' => $item->getId()),
true // absolute
)
);
}
else {
$response->setStatusCode(204);
}
$this->em->flush();
return $response;
}
$this->em->detach($item);
return RestView::create($form, 400);
}
Although the test fails, the entity is patched, and of course it must not.
After some search what I've learnt is:
The parameters enter the form validator
The validation fails, thus returning a 400 http code without flushing the entity
However during the validation process, the entity gets hydrated with the invalid data
When the service is called on kernel.response, the $manager->flush(); flush all the data... including the bad data provided by the PATCH test.
What I've tried thus far:
1) Do a $manager->clear(); before $manager->persist(); ... doesn't change anything
2) Do a $manager->detach($item); if the form validation failed... doesn't change anything
Thanks !
I recently stumbled across problems with flushing in kernel.response when upgrading from Doctrine 2.3.4 to the latest 2.4 branch. Try flusing the log entities from kernel.terminate. Leave any modifications to the Response in kernel.response.

Resources