handle request of incomplete missing fields symfony form - forms

I have created a small Symfony (Sf3.2 + php7) web with a Task Entity. I have my controller where I have a new action to create a new task. Everything is fine. Now, some of the form fields are not necessary to be filled (created date, for example). So I have removed from my taskType:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add->('name')->add('description');
}
Now my form looks exactly as I want. The surprise is that when I submit it my controller pass it to doctrine,and every missing field is write as a NULL. NO!, I don't want that, what I wanted is my default mysql value.
Ok, I read the doc. There seems to be two ways of handling data:
$form->handleRequest($request);
$form->submit($request->request->get($form->getName()));
I've found in the API, (not in the doc) that submit have a second parameter, a boolean:
API says:
bool $clearMissing Whether to set fields to NULL when they are missing in the submitted data.
Great! this is exactly what I need. Lets see it:
public function newAction(Request $request) {
$task = new Task();
$form = $this->createForm('myBundle\Form\TaskType', $task);
$form->submit($request->request->get($form->getName()), false);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($task);
$em->flush();
$response = new Response();
$response->setContent(json_encode(array(
'data' => 'success',
)));
$response->headers->set('Content-Type', 'application/json');
return $response;
return $this->redirectToRoute('task_success');
}
return $this->render('task/new.html.twig', array(
'task' => $task,
'form' => $form->createView(),
));
}
Well, after tried everything I always get NULL in all missing fields. What am I doing wrong.
Thank you, Caterina.
I'm afraid there must be something more. And I was thinking that perhaps is not Doctrine the solution. The Symfony log shows me the insert and it's setting a Null in every empty field, on empty fields and missing fields. In fields like status i could set a default value, at doctrine level, but if I want to set created_at field, I suppose that must be Mysql the responsible is setting current timeStamp.
Anyway this is my ORM property status from Task Entity.
/**
* #var \scrumBundle\Entity\Taskstatus
*
* #ORM\ManyToOne(targetEntity="scrumBundle\Entity\Taskstatus")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="status", referencedColumnName="id")
* })
* #ORM\Column(name="status", type="integer", nullable=false,options={"default":1})
*/
private $status;
And this is the property id from TaskStatus:
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false, options={"default":1})
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
As you see I tried to follow your advise of setting the option default value. I've tried to create to set the form with handleRequest and submit with same result
$task = new Task();
$form = $this->createForm('scrumBundle\Form\TaskType', $task);
//$form->handleRequest($request);
$form->submit($request->request->get($form->getName()), false);
if ($form->isSubmitted() && $form->isValid()) {
I even tried to debug step by step submit function, a real Hell, because submit is a recursive function to much complex for me.
Ok , again thank for you time.
Regards, Cate
PS. Sorry about my poor english.;^)

Short answer - when you persist a PHP object through Doctrine, you must have absolutely every value set that you want. If your PHP object has null fields, Doctrine will manually set them as null in your entity. Doctrine doesn't assume that just because a value is null in your PHP object, that you don't want it included on your INSERT statement. This is because null is a perfectly valid SQL value.
So, whatever your PHP object is at the time of insert is exactly what is going to be inserted into your database, null values and all.
When you are editing an entry, Doctrine will only update the fields that are different, so this isn't a concern. Your concern is when persisting entities.
The easiest solution is to copy your MySQL default value into your PHP entity. Like one of these many ways:
// set default value on the variable itself
public $someVar = 100;
// set default values in the constructor (so when creating a new entry)
public function __construct()
{
$this->createdAt = new \DateTime();
$this->isActive = true;
}
You can also use Lifecycle Callbacks to set when inserting a new entry:
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Task
{
// ...
/**
* #ORM\PrePersist
*/
public function setCreatedAtValue()
{
$this->createdAt = new \DateTime();
}
}

Related

Submitted Symfony form return wrong field value

