Symfony custom form weird property access errors - symfony

I've got this strange problem, here is example usage of my custom ThingType class.
->add('photos', 'namespace\Form\Type\ThingType', [
'required' => false,
])
if the field name is photos everything works as expected, but if I change my entity field to let's say photosi, run generate entities, and change the form field name, this error is thrown:
Neither the property "photosi" nor one of the methods
"addPhotosus()"/"removePhotosus()", "setPhotosi()", "photosi()",
"__set()" or "__call()" exist and have public access in class
"AppBundle\Entity\Product".
I guess the problem comes from Symfony trying to generate getter method name for my entity. Why is this addPhotosus method name generated? How can I solve this?
EDIT:
I'm using model transformer when showing the data to the user.
$builder->addModelTransformer(new CallbackTransformer(
function ($imagesAsText) {
if (!$imagesAsText) {
return null;
}
$newImages = [];
foreach($imagesAsText as $img) {
$newImages[] = $img->getID();
}
return implode(',', $newImages);
},
function ($textAsImages) use ($repo) {
$images = [];
foreach(explode(',', $textAsImages) as $imgID) {
$img = $repo->findOneById($imgID);
if ($img) {
$images[] = $img;
}
}
return $images;
}
));
The actual field is TextType::class with entity ids in it for example 1,10,32,51. The model transformer transforms this data to entities. Setting 'data_class' to my form type seems irrelevant, because the actual form type is a part of entity. I mean I have Product entity and Photo entity, photos is array of photo entity. So in my ThingType, what data_class should I use, photo or product?
Thanks

The fist parameter of the add method for a form, should be one of the mapped attributes of the data_class of the form, usually selected inside the form as
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product'
));
}
That isn't related to the form name. So , you are trying to access to a "photosi" attribute inside your Product class.
Hope this help you.

