Symfony 2 add dynamic form; values from database - php

I would like to create a google categories matching(first field categorie from database and second field a user autocomplete field from google categories) form where i have an entity CategoriesConfig :
private $id;
/**
* #var string
*
* #ORM\Column(name="category_site", type="string", length=100)
*/
private $categorySite;
/**
* #var string
*
* #ORM\Column(name="category_google", type="string", length=100)
*/
private $categoryGoogle;
In my Controller i tried this
/**
* #Route("/adminDashboard/categoriesMatching", name="googleShopping_categories")
* #Security("has_role('ROLE_SUPER_ADMIN')")
*/
public function categoriesMatchingAction(Request $request)
{
// create a task and give it some dummy data for this example
$idSite = $this->get('session')->get('_defaultWebSite')->getId();
$categories = $this->getDoctrine()->getRepository('DataSiteBundle:SiteCategory')->findBy(array('IdSite' => $idSite));;
$categories_config = new CategoriesConfig();
//var_dump($categories);exit;
$form = $this->createForm(new CategoriesConfigType($categories), $categories_config);
return $this->render('GoogleShoppingBundle:Default:categoriesMatching.html.twig', array(
'form' => $form->createView()
));
}
And my form type : CategoriesConfigType:
class CategoriesConfigType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
private $site_categories;
public function __construct ($site_categories) {
$this->site_categories = $site_categories;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach($this->site_categories as $k => $categorie){
$builder
->add('categorySite')
->add('categoryGoogle');
}
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sp\GoogleShoppingBundle\Entity\CategoriesConfig'
));
}
}
I would like to have as many categories rows as row fields(website itecategorie and google categorie)
The result is like that:
Thank you in advance!

Your loop on $this->categories is uneffective, because the elements you add have the same name each time (categorySite and categoryGoogle), so the FormBuilder replaces the form field each time, instead of adding another one.
However, if you want your form to handle a Collection of CategoryConfigs, you need to take a different approach.
1) Create a CategoriesConfigType (as you did), but who is responsible of only a single CategoriesConfig entity
class CategoriesConfigType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('categorySite')
->add('categoryGoogle');
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sp\GoogleShoppingBundle\Entity\CategoriesConfig'
));
}
}
2) Then use CollectionType field to manipulate your form as a whole collection of CategoryConfigTypes:
class YourCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('categoriesConfigs', CollectionType::class, array(
'entry_type' => CategoriesConfigType::class,
'entry_options' => array('required' => false)
);
}
}

Related

Symfony Forms and ManyToMany. How to configure form with file upload field that is also EntityType field?