I am doing some tests with Symfony 4 Forms and the answer to my question might be very simple but I am really confused right now and I need fresh eye to understand the following strange behavior.
Here is the context : I have a page /edit which handle a form for an entity named Promoter in a very classic way but after passing the validation I am generating and setting a randon firstname then flush the entity (which is updated with the new data form the form and the new firstname), and after instead of redirecting to another page, I return to the same page with the new Promoter entity.
My expectation is the see the new generated firstname but in fact it shows the old one(the one send by the form to the controller) and I cannot understand why? Can somebody has a clue?
I should add that data in db are correctly updated.
The Controller code is:
/**
* #Route("/{id}/edit", name="promoter_edit", methods={"GET","POST"})
* #param Request $request
* #param Promoter $promoter
* #return Response
*/
public function editAction(Request $request, Promoter $promoter): Response
{
$form = $this->createForm(PromoterFormType::class, $promoter);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$firstname = substr(str_shuffle('abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'),0,5);
$promoter->setFirstname( $firstname );
$this->getDoctrine()->getManager()->persist($promoter);
$this->getDoctrine()->getManager()->flush();
}
return $this->render('promoter/edit.html.twig', [
'promoter' => $promoter,
'form' => $form->createView(),
]);
}
Thanks
It seems to me that when you post your form, you create the new one before updating your 'promoter'. You should try to reload your page after submitting your form.
if ($form->isSubmitted() && $form->isValid()) {
$firstname = substr(str_shuffle('abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'),0,5);
$promoter->setFirstname( $firstname );
$this->getDoctrine()->getManager()->persist($promoter);
$this->getDoctrine()->getManager()->flush();
return $this->redirect($request->getUri());
}
Does your PromoterFormType class have a field for firstname? Because otherwise it could override your newly set Firstname.
If so fix:
In PromoterFormType
->add('firstname')
needs the option: 'mapped' => false.
Or simple delete it from the formType.
More info
If not can you show us the FormType and the Promoter Entity?
Hope this helps for you!

Why doesn't non doctrine-mapped ArrayCollection get initialized?

My Setup is a Symfony 3.4 App with the typical 'ManyToMany'-Relation with additional fields, something like this:
Entity Article
Entity Specialty
Entity ArticleSpecialtyRelation
In a Form for an Article i wanted it to look like as if it were a ManyToMany-Relation rendered as an EntityType with multiple=true and expanded=true, so all entries of Specialty are rendered as checkboxes.
To achieve that i created a non orm-mapped property specialties that is an ArrayCollection, gets initialized in the Constructor and has a Getter, Adder and Remover.
/**
*
* #var ArrayCollection;
*
*/
protected $specialties;
public function __construct()
{
$this->specialties = new ArrayCollection();
}
/**
* #return Collection|Specialty[]
*/
public function getSpecialties()
{
return $this->specialties;
}
/**
* #param Specialty $specialties
*/
public function addSpecialties(Specialty $specialties)
{
$this->specialties->add($specialties);
}
/**
* #param Specialty $specialties
*/
public function removeSpecialties(Specialty $specialties)
{
$this->specialties->removeElement($specialties);
}
This property is used to render the Specialty Entity as checkboxes:
add('specialties', EntityType::class,array(
'class' => Specialty::class,
'expanded'=>true,
'multiple'=>true,
'label'=>'Specialties',
'required' => false,
'mapped'=>true,
));
To populate it with the data from SpecialtyRelation i added a PreSetData Formevent:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$article = $event->getData();
if ($article instanceof Article) {
$form->get('specialties')->setData($article->getUsedSpecialties());
}
});
The used Getter of $artikel just iterates over $article->getArtikelSpecialties and returns a collection of Specialty.
It all works until the submit. Because the formfield is mapped=true, somewhere in handleRequest($form) where the entity is hydrated with the form data, it explodes when the Adder for $specialty is called:
Call to a member function add() on null
Because as i just learned, the Constructor is never called by Doctrine and obviously initializes all ORM-ArrayCollections but not the ArrayCollection for the non-mapped property specialties -
Of course I can check if the ArrayCollection is initialized in the Adder and Remover and initialize it there if it is null, but that just feels a bit hacky in a already at least hacky-felt setup and i am wondering if my setup is completely stupid, especially since i didn't find anybody trying to do that (or getting problems with that) on here or elsewhere.
Is there a better solution to this or should i just check the ArrayCollection in Adder and Remover and live happily ever after?
Also, just curious, is there any other way to initialize the ArrayCollection?
P.S. If there are typos in the names it's because i translated the names into english.
Partial Stacktrace
Symfony\Component\Debug\Exception\FatalThrowableError: Call to a
member function add() on null
at src/Test/Bundle/TestBundle/Entity/Article.php:312 at
Test\Bundle\TestBundle\Entity\Article->addSpecialties(object(Specialty))
(vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:674)
at
Symfony\Component\PropertyAccess\PropertyAccessor->writeCollection(array(object(Article),
object(Article)), 'specialties', object(ArrayCollection),
'addSpecialties', 'removeSpecialties')
(vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:622)
at
Symfony\Component\PropertyAccess\PropertyAccessor->writeProperty(array(object(Article),
object(Article)), 'specialties', object(ArrayCollection))
(vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:216)
at
Symfony\Component\PropertyAccess\PropertyAccessor->setValue(object(Article),
object(PropertyPath), object(ArrayCollection))
(vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php:86)
at
Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper->mapFormsToData(object(RecursiveIteratorIterator),
object(Article))
(vendor/symfony/symfony/src/Symfony/Component/Form/Form.php:636) at Symfony\Component\Form\Form->submit(array(), true)
(vendor/symfony/symfony/src/Symfony/Component/Form/Form.php:580)