Ok so for the first point you need to remember that Symfony is looking for setXX() and getXX()method in your entity for each entry of your form.
If you change your variable name you need to update the form :
->add('newName', XXType::class, [
'required' => false,
])
and you're entity by changing the variable
class Entity
{
/**
* #ORM\Column(type="string", length=255)
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName(oldName){
$this->oldName = $oldName;
return $this
}
}
then run the command
php bin/console make:entity --regenerate
and symfony will upload your entity by itself
class Entity
{
/**
* #ORM\Column(type="string", length=255)
* #SerializedName("title")
* #Groups({"calendar"})
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName($oldName){
$this->oldName = $oldName;
return $this
}
public function getNewName(){
return $this->newName;
}
public function setNewName($newName){
$this->newName = $newName;
return $this
}
note that the old get and set method are not deleted by the script
note as well that in your specific case of photosi, symfonyguess that the "i" is a plural mark and look for addPhotosus() methods
For the edit it looks very unclear and has nothing to do with the first question. Consider reading : doc on collectionType

Related

Symfony: How to persist new entities with ManytoMany relation in embedded form

Use case
TLDR;
I have two entities with a ManytoMany relation. I want to persist two new objects at the same time with one single form. To do so, I created two FromTypes with one embedding the other.
A bit more...
The goal is to provide users with a form to make an inquiry for an event. The Event entity consists of properties like starttime, endtime e.g. that are simple properties of Event aswell as a location (Location entity with a OneToMany relation, one Event has one Location, one Location can have many Events) and a contactperson (Contact entity with a ManyToMany relation, one Event can have multiple Contacts, one Contact can have multiple Events). For the particular form in question it is enough (and a deliberate choice) for the user to provide only one Contact as that is the bare minimum needed and enough for a start.
To build reusable forms, there are two simple forms with LocationFormType and ContactFormType and a more complex EventFormType. More complex as it embedds both LocationFormType and ContactFormType to create an Event entity "in one go" so to speak.
When I build the EventFormType with option A (see code below), the form renders correct and the way it is intended. Everything looks fine until the form is submitted. Then the problem starts...
Problem
On $form->handleRequest() the FormSystem throws an error because the embedded form is not providing a Traversable for the related object.
The property "contact" in class "App\Entity\Event" can be defined with the methods "addContact()", "removeContact()" but the new value must be an array or an instance of \Traversable.
Obviously the embedded FormType is providing a single object, while the property for the relation needs a Collection. When I use CollectionType for embedding (option B, see code below), the form is not rendering anymore as CollectionType seemingly expects entities to be present already. But I want to create a new one. So there is no object I could pass.
My Code
#[ORM\Entity(repositoryClass: EventRepository::class)]
class Event
{
...
#[ORM\ManyToMany(targetEntity: Contact::class, inversedBy: 'events')]
...
private Collection $contact;
...
public function __construct()
{
$this->contact = new ArrayCollection();
}
...
/**
* #return Collection<int, Contact>
*/
public function getContact(): Collection
{
return $this->contact;
}
public function addContact(Contact $contact): self
{
if (!$this->contact->contains($contact)) {
$this->contact->add($contact);
}
return $this;
}
public function removeContact(Contact $contact): self
{
$this->contact->removeElement($contact);
return $this;
}
...
}
#[ORM\Entity(repositoryClass: ContactRepository::class)]
class Contact
{
...
#[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'contact')]
private Collection $events;
public function __construct()
{
$this->events = new ArrayCollection();
}
...
/**
* #return Collection<int, Event>
*/
public function getEvents(): Collection
{
return $this->events;
}
public function addEvent(Event $event): self
{
if (!$this->events->contains($event)) {
$this->events->add($event);
$event->addContact($this);
}
return $this;
}
public function removeEvent(Event $event): self
{
if ($this->events->removeElement($event)) {
$event->removeContact($this);
}
return $this;
}
}
class EventFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
...
// option A: embedding related FormType directly
->add('contact', ContactFormType::class, [
...
])
// option B: embedding with CollectionType
->add('contact', CollectionType::class, [
'entry_type' => ContactFormType::class
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Event::class,
]);
}
}
class ContactFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add(
... // here I only add the fields for Contact entity, no special config
)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Contact::class,
]);
}
}
Failed solutions
'allow_add' => true with prototype
I found solutions suggesting to set 'allow_add' => true on the CollectionType and render the form in Twig with ..vars.prototype
Thats a hacky solution (so I think) in my use case. I don't want to add multiple forms. And without 'allow_add' there is no prototype in CollectionType, so the data to render the form is missing.
provide empty object to CollectionType
To omit 'allow_add' => true but have an object to render the form correctly, I tried passing an empty instance of Contact in my controller
$eventForm = $this->createForm(EventFormType::class);
if(!$eventForm->get('contact')) $eventForm->get('contact')->setData(array(new Contact()));
That works on initial load, but creates issues when the form is submitted. Maybe I could make it work, but my gut gives me 'hacky vibes' once again.
Conclusion
Actually I think I'm missing some basic point here as I think my use case is nothing edgy or in any way unusual. Can anyone give me a hint as where I'm going wrong with my approach?
P.S.: I'm unsure wether my issue was discussed (without a solution) over on Github.
Okay, so I solved the problem. For this scenario one has to make use of Data Mappers.
It is possible to map single form fields by using the 'getter' and 'setter' option keys (Docs). In this particular case the setter-option is enough:
->add('contact', ContactFormType::class, [
...
'setter' => function (Event &$event, Contact $contact, FormInterface $form) {
$event->addContact($contact);
}
])
The addContact()-method is provided by Symfonys CLI when creating ManyToMany relations, but can be added manually aswell (Docs, SymfonyCast).

Symfony choice type array : Unable to transform value for property path : Expected an array

