ManyToMany new value must be an array or an instance of \Traversable, "NULL" given - symfony

I have a ManyToMany relation in my Symfony 4.2.6 application and I would like for it to be possible to have this to be null.
So my first entity SpecialOffers is as follows :
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\SpecialOfferRepository")
*/
class SpecialOffer
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Neighbourhood", inversedBy="specialOffers")
*/
private $neighbourhood;
public function __construct()
{
$this->neighbourhood = new ArrayCollection();
}
/**
* #return Collection|Neighbourhood[]
*/
public function getNeighbourhood(): Collection
{
return $this->neighbourhood;
}
public function addNeighbourhood(Neighbourhood $neighbourhood): self
{
if (!$this->neighbourhood->contains($neighbourhood)) {
$this->neighbourhood[] = $neighbourhood;
}
return $this;
}
public function removeNeighbourhood(Neighbourhood $neighbourhood): self
{
if ($this->neighbourhood->contains($neighbourhood)) {
$this->neighbourhood->removeElement($neighbourhood);
}
return $this;
}
}
It is related to the neighbourhood class :
/**
* #ORM\Entity(repositoryClass="App\Repository\NeighbourhoodRepository")
*/
class Neighbourhood implements ResourceInterface
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\SpecialOffer", mappedBy="neighbourhood")
*/
private $specialOffers;
public function __construct()
{
$this->specialOffers = new ArrayCollection();
}
/**
* #return Collection|SpecialOffer[]
*/
public function getSpecialOffers(): Collection
{
return $this->specialOffers;
}
public function addSpecialOffer(SpecialOffer $specialOffer): self
{
if (!$this->specialOffers->contains($specialOffer)) {
$this->specialOffers[] = $specialOffer;
$specialOffer->addNeighbourhood($this);
}
return $this;
}
public function removeSpecialOffer(SpecialOffer $specialOffer): self
{
if ($this->specialOffers->contains($specialOffer)) {
$this->specialOffers->removeElement($specialOffer);
$specialOffer->removeNeighbourhood($this);
}
return $this;
}
}
And finally the form is
class SpecialOfferType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'neighbourhood',
EntityType::class,
[
'class' => Neighbourhood::class,
'label' => 'form.neighbourhood.label',
'translation_domain' => 'Default',
'required' => false,
'placeholder' => 'form.neighbourhood.all'
]
);
}
}
But when I don't select a specific neighbourhood for the Special offer in my form I get the following error :
Could not determine access type for property "neighbourhood" in class "App\Entity\SpecialOffer": The property "neighbourhood" in class "App\Entity\SpecialOffer" can be defined with the methods "addNeighbourhood()", "removeNeighbourhood()" but the new value must be an array or an instance of \Traversable, "NULL" given.
Is there anyway I can make it so that my special offer either contains and array of neighbourhoods or just null ?
I feel like I'm overlooking something really obvious, any help would be greatly appreciated

Test =>
$builder
->add(
'neighbourhood',
EntityType::class,
[
'class' => Neighbourhood::class,
'label' => 'form.neighbourhood.label',
'translation_domain' => 'Default',
'required' => false,
'multiple' => true,
'placeholder' => 'form.neighbourhood.all'
]
);

Since your fields on the entities are both many-to-many, thus expecting an array (or similar) and the form field is of EntityType, which will return one Entity of the expected type or null, I feel like there is some form of asymmetry.
I would consider using the CollectionType from the start or at least setting the multiple option on the form to true, so that the return value is an array.
Another option would be to add a DataTransformer to the form field, which turns null into an empty array and one entity into an array of one entity, and vice-versa.

Related

easyadmin entity field's dynamic custom choices

