Symfony 2 form validation - extending request - php

Im using FOSRestBundle to build a REST API in Symfony.
In my tax Controller i have:
private function processForm(Request $request, Tax $tax )
{
$form = $this->createForm(new TaxType(),$tax);
$req = $request->request->all();
$form->submit($req['tax']);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($tax);
$em->flush();
return $this->getTaxAction($tax->getId());
}
return array(
'form' => $form,
'request' => $request->request->all()
);
}
This function called from the POST and PUT functions.
TaxType
class TaxType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('createDate')
->add('writeDate')
->add('name')
->add('amount')
->add('active')
->add('createUid')
->add('writeUid')
->add('company')
;
}
...
It worked fine so far, but now i added some extra column to the table (and proprty to the Tax Entity) like: to which company belongs the record, date of creation. This wont come from the client but set on server side.
How do i add eg createData?
I have tried
$req['tax']['createDate'] = new \DateTime("now");
but im getting:
{"code":400,
"message":"Validation Failed",
"errors":{"children":{"createDate":{"errors":["This value is not valid."],
"children":{"date":{"children":{"year":[],"month":[],"day":[]}},
"time":{"children":{"hour":[],"minute":[]}}}},
"writeDate":{"children":{"date":{"children":{"year":[],"month":[],"day":[]}},
"time":{"children":{"hour":[],"minute":[]}}}},"name":[],"amount":[],"active":[],"createUid":[],"writeUid":[],"company":[]}}}
from entity Tax.php
/**
* #var \DateTime
*/
private $createDate;
I guess im extending the request with the correct data type, but im getting validation error.

If I understand the question correctly you don't have to take the request but just change the property on the $tax-object.
You can also do this after the form validation if you want.

You can Remove add('createDate') from your form builder.
$builder
->add('createDate')
->add('writeDate')
->add('name')
->add('amount')
->add('active')
->add('createUid')
->add('writeUid')
->add('company')
Then set the createdDate value on your $tex object before persist.
if ($form->isValid()) {
$tax->setCreateDate(new \DateTime("now"));
$em = $this->getDoctrine()->getManager();
$em->persist($tax);
$em->flush();
return $this->getTaxAction($tax->getId());
}
Alternative Method
Or you can use Doctrine's Lifecycle Callbacks to achieve this.
Happy coding!!

Related

Deserialize entity with null id and doctrine constructor

I'm having many problems when I receive an entity to deserialize with null id.
My JMS Serializer configuration file is:
jms_serializer:
handlers:
datetime:
default_format: Y-m-d\TH:i:s+
array_collection:
initialize_excluded: false
property_naming:
id: jms_serializer.identical_property_naming_strategy
separator: ''
lower_case: false
enable_cache: true
object_constructors:
doctrine:
fallback_strategy: "fallback"
So, when the entity has an id field, it tries to retrieve the entity with that id from database via doctrine (something like $repository->find($id)).
If the Id exists, it retrieves the entity filled. If it doesn't exist return an exception. The problem is that when I receive a JSON with an entity to persist, the id field is null and it tries to find an entity in the database with ìd==null, so it throws an exception.
I have tried to change the fallback_strategy to: "null","exception" or "fallback" without success.
Edit: the Controller where it happens
protected function post(Request $request)
{
$content = $request->getContent();
try {
$entity = $this->get('jms_serializer')->deserialize($content, 'App\Entity\Service', 'json');
} catch (\Exception $e){
throw new ValidationException($e);
}
....
}
I put a try-catch block to capture and log the exception with a custom class.
I hope you can help me,
Thanks
It's more a workaround but for my CRUD controllers I prefer to use form to deserialize my objects. It allows me to be more flexible, I can have different form of payload for the same entity and I can check in a better way my payload.
The common form looks like this (you have to adapt it to your entity)
class YourEntityType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('description')
...
}
}
And in the controller :
protected function post(Request $request)
{
$yourEntity = new YourEntity();
$form = $this->createForm(YourEntityType::class, $yourEntity);
$form->submit($request->request->all());
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($yourEntity);
$em->flush();
return $yourEntity;
}
return $form;
}
Don't know if it's gonna fit with your project but that the cleanest way I know and it bypass you id issue because you don't have to put it in your form.

Set entity parameter before form validation

