how to access collection member labels in collection extension - symfony

I have extended the collection form field type with CollectionTableExtension. I am trying to set a variable, FormView::$vars['headers'] so that collection can be output with default headers when is_table is true and no user-defined headers are available. The default headers should be the same as the labels which would normally be applied to collection members.
e.g.
if FooType has 3 fields, foo, bar and fibble, its labels will be Foo, Bar and Fibble (after humanizing with Twig template) or the values stored in the label attribute of each property.
So if my DoofusType has a collection of FooTypes,
$builder->add('foos', 'collection', array('type'=>'acc_foo', 'is_table'=>true));
should result in a view where headings for the collection will be Foo, Bar and Fibble or the values of the label attributes of the FooType if they are set.
Here is my collection extension:
<?php
namespace ACC\MainBundle\Form;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CollectionTableExtension extends AbstractTypeExtension
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setOptional(array('is_table'));
$resolver->setOptional(array('headers'));
$resolver->setOptional(array('caption'));
}
public function getExtendedType()
{
return 'collection';
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (array_key_exists('is_table', $options)) {
$view->vars['is_table'] = $options['is_table'];
if (array_key_exists('caption', $options)) $view->vars['caption'] = $options['caption'];
if (array_key_exists('headers', $options)) {
$view->vars['headers'] = $options['headers'];
}else{
//harvest labels from collection members, but HOW?
}
}
}
}
The issue is that I don't know how to access the properties of the collection element type from inside the collection extension. I could access the first element in the collection like this:
if($form->has('0')) {
foreach($form->get('0')->all() as $item)
$view->vars['headers'][] = $item->getName();
}
but that doesn't help if the collection is empty. And it doesn't help if the collection element type has defined labels. Any ideas?

After much slogging through Symfony source code, I've found a way to do it. Essentially, in ::buildView a dummy collection element of the correct type must be instantiated so that its labels or properties can be extracted. Here's what I did:
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (array_key_exists('is_table', $options)) {
$view->vars['is_table'] = $options['is_table'];
if (array_key_exists('caption', $options)) $view->vars['caption'] = $options['caption'];
if (array_key_exists('headers', $options)) {
if($options['headers']===false){ //no headers
unset($view->vars['headers']);
}else if (is_array($options['headers'])){ //headers passed in
$view->vars['headers'] = $options['headers'];
}
}else { //harvest labels from collection elements
$elementtype = $form->getConfig()->getOption('type');
//should be a guard clause here so types that won't supply good headers (e.g. a collection of text fields) will skip the rest
$element = $form->getConfig()->getFormFactory()->create($elementtype); //get dummy instance of collection element
$fields = $element->all();
$headers = array();
foreach($fields as $field){
$label= $field->getConfig()->getOption('label');
$headers[] = empty($label) ? $field->getName() : $label;
}
$view->vars['headers'] = $headers;
}
}
}
If anyone has a cleaner method, I'm all ears.

Related

Normalize a collection with API Platform

I manage to get a filtered collection of my Note entities with API Platform, using the #ApiFilter(SearchFilter::class) annotation.
Now I want to convert the json response which is an hydra collection
Example :
{
"#context": "/api/contexts/Note",
"#id": "/api/notes",
"#type": "hydra:Collection",
"hydra:member": []
}
to an archive containing one file by Note and return its metadata.
Example :
{
"name": "my_archive.zip",
"size": 12000,
"nb_of_notes": 15
}
I want to keep the SearchFilter benefits. Is the Normalization the good way to go ?
How to declare the normalizer ? How to access the collection/array of Notes in my normalize() method ?
According to the documentation symfony custom_normalizer , you can create a custom normalizer for your Note entity (for example NoteNormalizer). In the supportsNormalization method your must precise that the normalizer will only affect your Note entity by providing Note entity class. So in the normalize method, you will get each item of your ArrayCollection of Note. If you want to be sure, you can make a dump to $data variable (dd($data)) inside this normalize method, and you will have the first element of you ArrayCollection.
that's how I tried to understand it.
namespace App\Serializer;
use App\Entity\Note;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class NoteNormalizer implements ContextAwareNormalizerInterface
{
private $normalizer;
public function __construct(ObjectNormalizer $normalizer) // Like in documentation you can inject here some customer service or symfony service
{
$this->normalizer = $normalizer;
}
public function normalize($topic, $format = null, array $context = [])
{
$data = $this->normalizer->normalize($topic, $format, $context);
$data['name'] = 'some name';
$data['size'] = 12000;
$data['nb_of_notes'] = 15;
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof Note;
}
}
Or if you want you can use this command to generate it automatically :
php bin/console make:serializer:normalizer
And give the name : NoteNormalizer
Simply create a "collection Normalizer" :
note: works the same for vanilla symfony projects too.
namespace App\Serializer;
use App\Entity\Note;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
class NoteCollectionNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function supportsNormalization($data, $format = null, array $context = []): bool
{
if(!is_array($data) || (!current($data) instanceof Note)) {
return false;
}
return true;
}
/**
* #param Note[] $collection
*/
public function normalize($collection, $format = null, array $context = [])
{
// ...
}
}