First of all, I'm a new user of Symfony
And actually I'm customing my form EasyAdmin with some fields and I have an issue with this one :
ChoiceField::new('villa')->setChoices($villasChoices)->allowMultipleChoices()
I get this error because of the allowMultipleChoices() func :
Unable to transform value for property path "villa": Expected an array.
My field is actually a collection type, That's why I have this error, there is my entity
#[ORM\OneToMany(mappedBy: 'Name', targetEntity: Villa::class)]
private Collection $Villa;
public function __construct()
{
$this->Villa = new ArrayCollection();
}
/**
* #return Collection<int, Villa>
*/
public function getVilla(): Collection
{
return $this->Villa;
}
How can I remplace Collection type by Array ?
Try to use AssociationField instead of ChoiceField.
AssociationField list entities automatically, while ChoiceField is made to pass array manually.
->setFormTypeOption('multiple', true)
Will do the trick for multiple choice, if your property allow multiples values (OneToMany, ManyToMany)
You must have a form like this:
$form = $this->createFormBuilder($yourEntity)
->add('villa', EntityType::class, [
'class' => Villa::class
'multiple' => true
])
;
Set the 'villa' item with EntityType::class and set in the options 'multiple' => true.
In your Villa entity you must set a __tostring method like this:
public function __toString(): string
{
return $this->name; //name is a string value
}

Easy Admin 3 (Symfony 4) AssociationField in OneToOne relationship shows already associated entities

