I'm pretty new to Symfony and I'm trying to upload multiple files (images) using Symfony Forms and the Vichuploader-Bundle.
My TestObjectType looks like this, which should hold a collection of Image objects:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('images', CollectionType::class, array(
'entry_type' => new ImageFormType(),
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => TestObject::class
));
}
ImageFormType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('imageFile', VichImageType::class, array(
'required' => false,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Image::class
));
}
Part of the controller code:
public function newAction(Request $request)
$form = $this->createForm(TestObjectType::class, $testObject);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($testObject);
$em->flush();
return $this->redirectToRoute("home");
}
}
I get the following error while submitting:
Neither the property "imageFile" nor one of the methods "getImageFile()", "imageFile()", "isImageFile()", "hasImageFile()", "__get()" exist and have public access in class "TestBundle\Entity\TestObject".
My TestObject does not have an imageFile property, however my Image object does. So what is missing here? Why is the imageFile property of the Image object not used? I've already read How to Upload Files and CollectionType Field but it did not helped much.
EDIT
Maybe a snippet of my entities could be useful:
TestObject
/**
* #ORM\OneToMany(targetEntity="TestBundle\Entity\Image", mappedBy="testObject")
*/
private $images;
Image
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #Vich\UploadableField(mapping="product_image", fileNameProperty="imageName")
*
* #var File|\Symfony\Component\HttpFoundation\File\UploadedFile $image
*/
private $imageFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $imageName;
/**
* #ORM\ManyToOne(targetEntity="TestBundle\Entity\TestObject", inversedBy="images")
* #ORM\JoinColumn(name="test_object_id", referencedColumnName="id")
*/
private $testObject;
I think I found the solution: Symfony`s cache make me angry! A simple
php app/console cache:clear
did the trick! Argh, that took me hours!
Related
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?
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.
I have a class with a multiple choice property:
...
/**
* #ORM\Column(type="array", name="majority_types")
* #Constraints\Choice(callback="getAvailableMajorityTypes", multiple="true")
*/
private $majorityTypes;
...
public static function getAvailableMajorityTypes()
{
return array(
self::SIMPLE_MAJORITY,
self::UNANIMITY_MAJORITY,
self::THREE_FIFTHS_MAJORITY,
self::ONE_THIRD_MAJORITY,
self::FOUR_FIFTHS_MAJORITY
);
}
...
I also have a form class for this class:
...
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('majorityTypes', ChoiceType::class, array(
'multiple' => true,
))
...
->getForm();
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyClass',
));
}
But the choices from getAvailableMajorityTypes are not rendered.
I simply followed these steps: http://symfony.com/doc/master/reference/constraints/Choice.html#supplying-the-choices-with-a-callback-function, but for some reason it doesn't work.
Edit:
I see that using static choices as annotations neither works (choices={"foo1", "foo2"}). The only way it works is passing the choices directly in the add method when creating the form. I've not found out the problem yet.
If I refer to your words:
But the choices from getAvailableMajorityTypes are not rendered.
It seems you're confused between rendering of options in your form's select field and the Choice constraint.
You've only implemented the constraint in your code, but you also need to add the options to your select. Like this:
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('majorityTypes', ChoiceType::class, array(
'multiple' => true,
'choices' => YourEntity::getAvailableMajorityTypes()
))
I've never used this annotation, however in the documentation the callback is public static:
// src/AppBundle/Entity/Author.php
namespace AppBundle\Entity;
class Author
{
public static function getGenders()
{
return array('male', 'female');
}
}
If you follow the documentation and make your method static as well the annotation should work.
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)
);
}
}
I have a many to many relation that is actually made of two many to ones on a third entity.
Domain ← one-to-many → DomainTag ← many-to-one → Tag
I'm building a Rest API, and I want to add domains to a Tag via a specific route. Everything works fine when the tags has no association.
But when I try to add new domains to a tag, there is an error if the tag already as one (or more) domain(s).
The error is
The form's view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is an instance of class AppBundle\Entity\Domain. You can avoid this error by setting the "data_class" option to "AppBundle\Entity\Domain" or by adding a view transformer that transforms an instance of class AppBundle\Entity\Domain to scalar, array or an instance of \ArrayAccess.
Here are the three entities :
Tag
class Tag
{
…
/**
* #ORM\OneToMany(targetEntity="DomainTag", mappedBy="tag", cascade={"persist", "remove"})
*/
private $domainTags;
…
}
DomainTag
class DomainTag
{
…
/**
* #var Tag
*
* #ORM\ManyToOne(targetEntity="Tag", inversedBy="domainTags")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="tag_id", referencedColumnName="id")
* })
*/
private $tag;
/**
* #var Domain
*
* #ORM\ManyToOne(targetEntity="Domain", inversedBy="domainTags")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="domain_id", referencedColumnName="id")
* })
*/
private $domain;
…
}
Domain
class Domain
{
…
/**
* #ORM\OneToMany(targetEntity="DomainTag", mappedBy="domain", cascade={"persist", "remove"})
*/
private $domainTags;
…
}
Here is the form
class AddDomainTagFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('domainTags', 'collection', array(
'type' => new DomainTagFormType(),
'allow_add' => true,
))
;
}
// BC for SF < 2.7
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Tag',
'csrf_protection' => false,
));
}
public function getName()
{
return 'api_set_domaintag';
}
}
DomainTagFormType
class DomainTagFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('weight')
->add('domain', 'entity', array(
'class' => 'AppBundle:Domain',
'property' => 'id',
'multiple' => false,
'expanded' => true
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\DomainTag',
'csrf_protection' => false,
));
}
// BC for SF < 2.7
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
public function getName()
{
return 'api_domaintag';
}
}
And finally, the controller
/**
* #Rest\Put("/{id}/domains", requirements={"id" = "\d+"})
* #Security("has_role('ROLE_SUPER_ADMIN')")
* #Rest\View
*/
public function setDomainAction(Request $request, $id)
{
$tagManager = $this->get('app.manager.tag');
$domainManager = $this->get('app.manager.domain');
$tag = $tagManager->find($id);
if (!$tag) {
return new Response('Tag not found', 404);
}
$form = $this->createForm(new AddDomainTagFormType(), $tag, array('method' => 'PUT'));
$form->handleRequest($request);
if ($form->isValid()) {
foreach ($tag->getDomainTags() as $dt) {
$dt->setTag($tag);
$tagManager->persist($dt);
}
$tagManager->flush($tag);
return new Response('', 204);
} else {
return $this->view($form, 400);
}
}
Do I have to remove all DomainTags from the Tag and the recreate all associations ? Or is there a way to just add/remove DomainTags ?
Thank you