Array collection is automatically converting to object when flusing to mongodb - php

I have following collection called config_settings in my mongo db
/**
* #MongoDB\Document(collection="config_settings", repositoryClass="AppBundle\Repository\SettingRepository")
* #MongoDB\HasLifecycleCallbacks()
*/
class Setting
{
/**
* #MongoDB\Id()
* #var ObjectId $id
*/
protected $id;
/**
* #MongoDB\Field(type="string")
* #var string $name
*/
protected $name;
/**
* #MongoDB\Field(type="raw")
* #var array<array> $value
*/
protected $value;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param mixed $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* #return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* #param mixed $value
*/
public function setValue($value): void
{
$this->value = $value;
}
}
I have a form that take values and name and saved it to the database a follow
<?php
namespace AppBundle\Form;
use AppBundle\Document\Setting;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SettingCollectionType extends AbstractType
{
/**
* {#inheritdoc}
* #param FormBuilderInterface<string|FormBuilderInterface> $builder
* #param array<array> $options
* #return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [];
foreach ($options['featureKeys'] as $key) {
$choices[$key] = $key;
}
$builder
->add('name', ChoiceType::class, [
'label' => 'Feature',
'choices' => $choices,
'required' => true,
])
->add('value', CollectionType::class, array(
'entry_type' => TextType::class,
'prototype' => true,
'allow_add' => true,
'allow_delete' => true,
'label' => false,
))
->add('submit', SubmitType::class, [
'attr' => [
'class' => 'btn btn-outline-secondary'
],
]);
}
/**
* {#inheritdoc}
* #param OptionsResolver $resolver
* #return void
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Setting::class,
'featureKeys' => null
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return '';
}
}
So here is the problem, whenever I try to update the form and change the value, it always flush and saved it as an object instead of saving it as an array.
This is before flushing into my database.
And this is after flushed into my database.
Here is how i saved form inside my controller and it is very simple.
if ('POST' === $request->getMethod()) {
$form->handleRequest($request);
if ($form->isValid()) {
$dm->persist($entity);
$dm->flush();
}
}
Am i doing something wrong ?

MongoDB needs to have an array with consecutive indices to have it saved as an array in the database. Most probably it's not hence it's being saved as an object (with numeric keys I presume). Try using a collection type for your value field as it ensures what you're saving to the database is in fact a list. What collection does behind a scene is an additional array_values call while raw type saves the data as is.

Related

Symfony: Expected argument of type "?Doctrine\Common\Collections\Collection", "array" given at property path

i have a entity called DynamicForm, which looks like this:
<?php
namespace App\Entity\Product\DynamicForm;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Entity;
/**
* Class DynamicForm
* #package App\Entity\Product
*
* #Entity
* #ORM\Table(name="product_dynamic_form")
*/
class DynamicForm
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="id", type="integer", unique=true, nullable=false)
*/
private ?int $id = null;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Product\DynamicForm\Component\Text", mappedBy="dynamicForm")
* #ORM\JoinColumn(name="component_text_id", referencedColumnName="id")
*/
private ?Collection $textComponents;
/**
* #return Collection|null
*/
public function getTextComponents(): ?Collection
{
return $this->textComponents;
}
/**
* #param Collection|null $textComponents
*
* #return DynamicForm
*/
public function setTextComponents(?Collection $textComponents): DynamicForm
{
$this->textComponents = $textComponents;
return $this;
}
}
Also i created a related type for it - DynamicFormType:
<?php
namespace App\Type\Product\DynamicForm;
use App\Entity\Product\DynamicForm\DynamicForm;
use App\Type\Product\DynamicForm\Component\TextType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DynamicFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('textComponents', CollectionType::class, [
'entry_type' => TextType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'label' => ' '
])
->add('submit', SubmitType::class, [
'label' => 'form.basic.save'
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', DynamicForm::class);
}
}
The TextType entry type class from the namespace App\Type\Product\DynamicForm\Component\TextType looks like this:
<?php
namespace App\Type\Product\DynamicForm\Component;
use App\Entity\Product\DynamicForm\Component\Text;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type as FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class TextType extends AbstractType
{
private TranslatorInterface $translator;
/**
* TextType constructor.
*
* #param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label', FormType\TextType::class, [
'required' => true,
'translation_domain' => 'wedding',
'label' => 'setting.form.dynamic_by_user.query_to_guest'
])
->add('type', FormType\ChoiceType::class, [
'required' => true,
'translation_domain' => 'wedding',
'label' => 'setting.form.dynamic_by_user.select_type',
'attr' => [
'class' => 'enriched',
'data-search-placeholder' => $this->translator->trans('select.search'),
'data-search-no-results-text' => $this->translator->trans('select.search_no_results_found')
],
'choice_translation_domain' => 'wedding',
'choices' => [
'setting.form.dynamic_by_user.type_text' => Text::TYPE_TEXT_FIELD,
'setting.form.dynamic_by_user.type_textarea' => Text::TYPE_TEXT_AREA,
'setting.form.dynamic_by_user.type_email' => Text::TYPE_EMAIL_FIELD,
'setting.form.dynamic_by_user.type_number' => Text::TYPE_NUMBER_FIELD,
]
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', Text::class);
}
}
While i try to transmit the form, the request in the controller contains the form data as you can see it, in the following screenshot:
My problem is now that i always get the following error (while handling the request for the form via ->handleRequest($request) on created form in controller):
Expected argument of type "?Doctrine\Common\Collections\Collection", "array" given at property path "textComponents".
I have such collection settings also in other classes, but without problems - I don't know any further, can anyone please assist me or see the error?
(I am using Symfony version 5.2.9, if u need any further info just ask for it - I will give it to you as soon as possible)
Trying to add a constructor in your entity
public function __construct()
{
$this->textComponents = new ArrayCollection();
}
Add addTextComponent and removeTextComponent methods intead of setTextComponents
public function addTextComponent(Text $textComponent): self
{
$textComponent->setDynamicForm($this);
$this->textComponents->add($textComponent);
return $this;
}
public function removeTextComponent(Text $textComponent): self
{
$this->textComponents->removeElement($textComponent);
return $this;
}
Add 'by_reference' => false in the textComponents form params
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('textComponents', CollectionType::class, [
'entry_type' => TextType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'label' => ' ',
'by_reference' => false,
])
->add('submit', SubmitType::class, [
'label' => 'form.basic.save'
]);
}
I was able to fix this by doing this, that allows us to pass a Collection or an empty array or an array of your entity which I think is Text:
/**
* #param Collection|Text[] $textComponents
*
* #return DynamicForm
*/
public function setTextComponents($textComponents): DynamicForm
{
$this->textComponents = $textComponents;
return $this;
}

Symfony CollectionType many to many relation in edit form

I have crud with 3 entities. Meal, Product and ProductsQuantity. Between Meal and ProductQuantity is relation many to many. Adding data is working fine, all data are saving to entities but problem is when in want to edit form. Then I got error:
The form's view data is expected to be an instance of class MealBundle\Entity\ProductsQuantity, but is an instance of class Doctrine\ORM\PersistentCollection. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms an instance of class Doctrine\ORM\PersistentCollection to an instance of MealBundle\Entity\ProductsQuantity.
I tried with data_class option to null, and setting fetch="EAGER" on the relation but it doesn't solved the problem.
Meal:
/**
* #Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string
* #ORM\Column(name="name", type="string", length=255)
*/
protected $name = "";
/**
* #var ProductsQuantity[]|Collection
* #ORM\ManyToMany(targetEntity="ProductsQuantity", inversedBy="meal", cascade={"persist"}, fetch="EAGER")
* #ORM\JoinTable(name="meal_products_quantity_relations",
* joinColumns={#ORM\JoinColumn(name="meal_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="products_quantity_id", referencedColumnName="id")}
* )
*/
protected $productsQuantity;
/**
* Meal constructor.
*/
public function __construct()
{
$this->productsQuantity = new ArrayCollection();
}
/**
* #return int
*/
public function getId(): int
{
return $this->id;
}
/**
* #param int $id
* #return Meal
*/
public function setId(int $id): Meal
{
$this->id = $id;
return $this;
}
/**
* #return string
*/
public function getName(): string
{
return $this->name;
}
/**
* #param string $name
* #return Meal
*/
public function setName(string $name): Meal
{
$this->name = $name;
return $this;
}
/**
* #return Collection|ProductsQuantity[]
*/
public function getProductsQuantity()
{
return $this->productsQuantity;
}
/**
* #param Collection|ProductsQuantity[] $productsQuantity
* #return Meal
*/
public function setProductsQuantity($productsQuantity)
{
$this->productsQuantity = $productsQuantity;
return $this;
}
ProductQuantity:
/**
* #Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var Product
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
* #ORM\ManyToOne(targetEntity="Product", inversedBy="productsQuantity")
*/
protected $product;
/**
* #var integer
* #ORM\Column(name="amount", type="integer")
*/
protected $amount;
/**
* #var $meal
* #ORM\ManyToMany(targetEntity="MealBundle\Entity\Meal", mappedBy="productsQuantity")
*/
protected $meal;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #param mixed $id
* #return ProductsQuantity
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* #return Product
*/
public function getProduct(): ?Product
{
return $this->product;
}
/**
* #param Product $product
* #return ProductsQuantity
*/
public function setProduct(Product $product): ProductsQuantity
{
$this->product = $product;
return $this;
}
/**
* #return int
*/
public function getAmount(): ?int
{
return $this->amount;
}
/**
* #param int $amount
*/
public function setAmount(int $amount): void
{
$this->amount = $amount;
}
/**
* #return mixed
*/
public function getMeal()
{
return $this->meal;
}
/**
* #param mixed $meal
* #return ProductsQuantity
*/
public function setMeal($meal)
{
$this->meal = $meal;
return $this;
}
Meal form:
class MealType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
parent::buildForm($builder, $options);
/** #var Meal $meal */
$meal = $options['data'];
$data = null;
if(!empty($meal)) {
$data = $meal->getProductsQuantity();
} else {
$data = new ProductsQuantity();
}
$builder
->add('name', TextType::class,[
'label' => 'Nazwa Dania',
'required' => true
])->add('productsQuantity', CollectionType::class, [
'data_class' => null,
'label' => 'Produkty',
'entry_type' => ProductsQuantityType::class,
'allow_add' => true,
'data' => ['productsQuantity' => $data],
'prototype_name' => '__product__',
'entry_options' => [
'allow_extra_fields' => true,
'label' => false
],
'prototype' => true
])->add('addProduct', ButtonType::class, [
'label' => 'Dodaj kolejny produkt',
'attr' => [
'class' => 'btn-default addProductEntry'
]
])->add('submit', SubmitType::class, [
'label' => 'Dodaj'
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired('productsQuantity');
$resolver->setDefaults([
'data_class' => Meal::class
]);
}
}
ProductsQuantity form:
class ProductsQuantityType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('product', EntityType::class,[
'class' => Product::class,
'label' => 'Nazwa produktu',
])
->add('amount', NumberType::class, [
'label' => 'Ilość',
'required' => true,
'attr' => [
'placeholder' => 'ilość'
]
])
->add('removeProduct', ButtonType::class, [
'label' => 'X',
'attr' => [
'class' => 'btn-danger removeProductEntry'
]
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ProductsQuantity::class,
]);
}
}
If I change 'data' => ['productsQuantity' => $data] to 'data' => ['productsQuantity' => new ProductsQuantity()] there is no error but have empty ProductsQuantity part of MealType form. Can anyone tell me how to fix this?