[SETTINGS]
Symfony 3
BoxEntity: [id, name]
CandyEntity: [id, name]
[PROBLEM]
Currently, when creating a new candy, I must choose a box as parent entity.
The thing is, I would like this choice to be automated.
The box is already registered in the database, and the session is holding the current box parameters to find it back easily.
But I can't figure out how to apply it to the candy entity once the data have been posted.
[FILES]
AppBundle/Controller/CandyController.php
public function newAction(Request $request) {
$$candy= new Candy();
$form = $this->createForm('AppBundle\Form\CandyType', $conference);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($candy);
$em->flush();
return $this->redirectToRoute('candy_show', array('id' => $candy->getId()));
}
return $this->render('candy/new.html.twig', array(
'candy' => $candy,
'form' => $form->createView(),
));
}
AppBundle/Form/CandyType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('nom')
->add('box'); //Remove from form, and set manually
}
I did read this page, but can't figure out how to do it properly.
If someone would be so kind as to give me a full example to solve my problem, it would be much appreciated.
You have multiple options to perform what you want. You could set the value after your form submits:
public function newAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$candy = new Candy();
$box = $em->find('AppBundle\Entity\Box', $this->get('session')->get('boxId'));
$form = $this->createForm('AppBundle\Form\CandyType', $candy);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// add the box entity to the candy
$candy->setBox($box);
$em->persist($candy);
$em->flush();
return $this->redirectToRoute('candy_show', array('id' => $candy->getId()));
}
return $this->render('candy/new.html.twig', array(
'candy' => $candy,
'form' => $form->createView(),
));
}
You could set it on the Candy entity before passing it to the createForm() call, although it may not stay on the entity after doing the form handleRequest() call:
$em = $this->getDoctrine()->getManager();
$candy = new Candy();
$box = $em->find('AppBundle\Entity\Box', $this->get('session')->get('boxId'));
$candy->setBox($box);
$form = $this->createForm('AppBundle\Form\CandyType', $candy);
$form->handleRequest($request);
You could do it in the way that you are attempting, in a form event. What you would want to do is inject the entity manager and session into your form and treat your form as a service:
public function CandyType extends AbstractType
{
private $em;
private $session;
public function __construct(EntityManager $em, SessionInterface $session)
{
$this->session = $session;
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ... build the form
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
$candy = $event->getData();
$box = $this->em->find('AppBundle\Entity\Box', $this->session->get('boxId');
$candy->setBox($box);
}
);
}
}
You might need to do that on the POST_SET_DATA or POST_SUBMIT event instead, but I'm not sure. Also I used $this->get('session') in the Controller, but depending on your Symfony version (> 3.3) you could inject that into your controller as a service as well.
Either way the main concept is to use Doctrine to grab your Box entity from the session itself using the stored box id in the session, then set that on your Candy entity. You could even use a hidden field to achieve the same results. As I said before there are lots of ways to solve your issue.

Service argument injection: "Argument 1 passed to entity constructor must be must be an instance of Entity, none given"

I'm working with forms on my Symfony 3 app.
I have a ClassType created to create the form of a specific entity:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$currencies = $this->doctrine->getRepository('AppBundle:Currency')->findAll();
$currenciesFormat = array();
foreach($currencies as $currency){
$currenciesFormat += array($currency->getName() .' ('. $currency->getShortName() . ')' => $currency);
}
$cycles = $this->doctrine->getRepository('AppBundle:Cycle')->findAll();
$cyclesFormat = array();
foreach($cycles as $cycle){
$cyclesFormat += array($cycle->getName() => $cycle);
}
$builder
->add('currency', ChoiceType::class, array(
'label' => 'Waluta',
'choices' => $currenciesFormat,
))
->add('cycle', ChoiceType::class, array(
'label' => 'Cykl',
'choices' => $cyclesFormat,
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => CyclicNewsletter::class,
));
}
Also I have a universal POST action ment to handle most of the submitting (regullar add to db type stuff). It uses variable classnames to match many forms at once.
For now, all it does it var_dumps the class it is supposed persist into the database:
/**
* #Route("/form/{url}", name="entity_form_post")
* #Method("POST")
*
*/
public function formPostAction(EntityForm $entityForm, Request $request)
{
$em = $this->getDoctrine()->getManager();
$form = $this->createForm($entityForm->createType())
->add('Dodaj!', SubmitType::class)
;
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$formData = $form->getData();
$em->persist($formData);
var_dump($formData);
exit;
}
}
Now I want the CyclicNewsletter constructor to by default set the currently logged in user as $user field.
For this I have created a service working for the class:
cyclic_newsletter:
class: AppBundle\Entity\CyclicNewsletter
arguments: ["#security.token_storage"]
And made the entity's constructor make use of the argument:
public function __construct(TokenStorage $tokenStorage)
{
$this->user = $tokenStorage->getToken()->getUser();
$this->date = new DateTime();
}
However the output I am getting when submitting the form is:
Type error: Argument 1 passed to
AppBundle\Entity\CyclicNewsletter::__construct() must be an instance
of AppBundle\Entity\TokenStorage, none given, called in
C:\PHP\Repos\centaur\vendor\symfony\symfony\src\Symfony\Component\Form\Extension\Core\Type\FormType.php
on line 136
I have tried using a separate method inside the class, but it too requires an argument.
Is there any way I can make this work?
ps. All cache have been cleared
Edit:
- Running debug:container displays the service properly.
- Searching to my service in appDevDebugProjectContainer.php returns this:
/**
* Gets the 'cyclic_newsletter' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* #return \AppBundle\Entity\CyclicNewsletter A AppBundle\Entity\CyclicNewsletter instance
*/
protected function getCyclicNewsletterService()
{
return $this->services['cyclic_newsletter'] = new \AppBundle\Entity\CyclicNewsletter($this->get('security.token_storage'));
}