Unable to pass a specific form in the view

I have created a CRUD with Symfony 3 that allows me to create different missions with a few specificities. I want to to create a function that allows someone with a specific role to change a mission's status just by clicking a button, that would be shown in the view like this
{{form_start(missionInProgress) }}
<input type="submit" value="Submit" />
{{form_end(missionInProgress) }}
Since I'm a real newbie and I can't find concrete example on Google, I tried a lot of things, but none worked so far. I tried to create a public function that would modify the mission's status when someone clicks on the input button
public function that updates the mission's status:
/**
* #Route("/{id}/encours", name="mission_encours")
* #Security has_role('ROLE_RECRUTEUR')
* #Method("POST")
*/
public function enCoursAction(Request $request, Mission $mission){
$form = $this->missionInProgress($mission);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$em = $this->getDoctrine()->getManager();
$mission->setStatut("En cours");
$em->persist($mission);
}
}
And I also tried to create a private function like the one that allows a mission to be deleted from anywhere.
**Private function that calls the public function: **
/**
* #param Mission $mission
* #Security has_role('ROLE_RECRUTEUR')
* #return \Symfony\Component\Form\Form The form
*/
private function missionInProgress(Mission $mission){
$this->createFormBuilder()
->setAction($this->generateUrl('mission_encours', array('id' => $mission->getId())))
->setMethod('POST')
->getForm();
}
Following the "createDeleteForm" example, I implemented it in the showAction as follow:
/**
* Finds and displays a Mission entity.
*
* #Route("/{id}", name="mission_show")
* #Method("GET")
*/
public function showAction(Mission $mission)
{
$deleteForm = $this->createDeleteForm($mission);
$enCours = $this->missionInProgress($mission); /* There */
return $this->render('mission/show.html.twig', array(
'mission' => $mission,
'delete_form' => $deleteForm->createView(),
'missionInProgress' => $enCours->createView(), /* And there */
));
}
But when I try to see the result, I get the following error:
Error: Call to a member function createView() on null
Obviously nothing gets inside missionInProgress(), but I can't figure why and how to make this damn thing work. I also don't think that everything I did was necessary, but I thought that if I do this, I might increase my success chances...
Anyone has an idea ?
Thank you in advance
Try to add returnin your missionInProgress() method

Extbase property mapping for deleted record