I need to make form field with file upload that is also part of ManyToMany entity. Now my configuration looks like below, and it works...
class ProductTypeNew extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('price')
->add('description', TextareaType::class)
->add('quantity')
->add('file', FileType::class, array('label' => 'Zdjęcie'))
;
... but I need to manually get form input in controller and sets to form entity
if ($form->isSubmitted() && $form->isValid())
{
$image = new ShopProductImages();
$file = $product->getFile();
$fileName = $this->generateUniqueFileName().'.'.$file->guessExtension();
$file->move(
$this->getParameter('shop_images_directory'),
$fileName
);
$image->setFile($fileName);
$product->addShopProductImages($image);
$product->setFile($fileName);
$em = $this->getDoctrine()->getManager();
$em->persist($image);
$em->persist($product);
$em->flush();
I would like to do something like this (but it's not working):
->add('shopProductImages', EntityType::class, array(
'by_reference' => false,
'entry_type' => FileType::class,
)
New version of form types with Embeded Forms that also cause problem:
Expected value of type "Doctrine\Common\Collections\Collection|array"
for association field
"AppBundle\Entity\ShopProducts#$shopProductImages", got
"Symfony\Component\HttpFoundation\File\UploadedFile" instead.
... with below configuration:
ProductTypeNew:
class ProductTypeNew extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', null, array('label' => 'Nazwa'))
->add('price', null, array('label' => 'Cena'))
->add('description', TextareaType::class, array('label' => 'Opis'))
->add('quantity', null, array('label' => 'Ilość'))
->add('shopProductImages', ShopProductsImagesType::class);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ShopProducts::class,
]);
}
ShopProductsImagesType:
class ShopProductsImagesType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('file', FileType::class, array('label' => 'Zdjęcie'))
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// 'data_class' => ShopProductImages::class,
'data_class' => null,
]);
}
Entity ShopProducts:
/**
* ShopProducts
*
* #ORM\Table(name="shop_products")
* #ORM\Entity
*/
class ShopProducts
{
....
/**
* INVERSE SIDE
*
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\ShopProductImages",
* mappedBy="shopProducts",
* cascade={"persist"}
* )
*/
private $shopProductImages;
Entity ShopProductImages:
* #ORM\Entity
*/
class ShopProductImages
{
/**
* #var string
*
* #ORM\Column(name="file", type="text", length=255, nullable=true)
*/
private $file;
If you use EntityType field class, the entry_type is not an expected type. It expects to use an Entity binded to your database through Doctrine, from yourbundleapp/Entity. EntityType acts like a ChoiceType, but it directly interact with the Doctrine entity declared in parameter class. You can find how it works here: https://symfony.com/doc/current/reference/forms/types/entity.html
From what I can understand, you want to be able to download files on your app, so maybe you have difficulties to understand how submission forms work on Symfony.
You have to first define your new entity (from yourbundleapp/Entity), and then pass it as an argument to your form (from yourbundleapp/Form), like this:
$image = new ShopProductImages();
$form = $this->get('form.factory')->create(ProductTypeNew::class, $image);
If you want, you can also add form in your first form by embedding it: https://symfony.com/doc/current/form/embedded.html
If I understood bad, please could you be more verbose about what you want to do and what you did?

Why did Symfony forms does not using validator normally?

I have the collection model:
class EntityList {
/**
* #var Entity[]
* #Assert\Count(min=1)
* #Assert\Valid()
*/
private $entityList;
// ...
}
The single model:
class Entity {
/**
* #var int
* #Assert\NotBlank()
*/
private $id;
/**
* #var string
* #Assert\NotBlank()
*/
private $name;
// ...
}
The collection's form:
class EntityListType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('entity', CollectionType::class, [
'entry_type' => EntityType::class,
'allow_add' => true,
'property_path' => 'entityList',
]);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefault('data_class', EntityList::class);
}
public function getBlockPrefix() {
return null;
}
}
And the entity's form:
class EntityType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('id', NumberType::class)
->add('name', TextType::class)
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefault('data_class', Entity::class);
}
}
The collection's form using in a controller:
$form = $this->createForm(EntityListType::class);
$form->handleRequest($request);
I send the package with no required title (it's an API):
curl -X POST ...
-d entity[0][id]=10
And that accept it! The method isValid returns true. But if form's data check manually with ValidationComponent as:
if ($form->isValid()) { // it's valid, okay
echo $this->get('validator')->validate($form->getData()); // it's still not valid!
}
I see correct errors ('title is required')! Why?
P.S. Symfony 3.2, stable.
P.P.S. I was found temporarily solution now. It's use $form->submit($request->request->all()); instead of $form->handleRequest($request);. But I don't understand this feature.

Symfony2 setNormalizer with defined options

I'm having a problem with Symfony options resolver where I need to specify a list of defined variables that should be normalized.
The problem is: I don't want to define all these variables again in
$resolver->setDefined();
because I have a list of defined fields in $builder and the same fields are defined in the entity SlotRequest.
Is there a different way of assigning all fields/variables from entity to resolver?
First approach:
$resolver->setDefined([
'date_form','etc..' ]);
But, it pointless because in the real world I have to normalize 10+ variables + 20 fields)
Second approach would be to parse all annotations from the entity 'SlotRequest', and then fill up an array with that object.
$resolver->setDefined($anArrayOfParsedFieldsFromEntity);
Is there a better way of doing this?
An example of using:
In controller:
$form = $this->createForm(new SlotRequestType(), new SlotRequest());
SlotRequestType:
class SlotRequestType extends AbstractType
{
/**
* #var CCriteria
*/
protected $resolved = null;
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('date_from',null,['property_path'=>'dateFrom']);
//more fields
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$this->resolved = new \CCriteria();
$resolver->setDefaults(array(
'data_class' => SlotRequest::class,
'allow_extra_fields' => true,
'method' => 'GET'
));
$resolver->setDefined([....]);// the list of fields
$resolver->setNormalizer('date_from', function (Options $options, $value) {
$dateFrom = new \DateTime($value);
$this->resolved->setStartDate($dateFrom->getTimestamp());
return $value;
});
//more normalizers
}
/**
* #return null
*/
public function getName()
{
return null;
}
/**
* #return CCriteria
*/
public function getResolved()
{
return $this->resolved;
}
Entity SlotRequest
<?php
namespace Test/Entity;
class SlotRequest
{
/**
* #var string
* #Assert\NotBlank(message="Parameter [date_from] is missing.")
* #Assert\Type(
* type="string",
* message="The value {{ value }} is not a valid {{ type }}."
* )
* #Assert\Date()
*/
public $dateFrom;
//more fields
}

save form with vich_image onetone