Installed easyadminbundle with symfony 4, configured for an entity name Delivery and it has a field associated to another entity name WeeklyMenu:
easy_amin.yaml:
Delivery:
...
form:
fields:
- { property: 'delivered'}
- { property: 'weeklyMenu', type: 'choice', type_options: { choices: null }}
I need a dynamically filtered results of weeklyMenu entity here, so I can get a list of the next days menus and so on. It's set to null now but have to get a filtered result here.
I've read about overriding the AdminController which I stucked with it. I believe that I have to override easyadmin's query builder that listing an associated entity's result.
i've figured out, here is the solution if someone looking for:
namespace App\Controller;
use Doctrine\ORM\EntityRepository;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilder;
class AdminController extends EasyAdminController {
public function createDeliveryEntityFormBuilder($entity, $view) {
$formBuilder = parent::createEntityFormBuilder($entity, $view);
$fields = $formBuilder->all();
/**
* #var $fieldId string
* #var $field FormBuilder
*/
foreach ($fields as $fieldId => $field) {
if ($fieldId == 'weeklyMenu') {
$options = [
'attr' => ['size' => 1,],
'required' => true,
'multiple' => false,
'expanded' => false,
'class' => 'App\Entity\WeeklyMenu',
];
$options['query_builder'] = function (EntityRepository $er) {
$qb = $er->createQueryBuilder('e');
return $qb->where($qb->expr()->gt('e.date', ':today'))
->setParameter('today', new \DateTime("today"))
->andWhere($qb->expr()->eq('e.delivery', ':true'))
->setParameter('true', 1)
->orderBy('e.date', 'DESC');
};
$formBuilder->add($fieldId, EntityType::class, $options);
}
}
return $formBuilder;
}
}
so the easyAdmin check if a formbuilder exists with the entity's name i.e. create<ENTITYNAME>FormBuilder(); and you can override here with your own logic.
Another approach to this would be to create new FormTypeConfigurator and overwrite choices and/or labels. And tag it as:
App\Form\Type\Configurator\UserTypeConfigurator:
tags: ['easyadmin.form.type.configurator']
and the configurator looks like this:
<?php
declare(strict_types = 1);
namespace App\Form\Type\Configurator;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Configurator\TypeConfiguratorInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormConfigInterface;
final class UserTypeConfigurator implements TypeConfiguratorInterface
{
/**
* {#inheritdoc}
*/
public function configure($name, array $options, array $metadata, FormConfigInterface $parentConfig)
{
if ($parentConfig->getData() instanceof User) {
$options['choices'] = User::getUserStatusAvailableChoices();
}
return $options;
}
/**
* {#inheritdoc}
*/
public function supports($type, array $options, array $metadata)
{
return in_array($type, ['choice', ChoiceType::class], true);
}
}

Symfony2 Conditional Validation

I have a FormType method.
<?php
class PostType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('text')
->add('paymentMethod', 'choice', array(
'required' => true,
'choices' => array(
'cach' => 'Cach',
'check'=> 'Check'
)
))
->add('checkId', 'integer', array(
'required' => true
))
->add('submit', 'submit');
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Post',
]);
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_post';
}
}
?>
The field checkId is displaying only when the choice 'check' is selected with the field paymentMethod.
Here is the javascript:
PaymentMethod = {
hideCheckId: function(){
$('.check-id').hide();
},
showCheckId: function(){
$('.check-id').fadeIn();
},
whenPaymentMethodChange: function(){
$('#appbundle_post_paymentMethod').on('change', function(){
var method = this.value ;
if(method == 'check'){
PaymentMethod.showCheckId();
}else{
PaymentMethod.hideCheckId();
}
})
}
};
PaymentMethod.hideCheckId();
PaymentMethod.whenPaymentMethodChange();
I would like the field 'checkId' to be required to TRUE only when the option check is selected.
How can I do that?
Ok thanks, Here is the solution for everyone:
Add a callback method function in your entity class.
/**
* #param ExecutionContextInterface $context
* #Assert\Callback()
*/
public function isPaymentIsCheck(ExecutionContextInterface $context)
{
if ($this->getPaymentMethod() == 'check' and $this->getCheckId() == '') {
$context->buildViolation('A check ID as to be defined')
->atPath('checkID')
->addViolation();
}
}
Don't forget to add the Assert and ExecutionContextInterface component in you entity class:
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
And finally don't forget to display errors in the twig template:
{% if form.vars.errors|length %}
<div class="alert alert-danger">{{ form_errors(form) }}</div>
{% endif %}
Hope it will help you.
More info here: http://symfony.com/doc/current/reference/constraints/Callback.html
What KevinR wrote would definitely solve your problem, but in my opinion there's even cleaner solution:
http://symfony.com/doc/current/book/validation.html#group-sequence-providers
Imagine a User entity which can be a normal
user or a premium user. When it's a premium user, some extra
constraints should be added to the user entity (e.g. the credit card
details). To dynamically determine which groups should be activated,
you can create a Group Sequence Provider. First, create the entity and
a new constraint group called Premium:
This is exactly your how you would solve this kinda of problem
public function getGroupSequence()
{
$groups = array('Post'); //Or array('Default') whichever you prefer
if ($this->getPaymentMethod() === 'Check') {
$groups[] = 'Check';
}
return $groups;
}
Of course you'll have to add validation group paymentMethod property in your entity mapping. Here's annotation example, for more examples check above link.
/**
* #Assert\IsTrue(groups={"Check"})
*/
private $checkId;

