How to create a tag system with addition when sending a post creation - forms

I'm trying to get a tag system working with symfony 6.
so I find several examples on the net but I have the impression that it is no longer relevant
I wish
Search for tags in the list of tags already saved in the database,
Or if the tag does not exist I want to create it when validating the form
I don't know how to proceed; I have read several things and tried but it does not work.
so I have a ManyToMany relationship between POST and TAGS.
When in the form I try this method:
->add('tags', SearchableEntityType::class, [
'class' => Tags::class
])
Then in the SearchableEntityType class
<?php
namespace App\Form\SearchTags;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SearchableEntityType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver) {
$resolver->setRequired('class');
$resolver->setDefaults([
'compound' => false,
'multiple' => true,
]);
}
public function buildView(FormView $view, FormInterface $form, array $options) {
$view->vars['expanded'] = false;
$view->vars['placeholder'] = null;
$view->vars['placeholder_in_choices'] = false;
$view->vars['multiple'] = true;
$view->vars['preferred_choices'] = [];
$view->vars['choices'] = $this->choices($form->getData());
$view->vars['choice_translation_domain'] = null;
$view->vars['full_name'] .= '[]';
}
public function getBlockPrefix()
{
return 'choice';
}
public function choices(Collection $value) {
$x =$value
->map(fn ($d) => new ChoiceView($d, (string)$d->getId(), (string) $d))
->toArray();
return $value
->map(fn ($d) => new ChoiceView($d, (string)$d->getId(), (string) $d))
->toArray();
}
}
I have this error
Symfony\Bridge\Twig\Extension\twig_is_selected_choice(): Argument #2 ($selectedValue) must be of type array|string|null, Doctrine\ORM\PersistentCollection given, called in *var\cache\dev\twig\a5\a5b3010b2620ea40fefcaf4778f4ed00.php on line 532
Thanks a lot!

Related

Symfony 5 dynamic form conditional default logic