Could not determine access type for property

I am trying to create a form with an entity collection.
Here is my code :
Colle entity :
* #ORM\Table(name="colle")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"colle"="Colle","colleQC"="ColleQC", "colleQR"="ColleQR"})
*/
class Colle
{
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="Polycopie", mappedBy="colle", cascade={"persist", "remove"})
*/
protected $polycopies;
public function __construct()
{
$this->polycopies = new ArrayCollection();
}
/**
* #return ArrayCollection
*/
public function getPolycopies()
{
return $this->polycopies;
}
/**
* Add Polycopie
*
* #param Polycopie $polycopie
* #return Colle
*/
public function addPolycopie(Polycopie $polycopie)
{
$this->polycopies[] = $polycopie;
return $this;
}
/**
* Remove Polycopie
*
* #param Polycopie $polycopie
*/
public function removePolycopie(Polycopie $polycopie)
{
$this->polycopies->removeElement($polycopie);
}
}
Polycopie entity :
class Polycopie
{
/**
* #ORM\ManyToOne(targetEntity="Colle", inversedBy="polycopies", cascade={"persist"})
* #ORM\JoinColumn(name="id_colle", referencedColumnName="id")
*/
protected $colle;
public function getColle(): ?Colle
{
return $this->colle;
}
public function setColle(Colle $colle): self
{
$this->colle = $colle;
return $this;
}
}
Form :
class MiseEnLigneFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('polycopies', CollectionType::class,
['label' => false,
'label_attr' => ['class' => 'active'],
'entry_type' => PolycopieFormType::class,
'entry_options' => [
'data_class' => Polycopie::class],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => Colle::class]);
}
}
PolycopieForm :
class PolycopieFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('nom', TextType::class, ['label' =>'Nom',
'label_attr' => ['class'=>'active']]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => Polycopie::class]);
}
}
I keep getting this error :
Could not determine access type for property "polycopies" in class "App\Entity\ColleQC"
I tried clearing the cache but it doesn't work.
I made some research and it should be because something is missing such as a getter or setter but I can't find what is missing in my code.