how to visit each Entity's property when `serializer.pre_serialize` event is raised

I would like to visit recursively each property of the serializing Entity, check if a string is set and verify that the metadata property is properly set to string, otherwise change it in order to allow the serialization.
Imagine a users property which is an ArrayCollection, but I force the value to be a string in corner cases.
I set a SerializationSubscriber to catch the serializer.pre_serialize event, but I'm not finding any doc for take advantage of the Visitor and surroundings.
Any hint?
class MyEventSubscriber implements JMS\Serializer\EventDispatcher\EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
array('event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize'),
);
}
public function onPreSerialize(JMS\Serializer\EventDispatcher\PreSerializeEvent $event)
{
/*
* #var YourEntity $object
*/
$object = $event->getObject();
$reflect = new \ReflectionClass($foo);
$props = $reflect->getProperties(\ReflectionProperty::IS_PRIVATE);
foreach ($props as $prop) {
$method = 'get'.ucfirst($prop->getName());
// here is call of methods like getId(), getName() etc,
// depending on name of entity properties
$object->$method();
}
}
}

SonataAdminBundle Exporter issue with mapped entities

There is a standard feature in sonata-admin-bundle to export data using exporter; But how to make export current entity AND mapped ManyToOne entity with it?
Basically what I want, is to download exactly same data as defined in ListFields.
UPD: In docs, there is only todo
UPD2: I've found one solution, but I do not think it is the best one:
/**
* Add some fields from mapped entities; the simplest way;
* #return array
*/
public function getExportFields() {
$fieldsArray = $this->getModelManager()->getExportFields($this->getClass());
//here we add some magic :)
$fieldsArray[] = 'user.superData';
$fieldsArray[] = 'user.megaData';
return $fieldsArray;
}
I created own source iterator inherited from DoctrineORMQuerySourceIterator.
If value in method getValue is array or instance of Traversable i call method getValue recursive to get value for each "Many" entity:
protected function getValue($value)
{
//if value is array or collection, creates string
if (is_array($value) or $value instanceof \Traversable) {
$result = [];
foreach ($value as $item) {
$result[] = $this->getValue($item);
}
$value = implode(',', $result);
//formated datetime output
} elseif ($value instanceof \DateTime) {
$value = $this->dateFormater->format($value);
} elseif (is_object($value)) {
$value = (string) $value;
}
return $value;
}
In your admin class you must override method getDataSourceIterator to return your own iterator.
This
$this->getModelManager()->getExportFields($this->getClass());
returns all entity items. Better practice is to create explicit list of exported items in method getExportFields()
public function getExportFields()
{
return [
$this->getTranslator()->trans('item1_label_text') => 'entityItem1',
$this->getTranslator()->trans('item2_label_text') => 'entityItem2.subItem',
//subItem after dot is specific value from related entity
....
Key in array is used for export table headers (here is traslated).

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.

Passing custom variable to form validation

I try to pass my variable to constraint in form validator, but can't.
i'm doing that:
$payForm = $this->createForm(new CableTVPayType(), null, array('balance' => $balance));
And in CableTVPayType:
public function getDefaultOptions(array $options)
{
$maxSumm = $options['balance'] - 100;
[...]
It works fine, my maxSumm is what i want, but Symfiony checks $options array. 'balance' isn't a default option, and complain about this:
The option "balance" does not exist
Is there another, more right way to pass custom variable to validation?
Use the constructor for stuff to be used by all instances of a type. For example, your type might need an entity manager for it to work. It will be reused across all the form instances.
For instance specific stuff use options. If you use the constructor for instance specific stuff, all the instances will get the value you pass to the constructor of the first instance.
/**
* #FormType
*/
class PayType extends AbstractType {
private $someService;
/**
* #InjectParams
*/
public function __construct(SomeService $someService)
{
$this->someService = $someService;
}
public function getDefaultOptions(array $options)
{
return array(
'balance' => 0
);
}
public function getName()
{
return 'pay';
}
}
$form = $this->createForm('pay', null, array('balance' => $balance));
Note that the #FormType annotation registers the type as a service. It allows you to use the type's name instead of creating an instance manually. It gets even more convenient when a type needs a service to be injected into it. You use just the name — pay in this case — instead of something like this:
$form = $this->createForm(new PayType($this->get('some_service')), null, array(
'balance' => $balance
));
Done with this!
Crate variable for a class, and passing value to it through construct method
class CableTVPayType extends AbstractType {
private $maxSumm;
public function __construct($maxSumm) {
$this->maxSumm = $maxSumm;
}
Create form with argument
$payForm = $this->createForm(new CableTVPayType($someValue));
Now i can use this variable as i want in my form.

Resources