Symfony2 create a form type to add multiple records of the same type

I need to create a form type that will create multiple records of a certain Entity.
I figured out to create a parent Form Type and then add a collection inside. This parent Type isn't bind to any entity, but child Type is. The problem is, that when I process the request, I get an empty data object.
Here is my parent Type:
class ChallengeCollectionType extends AbstractType {
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('email_invitations', CollectionType::class, [
'entry_type' => EmailInvitationType::class
]);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false
));
}
}
My Child Type:
class EmailInvitationType extends AbstractType {
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('email', EmailType::class);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Challenge::class,
'csrf_protection' => false
));
}
}
Here is how I process my data in Controller:
public function inviteEmailAction(Request $request){
$form = $this->createForm(ChallengeCollectionType::class);
$form->handleRequest($request);
dump($form->getData());
dump($form->get('email_invitations')->getData());
die();
}
I submit data in a POST request (this is API and there's no HTML form) like this:
challenge_collection[email_invitations][0][email] = "email#address.com"
challenge_collection[email_invitations][1][email] = "address#email.com"
And Controller returns empty [] for all dumps.
I've actually found a workaround, but it looks bad:
$data = ['email_invitations' => [new Challenge(), new Challenge()]];
$form = $this->createForm(ChallengeCollectionType::class, $data);
$form->handleRequest($request);
And this will work, so I have to send an array of empty objects of the type of the Entity to get request processed.
Or even this way:
$data = ['email_invitations' => []];
if(!empty($request->request->get('challenge_collection'))){
if(!empty($request->request->get('challenge_collection')['email_invitations'])){
for($i = 0; $i < count($request->request->get('challenge_collection')['email_invitations']); $i ++){
$data['email_invitations'][] = new Challenge();
}
}
}
There should be a better way to do it, please help me to find it.
You should try using allow_add and allow_delete options in collection field type. See the Collection field type documentation
Also, this is important for parent entity to have an mapped-by association with child entity, which would get you child objects in an ArrayCollection object in controller.
Further, if you want to save/remove them automatically as per the request, you need to implement a 'by_referencefalse and set association withcascade-persistandcascade-remove`.
I have used collection field type with API numerous times, and it worked like charm. Let me know if you need further explanation.
Maybe you should try in your controller something like :
$invitations = $this->getDoctrine()->getRepository('yourInvitationEntityClass')->findAll());
$form = $this->createForm(ChallengeCollectionType::class, $invitations );
You could find explanations here :
http://symfony.com/doc/current/cookbook/form/form_collections.html
One form with all row of one entity

How to properly do a REST API POST call using FOSRest and Symfony 3.0

For an API I'm currently building I'd like to be able to send a request with a JSON body with the following content
{"title": "foo"}
to create a new database record for an Entity called Project.
I made a controller which subclasses FOSRestController. To create a project, I made an action
/**
* #Route("/")
*
* #ApiDoc(
* section="Project",
* resource=true,
* input={"class"="AppBundle\Form\API\ProjectType"},
* description="Creates a new project",
* statusCodes={
* 201="Returned when successful",
* }
* )
*
* #Method("POST")
* #Rest\View(statusCode=201)
*/
public function createProjectAction(Request $request)
{
$project = new Project();
$form = $this->createForm(ProjectType::class, $project);
$form->submit(($request->request->get($form->getName())));
if ($form->isSubmitted() && $form->isValid()) {
return $project;
}
return View::create($form, 400);
}
The ProjectType looks like this
class ProjectType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Project'
));
}
}
However, when I try to post said JSON to the API, it responds that the title property cannot be blank, which is good because that's the validation rule set for it. However, it IS set. I suddenly realized I have to send the JSON prefixed by the actual object's name to make this work:
{"project":{"title": "bla"}}
Which feels a little strange to be fair, it should be enough to just post the properties.
So, based on this information I simply have 2 questions:
Why do I need to "submit" this form with ($request->request->get($form->getName())), shouldn't $request be enough?
What do I need to change for the FormType to validate the entity as is, instead of prefixing it with the entity's name?
Edit 1: adding or removing the data_class in the Default Options does not change the behaviour at all.
This is because of how Symfony Controller "createForm" helper method works. Reasoning behind it is that multiple forms could have same target URL. By prefixing with form name, Symfony can know which form was submitted.
This can be seen by looking at "createForm" method implementation:
public function createForm($type, $data = null, array $options = array())
{
return $this->container->get('form.factory')->create($type, $data, $options);
}
If you don't want this behavior, it's really easy to change it:
public function createProjectAction(Request $request)
{
$project = new Project();
$form = $this->get('form.factory')->createNamed(null, new ProjectType(), $project);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return $project;
}
return View::create($form, 400);
}
So you're basically creating a "nameless" form. Since you're building an API, it's probably a good idea to pull this into a createNamelessForm($type, $data, $options) helper method in your base controller so that you don't have to get Form Factory from container explicitly all the time and make it easier on the eyes.
Comment on your edit
Wrapper key is not generated by "data_class" option, but by "getName()" method on your form type.

Categories