I've an use case where i need some default conditional logic on my dynamic form build in Symfony 5.
Let me try to explain what my use case is and my problem with a simple form.
For example i've a form Product with two fields:
Part (choiceType => left, right)
Length (numberType)
On change all fields (:input) are being submitted through an Ajax request.
I've two controller methods one for visiting the page (form is being build), the other
is being called for rendering the form through the ajax request (handle conditional logic).
For the conditional logic part the following needs te be done
When part is left, default length needs to be 50
When part is right, default length needs to be 100
user could change default data
Setting the default data on length based on left or right is not the problem.
When left is selected, default length becomes 50. When changing the value to 55 (form is being submitted through every change) it becomes 50 again.
This behaviour is logic, but how could the default data been overwritten?
Above situation could also been described as give user default data with option to change it
form type
<?php
// ... namespace, use statments
class ProductType extends AbstractType
{
/**
* {#inheritDoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('part', ChoiceType::class, array(
'choices' => array(
'Left' => 'left',
'Right' => 'right',
)
));
$builder->add('length', NumberType::class);
$builder->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) use ($options)
{
$form = $event->getForm();
if(null === $product = $event->getData()) {
return;
}
switch($product->getPart()) {
case 'left': $defaultLength = 50; break;
case 'right': $defaultLength = 100; break;
default: $defaultLength = 0;
}
$form->get('length')->setData($defaultLength);
});
}
/**
* {#inheritDoc}
*/
public function getName(): string
{
return 'product';
}
/**
* {#inheritDoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(array(
'data_class' => Product::class,
'translation_domain' => 'forms',
));
}
}
controller
// src/Controller/ProductController.php
// ... namespace, use statments
namespace App\Controller;
class ProductController extends AbstractController
{
public function productAction(Request $request): Response
{
$product = new Product();
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$product = $form->getData();
dd($product);
}
return $this->render('product_view.html.twig', array(
'form' => $form->createView()
));
}
public function productConfigureAjaxAction(Request $request): Response
{
$product = new Product();
$part = $request->request->get('product')['part'] ?? null;
$product->setPart($part);
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
// product_form.html.twig is an separated file and included in product_view.html.twig
// by making the form separated is could been used for an ajax response
return $this->render('product_form.html.twig', array(
'form' => $form->createView()
));
}
}

Symfony form with entity

Hi I need a thinking help abaout form with entity class.
I have the edit function
/**
* #Route("/{id}/edit", name="admin_product_group_edit", methods={"GET","POST"})
*/
public function edit(Request $request, ProductGroup $productGroup): Response
{
$form = $this->createForm(ProductGroupType::class, $productGroup);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('admin_product_group_index', [
'id' => $productGroup->getId(),
]);
}
return $this->render('admin/product_group/edit.html.twig', [
'product_group' => $productGroup,
'form' => $form->createView(),
'scrollUp' => true,
]);
}
and I have the form Type
use App\Entity\ProductGroup;
use App\Entity\ProductType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProductGroupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('nameDe')
->add('nameEn')
->add('descriptionDe')
->add('descriptionEn')
->add('rank')
->add('active')
->add('creatDate')
->add('updateDate')
->add('productTypes', EntityType::class, [
// looks for choices from this entity
'class' => ProductType::class,
// uses the User.username property as the visible option string
'choice_label' => 'nameDe',
// used to render a select box, check boxes or radios
// 'multiple' => true,
// 'expanded' => true,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ProductGroup::class,
]);
}
}
by creating an new ProductGroup all is fin but by edit it, I cannot get the edit Form.
I get this error
Argument 1 passed to App\Controller\Admin\ProductGroupController::edit() must be an instance of App\Entity\ProductGroup, instance of App\Entity\ProductType given, called in /var/www/symfony-michael-roskosch/htdocs/vendor/symfony/http-kernel/HttpKernel.php on line 150
This is symfony 4.3 with symfony2 i had no problems with that, can you giv me a tip?
ok i found it
I had the wrong repositoryClass llinked by the orm annotation
/**
- * #ORM\Entity(repositoryClass="App\Repository\ProductTypeRepository")
+ * #ORM\Entity(repositoryClass="App\Repository\ProductGroupRepository")
*/
class ProductGroup
{
It was not a good idea to take the entity name ProductType because than you have the form ProductTypeType. After bin/console make:crud some mistakes was in the code. I don't know if I did them last night or it cames by the crud process it self.
nevermind now it works :-)

extend EntityType, set choices to selected Entity only

