I'm working on a Symfony application to add meter values to a meter. A meter can have a set of measurements, and for each measurement I want to display a value form to enter values.
For this I have a function in a controller that creates an ArrayCollection of new elements depending on the corresponding measurements like so:
/**
* #Route("/{id}/add", name="metervalue_add", methods={"GET","POST"})
*/
public function add(Request $request, Meter $meter): Response
{
$metervalues = new ArrayCollection();
$measurements = $meter->getMeasurements();
// create an empty metervalue for each measurement of the meter
foreach ($measurements as $measurement) {
$mv = new MeterValue();
$mv->setMeter($meter);
$mv->setMeasurement($measurement);
$metervalues->add($mv);
}
$form = $this->createForm(MeterValueAddType::class, ['metervalues' => $metervalues]);
$form->handleRequest($request);
// ... form submitting stuff
// ...
return $this->renderForm('metervalue/add.html.twig', [
'form' => $form
]);
}
The corresponding MeterValueAddType looks like
class MeterValueAddType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('metervalues', CollectionType::class, [
'entry_type' => MeterValueType::class
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}
When I render the form all works fine, the empty objects are rendered as expected, I can submit the form and all data is inserted correctly in the DB, including measurement and meter ids.
However, in my template I cannot access the properties of a metervalue object, like metervalues.measurement or metervalue.meter
{% for metervalue in form.metervalues %}
{{ form_widget(metervalue.value) }}
{{ form_widget(metervalue.date) }}
Name of measurement: {{ metervalue.measurement.name }} <-- this throws the following error
{% endfor %}
Error: Neither the property "measurement" nor one of the methods
"measurement()",
"getmeasurement()"/"ismeasurement()"/"hasmeasurement()" or "__call()"
exist and have public access in class
"Symfony\Component\Form\FormView".
I don't understand why I can't access the properties in here just to display them, as they are assigned above in the controller and stored correctly in the DB on save...
The property "measurement" and a correspoding "getmeasurement()" exist and e.g. if I display all saved objects in a list I can access these
The hint from msg was pointing me in the right direction. Had to use
{{ metervalue.vars.data.meter.name }}
to get it working! Thanks
Related
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).
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
}
I've got a Company that has many Employees. In my form, I want the user to be able to dynamically add employees (easy enough). EmployeeType (an AbstractType) is compound, containing a first and last name. On form submission, Symfony doesn't seem to carry over the data from the form into the constructor for the "new" Employee. I get an erro
ArgumentCountError: Too few arguments to function Employee::__construct() ... 0 passed in ... and exactly 3 expected
Showing and editing existing Employees works, so I'm confident my relationships, etc., are all correct.
Abbreviated code:
Company
class Company
{
protected $employees;
public function __construct()
{
$this->employees = new ArrayCollection();
}
public function addEmployee(Employee $employee)
{
if ($this->employees->contains($employee)) {
return;
}
$this->employees->add($employee);
}
public function removeEmployee(Employee $employee)
{
if (!$this->employees->contains($employee)) {
return;
}
$this->employees->removeElement($employee);
}
}
Employee
class Employee
{
// ... firstName and lastName properties...
public function __construct(Company $company, $firstName, $lastName)
{
$this->company = $company;
$this->company->addEmployee($this);
}
// ...getter and setter for firstName / lastName...
}
CompanyType
class CompanyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('employees', CollectionType::class, [
'entry_type' => EmployeeType::class,
'allow_add' => true,
'allow_delete' => false,
'required' => false,
]);
// ...other fields, some are CollectionType of TextTypes that work correctly...
}
}
EmployeeType
class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('firstName')
->add('lastName');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Employee::class,
]);
}
}
CompanyController
class CompanyController
{
// Never mind that this is a show and not edit, etc.
public function showAction()
{
// Assume $this->company is a new or existing Company
$form = $this->createForm(CompanyType::class, $this->company);
$form->handleRequest($this->request);
if ($form->isSubmitted() && $form->isValid()) {
$company = $form->getData();
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($company);
$entityManager->flush();
}
// set flash message, redirect, etc.
}
// ...render view...
}
The above will work when modifying existing Employees, just not when creating new ones. Debugging from within the Symfony code, I can see that no data exists for the new employees, so it's trying to find a closure or definition for empty_data in CompanyType. I've tried this every which way (via configureOptions, and empty_data option when building the CompanyType::buildForm form), e.g. https://symfony.com/doc/current/form/use_empty_data.html. My gut tells me I don't even need to do this, because the form data should not be empty (I explicitly filled out the fields).
I tried using a model transformer as well. In that case, the transformation from the form (second function argument passed to new CallbackTransformer) isn't even hit.
The view properly sets name attributes when adding new employee fields, e.g. form[employees][1][firstName], etc. That isn't the problem. It also sends the right data to the controller. I confirmed this by inspecting the form submission data via CompanyType::onPreSubmit (using an event listener).
I also have a CollectionType of TextTypes for other things in CompanyType, those work fine. So the issue seems to be related to the fact that EmployeeType is compound (containing multiple fields).
Hopefully the above is enough to illustrate the problem. Any ideas?
UPDATE:
It seems the issue is there isn't an instantiation of Employee for Symfony to work with. Internally, each field gets passed to Symfony\Component\Form\Form::submit(). For existing employees, there is also an Employee passed in. For the new one, it's null. That explains why it's looking for empty_data, but I don't know why I can't get empty_data to work.
The solution was to define empty_data in the compound form, and not the CollectionType form.
My situation is a little weird, because I also need the instance of Company in my EmployeeType, as it must be passed to the constructor for Employee. I accomplished this by passing in the Company as form option into configureOptions (supplied by the controller), and then into entry_options. I don't know if this is best practice, but it works:
CompanyController
Make sure we pass in the Company instance, so it can be used in EmployeeType when building a new Employee:
$form = $this->createForm(CompanyType::class, $this->company, [
// This is a form option, valid because it's in CompanyType::configureOptions()
'company' => $this->company,
]);
CompanyType
class CompanyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('employees', CollectionType::class, [
// ...
// Pass the Company instance to the EmployeeType.
'entry_options' => [ 'company' => $options['company'] ],
// This was also needed, apparently.
'by_reference' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// Allows the controller to pass in a Company instance.
'company' => null,
]);
}
}
EmployeeType
Here we make sure empty_data properly builds an Employee from the form data.
class EmployeeType extends AbstractType
{
private $company;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('firstName')
->add('lastName');
// A little weird to set a class property here, but we need the Company
// instance in the 'empty_data' definition in configureOptions(),
// at which point we otherwise wouldn't have access to the Company.
$this->company = $options['company'];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Employee::class,
'empty_data' => function (FormInterface $form) use ($resolver) {
return new Employee(
$this->company,
$form->get('firstName')->getData(),
$form->get('lastName')->getData(),
);
},
]);
}
}
Viola! I can now add new employees.
Hope this helps other people!
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
I am trying to get a dataTransformer to work on an entity field in symfony 2.
context:
form displays sails that user can select (checkboxes)
this is the first step in a multi-step sail ordering process (later steps display options available for each sail, colors, etc)
This is my form type class:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Co\QuoteBundle\Form\DataTransformer\SailCollectionToStringsTransformer;
class PartsTypeStep1 extends AbstractType {
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Co\QuoteBundle\Entity\Parts',));
$resolver->setRequired(array('sailsAvailable', 'em'));
}
public function buildForm(FormBuilderInterface $formBuilder, array $options)
{
$transformer = new SailCollectionToStringsTransformer($options['em']);
$formBuilder->add(
$formBuilder->create('mainsailparts', 'entity', array(
'class' => 'CoQuoteBundle:Mainsail',
'choices' => $options['sailsAvailable']['mains'],
'multiple' => true,
'expanded' => true,
'label' => 'Mainsails',))
->addModelTransformer($transformer)); //line 58
}
public function getName() {
return 'partsStep1';
}
}
The above works with no errors, but does not display the transformed data. The view is:
__ Race main
__ Cruising main
(__ stands for checkbox)
However, the view I want is:
__ Race main ($1400)
__ Cruising main ($800)
The transformer I have is:
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Co\QuoteBundle\Entity\Sail;
use Doctrine\Common\Collections\ArrayCollection;
class SailCollectionToStringsTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* Transforms a collection of sails to a collection of strings.
* #param ISail|null $sail
* #return string
*/
public function transform($sailCollection)
{
if (null === $sailCollection) {
return null;
}
$labels = new ArrayCollection();
foreach($sailCollection as $sail){
$labels[] = $sail->getName().' ($'.$sail->getBuildPrice().')';
}
return $labels;
}
//reverse transformer... not the issue (yet) because the forward transformer doesn't work
}
When running this through the netbeans debugger, an empty array is passed to the transformer. However, if I change line 58 to ->addViewTransformer($transformer)); and debug, it correctly passes two booleans with the sail id's as the array keys to the transformer. Unfortunately, I can't use the ViewTransformer because that no longer contains the original strings to change.
Why does the ArrayCollection that should contain the main sails get passed to the transformer as an empty ArrayCollection? The function returns an empty $labels collection.
I'm not sure what I am doing wrong... Help is much appreciated!!!!
Thanks.
I never did find a way how to implement what I was attempting to do. However, the workaround I used is described below.
for the form type class, I used a form event (symfony2 book), and I saved the boatId (that the sails correspond to) in the parts object in the controller, like so:
$partsObject = new Parts($boat->getId());
$form = $this->createForm(new PartsTypeStep1(), $partsObject, array(
'em' => $this->getDoctrine()->getManager()));
The form type class now looks like this:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Doctrine\ORM\EntityRepository;
class PartsTypeStep1 extends AbstractType {
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Co\QuoteBundle\Entity\Parts',));
$resolver->setRequired(array('em'));
}
public function buildForm(FormBuilderInterface $formBuilder, array $options)
{
$factory = $formBuilder->getFormFactory();
$em = $options['em'];
$formBuilder->addEventListener(
FormEvents::PRE_SET_DATA,
function(FormEvent $event) use($factory, $em){
$form = $event->getForm();
$data = $event->getData();
if (!$data || !$data->getDateTime()) {
return;
}
else {
$boatClass = $data->getBoatId();
$formOptions = array(
'class' => 'CoQuoteBundle:Mainsail',
'multiple' => true,
'expanded' => true,
'property' => 'displayString',
'label' => 'Mainsails',
'query_builder' => function(EntityRepository $er) use ($boatClass) {
return $er->createQueryBuilder('m')
->where('m.boatType = :boatClass')
->setParameter('boatClass', $boatClass);
},
);
$form->add($factory->createNamed('mainsailparts', 'entity', null, $formOptions));
}
}
);
}
public function getName() {
return 'partsStep1';
}
I also needed to add the displayString property in the Mainsail class (I only added a getter, not an actual variable for the string). So the Mainsail class now has this:
public function getDisplayString(){
return $this->name . ' - ' . $this->descr . ' ($' . $this->buildPrice . ')';
}
The only issue I ran into with this workaround is what happens if the query returns an empty result, because twig will automatically render the form label ('Mainsails') whether or not it has any checkboxes to render. I got around that issue like this:
{% if form.mainsailparts|length > 0 %}
<div class="groupHeading">{{ form_label(form.mainsailparts) }}</div>
{% for child in form.mainsailparts %}
{# render each checkbox .... #}
{% endfor %}
{% else %}
{% do form.mainsailparts.setRendered %}
{% endif %}
I don't know if this is the recommended solution in this case, but it does work with form validation (at least disallowing progression if no sails are selected, I don't need anything more rigorous).
I'm not going to mark this as the answer since it doesn't answer the question (how to apply transformer to entity field), but it is a workaround for anyone dealing with the same problem.