Model transformer and expected form view data mismatch

I have a Symfony 2 application, with a form that needs to store a reference to another entity (project) in a hidden field. The project entity is passed in via the form options, my plan was to have a field of the type 'hidden' that simply contains the entity id, this should then be transformed into a project entity on when the form is submitted.
I'm going about this by using a model transformer to transform between the entity to a string (it's ID). However when I try to view the form, I get the following error:
The form's view data is expected to be an instance of class Foo\BarBundle\Entity\Project, but is a(n) string. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) string to an instance of Foo\BarBundle\Entity\Project.
Here is my form class:
<?php
namespace Foo\BarBundle\Form\SED\Waste;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Foo\BarBundle\Form\DataTransformer\EntityToEntityIdTransformer;
/**
* Class WasteContractorEntryType
* #package Foo\BarBundle\Form\CommunityInvestment\Base
*/
class WasteContractorEntryType extends AbstractType
{
protected $name;
protected $type;
protected $phase;
protected $wasteComponent;
public function __construct($formName, $type, $phase, $wasteComponent)
{
$this->name = $formName;
$this->type = $type;
$this->phase = $phase;
$this->wasteComponent = $wasteComponent;
}
/**
* #return mixed
*/
public function getType()
{
return $this->type;
}
/**
* #return mixed
*/
public function getPhase()
{
return $this->phase;
}
/**
* #return mixed
*/
public function getProject()
{
return $this->project;
}
/**
* #return mixed
*/
public function getWasteComponent()
{
return $this->wasteComponent;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$em = $options['em'];
$wasteComponentTransformer = new EntityToEntityIdTransformer($em,
'Foo\BarBundle\Entity\SED\Waste\WasteComponent');
$projectTransformer = new EntityToEntityIdTransformer($em, 'Foo\BarBundle\Entity\Project');
$builder->add('id', 'hidden');
$builder->add(
$builder->create('project', 'hidden', array(
'data' => $options['project'],
'by_reference' => false
))
->addModelTransformer($projectTransformer)
);
$builder->add(
$builder->create('wasteComponent', 'hidden', array(
'data' => $this->getWasteComponent()
))
->addModelTransformer($wasteComponentTransformer)
);
$builder->add('phase', 'hidden', array(
'data' => $this->getPhase()
));
$builder->add('type', 'hidden', array(
'data' => $this->getType()
));
$builder->add('percentDivertedFromLandfill', 'text', array());
$builder->add('wasteContractor', 'entity', array(
'class' => 'Foo\BazBundle\Entity\Contractor',
'property' => 'name',
'attr' => array(
'class' => 'js-select2'
)
));
}
public function getName()
{
return $this->name;
}
/**
* {#inheritDoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => true,
'data_class' => 'Foo\BarBundle\Entity\SED\Waste\WasteContractorEntry'
))
->setRequired(array(
'em',
'project'
))
->setAllowedTypes(array(
'em' => 'Doctrine\Common\Persistence\ObjectManager',
'project' => 'Foo\BarBundle\Entity\Project'
));
}
}
And my model transformer class:
<?php
namespace Foo\BarBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
class EntityToEntityIdTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #var Entity class
*/
protected $entityClass;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om, $className)
{
$this->om = $om;
$this->entityClass = $className;
}
protected function getEntityClass()
{
return $this->entityClass;
}
/**
* Transforms an object (project) to a string (id).
*
* #param Project|null $issue
* #return string
*/
public function transform($entity)
{
if (null === $entity) {
return "";
}
return $entity->getId();
}
/**
* Transforms a string (id) to an object (project).
*
* #param string $id
*
* #return Issue|null
*
* #throws TransformationFailedException if object (project) is not found.
*/
public function reverseTransform($id)
{
if (!$id) {
return null;
}
$entity = $this->om
->getRepository($this->getEntityClass())
->find($id);
if (null === $entity) {
throw new TransformationFailedException(sprintf(
'An entity of class %s with id "%s" does not exist!',
$this->getEntityClass(),
$id
));
}
return $entity;
}
}
I've tried using a adding the transformer as a view transformer instead of a model transformer, however then I just get a slightly different error:
The form's view data is expected to be an instance of class
Foo\BarBundle\Entity\Project, but is a(n) integer. You can avoid this
error by setting the "data_class" option to null or by adding a view
transformer that transforms a(n) integer to an instance of
Foo\BarBundle\Entity\Project.
It seems that setting 'data_class' to null as suggested by the exception message above is the solution. I had previously rejected this as it seems counter-intuitive when we know that the purpose of the field is to reference a project entity.
With the 'data_class' option set to null, the hidden project field contains the project id, and upon submission, calling getProject() on the created entity returns the correct project object.

