I learned Symfony 3 and I want create form class to upload File, so i created ImageType, cutom form type to handle image uploaded in NewsType (form with some description and this field):
class ImageType extends AbstractType
{
private $path;
public function __construct($path)
{
$this->path = $path;
}
public function getParent()
{
return FileType::class;
}
public function getName()
{
return 'image';
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'image_name' => ''
));
}
/**
* #param FormView $view
* #param FormInterface $form
* #param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['image_name'] = $options['image_name'];
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->setAttribute('image_name', $options['image_name'])
->addModelTransformer(new ImageTransformer($this->path))
}
}
I use ImageTransformer to transform file name like 124324235342.jpg to instance File class. Form work fine when i created and saved date to database, but how manage entity in edit mode ?
public function editAction(Request $request, News $news)
{
$path = $this->getParameter('upload_directory') . $news->getImage();
$image = $news->getImage();
$form = $this->createForm(NewsType::class, $news, ['image_name' => $image]);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid())
{
$this->get('app.image_uploader')->uploadNews($news, $image);
$em = $this->getDoctrine()->getManager();
$em->persist($news);
$em->flush();
return $this->redirectToRoute('admin_news_index');
}
return $this->render('admin/news/form.html.twig', [
'form' => $form->createView(),
'news' => $news
]);
}
I want handle case to use same form to edit database entity. I populated form, but when user not upload image I don't want change this field and live old value. How accomplish this ?
The simplest method for your code:
In setter of your News type:
function setImage($image) {
if(strlen($image)) {
$this->image = $image;
}
}
This allow you to do not worry about case of lacking image, when user edits other fields. In this case $this->fileName in News will be not overwritten.
Then in service app.image_uploader you should check if file with given name exist. If not then you should not overwrite this file.
But there are some other poroblems:
1) You should validate extension of file.
2) You should use unique names for your files in your server different than names from users. Yo can use both names, your unique for string files in hard drive, and name form user in user interface, but you should not use name of file form user to store file in your hard drive.
I recommend to read about these problems in docs:
https://symfony.com/doc/current/controller/upload_file.html
Or on stack overflow:
Symfony2 file upload step by step
Related
I am registering a doctor in my api through my symfony controller. And for that I created a form for the doctor, and then I have to persist these data in my database. And that's where I can not get the data from the form, that's what I tried on my postman, but it returns me a 500 error saying:
Child \"username\" does not exist.
I'm trying to recover my form data through this query:
$user->setUsername($form['username']);
Controller
/**
* #Route("/api/inscription/medecin")
* #Rest\View(statusCode=Response::HTTP_CREATED)
* #Method("POST")
*/
public function postDocAction(Request $request){
$medecin = new Medecin();
$user = new User();
$user->setSalt('');
$form = $this->createForm('Doctix\MedecinBundle\Form\MedecinType', $medecin);
$form->submit($request->request->all()); // Validation des données
if ($form->isValid()){
$encoder = $this->get('security.password_encoder');
$encoded = $encoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($encoded);
$user->setUsername($form['username']);
$user->setRoles(array('ROLE_MEDECIN'));
$user->setNom($form['nom']);
$user->setPrenom($form['prenom']);
$user->setNumTel($form['numTel']);
$user->setAdresse($form['adresse']);
$medecin->setUser($user);
$medecin->setSexe($form['sexe']);
$medecin->setQuartier($form['quartier']);
$medecin->setNumOrdre($form['numordre']);
$medecin->setSpecialite($form['specialite']);
$medecin->setClinique($form['clinique']);
$em = $this->getDoctrine()->getManager();
$em->persist($medecin);
$em->flush();
}
}
Form
class MedecinType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user',UserType::class)
->add('sexe', TextType::class)
->add('specialite',SpecialiteType::class)
->add('clinique',CliniqueType::class)
->add('quartier', TextType::class)
->add('numordre', TextType::class)
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Doctix\MedecinBundle\Entity\Medecin',
'csrf_protection' => false
));
}
}
Test in Postman
After you do $form->handleRequest($request);,
$form->getData() holds the submitted values.
But, the original $medecin variable has also been updated. So non need to set all the fields again.
https://symfony.com/doc/current/forms.html#handling-form-submissions
short answer :
According to the json, user seems to be an object so to access username, you should probably write:
$form["user"]["username"];
You should apply the same logic to all elements inside user.
complete answer :
There are multiple incoherences in your code. The advantage of passing the object into the createForm() method, is that the object will automaticaly be updated once the form is submited and valid. This avoid you to go throught all the setters later as you are doing now.
So if you want the user object to be correcly updated into the medecin's object, you should set the user before creating the form :
<?php
/**
* #Route("/api/inscription/medecin")
* #Rest\View(statusCode=Response::HTTP_CREATED)
* #Method("POST")
*/
public function postDocAction(Request $request){
$medecin = new Medecin();
$user = new User();
$user->setSalt('');
$user->setRoles(['ROLE_MEDECIN']);
$medecin->setUser($user);
$form = $this->createForm('Doctix\MedecinBundle\Form\MedecinType', $medecin);
$form->submit($request->request->all()); // Validation des données
if ($form->isValid()) {
$encoder = $this->get('security.password_encoder');
$encoded = $encoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($encoded);
$em = $this->getDoctrine()->getManager();
$em->persist($user); // Safety, as i don't know how you configured your entities
$em->persist($medecin);
$em->flush();
}
}
I usually find answers to solve my problems after some research, but that is not the case today.
Let's suppose I have 2 entities, "Task" and "TaskCategory", each task has one category.
I want to let my users not only attribute an existing category to their tasks but also create new ones.
Here's the code I have so far:
<?php
// TaskCategoryType.php
class TaskCategoryType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
;
}
// Some stuff...
and
<?php
// TaskType.php
// Some stuff...
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Add some fields there
->add(
'category',
TransformableEntityType::class,
['class' => 'AppBundle:TaskCategory']
)
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$task = $event->getData();
$form = $event->getForm();
if (empty($task['category']['id']) && !empty($task['category']['name'])) {
$task['category']['id'] = null;
$event->setData($task);
$form->remove('category');
$form->add('category', TaskCategoryType::class);
}
})
;
}
// Some stuff...
It works fine for task creation, but when I edit an existing task, it edit the associated TaskCategory instead of creating a new one.
I was trying to force the creation of a new TaskCategory with : $task['category']['id'] = null; but it does not work.
Thanks for your help I'm really stuck :(
EDIT: I forgot to mention that I'm using this form only as an API that's why I have only one 'category' field, otherwise I would have used another 'category_new' field.
Well it seems I finally found something working, it's not pretty, I'm not pretty happy with how it's done but at the moment I have not found any alternative.
If you have a cleaner solution I'll be happy to learn.
<?php
class TaskType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Some fields...
->add(
'category',
TransformableEntityType::class,
['class' => 'AppBundle:TaskCategory']
)
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$task = $event->getData();
$form = $event->getForm();
if (empty($task['category']['id']) && !empty($task['category']['name'])) {
// If there is no category id but there is a category
// name it means it is a new category. so replace
// the custom entity field with this simple field
$form->remove('category');
$form->add('category', TaskCategoryType::class, ['allow_extra_fields' => true]);
}
})
->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
$catForm = $event->getForm()->get('category');
if ($catForm->has('name') && $catForm->get('name')->getData() !== "") {
// If there is a category name it means
// it is a new category.
$category = new TaskCategory(); // Create new category
$category->setName($catForm->get('name')->getData());
$task = $event->getData()->setCategory($category);
$event->setData($task);
}
})
;
}
Considering you have cascade-persist enabled for your association from Task to TaskCategory. I guess the best work around will be, to use an autocompleter which should work as tag based.
The Transformer should check with database if given TaskCategory is available, else create a new one.
Having read through the Symfony forms collection/entity types I'm trying to generate a form that only involves these two attributes of an entity (Person) but uses all instances of Person in the database. The purpose of the form is to provide a activation tickbox for every person on a single page so that multiple status' can be flushed to the database on form submission. Eg, the form should look something like:
☑ John Smith
☐ Jane Doe
☑ ...etc
☑ ...etc
My attempts below are not working as they just return an empty page, although I can see that $allpersons is populated
My Person entity:
class Person
{
/**
* #ORM\COLUMN(type="boolean")
*/
protected $active = true;
/**
* #ORM\COLUMN(type="string", length=100)
*/
protected $fullname ;
/* ...many other attributes... */
}
My Controller:
class DefaultController extends BaseController {
public function activePersonsAction(Request $request) {
$em = $this->getDoctrine()->getManager();
$persons = $em->getRepository('AppBundle:Person')->findAll();
$form = $this->createForm(AllPersonsType::class, $persons);
$form->handleRequest($request);
if ($form->isSubmitted() && ($form->isValid())) {
$em = $this->getDoctrine()->getManager();
$persons = $form->getData();
foreach ($persons as $person) {
$em->persist($person);
}
$em->flush();
return $this->redirectToRoute('home_user');
}
return $this->render('activatePersons.html.twig', array(
'page_title' => 'Active Persons',
'form' => $form->createView(),
));
}
}
My FormTypes:
class AllPersonsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('persons', CollectionType::class, array(
'entry_type' => ActivatePersonType::class
));
}
public function getName()
{
return 'person';
}
}
class ActivatePersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('active',CheckboxType::class)
->add('fullname', TextType::class);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Person',
));
}
public function getName()
{
return 'person';
}
}
You pass $persons collection as form data to AllPersonsType, while the type expects that you pass an array with persons key and persons collection as value.
$form = $this->createForm(AllPersonsType::class, ['persons' => $persons]);
I have created form which requires data transformer, but got myself into single problem: I transform data by exploding string (string should be be exploded to 3 parts), everything works, if I supply correct format string, but otherwise it throws error inside data transformer, because transformation cannot occur if wrong string format is supplied (this is expected behavior).
So the question is is there a way to validate form field for correct string before data transformation? I know that data transformation by default occurs before validation, but maybe there's a way to do it other way around?
I found one solution that might work on this thread: Combine constraints and data transformers ,
but it's looks like rough solution, besides I need to translate validation message, and I would really like to do it using default translation methods for symfony forms (without using translation service)
I thought, and also someone from symfony IRC (Iltar) suggested do it by using events, but I'm not sure how to go about this - how to attach data transformer dynamically to form field? Or maybe there's other way?
It's maybe too late but I eventually manage to do it.
Maybe it will help you.
Here is my FormType:
class PersonType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('mother', 'personSelector', array('personEntity' => $options['personEntity']));
}
}
Here is my customField where are validations:
class PersonSelectorType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array $options){
$transformer = new PersonByFirstnameAndLastnameTransformer($this->entityManager,$options);
$builder->addModelTransformer($transformer);
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmitForm'));
}
public function onPreSubmitForm(FormEvent $event){
$mother = $event->getData();
$form = $event->getForm();
$options = $form->getConfig()->getOptions();
if (!empty($mother)){
preg_match('#(.*) (.*)#', $mother, $personInformations);
if (count($personInformations) != 3){
$form->addError(new FormError('[Format incorrect] Le format attendu est "Prénom Nom".'));
}else{
$person = $this->entityManager->getRepository($options['personEntity'])->findOneBy(array('firstname' => $personInformations[1],'lastname' =>$personInformations[2]));
if ($person === null) {
$form->addError(new FormError('Il n\'existe pas de person '.$personInformations[1].' '.$personInformations[2].'.'));
}
}
}
}
}
Here is my transformer:
class PersonByFirstnameAndLastnameTransformer implements DataTransformerInterface{
public function reverseTransform($firstnameAndLastname) {
if (empty($firstnameAndLastname)) { return null; }
preg_match('#(.*) (.*)#', $firstnameAndLastname, $personInformations);
$person = $this->entityManager->getRepository($this->options['personEntity'])->findOneBy(array('firstname' =>$personInformations[1],'lastname' =>$personInformations[2]));
if (count($personInformations) == 3){
$person = $this->entityManager->getRepository($this->options['personEntity'])->findOneBy(array('firstname' =>$personInformations[1],'lastname' =>$personInformations[2]));
}
return $person;
}
public function transform($person) {
if ($person === null) { return ''; }
return $person->getFirstname().' '.$person->getLastname();
}
}
Perhaps you could pass the instance of your form to your transformer. If the string doesn't parse correctly, simply add a validation error to the form, like so:
<?php
// src/Acme/MyBundle/Form/DataTransformer/StringTransformer.php
namespace Acme\MyBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\MyBundle\Entity\MyEntity;
use Acme\MyBundle\Entity\AnotherEntity;
use Acme\MyBundle\Type\MyEntityType;
class StringTransformer implements DataTransformerInterface
{
/**
* #var MyEntityType
*/
private $form;
/**
* #param ObjectManager $om
*/
public function __construct(MyEntityType $form)
{
$this->form = $form;
}
/**
* Transforms an object (entity) to a string (number).
*
* #param MyEntity|null $entity
* #return string
*/
public function transform($value)
{
// ...
}
/**
* Transforms a string (number) to an object (entity).
*
* #param string $number
*
* #return MyEntity|null
*
* #throws TransformationFailedException if object (entity) is not found.
*/
public function reverseTransform($value)
{
$collection = new ArrayCollection();
try{
$vals = explode(',', $value);
foreach($vals as $v){
$entity = new AnotherEntity();
$entity->setValue($v);
$collection->add($v);
}
} catch(\Exception $e){
$this->form
->get('my_location')
->addError(new FormError('error message'));
}
return $collection;
}
}
but it's looks like rough solution, besides I need to translate validation message, and I would really like to do it using default translation methods for symfony forms (without using translation service)
I know this question is old, but as any answer has been marked yet as the right solution, I share with you another approach.
emottet solution, using a presubmit listener to validate the data before the model transformer has been applied, is a good approach, based on this discussion.
If you want to keep using Symfony validation system for these errors too, you could use Symfony validator service (ValidatorInterface) in your pre-submit listener and pass it the required constraints, for example:
$builder
->add('whatever1', TextType::class)
->add('whatever2', TextType::class)
;
$builder->get('whatever1')
->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
/** #var ConstraintViolationListInterface $errors */
if ($errors = $this->validator->validate($data, new Choice([
'choices' => $allowedChoices,
'message' => 'message.in.validators.locale.xlf'
]))) {
/** #var ConstraintViolationInterface $error */
foreach ($errors as $error) {
$form->addError(new FormError($error->getMessage()));
}
}
})
->addModelTransformer($myTransformer)
;
Kind of redundant, but it works. More info here.
I'm writing my own CAPTCHA class and when the form doesn't validate, I don't want to pre-populate the captcha input with the previous answer, for obvious reasons. I just want to the clear the input before it's rendered.
I've discovered the data option is only for the default value, which is overwritten by what the user enters. I tried the following code:
$form->get('captcha')->setData(null);
.. After the request is bound with the form, but an AlreadyBoundException is thrown. I have actually managed to get it working with:
if (isset($formView->children['captcha'])) {
$formView->children['captcha']->vars['value'] = null;
}
But that just looks wrong, and definitely not up to Symfony standards. I've looked through the other options you can provide when building the form, but I can't see anything of note.
Does anyone have any idea?
By the way, I half expect Symfony2 comes packaged with a CAPTCHA solution, this is mainly a learning exercise while I get used to the framework.
I think you want to handle this form field like Symfony handles a password field: it won't get populated. Let's take a look at the PasswordType:
namespace Symfony\Component\Form\Extension\Core\Type;
class PasswordType extends AbstractType
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['always_empty'] || !$form->isSubmitted()) {
$view->vars['value'] = '';
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'always_empty' => true,
'trim' => false,
));
}
//...
}
So, it's pretty simple: just add $view->vars['value'] = '' in the buildView method of your FormType (i.e. CaptchaType). That means the data of the field is not being cleared, but it won't be passed to the Twig template. Different approach, but the result is the same: the password field stays empty after validation failed.
If you are really lazy, you can use the PasswordType, but since the input of that field will be masked (*****), will that make an annoying captcha field even worse.
Your Form Type maybe look like this:
class CaptchaType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['value'] = '';
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {#inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'captcha';
}
}
Edit:
Just found that CaptchaBundle took the same approach.
There is a funny way to modify the Request before handling it. However I'd look into Stephan's answer as it seems more clean.
Something like so:
public function indexAction(Request $request)
{
$form = $this->createForm(Form::class);
$subData=$request->request->get('form');
$subData['task']=null;
$request->request->set('form',$subData);
$form->handleRequest($request);
if ($form->isValid()) {
//do stuff
}
return $this->render('default/index.html.twig', array(
'form' => $form->createView()
));
}
Get submitted data with the name 'form' as an array of values, change the said value to null, then set the request's value with the new one and have the form handle it.
And a simple form
class Form extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task')
->add('save', SubmitType::class);
}
}
No matter what you type, the data will always be null after submitting the form. Of course, you need to verify the captcha before setting the value to null.
You can pass an incomplete entity to the action called when your control finds form invalid.
public function updateAction(Request $request, $id)
{
$entity = $this->EM()->getRepository('Bundle:Entity')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Entity entity.');
}
$form = $this->createForm(new RecommendationType()
,$entity
,array(
'attr' => array(
,'entity' => $entity
)
)
);
$form->bind($request);
if ($form->isValid()) {
$this->EM()->persist($entity);
$this->EM()->flush();
return $this->redirect($this->generateUrl('entity_show'
,array('id' => $id)));
} else {
$entity->setCapthca(Null);
}
return $this->render('Bundle:Entity:edit.html.twig'
,array(
'entity' => $entity
,'form' => $form->createView()
)
);
}
The create action would have similar modification.