I've extended Symfony's EntityType as UserChooserType for use with my User entity and Select2. The choice list for the UserChooserType comes from an ldap query (via an ajax call), not a Doctrine query. So the field starts out blank.
The User entity is related to many different entities across my application. But if I want the UserChooserType to load with a current the selected User I have to add a listener to every form that uses it. e.g.:
class SiteType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$siteAdminOpts = array('label' => 'entity.site.admin', 'required'=>false);
//opts for the UserChooserType
$builder
->add('siteName', FT\TextType::class, array('label' => 'entity.site.name'))
->add('siteAdmin', UserChooserType::class, $siteAdminOpts )
//must be added to every form type that uses UserChooserType with mod for the datatype that $event->getData() returns
->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){
$site = $event->getData();
$form = $event->getForm(); //SiteType
if($user = $site->getSiteAdmin()) $siteAdminOpts['choices'] = array($user);
$form->add('siteAdmin', UserChooserType::class, $siteAdminOpts);
});
}
//etc.
tldr;
I'd like to either:
set UserChooserType's choices option to the selected user in UserChooserType::configureOptions(), or
move ->addEventListener(...) into UserChooserType::buildForm().
Any idea how it might be done?
Here is the UserChooserType:
class UserChooserType extends AbstractType
{
/**
* #var UserManager
*/
protected $um;
/**
* UserChooserType constructor.
* #param UserManager $um
*/
public function __construct(UserManager $um){
$this->um = $um; //used to find and decorate User entities. It is not a Doctrine entity manager, but it uses one.
}
/**
* #inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (!$data) return;
$user = $this->um->getUserByUserName($data);
if(!$user->getId()) $this->um->saveUser($user); //create User in db, if it's not there yet.
});
$builder->resetViewTransformers(); //so new choices aren't discarded
$builder->addModelTransformer(new CallbackTransformer(
function ($user) { //internal storage format to display format
return ($user instanceof User) ? $user->getUserName() : '';
},
function ($username) { //display format to storage format
return ($username) ? $this->um->getUserByUserName($username) : null;
}
));
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'class' => 'ACRDUserBundle:User',
'label' => 'ldap.user.name',
'choice_label' => function($user, $key, $index){
$this->um->decorateUser($user);
$label = $user->getDetail('displayName');
return $label ? $label : $user->getUserName();
},
'choice_value' => 'userName',
'choices' => [],
'attr' => array(
'class' => 'userchooser',
'placeholder' => 'form.placeholder.userchooser'
)
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'my_userchooser';
}
/**
* #inheritDoc
*/
public function getParent() {
return EntityType::class;
}
}
This helped me out:
https://symfony.com/doc/current/reference/forms/types/entity.html#using-choices
Here is how I used it (again with select 2):
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** #var ProductCollection $productCollection */
$productCollection = $builder->getData();
$builder
->add('products', EntityType::class,
[
'class' => Product::class,
'required' => FALSE,
'expanded' => FALSE,
'multiple' => TRUE,
'choices' => $productCollection->getProducts(), // We only preload the selected products. The rest come from api.
'attr' => [
'data-toggle' => 'select',
'data-options' => json_encode([
'ajax' => [
'url' => '/admin/product/autocomplete',
'dataType' => 'json'
]
])
],
'choice_label' => function (Product $product) {
return $product->getName();
},
'choice_attr' => function (Product $product) {
return [
'data-avatarsrc' => $product->getImage()->getUrl(),
'data-caption' => $product->getSkus()->first()->getSku(),
];
},
]
)
;
}
And here is my JS for the init of the select2 (copy/paste):
function() {
var elements = document.querySelectorAll('[data-toggle="select"]');
function templateResult(element) {
let avatarsrc, caption;
if (element.id && element.avatarsrc) {
avatarsrc = element.avatarsrc
caption = element.caption ? ' ' + element.caption : ''
}
else if (element.element) {
avatarsrc = element.element.dataset.avatarsrc ? element.element.dataset.avatarsrc : ''
caption = element.element.dataset.caption ? ' ' + element.element.dataset.caption : ''
}
if (!avatarsrc) {
return element.text + caption
}
let wrapper = document.createElement("div");
return wrapper.innerHTML = '<span class="avatar avatar-xs mr-3 my-2"><img class="avatar-img rounded-circle" src="' + avatarsrc + '" alt="' + element.text + '"></span><span>' + element.text + '</span><span class="badge badge-soft-success ml-2">' + caption + '</span>', wrapper
}
jQuery().select2 && elements && [].forEach.call(elements, function(element) {
var select, additionalOptions, select2options;
additionalOptions = (select = element).dataset.options ? JSON.parse(select.dataset.options) : {};
select2options = {
containerCssClass: select.getAttribute("class"),
dropdownCssClass: "dropdown-menu show",
dropdownParent: select.closest(".modal") ? select.closest(".modal") : document.body,
templateResult: templateResult,
templateSelection: templateResult
}
$(select).select2({...select2options, ...additionalOptions})
})
}(),
Some remarks first:
The field name UserChooserType is a bit elaborated. UserType is shorter and just as clear, and fits the Symfony naming conventions better
I wouldn't extend UserChooserType from EntityType. Doc says EntityType is specifically dedicated to entities coming from Doctrine. It adds magic so that the field is easily configured around Doctrine: transformers, automatic fetching of choices from the DB, etc. If your users are fully defined in the LDAP, I would instead recommend extending ChoiceType and populate your choices from the LDAP directly.
Coming back to your problem, what you want here is to leverage the almighty power of dependency injection, by declaring your field type as a service. After having done that, instead of:
$builder->add('siteAdmin', UserChooserType::class, $siteAdminOpts)
You will do (assuming you chose the form name user_chooser_type):
$builder->add('siteAdmin', 'user_chooser_type', $siteAdminOpts)
You need to inject the security.token_storage service into your field type. This is the service that holds the current user's information, you can access it like this:
$user = $securityTokenStorage->getToken()->getUser();
With this the population of the default value can happen at the field type level instead of its parent.
Using this, you could also add the LDAP choices at field type level too, by injecting a service able to communicate with the LDAP.
It should finally noted that all default symfony field types are also declared as services.
EDIT:
Previous answer is beside the point.
In Symfony, I'm not aware of any possible simple way to modify the choice list of a choice/entity field after it has been created. I don't think there is, and actually I might have done the same as you to tackle the issue you're describing.
My opinion is that you're already doing what needs to be done in this case.
If you're really motivated though, I think it might just be possible to move the event listener in the form type like this:
->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($options) {
if ($form->hasParent()) {
$data = $event->getData();
$form = $event->getForm();
$method = 'get' . ucfirst($form['action_name']);
if($user = $data->$method()) $siteAdminOpts['choices'] = array($user);
$form->getParent()->add($options['form_name'], UserChooserType::class, $siteAdminOpts);
}
});
Something like that. Not sure how that would work though.
I'm still interested in seeing if anyone comes up with a clean solution.