Using Symfony 4.4 with Easy Admin 3:
I've a OneToOne relationship
class Usuario
{
...
/**
* #ORM\OneToOne(targetEntity=Hora::class, inversedBy="usuario", cascade={"persist", "remove"})
*/
private $hora;
...
}
class Hora
{
...
/**
* #ORM\OneToOne(targetEntity=Usuario::class, mappedBy="hora", cascade={"persist", "remove"})
*/
private $usuario;
...
}
I've got a CRUD Controller for Usuario:
class UsuarioCrudController extends AbstractCrudController
{
public function configureFields(string $pageName): iterable
{
...
return [
...
AssociationField::new('hora', 'Hora'),
];
Everything seems ok, but in the admin form for "Usuario", the field "hora" shows all values in database, even the ones already assigned to other "Usuario" entities:
I would like the dropdown control to show only not assigned values, PLUS the value of the actual "Usuario" entity, so the control be easy to use.
Which is the proper way to do this with easyadmin?
I've managed to code the field to show only the not associated "Hora" values, using $this->getDoctrine() and ->setFormTypeOptions([ "choices" => in UsuarioCrudController class,
but I am not able to access the actual entity being managed, nor in UsuarioCrudController class (maybe there it is not accesible) neither in Usuario class (I've tried here __construct(EntityManagerInterface $entityManager) to no avail as the value doesn't seem to be injected, dunno why).
It is possible to customize a few things in easy admin by either overriding EasyAdmin methods or listening to EasyAdmin events.
Example of methods:
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
public function createEntity(string $entityFqcn)
public function createEditForm(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormInterface
//etc..
Example of events:
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityDeletedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityUpdatedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityDeletedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
You could override easy admin createEditFormBuilder or createNewFormBuilder method, this way you could access the current form data and modify your hora field.
Something like :
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface {
$formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
$unassignedValues = $this->yourRepo->findUnassignedValues();
$data = $context->getEntity()->getInstance();
if(isset($data) && $data->getHora()){
//if your repo return an ArrayCollection
$unassignedValues = $unassignedValues->add($data->getHora());
}
// if 'class' => 'App\Entity\Hora' is not passed as option, an error is raised (see //github.com/EasyCorp/EasyAdminBundle/issues/3095):
// An error has occurred resolving the options of the form "Symfony\Bridge\Doctrine\Form\Type\EntityType": The required option "class" is missing.
$formBuilder->add('hora', EntityType::class, ['class' => 'App\Entity\Hora', 'choices' => $unassignedValues]);
return $formBuilder;
}
Currently, easyadmin3 still lack documentation so sometimes the best way to do something is to look at how easy admin is doing things.
fwiw, the actual entity being edited can be accessed in a Symfony easyadmin CrudController's configureFields() method using:
if ( $pageName === 'edit' ) {
...
$this->get(AdminContextProvider::class)->getContext()->getEntity()->getInstance()
...
This way in configureFields() I could add code to filter my entities:
$horas_libres = $this->getDoctrine()->getRepository(Hora::class)->findFree();
and then add the actual entity value also, which is what I was trying to do:
array_unshift( $horas_libres,
$this->get(AdminContextProvider::class)->getContext()->getEntity()->getInstance()->getHora() );
Now the field can be constructed in the returned array with "choices":
return [ ...
AssociationField::new('hora', 'Hora')->setFormTypeOptions([
"choices" => $horas_libres
]),
]

Display empty form collection item in Symfony without using javascript

I'm trying to display a form with a collection. The collection should display an empty sub-form. Due to the projects nature I can't rely on JavaScript to do so.
Googling didn't help and I does not seem to work by adding an empty entity to the collection field.
What I have so far:
public function indexAction($id)
{
$em = $this->getDoctrine()->getManager();
$event = $em->getRepository('EventBundle:EventDynamicForm')->find($id);
$entity = new Booking();
$entity->addParticipant( new Participant() );
$form = $this->createForm(new BookingType(), $entity);
return array(
'event' => $event,
'edit_form' => $form->createView()
);
}
In BookingType.php buildForm()
$builder
->add('Participants', 'collection')
In the Twig template
{{ form_row(edit_form.Participants.0.companyName) }}
If I put the line $entity->addParticipant( new Participant() ); in indexAction() I get an error saying:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
Yanic\EventBundle\Entity\Participant. You can avoid this error by
setting the "data_class" option to
"Yanic\EventBundle\Entity\Participant" or by adding a view transformer
that transforms an instance of class
Yanic\EventBundle\Entity\Participant to scalar, array or an instance
of \ArrayAccess.
If I delete the said line Twig complains:
Method "0" for object "Symfony\Component\Form\FormView" does not exist in
/Applications/MAMP/htdocs/symfony-standard-2.1/src/Yanic/EventBundle/Resources/views/Booking/index.html.twig
at line 27
EDIT: The addParticipant is the default methos generated by the doctrine:generate:entities command
/**
* Add Participants
*
* #param \Yanic\EventBundle\Entity\Participant $participants
* #return Booking
*/
public function addParticipant(\Yanic\EventBundle\Entity\Participant $participants)
{
$this->Participants[] = $participants;
return $this;
}
I'm sure that I'm doing something wrong, but can't find the clue :-(
I guess you are a bit lost on Symfony2 form collection, though I think you already read http://symfony.com/doc/current/cookbook/form/form_collections.html.
Here I will just emphasize the doc, help other SO readers, and exercise myself a bit on answering question.. :)
First, you must have at least two entities. In your case, Booking and Participant. In Booking entity, add the following. Because you use Doctrine, Participant must be wrapped in ArrayCollection.
use Doctrine\Common\Collections\ArrayCollection;
class Booking() {
// ...
protected $participants;
public function __construct()
{
$this->participants = new ArrayCollection();
}
public function getParticipants()
{
return $this->participants;
}
public function setParticipants(ArrayCollection $participants)
{
$this->participants = $participants;
}
}
Second, your Participant entity could be anything. Just for example:
class Participant
{
private $name;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
}
Third, your BookingType should contain collection of ParticipantType, something like this:
// ...
$builder->add('participants', 'collection', array('type' => new ParticipantType()));
Fourth, the ParticipantType is straightforward. According to my example before:
// ...
$builder->add('name', 'text', array('required' => true));
Last, in BookingController, add the necessary amount of Participant to create a collection.
// ...
$entity = new Booking();
$participant1 = new Participant();
$participant1->name = 'participant1';
$entity->getParticipants()->add($participant1); // add entry to ArrayCollection
$participant2 = new Participant();
$participant2->name = 'participant2';
$entity->getParticipants()->add($participant2); // add entry to ArrayCollection
think you have to add here the type:
->add('Participants', 'collection', array('type' => 'YourParticipantType'));
Could you also paste in here the declaration of your addParticipant function from the model? Seems that there's something fishy too.

Doctrine 2.2.2, cascade not working with many-to-many association?

I'm using Doctrine 2.2.2 on Symfony 2.0.17-DEV and PHP 5.3.14. I've a problem with many-to-many associations with cascade options. Example is so simple, hope than someone from this fantastic board can help me.
Anyway, Meta superclass is the owner of a relation with User. Relevant field and constructor only:
abstract class Meta
{
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="meta")
* #ORM\JoinTable(name="meta_users",
* joinColumns={#ORM\JoinColumn(onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(onDelete="CASCADE")}
* )
*/
protected $users;
public function __construct()
{
$this->users = new ArrayCollection();
}
public function addUser(User $user)
{
$this->users[] = $user;
return $this;
}
public function getUsers()
{
return $this->users;
}
}
(Implementations, just empty classes, are Label and Category)
It's simple and it works, actually. I mean adding or removing users from a meta actually adds/deletes the corresponding rows in join table.
The problem occurs doing the opposite: creating/editing an user and assigning meta. User define the association with meta this way, and adds a cascade="all" option:
class User
{
/**
* #ORM\ManyToMany(targetEntity="Meta", mappedBy="users", cascade={"all"})
*/
protected $meta;
public function __construct()
{
$this->meta = new ArrayCollection();
}
public function addMeta(Meta $meta)
{
$this->meta[] = $meta;
return $this;
}
public function getMeta()
{
return $this->meta;
}
}
I'm quite new to Doctrine, but this is not working. In my Symfony 2 form for creating/editing an User, i've added a field of type entity, just selecting all meta:
$builder
->add('meta', 'entity', array(
'label' => 'Meta',
'class' => 'Acme\HelloBundle\Entity\Meta',
'property' => 'select_label',
'multiple' => true,
'expanded' => true,
))
;
No changes on any table when assigning (using checkboxes) meta to an user. What's wrong? I'm sure i'm missing something, but i can't find what.
Speak for what i know.
Cascade option has nothing to do with persisting your join table associations. It's should be used when a new Meta entity is found in the relation meta for entity User (or an old entity is removed). That is when, in your form, you add some input to create new meta or remove existing one, for example using collection for field type that Symfony 2 provides). Or just when you do:
$newMeta = new Meta();
$user->addMeta($meta);
$em->persist($user); // A new entity was found in the relation meta
As i far as i understand you want to persist the relation itself; Doctrine always look for the owning side in order to persist entities. Meaning that from the inverse side you want to persist the user first, than persist each meta adding or removing the user if the corresponding checkbox is checked or not.
Your field is entity type, meaning all meta are fetched from your table and those already assigned to the user are marked as checked.
I remember doing something similar, here is a "pseudo" controller code:
$em->persist($user); // Perstist the inverse side
// This is what user selected
$selectedMeta = $user->getMeta();
// All meta coming from your database
$allMeta = $em->getRepository('YourBundle::Meta')->find();
// Loop on the owning side
foreach($allMeta as $meta)
{
// Is current meta selected?
$isSelected = $selectedMeta->contains($meta);
// Does this meta have already the user in it?
$hasUser = $meta->getUsers()->contains($user);
// To be removed: not selected and with the user
if(!$isSelected && $hasUser)
$meta->getUsers()->removeElement($user);
// To be added: selected and without the user
if($isSelected && !$hasUser)
$meta->addUser($user);
$em->persist($meta); // Persist the owning side and the association
}
// Apply
$em->flush();
Waiting for a confirmation too!

Resources