Symfony form validate entity

I have two entoty User and Location and I crate model with two entity and create form for this model and add validate_group for this form? but ahen I check form is valid - form always valid, but entity is emthy and entity have assert not blank fields, what I'am doing wrong ?
entities
class User implements UserInterface, \JsonSerializable
{
use GedmoTrait;
/**
* #var integer
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #Assert\NotBlank(groups={"admin_user_post"})
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $firstName;
class Location
{
/**
* #var integer
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #Assert\NotBlank(groups={"admin_user_post"})
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $address;
create form
class CreateUser extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('user', new UserType(), ['validation_groups' => ['admin_user_post']]);
$builder->add('location', new LocationType(), ['validation_groups' => ['admin_user_post']]);
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AdminBundle\Model\CreateUserModel',
'csrf_protection' => false,
'validation_groups' => ['admin_user_post']
));
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstName')
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\User',
'csrf_protection' => false,
'validation_groups' => ['admin_user_post']
));
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('address')
->add('cityObject', null, array('attr' => array('placeholder' => 'Select city')));
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Location',
'csrf_protection' => false,
'validation_groups' => ['admin_user_post']
));
}
and action
$entity = new CreateUserModel();
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()
&& $form->get('user')->isValid()
&& $form->get('location')->isValid()
) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity->getLocation());
$entity->getUser()->setLocation($entity->getLocation());
$em->persist($entity->getUser());
$em->flush();
$user = $entity->getUser();
return $this->redirect($this->generateUrl('admin_users_show', array('id' => $user->getId())));
}
/**
* Creates a form to create a User entity.
*
* #param CreateUserModel $entity The entity
*
* #return \Symfony\Component\Form\Form The form
*/
private function createCreateForm(CreateUserModel $entity)
{
$form = $this->createForm(new CreateUser(), $entity, array(
'validation_groups' => ['admin_user_post'],
'action' => $this->generateUrl('admin_users_create'),
'method' => 'POST',
));
$form->add('submit', 'submit', array('label' => 'Create'));
return $form;
}
I try in action
$error = $this->get('validator')->validate($form->getData()->getUser(), ['admin_create_user']);
but still have empty $error
Why form is valid true ? or how correct valid form model with my entities and assert in this entities ?
Add to 'CreateUser' form 'cascade_validation' option to validate nested forms, and check that's annotation method for your constrains was specified at config.yml
# app/config/config.yml
framework:
validation: { enable_annotations: true }

Adding choices in entityType

I'm learning by myself the symfony framework (my job is not about developing, I'm not a developer) and I find out most of case the solution but here, is one what I didn't know how to manage.
I have 2 entity :
Product:
/**
* Product
*
* #ORM\Table(name="product")
* #ORM\Entity(repositoryClass="ProductBundle\Repository\ProductRepository")
* #UniqueEntity("productNumber")
*/
class Product
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="productNumber", type="string", length=255, unique=true)
* #Assert\Regex(
* pattern="/[0-9][.][0-9]{3}/",
* message="It should be like 1.234"
* )
*/
private $productNumber;
/**
* #ORM\ManyToOne(targetEntity="ProductGroup")
*/
private $productGroup;
/**
* Constructor
*/
public function __construct()
{
}
}
Camera :
/**
* Camera
*
* #ORM\Table(name="camera")
* #ORM\Entity(repositoryClass="ProductBundle\Repository\CameraRepository")
*/
class Camera
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="modele", type="string", length=255, unique=true)
*/
private $modele;
/**
* #var string
*
* #ORM\Column(name="description", type="text")
*/
private $description;
/**
*
* #ORM\ManyToOne(targetEntity="Product")
*/
private $product;
/**
* #ORM\ManyToMany(targetEntity="CustomField", inversedBy="camera", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $customFields;
/**
* Constructor
*/
public function __construct()
{
$this->customFields = new ArrayCollection();
}
}
My form :
namespace ProductBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
class CameraType extends AbstractType {
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('product', EntityType::class, [
'class' => 'ProductBundle:Product',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('p')
->select('p')
->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
->where('c.product IS NULL')
;
},
'attr' => [
'required' => true,
],
'choice_label' => 'productNumber',
])
->add('modele', TextType::class, [
'label' => "Modele",
])
->add('description', TextType::class, [
'label' => "Description",
])
->add('customFields', CollectionType::class, [
'entry_type' => CustomFieldType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'required' => false,
'attr' => [
'class' => 'customfield'
]
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'ProductBundle\Entity\Camera'
));
}
}
When I add a camera, I would like only the Product:productNumber where are available (not take by a camera), the querybuilder is working but my issue concern the edit form, it show only available productNumber so it's changing every time I need to edit this camera.
What can I handle this ? Should I try to found another way to add a productNumber ? do you have a "trick" ?
I hope you will understand the problem and my english because it's not my first language.
Have a nice day.
Edit : I'm on Symfony 3.1.4
I presume on new form your choice field shows only unused ProductBundle:Camera entity, and on edit form it should show saved ProductBundle:Camera entity and all unused ones.
You should look into Form Event Subscribers
You need to implement two event listeners PRE_SET_DATA and PRE_SUBMIT.
Here is one way to do it. Something like this works on SF 2.8
First you will have to create product entity form from custom ProductFieldSubscriber which becomes EventSubscriberInterface:
$builder->addEventSubscriber(new ProductFieldSubscriber('product', [])
Now ProductFieldSubscriber should look something like this (untested)
namespace ProductBundle\Form\EventListener;
use Symfony\Component\Form\FormInterface,
Symfony\Component\Form\FormEvent,
Symfony\Component\EventDispatcher\EventSubscriberInterface,
Symfony\Component\Form\FormEvents,
Doctrine\ORM\EntityRepository,
Symfony\Bridge\Doctrine\Form\Type as DoctrineTypes
;
class ProductFieldSubscriber implements EventSubscriberInterface
{
private $propertyPathToSelf;
public function __construct($propertyPathToSelf, array $formOptions=[]) {
$this->propertyPathToSelf = $propertyPathToSelf;
$this->formOptions = $formOptions;
}
public static function getSubscribedEvents() {
return [
FormEvents::PRE_SET_DATA => 'onPreSetData',
FormEvents::PRE_SUBMIT => 'onPreSubmit',
];
}
private function addForm(FormInterface $form, $selfId = null) {
$formOptions = array_replace_recursive ([
'class' => 'ProductBundle:Product',
'placeholder' => null,
'compound' => false,
'query_builder' => function (EntityRepository $er) use ($selfId) {
$qb = $er->createQueryBuilder('p')
->select('p')
->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
->where('c.product IS NULL')
;
if (null !== $selfId) {
$qb
->orWhere($qb->expr()->eq('p.product', ':existingId'))
->setParameter('existingId', $selfId->getId())
;
}
return $qb;
},
],
$this->formOptions
);
if ($selfId) {
$formOptions['data'] = $selfId;
}
$form->add($this->propertyPathToSelf, DoctrineTypes\EntityType::class, $formOptions);
}
public function onPreSetData(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$selfIdTypeMethod = "get{$this->propertyPathToSelf}";
$selfId = $data->$selfIdTypeMethod();
$this->addForm($form, $selfId);
}
public function onPreSubmit(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
$selfId = array_key_exists($this->propertyPathToSelf, $data) ? $data[$this->propertyPathToSelf] : null;
$this->addForm($form, $selfId);
}
}
Query builder would be simpler if you had mapped entity relations.
Bonus update:
form option 'placeholder' => null, takes care that no default 'empty' option is available.
form option 'required' => true, forces html5 form popup validation.
Then you should use something like entity #assert notations and use validator constraints on entity attribute:
use Symfony\Component\Validator\Constraints as Assert;
/**
* #var string
*
* #Assert\NotNull()
* #ORM\Column(name="modele", type="string", length=255, unique=true)
*/
private $modele;
You could also disallow edit form from opening via controller editAction (maybe some redirect) and twig, where you could hide edit button.

Categories