Unit Test Form WIth Repository Method

I am attempting to unit test one of my forms that I have which includes an entity form type on it. I would like to test the full form, but I keep on running into the error message - Expected argument of type "Doctrine\ORM\QueryBuilder", "NULL" given
Which is obvious, I need to somehow mock Doctrine\ORM\QueryBuilder as the return type for the entity form type. I am not quiet sure how I go about doing that though.
Here is the code of the Form -
<?php
namespace ICS\BackEnd\BoardBundle\Form;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class BoardCollectionType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', 'text', array(
'disabled' => TRUE
))
->add('member', 'entity', array(
'class' => 'MemberBundle:Members',
'property' => 'fullName',
'query_builder' => function(EntityRepository $er) {
return $er->findAllActiveMembers();
},
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'ICS\BackEnd\BoardBundle\Entity\Board'
));
}
/**
* #return string
*/
public function getName()
{
return 'ics_boardbundle_board';
}
}
This is the test I am running on it -
<?php
namespace ICS\BackEnd\BoardBundle\Tests\Form;
use Doctrine\ORM\QueryBuilder;
use ICS\BackEnd\BoardBundle\Entity\Board;
use ICS\BackEnd\BoardBundle\Form\BoardCollectionType;
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
class BoardCollectionTypeTest extends TypeTestCase {
protected $repository;
protected function setUp()
{
parent::setUp();
$this->factory = Forms::createFormFactoryBuilder()
->addExtensions($this->getExtensions())
->getFormFactory();
}
public function testSubmittedValueData()
{
$formData = array(
'member' => NULL,
);
$type = new BoardCollectionType();
$form = $this->factory->create($type);
$object = new Board();
$object->createFromArray($formData);
// submit the data to the form directly
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($object, $form->getData());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
protected function getExtensions()
{
$this->repository = $this->getMockBuilder('Doctrine\ORM\EntityRepository')
->disableOriginalConstructor()
->getMock();
$mockEntityManager = $this->getMockBuilder('\Doctrine\ORM\EntityManager')
->disableOriginalConstructor()
->getMock();
$mockEntityManager->expects($this->any())
->method('getRepository')
->will($this->returnValue($this->repository));
$classMetadata = $this->getMockBuilder('\Doctrine\Common\Persistence\Mapping\ClassMetadata')
->disableOriginalConstructor()
->getMock();
$mockEntityManager->expects($this->any())
->method('getClassMetadata')
->will($this->returnValue($classMetadata));
$mockRegistry = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry')
->disableOriginalConstructor()
->setMethods(array('getManagerForClass'))
->getMock();
$mockRegistry->expects($this->any())
->method('getManagerForClass')
->will($this->returnValue($mockEntityManager));
$mockEntityType = $this->getMockBuilder('Symfony\Bridge\Doctrine\Form\Type\EntityType')
->setMethods(array('getName'))
->setConstructorArgs(array($mockRegistry))
->getMock();
$mockEntityType->expects($this->any())
->method('getName')
->will($this->returnValue('entity'));
$this->assertQueryBuilderCalled();
return array(new PreloadedExtension(array(
$mockEntityType->getName() => $mockEntityType,
), array()));
}
protected function assertQueryBuilderCalled()
{
$em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
->disableOriginalConstructor()->getMock();
$repo = $this->getMockBuilder('ICS\BackEnd\MemberBundle\Entity\Repository\MembersRepository')
->disableOriginalConstructor()->getMock();
$repo->expects($this->once())->method('findAllActiveMembers')
->will($this->returnValue(new QueryBuilder($em)));
/*$qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder')
->disableOriginalConstructor()
->getMock();
$query = $this->getMockBuilder('Doctrine\ORM\AbstractQuery')
->disableOriginalConstructor()
->setMethods(array('execute'))
->getMockForAbstractClass();
$query->expects($this->any())
->method('execute')
->will($this->returnValue(array()));
$qb->expects($this->any())
->method('getQuery')
->will($this->returnValue($query));
$this->repository->expects($this->any())
->method('findAllActiveMembers')
->will($this->returnValue($query));
$this->repository->expects($this->any())
->method('createQueryBuilder')
->will($this->returnValue($qb));*/
}
}
Thanks for any help!
I resolved the issue by changing the entity form type to choice. It made the test much easier to read and do. I injected the "choices" into the options of the form type.

How to manage collections through an API with symfony2 and forms

I'm building an API where a user can update an entity with a collection as part of it. This works fine if I use forms throughout, but I'm building up the API. My entity looks like this:
<?php
class MyEntity {
// ...
/**
* #ORM\OneToMany(targetEntity="TitleEntity", mappedBy="entityID", cascade={"persist"})
*/
protected $myTitles;
public function getMyTitles() {
return $this->myTitles;
}
public function setMyTitles($titles) {
foreach($titles as $key => $obj) { $obj->setEntity($this); }
$this->myTitles = $collection;
}
public function addMyTitle($obj) {
$obj->setEntity($this);
$this->myTitles[] = $obj;
}
public function removeMyTitle($obj) {
$this->myTitle->removeElement($obj);
}
}
The myTitles is an entity that has an ID, the ID of the entity it is attached to, and then a title.
For the API, I'm passing a JSON content body back as a PUT request for the MyEntity object, so I end up with an array of the titles, and I'm prepping them like this to bind to a form for validation:
$myTitles = array();
foreach($titles as $key => $title) {
$titleObj = new TitleEntity();
$titleObj->setTitle($title);
}
$myEntity->setTitles($titles);
but it complains with:
The form's view data is expected to be of type scalar, array or an instance of
\ArrayAccess, but is an instance of class stdClass. You can avoid this error by
setting the "data_class" option to "stdClass" or by adding a view
transformer that transforms an instance of class stdClass to scalar, array or
an instance of \ArrayAccess
It looks like this happens because I call getMyTitles() before I bind my entity to the form I'm using to validate against.
I'm binding to the form using an array:
$form = $this->createForm(new AddEntity(), $myEntity);
$data = array( // Set all my data );
$form->bind($data);
if($form->isValid() {
// ...
If I do the createForm() call first, and then add the titles afterward, I get this:
Call to a member function removeElement() on a non-object
which occurs inside removeMyTitle().
How do I handle this?
Edit
Here is the AddEntity() type:
<?php
class AddEntity extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', 'text')
->add('subheading', 'text')
->add('description', 'textarea')
->add('myTitles', 'collection', array(
'type' => new AddMyTitles(), // Basic type to allow setting the title for myTitle entities
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
'options' => array(
'required' => false,
),
));
}
public function getName()
{
return 'addEntity';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'My\TestBundle\Entity\Entity',
));
}
You need data transformers here.
http://symfony.com/doc/2.0/cookbook/form/data_transformers.html
Basically, you have told the form it is getting an array, and you've given it something else. The transformer is supposed to handle this.
If you need more help I'd need more information.
Also, somewhat bafflingly, you refer to 'myCollections' in your prose but don't show it, in your code.
^^^^^^^^^ fixed by edit.