I have created an EntityType form where you can add to your participation to an event the field and your position. Unfortunately the number and name of the fields is updated dynamically on the page so when creating the form, I don't have all the fields available (or worse some that doesn't exist anymore)
For simplicity I would like to use a simple textType, where my javascripts could enter an Id number and link the corresponding element from the db.
the code is very simple
class FieldPositionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('position', EntityType::class, [
'class' => Position::class,
'choice_label' => 'name'
])
->add('field', EntityType::class, [
'class' => Field::class,
'choice_label' => 'id'
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Participation::class,
]);
}
}
but instead of EntityType, I would prefer TextType.
I imagine that I need to perform some modification on my setter but I have no clue how to transform an Id into a Entity as the EntityType does.
As suggested by Cerad, the dataTransformer was the solution.
So just for the position, I created a dataTransformer
class PositionToIdTransformer implements DataTransformerInterface
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function transform($position)
{
if (null === $position) {
return '';
}
return $position->getId();
}
public function reverseTransform($id)
{
if (!$id){
return;
}
$position = $this->em->getRepository(Position::class)->find($id);
if (null === $position){
throw new TransformationFailedException(sprintf("the position '%s' does not exist!", $id));
}
return $position;
}
}
that I use in my formBuilder:
class FieldPositionType extends AbstractType
{
private $pt; //PositionToIdTransformer
public function __construct(PositionToIdTransformer $pt)
{
$this->pt = $pt;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('position', TextType::class)
;
$builder->get('position')->addModelTransformer($this->pt);
}
}
and it work like a charm!
Link for query builder: https://symfony.com/doc/current/reference/forms/types/entity.html#ref-form-entity-query-builder
As told in comment try using it. I can not halp you further because I do not see rest of your logic but to limit entries which are shown in entity field it looks like this:
$builder->add('users', EntityType::class, [
'class' => User::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->orderBy('u.username', 'ASC');
},
'choice_label' => 'username',
]);
Related
I use a Form Builder to create my form. I added the option team to the FormBuilderInterface to access this property in my form.
class PersonnalStatType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('tag', EntityType::class, [
'class' => StatTag::class,
'choice_label' => 'name',
'query_builder' => function (StatTagRepository $rep/*, $options*/)
{
return $rep->queryActivated($options['team']);
}
]
)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => PersonnalStat::class,
'team' => null
]);
$resolver->setAllowedTypes('team', ['null', Team::class]);
}
The first row is an EntityType and I need to use the team option to perform a query but I don't know how to proceed.
So far I tried :
'query_builder' => function (StatTagRepository $rep)
{
return $rep->queryActivated($options['team']);
}
but the query doesn't know $options
Notice: Undefined variable: options
And
'query_builder' => function (StatTagRepository $rep, $options)
{
return $rep->queryActivated($options['team']);
}
but it doesn't recognize $options
Too few arguments to function App\Form\PersonnalStatType::App\Form{closure}(), 1 passed in /Users/pierrickrambaud/Sites/team-manager/vendor/symfony/doctrine-bridge/Form/Type/EntityType.php on line 32 and exactly 2 expected
Anyone knows a hack ?
This should work:
'query_builder' => function (StatTagRepository $rep) use ($options) {
return $rep->queryActivated($options['team']);
}
A bit in the panic - I am generating Symfony form for a complex search, i.e. mapped data to the entity will be used just for a search query building.
I create simple form, model, some extended types from ChoiceType for prepopulation choices by some logic. The form is submitted with GET method.
In the model you find maker and model fields for example. The latter populated on the frontend with AJAX, after maker has been selected. When I do submit the form, and maker and model have non-default value, the handleRequest only populates the maker property of the Model, but the model is left empty. Also the checkboxes are correctly populated if checked. All in all, $form->getData() returns just Maker and checkboxes, other fields are null. $request->query has all parameters.
The data mappers are senseless here. And also there is nothing to transform in the data, the Model is mostly from scalar values. The request contains everything, but it is not handled correctly. I tried to implement ChoiceLoaderInterface, but that doesn't work for me, because during loading choices I have to have access to the options of the form, which I don't (I used this article https://speakerdeck.com/heahdude/symfony-forms-use-cases-and-optimization).
I am using Symfony 4.2.4; PHP 7.2.
Controller's method
/**
* #Route("/search/car", name="car_search", methods={"GET"})
* #param Request $request
*/
public function carSearchAction(Request $request)
{
$carModel = new CarSimpleSearchModel();
$form = $this->createForm(CarSimpleSearchType::class, $carModel);
$form->handleRequest($request);
$form->getData();
.....
}
CarSimpleSearchModel
class CarSimpleSearchModel
{
public $maker;
public $model;
public $priceFrom;
public $priceTo;
public $yearFrom;
public $yearTo;
public $isCompanyOwner;
public $isPrivateOwners;
public $isRoublePrice;
}
CarSimpleSearchType the form
class CarSimpleSearchType extends AbstractType
{
protected $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('vehicle_type', HiddenType::class, [
'data' => VehicleTypeType::CAR,
'mapped' => false,
])
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class)
->add(
'priceFrom',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'priceTo',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearFrom',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearTo',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add('isCompanyOwner', CheckboxType::class)
->add('isPrivateOwners', CheckboxType::class)
->add('isRoublePrice', CheckboxType::class)
->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => CarSimpleSearchModel::class,
'compound' => true,
'method' => 'GET',
'required' => false,
'action' => $this->urlGenerator->generate('car_search'),
]
);
}
public function getBlockPrefix()
{
return 'car_search_form';
}
}
CarMakerSelectType field
class CarMakerSelectType extends AbstractType
{
/**
* #var VehicleExtractorService
*/
private $extractor;
/**
* VehicleMakerSelectType constructor.
*
* #param VehicleExtractorService $extractor
*/
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'placeholder' => null,
'vehicle_type' => null,
'choices' => $this->getVariants(),
]
);
}
private function getVariants()
{
$makers = $this->extractor->getMakersByVehicleType(VehicleTypeType::CAR);
$choices = [];
foreach ($makers as $maker) {
$choices[$maker['name']] = $maker['id'];
}
return $choices;
}
}
CarModelSelectType field
class CarModelsSelectType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'disabled' => true,
]
);
}
}
VehiclePriceRangeType field
class VehiclePriceRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
private function getRange(int $vehicleType)
{
return PriceRangeGenerator::generate($this->extractor->getMaxVehiclePrice($vehicleType));
}
}
VehicleYearRangeType field
class VehicleYearRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractorService)
{
$this->extractor = $extractorService;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
protected function getRange(int $vehicleType): array
{
$yearRange = RangeGenerator::generate(
$this->extractor->getMinYear($vehicleType),
$this->extractor->getMaxYear($vehicleType),
1,
true,
true
);
return $yearRange;
}
}
So, I can use the raw data from the Request and manually validate-populate the model and send to further processing, but I guess that's not the Right Way, and I want to populated the form by the framework. How can I ?..
In my case, I had a dependent EntityType populated by ajax that is initially disabled. Since choices where null, it was returning an InvalidValueException on submission. What I had to do is create an EventListener and add the valid choices for the current 'main' field. This is basically it, more or less adapted to your case.
Original form:
// Setup Fields
$builder
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class, [
'choices' => [],
// I was setting the disabled on a Event::PRE_SET_DATA if previous field was null
// since I could be loading values from the database but I guess you can do it here
'attr' => ['disabled' => 'disabled'],
]
);
$builder->addEventSubscriber(new ModelListener($this->extractor));
Event Subscriber that adds back valid choices:
class ModelListener implements EventSubscriberInterface
{
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => 'onPreSubmitData',
];
}
public function onPreSubmitData(FormEvent $event)
{
// At this point you get only the scalar values, Model hasn't been transformed yet
$data = $event->getData();
$form = $event->getForm();
$maker_id = $data['maker'];
$model= $form->get('model');
$options = $model->getConfig()->getOptions();
if (!empty($maker_id)) {
unset($options['attr']['disabled']);
$options['choices'] = $this->extractor->getModelsFor($maker_id);
$form->remove('model');
$form->add('model', CarModelsSelectType::class, $options );
}
}
}
}
I faced up with some non-ordinary situation for me.
1) I have a dependent list that rendering by Symfony FormType like this:
2) Location and Instruction fields are depend from Company field.
3) When I change Company field (onchange event js) then goes ajax request that retrieves data from the database and build a dropdown list.
4) But when form is submitted I have an error:
Please help me to resolve this. Thanks in advance.
My formType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('hours')
->add('messageText', null, ['required' => false])
->add('company', null, [
'choice_label' => 'name',
'placeholder' => 'Please select company',
'required' => true
])
->add('procedure', TextType::class, ['attr' => ['placeholder' => 'Please type code or description'] ])
->add('instruction', ChoiceType::class, ['mapped' => false])
->add('location', ChoiceType::class, ['mapped' => false])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => InstructionNotification::class
));
}
Action from controller:
/**
* #Route("/admin/api/instructions", name="admin_api_instructions")
* #param Request $request
* #return JsonResponse
*/
public function getInstructionsByCompanyId(Request $request)
{
$id = $request->get('id');
if (!$id) {
return new JsonResponse('No data', 404);
}
$instructions = $this->getDoctrine()->getRepository('OctaneBundle:Instruction')->findInstructionsByCompanyId($id);
return new JsonResponse($instructions);
}
findInstructionsByCompanyId($id):
public function findInstructionsByCompanyId($id)
{
$qb = $this->createQueryBuilder('i');
if ($id) {
$qb
->where('i.company = :id')
->setParameter('id', $id);
}
return $qb->getQuery()->getResult();
}
response from api (i.e.: admin/api/instructions?id=1):
[{"id":2,"label":"First instruction"},{"id":3,"label":"First instruction"}]
If you need any additional information please leave comments below. Thanks
Symfony's Validator expects that your submitted form will have a submitted instruction and location value that exists in the list you provided when creating your form in form type class. Since you are not providing any options for instructions and locations, you are getting a validation error.
In order to bypass this error you should use Symfony's Form Events in your buildForm function in your form type like this:
$builder->get('company')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) {
$company = $event->getForm()->getData();
$form = $event->getForm()->getParent();
$form->add('location', EntityType::class, array(
'class' => Location::class,
'query_builder' => function (EntityRepository $repo) use ($company) {
return $repo->createQueryBuilder('location')
->where('location.company = :company')
->setParameter('company', $company->getId());
}
));
$form->add('instruction', EntityType::class, array(
'class' => Instruction::class,
'query_builder' => function (EntityRepository $repo) use ($company) {
return $repo->createQueryBuilder('instruction')
->where('instruction.company = :company')
->setParameter('company', $company->getId());
}
));
}
);
Thanks for the answer but I found out more elegant solution for my case. So,
my formType now:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('instruction', FormattedSelectType::class, ['class' => Instruction::class])
->add('location', FormattedSelectType::class, ['class' => Location::class])
;
}
FormattedSelectType:
class FormattedSelectType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_label' => function (FormattedLabelInterface $entity) {
return $entity->getFormattedLabel();
}
));
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return EntityType::class;
}
}
Etities Location and Instruction entities implement JsonSerializable and custom FormattedLabelInterface interface and have the next methods:
/**
* #return string
*/
public function getFormattedLabel()
{
return sprintf(self::LABEL_FORMATTED, $this->zip, $this->city, $this->name, $this->address, $this->phone);
}
/**
* #return array|mixed
*/
public function jsonSerialize()
{
return [
'id' => $this->id,
'label' => $this->getFormattedLabel()
];
}
I'm trying to implement an Facebook style autocomplete field using a custom form type. The JavaScript widget depends on a class name so I added it to the attr key in the custom form type class as I have done at other places, but for some reason it never gets displayed in the output HTML :-(
The relevant code:
class AutocompleteType extends AbstractType
{
protected $em;
public function __construct(ObjectManager $em)
{
$this->em = $em;
}
public function getDefaultOptions(array $options)
{
return array(
'attr' => array(
'class' => 'autocomplete',
'data-autocomplete' => '{"url":"'.$options['url'].'"}'
)
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'url' => false,
'object' => false,
'repository' => false,
'field' => false
));
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new AutocompleteTransformer($this->em, $options['object'], $options['repository'], $options['field']);
$builder->addModelTransformer($transformer);
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, array(
'url' => $options['url'],
'object' => $options['object'],
'repository' => $options['repository'],
'field' => $options['field']
));
}
public function getParent()
{
return 'text';
}
public function getName()
{
return 'autocompleter';
}
}
The resulting HTML:
<input type="text" required="required" name="post[Tags]" id="post_Tags">
Expected HTML
<input type="text" class="autocomplete" data-autocomplete="url/passed/from/builder" required="required" name="post[Tags]" id="post_Tags">
there are three ways, I don't know what is better
1) under buildForm() method you can get and set attribute 'attr':
public function buildForm(FormBuilderInterface $builder, array $options)
{
$attrs = $builder->getAttribute('attr');
...
$builder->setAttribute('attr', $attrs);
}
2) another way is to set attributes under build view, I think it's a bit more logic
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr'] = array_merge(array(
....
), $view->vars['attr']);
}
3) latest way is to pass them directly as variables in the view and not in the 'attr', then display them in the appropriate twig block.
I think the third one is the best way but i'm not sure.
I'm trying to add a formType in a other form type.
CustomerType:
class CustomerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('firstname', 'text', array(
'required' => 'required'
))
->add('middlename')
->add('lastname')
->add('email', 'email')
->add('groups', 'entity', array(
'class' => 'MV\CMSBundle\Entity\Group',
'property' => 'name',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('g')
->orderBy('g.name', 'ASC');
}
))
->add('profile', 'collection', array(
'type' => new ProfileType()
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MV\CMSBundle\Entity\User',
));
}
public function getName()
{
return 'customer';
}
}
ProfileType:
class ProfileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('isActive', 'checkbox', array(
'required' => false
))
->add('phone')
->add('address')
->add('city')
->add('zipcode')
->add('country')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MV\NameBundle\Entity\Profile',
));
}
public function getName()
{
return 'profile';
}
}
Then I get this message:
Expected argument of type "array or (\Traversable and \ArrayAccess)", "MV\NameBundle\Entity\Profile" given.
If I comment that line i get: (just to check what will goes wrong ;) )
The form's view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is an instance of class MV\NameBundle\Entity\Profile. You can avoid this error by setting the "data_class" option to "MV\NameBundle\Entity\Profile" or by adding a view transformer that transforms an instance of class MV\NameBundle\Entity\Profile to scalar, array or an instance of \ArrayAccess.
What do I do wrong?
Symfony2: 2.1.8-DEV
It's not clear what your mapping is between your Customer entity and your Profile entity, but I guess there are 2 options:
Option 1: You have a OneToOne relationship (One customer can only have One profile). Therefore, you don't need the collection at all but simply need to embed a single object.
->add('profile', new ProfileType());
Option 2: You want a ManyToOne relationship (One customer can have many profiles). In that case, you need to use to embed a collection of forms. If this is what you want, rename $profile to $profiles in your Customer entity, and do the following:
//MV\CMSBundle\Entity\Customer
use Doctrine\Common\Collections\ArrayCollection;
/**
* Your mapping here.
*
* #ORM\ManyToOne(targetEntity="MV\NameBundle\Entity\Profile")
*/
protected $profiles;
public function __construct()
{
//This is why you had an error, this is missing from your entity
//An object was sent instead of a collection.
$this->profiles = new ArrayCollection();
}
Finally, update your database.