I would like to build a preview page for a create form. I set "deleted" property of the record to "1" when in previewAction because in the BE the list module is used to approve the inserted records - so if the record was never finally saved its deleted anyway.
Problem: I can create the record (deleted=1) - I can jump back to the form (no history back for I have to keep the created object). But if I submit again the property mapping tells me
Object of type MyModel with identity "3" not found.
Of course that's because its deleted. The settings in the Repository to ignore deleted are not taking action here.
Yes I could bypass the Extbase magic by filling up everything manually, but this is not what I want.
Here is the action to get an idea what I'm trying
/**
* action preview
*
* #param MyModel
* #return void
*/
public function previewAction(MyModel $newModel)
{
//check if model was already saved
$uid = $this->request->hasArgument('uid') ? this->request->getArgument('uid') : 0;
if($uid){
$newModel = $this->myRepository->findDeletedByUid($uid);
$this->myRepository->update($newModel);
}
else{
$newModel->setDeleted(true);
$this->myRepository->add($newModel);
}
$this->view->assign('ad', $newModel);
$this->persistenceManager->persistAll();
$uid = $this->persistenceManager->getIdentifierByObject($newModel);
$this->view->assign('uid', $uid);
}
Any ideas?
The Extbase default query settings suppress deleted objects.
Since you've already stated the custom query findDeletedByUid() in your repository, you just need to set it to include deleted records. It is important, however, that if you want to call your controller action using the object, you'll have to retrieve it before calling the action. Use an initialization action for that. The initializaton will be called automatically before the action.
If you want to set wether the object is deleted, you'll also going to need to define a property, getter and setter in your Domain Model and a proper definition in your tca to enable the data mapper to access the column.
In the repository:
public function findDeletedByUid($uid) {
$query = $this->createQuery();
$query->getQuerySettings()->setIncludeDeleted(true);
$query->matching(
$query->equals('uid',$uid)
);
return $query->execute();
}
In your Controller class:
/**
* initialize action previewAction
* Overrides the default initializeAction with one that can retrieve deleted objects
*/
public function initializePreviewAction(){
if( $this->request->hasArgument('mymodel') ){
$uid = $this->request->getArgument('mymodel');
if( $mymodel = $this->mymodelRepository->findDeletedByUid($uid) ){
$this->request->setArgument($mymodel);
} else {
// handle non retrievable object here
}
} else {
// handle missing argument here
}
}
In your Domain Model:
...
/**
* #var bool
*/
protected $deleted;
/**
* #return bool
*/
public function getDeleted() {
return $this->deleted;
}
/**
* #param bool $deleted
*/
public function setDeleted($deleted) {
$this->deleted = $deleted;
}
In your tca.php
...
'deleted' => array(
'exclude' => 1,
'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.deleted',
'config' => array(
'type' => 'check',
),
),
Instead of doing any magic with deleted, you should use the hidden field to allow editors to preview documents.
You can tell your query to include hidden records inside the repository.
Your findDeletedByUid($uid) function caught my eye. If it's not a custom function, should it use something like findByDeleted(TRUE) or findByDeleted(1) in combination with ->getFirst() or ->findByUid()? You can find discussions in the Extbase manual reference and the Repository __call() function API sections.
Thanks for all hints.
I think depending to the answers its not possible without bypass extbase property-mapping magic. So I think in general its not a good idea to do it like that.
So I put now my own flag "stored" to the model.
In BE List-Module the not "stored" objects are still visible, but using an own BE Module or deleting the not "stored" object by a cron-job should do the job.
If anyone has a bedder idea feel free to share it :-)

Simple date with Sonata Admin Bundle

I have a form with Sonata Admin Bundle with a date, to set the birthday of the user we want to add. Here goes MemberAdmin.php :
/**
* #param \Sonata\AdminBundle\Form\FormMapper $formMapper
*
* #return void
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General')
->add('username')
->add('name')
->add('surname')
->add('birthdate', 'birthday', array('format' => 'yyyy-MM-dd'))
// ...
}
And my problem is when I send the form, I obtain Error: Call to a member function format() on a non-object ... But if I do print_r($birthdate) in the Entity class it shows me the DateTime object ...
Here are the interesting Entity parts:
/**
* #var date
*
* #ORM\Column(name="birthdate", type="date", nullable=true, options={"default" = "1990-01-01 00:00:00"})
* #Assert\DateTime()
*/
private $birthdate;
/**
* Set birthdate
*
* #param \DateTime $birthdate
* #return Membre
*/
public function setBirthdate($birthdate)
{
$this->birthdate = $birthdate;
return $this;
}
/**
* Get birthdate
*
* #return \DateTime
*/
public function getBirthdate()
{
return $this->birthdate;
}
My problem, currently, is that I don't know what I should do, I just want the date, no time, no anything else, i don't know if the column should be date (I work with PostgreSQL). What should I use for the types of my variables, I feel lost here, no simple Date possible ??
I tried to figure out from where it could come, but when I change too much I end up with: This form should not contain extra fields directly in the form, or even Incorrect value, but the field is a valid date ...
Thanks for your help !!
Change your field type to sonata_type_date_picker and test if the error message persist.
->add('birthdate', 'sonata_type_date_picker', array(
'format' => 'dd/MM/yyyy',
'widget' => 'single_text',
'label' => 'Birthdate',
))
From manual (sonata-project.org) :
If no type is set, the Admin class will use the one set in the
doctrine mapping definition.
So, you can try this:
->add('birthdate', null, array('format' => 'yyyy-MM-dd'));
#wr0ng.name you should never overwrite vendor code. NEVER.
There is something wrong with your mapping somewhere. You can use doctrine's commands to check your entity.
Edit
As #rande said, modifying vendors files is not the way to go, it provided an easy temp workaround for a local private app. As it is not dedicated to stay like that, I took care of the issue once I had more time. Sorry for the delay to come back to you guys.
I played around, tried with multiple setups, it took me time to figure it out, but I finally came to the conclusion that the issue... was caused by another date, that I was generating wrong in the constructor one line above.
Also, thanks to all of you, that guided me on the right path!