How to change option dynamically for a symfony2 form field?

In symfony 2.5.6,
how to change options dynamically in symfony2 form, by example:
// src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task')
->add('dueDate', null, array('widget' => 'single_text'))
->add('save', 'submit');
if (condition) {
//how to change option of 'task' or 'dueDate' by example
//something like this, but addOption doesn't exist and i don't find any usefull method
$builder->get('dueDate')->addOption('read_only', true)
}
}
public function getName()
{
return 'task';
}
}
Need to use event ?
http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html
Or this
foreach($builder->all() as $key => $field) {
if ($key == 'dueDate')) {
$options = $field->getOptions();
$options = array_merge_recursive($options, array('read_only' => true));
$builder->remove($key);
$builder->add($key, $field->getName(), $options);
}
}
#with 'Could not load type "dueDate"' error when i display my form in a browser!
How to to do? Best practice?
Thanks!
I dont't know what do you mean by 'best practice', but why not to do it like this:
$builder
->add('dueDate', null, array('widget' => 'single_text'))
->add('save', 'submit');
$options = [
KEY => VALUE,
....
];
if (condition) {
$options = [
ANOTHER_KEY => ANOTHER_VALUE,
....
];
}
$builder->add('task', TYPE, $options);
Another approach would be to use PRE_SUBMIT event, something like this..
$builder
->add('task')
->add('dueDate', null, array('widget' => 'single_text'))
->add('save', 'submit');
$builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'preSubmit']);
....
public function preSubmit(FormEvent $event)
{
if (CONDITION) {
$builder->remove('task');
$builder->add('task', TYPE, $NEW_OPTIONS_ARRAY);
}
}
I use this function to update options after a field has been added to a form. It basically means to regenerate the field with the data that we have, add something to the options and re-add the field, re-add its transformers and so
Put this helper function somewhere in a FormHelper class, or wherever you like
/**
* #param FormBuilderInterface $builder
* #param string $fieldName
* #param string $optionName
* #param $optionData
*/
public static function setOptionToExistingFormField(
FormBuilderInterface $builder,
string $fieldName,
string $optionName,
$optionData
): void {
if (!$builder->has($fieldName)) {
// return or throw exception as you wish
return;
}
$field = $builder->get($fieldName);
// Get some things from the old field that we also need on the new field
$modelTransformers = $field->getModelTransformers();
$viewTransformers = $field->getViewTransformers();
$options = $field->getOptions();
$fieldType = get_class($field->getType()->getInnerType());
// Now set the new option value
$options[$optionName] = $optionData;
/**
* Just use "add" again, if it already exists the existing field is overwritten.
* See the documentation of the add() function
* Even the position of the field is preserved
*/
$builder->add($fieldName, $fieldType, $options);
// Reconfigure the transformers (if any), first remove them or we get some double
$newField = $builder->get($fieldName);
$newField->resetModelTransformers();
$newField->resetViewTransformers();
foreach($modelTransformers as $transformer) {
$newField->addModelTransformer($transformer);
}
foreach($viewTransformers as $transformer) {
$newField->addViewTransformer($transformer);
}
}
And then use it like this
$builder
->add('someField', SomeSpecialType::class, [
'label' => false,
])
;
FormHelper::setOptionToExistingFormField($builder, 'someField', 'label', true);