/**
* #Gedmo\Tree(type="nested")
* #ORM\Table(name="mKeyword")
* #ORM\Entity(repositoryClass="KeywordRepository")
*/
class Keyword {
/**
* #ORM\OneToOne(targetEntity="Image",mappedBy="keyword" ,cascade={"all"})
* #var Image
*/
private $logo;
}
/**
* #Vich\Uploadable
* #ORM\Entity
* #ORM\Table(name="mKeywordLogo")
*/
class Image {
}
form
class KeywordType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title','text')
->add('logo',new ImageType())
image form
class ImageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('file','vich_image',array(
'label'=>'logo'
));
}
when save form
get
Expected value of type "KeywordsBundle\Entity\Image" for association field "Mea\KeywordsBundle\Entity\Keyword#$logo", got "array" instead.
i add array parser in Keyword
public function setLogo($logo)
{
if(is_array($logo))
$logo = reset($logo);
$this->logo = $logo;
}
so get error
Expected value of type "KeywordsBundle\Entity\Image" for association field "KeywordsBundle\Entity\Keyword#$logo", got "Symfony\Component\HttpFoundation\File\UploadedFile" instead.
I Fond error, Form ImageType don't havee defaults, when i add this. save work fine
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Mea\KeywordsBundle\Entity\Image'
));
}

Symfony: Multiple money fields in one form

Imagine you have an item entity. Within this item entity, there is a method to get the prices for that item. Prices are saved in 3 different formats: EUR, USD and GBP.
The entities would look like this:
Entity WebshopItem.php
class WebshopItem
{
/**
* #var integer
*/
private $id;
/**
* #Gedmo\Translatable
* #var string
*/
private $title;
......
/**
* #var \Doctrine\Common\Collections\Collection
*/
private $prices;
}
Entity WebshopItemPrice.php
class WebshopItemPrice
{
/**
* #var integer
*/
private $id;
/**
* #var string
*/
private $currency;
/**
* #var string
*/
private $price;
/**
* #var \WebshopItem
*/
private $webshopItem;
}
Now I would like to create a form, which contains exactly 3 input fields. For that, I thought it would be best to use the money field. So I am creating the form like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
....
->add('prices', new WebshopPricesType());
}
The webshopPricesType looks like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('eur', 'money', array('currency' => 'EUR', 'data_class' => 'bundlePath\Entity\WebshopItemPrice'))
->add('usd', 'money', array('currency' => 'USD', 'data_class' => 'bundlePath\Entity\WebshopItemPrice'))
->add('gbp', 'money', array('currency' => 'GBP', 'data_class' => 'bundlePath\Entity\WebshopItemPrice'));
}
3 correct fields are rendered now. I only need to fill them on edit and when saving, I have to make sure they are saved. I was thinking about using a data transformer to find the correct entity, but that's not working.
How can I make sure that all 3 fields are prefilled correctly on edit and when clicking save, the 3 prices are saved?
Or should I do it on a whole different way?
Thanks!
I have never been too fond of DataTransformers and therefore I would not use them here, but they can be useful.
In this particular case, I would go for FormEvents and build the form dynamically, based on the data your entity contains.
WebShopItemType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
.....
->add('prices', 'collection', array(
'type' => new WebshopPricesType()
));
}
WebshopPricesType
class WebshopPricesType extends AbstractType{
.....
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Dynamically build form fields, **after** the data has been set
$builder->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) use ($builder){
/** #var $data WebshopItemPrice **/
$data = $event->getData();
$builder->add('price', 'money', array('currency' => $data->getCurrency()));
});
}
public function setDefaults(OptionsResolverInterface $resolver){
$resolver->setDefault(array(
'data_class' => 'bundlePath\Entity\WebshopItemPrice'
));
}
.....
}
Given that, let's glue it altogether:
public class SomeController extends Controller{
public function insertAction(){
$item = new WebshopItem();
// be sure to initialize the $prices with new `ArrayCollection`
// in order to avoid NullPointerException
// Also, be sure to bind WebshopItemPrice::$item
$item
->addPrice(new WebshopItemPrice('EUR', 0))
->addPrice(new WebshopItemPrice('USD', 0))
->addPrice(new WebshopItemPrice('GBP', 0));
// this is where POST_SET_DATA gets fired
$form = $this->createForm(new WebShopItemType(), $item);
// form is ready
}
public function editAction(){
$item = ... // fetch or whatever, be sure to fetch prices as well
// this is where POST_SET_DATA gets fired
$form = $this->createForm(new WebShopItemType(), $item);
// form is ready
}
}
I have put this together in Notepad++ and I not sure if I did some typos but as far as logic is concerned - it should work ;)

Categories