Two form fields into one entity value

How its possible to join two separated fields (must be separated) in one form (date and time for example) to one entity propery datetime for persisting after form post ?
What is better way ? Data Transofmers ? Form events ? Form Model ? Manual setting all entity properties before persist ?
Entity:
<?php namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="event")
*/
class EventEntity
{
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
// ...
/**
* #ORM\Column(name="date_time", type="datetime", nullable=false)
*/
protected $datetime;
public function getId()
{
return $this->id;
}
// ...
public function getDateTime()
{
return $this->datetime;
}
public function setDateTime(\DateTime $datetime)
{
$this->datetime = $datetime;
}
}
FormType:
<?php namespace Acme\DemoBundle\Form\Type;
use JMS\DiExtraBundle\Annotation as DI;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class EventType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('date', 'date', [
'required' => true,
'widget' => 'single_text',
'format' => 'dd.MM.yyyy'
]
)
->add('time', 'time', [
'required' => false,
'widget' => 'single_text'
]
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\EventEntity' //Acme\DemoBundle\Form\Model\EventModel ?
));
}
public function getName()
{
return 'event';
}
}
If you set the date and time widget seperately in the datetime type, then they get seperately rendered, but validated as one field.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('datetime', 'datetime', array(
'date_widget' => 'single_text',
'time_widget' => 'single_text',
'date_format' => 'dd.MM.yyyy',
));
}
I suggest using Pazis solution, since this is the most simple one. But that would also be a perfect job for a DataTransformer:
class MyDataTransformer implements DataTransformerInterface
{
public function transform($value)
{
if (null === $value)
return;
if ($value instanceof \DateTime)
return array(
'date' => $value->format('d.m.Y'),
'time' => $value->format('H:i:s')
);
return null;
}
public function reverseTransform($value)
{
if (null === $value)
return null;
if (is_array($value) && array_key_exists('date', $value) && array_key_exists('time', $value))
return new \DateTime($value['date'] . ' ' . $value['time']);
return null;
}
}
This has the drawback, that you'd need to map every single value in your entity with this transformer, what - for sure - you don't want to. But with small form-tricks, this can be avoided. Therefore you add a subform to your form, which includes a date and a time field and the added Transformer. You'll need to map ("property_path"-option) your DateTime object to this subform or just name it "correctly", so the form framework can map it by name.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
$builder->create('datetime', 'form')
->add('date', 'date', $optionsForDate)
->add('time', 'time', $optionsForTime)
->addViewTransformer(new MyDataTransformer())
);
}
The code may not be perfectly running, but i hope the idea behind splitting one entity property into two (or more) form fields is clear.
héhé, that's a good question.
I would choose the easiest, most generic, reusable solution.
I wouldn't implement methods on my model just for sake of form mapping, but if it makes sense, why not simply using the model api ?
<?php
class EventEntity
{
// assume $this->datetime is initialized and instance of DateTime
public function setDate(\DateTime $date)
{
// i don't know if this works!
$this->datetime->add($this->datetime->diff($date));
}
public function setTime(\DateTime $date)
{
$this->datetime->add($this->datetime->diff($date));
}
}

Resources