I'm having a REST-API built in Symfony3.
As an example here are the API-fields of Price in a form, made with the FormBuilderInterface. The code-example below is of ApiBundle/Form/PriceType.php
class PriceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, array(
'description' => 'Name',
))
->add('price_category', EntityPublicKeyTextType::class, array(
'class' => 'MyCustomBundle:PriceCategory',
'property_path' => 'priceCategory',
))
The issue is about good response messages of fields which have e.g. a validation error. For default symfony-types (e.g. IntegerType, TextType) it can find the property_path automatically and hands me out an useful error message. Here is the API-response with two errors:
name can be resolved in a good way (because I see what field it is about,
for price_category it can't resolve it (second message).
{
"name": [
"This value is too long. It should have 50 characters or less."
],
"0": "This value should not be null."
}
To resolve the issue. I add 'property_path' => 'priceCategory' for the field price_category. The value of property_path is matching with BaseBundle/Entity/Price.php where the var protected $priceCategory; is defined.
After adding property_path the error message looks fine.
{
"name": [
"This value is too long. It should have 50 characters or less."
],
"price_category": [
"This value should not be null."
]
}
The class of price_category is EntityPublicKeyTextType which is abstracted from TextType (which can do errors just fine).
Therefore I have the following question: What do i have to add to my inherited class EntityPublicKeyTextType to avoid adding the property_path for all fields by hand?
Any hint to fix this is highly welcome
Best endo
EDIT:
EntityPublicKeyTextType:
class EntityPublicKeyTextType extends AbstractType
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new ObjectToPublicKeyTransformer(
$this->om,
$options['class'],
$options['public_key'],
$options['remove_whitespaces'],
$options['multiple'],
$options['string_separator'],
$options['extra_find_by']
);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setRequired(array(
'class',
'public_key'
))
->setDefaults(array(
'multiple' => false,
'string_separator' => false,
'extra_find_by' => array(),
'remove_whitespaces' => true,
));
}
public function getParent()
{
return TextType::class;
}
public function getBlockPrefix()
{
return 'entity_public_key_text';
}
}
ObjectToPublicKeyTransformer:
class ObjectToPublicKeyTransformer implements DataTransformerInterface
{
/**
* #var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* #var ObjectManager
*/
private $om;
/**
* #var string
*/
private $class;
/**
* #var string|string[]
*/
private $publicKey;
/**
* #var bool
*/
private $removeWhitespaces;
/**
* #var boolean
*/
private $multiple;
/**
* #var boolean|string
*/
private $stringSeparator;
/**
* #var array
*/
private $extraFindBy;
public function __construct(
ObjectManager $om,
string $class,
$publicKey,
bool $removeWhitespaces,
bool $multiple = false,
$stringSeparator = false,
array $extraFindBy = array(),
PropertyAccessorInterface $propertyAccessor = null
) {
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->om = $om;
$classMetadata = $om->getClassMetadata($class);
$this->class = $classMetadata->getName();
$this->publicKey = $publicKey;
$this->stringSeparator = $stringSeparator;
$this->multiple = $multiple;
$this->extraFindBy = $extraFindBy;
$this->removeWhitespaces = $removeWhitespaces;
}
/**
* Transforms an object / Collection of objects to a publicKey string / array of publicKey strings.
*
* #param object|Collection $object
* #return string|array
*/
public function transform($object)
{
if (null == $object) {
return null;
}
if (is_array($this->publicKey)) {
$publicKey = $this->publicKey[0];
} else {
$publicKey = $this->publicKey;
}
if ($this->multiple) {
if ($object instanceof Collection) {
$values = array();
foreach ($object as $objectItem) {
$values[] = (string)$this->propertyAccessor->getValue($objectItem, $publicKey);
}
if ($this->stringSeparator) {
return implode($this->stringSeparator, $values);
}
return $values;
}
} else {
return (string)$this->propertyAccessor->getValue($object, $publicKey);
}
}
/**
* Transforms an publicKey string / array of public key strings to an object / Collection of objects.
*
* #param string|array $value
* #return object|Collection
*
* #throws TransformationFailedException if object is not found.
*/
public function reverseTransform($value)
{
if (null === $value) {
return $this->multiple ? new ArrayCollection() : null;
}
if (is_array($this->publicKey)) {
$publicKeys = $this->publicKey;
} else {
$publicKeys = array($this->publicKey);
}
if ($this->multiple) {
if ($this->stringSeparator) {
$value = explode($this->stringSeparator, $value);
}
if (is_array($value)) {
$objects = new ArrayCollection();
foreach ($value as $valueItem) {
foreach ($publicKeys as $publicKey) {
$object = $this->findObject($valueItem, $publicKey);
if ($object instanceof $this->class) {
$objects->add($object);
break;
}
}
}
return $objects;
}
}
foreach ($publicKeys as $publicKey) {
$object = $this->findObject($value, $publicKey);
if ($object instanceof $this->class) {
return $object;
}
}
return $this->multiple ? new ArrayCollection() : null;
}
private function findObject($value, $publicKey)
{
if ($this->removeWhitespaces) {
$value = str_replace(' ', '', $value);
}
$findBy = array_merge([$publicKey => $value], $this->extraFindBy);
$object = $this->om->getRepository($this->class)->findOneBy($findBy);
return $object;
}
}
It would be useful if you also provide your Price model/entity class. It seems that you are using camel case for the property name in your model (priceCategory) and then you use snake case in your form (price_category).
If you use the same convention for the model and the form, the validation errors will automatically map to the correct property.
The explanation is that Symfony's mappers can still map your fields by transforming snake to camel case and vice versa, that's why your form is still working and submitting values even without using the property_path option. But the problem is that the validator does not do this mapping and cannot match the correct property (price_category -> priceCategory).
Related
How to unit test Symfony 4 Form with EntityType field
When I run my test:
$ ./vendor/bin/simple-phpunit tests/Unit/Form/ProductFormTest.php
This is the output in my terminal:
PHPUnit 6.5.8 by Sebastian Bergmann
and contributors.
Runtime: PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 with Xdebug
2.7.0alpha2-dev Configuration: /var/www/project/phpunit.xml.dist
Testing App\Tests\Unit\Form\ProductFormTest E
1 / 1 (100%)
Time: 551 ms, Memory: 6.00MB
There was 1 error:
1) App\Tests\Unit\Form\ProductFormTest::formSubmitsValidData
Symfony\Component\Form\Exception\RuntimeException: Class
"App\Entity\Supplier" seems not to be a managed Doctrine entity. Did
you forget to map it?
/var/www/project/vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php:205
/var/www/project/vendor/symfony/options-resolver/OptionsResolver.php:858
/var/www/project/vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php:130
/var/www/project/vendor/symfony/options-resolver/OptionsResolver.php:766
/var/www/project/vendor/symfony/options-resolver/OptionsResolver.php:698
/var/www/project/vendor/symfony/form/ResolvedFormType.php:95
/var/www/project/vendor/symfony/form/FormFactory.php:76
/var/www/project/vendor/symfony/form/FormBuilder.php:97
/var/www/project/vendor/symfony/form/FormBuilder.php:256
/var/www/project/vendor/symfony/form/FormBuilder.php:206
/var/www/project/vendor/symfony/form/FormFactory.php:30
/var/www/project/tests/Unit/Form/ProductFormTest.php:86
ERRORS! Tests: 1, Assertions: 0, Errors: 1.
This error started after mocking the ManagerRegistry class. It seems that in this unit test there is no mapping for doctrine entities present.
Is there a clean way to test a form with "Symfony\Bridge\Doctrine\Form\Type\EntityType" fields?
src\App\Entity\Product.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Entity\Supplier;
/**
* Product Entity
*
* #ORM\Entity(repositoryClass = "App\Repository\ProductRepository")
* #ORM\Table(name = "product")
*/
class Product
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->setType(AbstractProduct::TYPE_PARENT);
}
/**
* To String
*
* #return string
*/
public function __toString()
{
return "[" . $this->id . "] Product: " . $this->ean . " | " . $this->name;
}
/**
* ID
*
* #var integer
*
* #ORM\Id
* #ORM\Column(name = "product_id", type = "integer")
* #ORM\GeneratedValue(strategy = "AUTO")
*/
protected $id;
/**
* EAN (European Article Number)
*
* #var string
*
* #ORM\Column(name = "product_ean", type = "string", length = 13)
*/
protected $ean;
/**
* Name
*
* #var string
*
* #ORM\Column(name = "product_name", type = "string", length = 128)
*/
protected $name;
/**
* Description
*
* #var string
*
* #ORM\Column(name = "product_description", type = "text", nullable = true)
*/
protected $description;
/**
* Supplier
*
* Many Products have one Supplier
*
* #var Supplier
*
* #ORM\ManyToOne(targetEntity = "Supplier", inversedBy = "products")
* #ORM\JoinColumn(name = "supplier_id", referencedColumnName = "supplier_id")
*/
protected $supplier;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set ean
*
* #param string $ean
*
* #return AbstractProduct
*/
public function setEan($ean)
{
$this->ean = $ean;
return $this;
}
/**
* Get ean
*
* #return string
*/
public function getEan()
{
return $this->ean;
}
/**
* Set name
*
* #param string $name
*
* #return AbstractProduct
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
*
* #return AbstractProduct
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set supplier
*
* #param \App\Entity\Supplier $supplier
*
* #return Product
*/
public function setSupplier(Supplier $supplier = null)
{
$this->supplier = $supplier;
return $this;
}
/**
* Get supplier
*
* #return \App\Entity\Supplier
*/
public function getSupplier()
{
return $this->supplier;
}
}
src\App\Form\ProductForm.php
namespace App\Form;
use App\Entity\Supplier;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class ProductForm extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$translationDomain = "product";
/*
* Card
*/
$builder->add("ean", TextType::class, [
"label" => "product.ean",
"required" => true,
"translation_domain" => $translationDomain,
]);
$builder->add("name", TextType::class, [
"label" => "product.name",
"required" => true,
"translation_domain" => $translationDomain,
]);
$builder->add("supplier", EntityType::class, [
"class" => Supplier::class,
"choice_label" => "name",
"label" => "supplier.name",
"required" => false,
"translation_domain" => "supplier",
]);
}
}
tests\Unit\Form\ProductFormTest.php
namespace App\Tests\Unit\Form;
use App\Entity\Product;
use App\Form\ProductForm;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bridge\Doctrine\ManagerRegistry;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
class ProductFormTest extends TypeTestCase
{
/**
* #var ManagerRegistry
*/
private $_managerRegistry;
/**
* {#inheritdoc}
*/
protected function setUp()
{
$this->_managerRegistry = $this->createMock(ManagerRegistry::class);
parent::setUp();
}
/**
* {#inheritdoc}
*/
protected function tearDown()
{
$this->_managerRegistry = null;
parent::tearDown();
}
/**
* {#inheritdoc}
*/
protected function getExtensions()
{
$entityType = new EntityType($this->_managerRegistry);
return [
new PreloadedExtension([$entityType], [])
];
}
/**
* #test
*/
public function formSubmitsValidData()
{
$createdAt = new \DateTime();
$formData = [
"ean" => "8718923400440",
"name" => "Plumbus",
"description" => "This is a household device so common it does not need an introduction",
];
$productComparedToForm = new Product();
$productComparedToForm
->setEan($formData["ean"])
->setName($formData["name"])
;
$productHandledByForm = new Product();
$form = $this->factory->create(ProductForm::class, $productHandledByForm);
$form->submit($formData);
static::assertTrue($form->isSynchronized());
static::assertEquals($productComparedToForm, $productHandledByForm);
$view = $form->createView();
foreach (array_keys($formData) as $key) {
static::assertArrayHasKey($key, $view->children);
}
}
}
First your test case should extends from Symfony\Component\Form\Test\TypeTestCase.
Then your test should look like this:
// Example heavily inspired by EntityTypeTest inside the Symfony Bridge
class ProductTypeTest extends TypeTestCase
{
/**
* #var EntityManager
*/
private $em;
/**
* #var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry
*/
private $emRegistry;
protected function setUp()
{
$this->em = DoctrineTestHelper::createTestEntityManager();
$this->emRegistry = $this->createRegistryMock('default', $this->em);
parent::setUp();
$schemaTool = new SchemaTool($this->em);
// This is the important part for you !
$classes = [$this->em->getClassMetadata(Supplier::class)];
try {
$schemaTool->dropSchema($classes);
} catch (\Exception $e) {
}
try {
$schemaTool->createSchema($classes);
} catch (\Exception $e) {
}
}
protected function createRegistryMock($name, $em)
{
$registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock();
$registry->expects($this->any())
->method('getManager')
->with($this->equalTo($name))
->will($this->returnValue($em));
return $registry;
}
protected function getExtensions()
{
return array_merge(parent::getExtensions(), array(
new DoctrineOrmExtension($this->emRegistry),
));
}
protected function tearDown()
{
parent::tearDown();
$this->em = null;
$this->emRegistry = null;
}
}
another solution if the one above does not work. Example on another application.
namespace App\Tests\Form;
use App\Entity\BusinessDepartment;
use App\Entity\Contact;
use App\Form\ContactForm;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Component\Form\Test\TypeTestCase;
class ContactFormTest extends TypeTestCase
{
protected function getExtensions() {
$mockEntityManager = $this->createMock(EntityManager::class);
$mockEntityManager->method('getClassMetadata')
->willReturn(new ClassMetadata(BusinessDepartment::class))
;
$execute = $this->createMock(AbstractQuery::class);
$execute->method('execute')
->willReturn([]);
$query = $this->createMock(QueryBuilder::class);
$query->method('getQuery')
->willReturn($execute);
$entityRepository = $this->createMock(EntityRepository::class);
$entityRepository->method('createQueryBuilder')
->willReturn($query)
;
$mockEntityManager->method('getRepository')->willReturn($entityRepository);
$mockRegistry = $this->createMock(ManagerRegistry::class);
$mockRegistry->method('getManagerForClass')
->willReturn($mockEntityManager)
;
return array_merge(parent::getExtensions(), [new DoctrineOrmExtension($mockRegistry)]);
}
public function testBuildForm()
{
$data = [
'name' => 'nameTest',
'firstName' => 'firstnameTest',
'email' => 'test_email#gmail.com',
'message' => 'messageTest'
];
$contact = new Contact();
$form = $this->factory->create( ContactForm::class, $contact);
$contactToCompare = new Contact();
$contactToCompare->setName($data['name']);
$contactToCompare->setFirstName($data['firstName']);
$contactToCompare->setEmail($data['email']);
$contactToCompare->setMessage($data['message']);
//check the submission
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($contact->getName(), $contactToCompare->getName());
$this->assertEquals($contact->getFirstName(), $contactToCompare->getFirstName());
$this->assertEquals($contact->getEmail(), $contactToCompare->getEmail());
$this->assertEquals($contact->getMessage(), $contactToCompare->getMessage());
}
}
another solution if the one above does not work. Example on another application.
ContactFromTest.php
namespace App\Tests\Form;
use App\Entity\BusinessDepartment;
use App\Entity\Contact;
use App\Form\ContactForm;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Component\Form\Test\TypeTestCase;
class ContactFormTest extends TypeTestCase
{
protected function getExtensions() {
$mockEntityManager = $this->createMock(EntityManager::class);
$mockEntityManager->method('getClassMetadata')
->willReturn(new ClassMetadata(BusinessDepartment::class))
;
$execute = $this->createMock(AbstractQuery::class);
$execute->method('execute')
->willReturn([]);
$query = $this->createMock(QueryBuilder::class);
$query->method('getQuery')
->willReturn($execute);
$entityRepository = $this->createMock(EntityRepository::class);
$entityRepository->method('createQueryBuilder')
->willReturn($query)
;
$mockEntityManager->method('getRepository')->willReturn($entityRepository);
$mockRegistry = $this->createMock(ManagerRegistry::class);
$mockRegistry->method('getManagerForClass')
->willReturn($mockEntityManager)
;
return array_merge(parent::getExtensions(), [new DoctrineOrmExtension($mockRegistry)]);
}
public function testBuildForm()
{
$data = [
'name' => 'nameTest',
'firstName' => 'firstnameTest',
'email' => 'test_email#gmail.com',
'message' => 'messageTest'
];
$contact = new Contact();
$form = $this->factory->create( ContactForm::class, $contact);
$contactToCompare = new Contact();
$contactToCompare->setName($data['name']);
$contactToCompare->setFirstName($data['firstName']);
$contactToCompare->setEmail($data['email']);
$contactToCompare->setMessage($data['message']);
//check the submission
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($contact->getName(), $contactToCompare->getName());
$this->assertEquals($contact->getFirstName(), $contactToCompare->getFirstName());
$this->assertEquals($contact->getEmail(), $contactToCompare->getEmail());
$this->assertEquals($contact->getMessage(), $contactToCompare->getMessage());
}
}
ContacForm.php
class ContactForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name',TextType::class, [
'label' => 'Nom'
])
->add('firstName',TextType::class, [
'label' => 'Prénom'
])
->add('email', EmailType::class, [
'label' => 'Email'
])
->add('businessDepartment', EntityType::class, [
'label' => 'Département à contacter',
'class' => BusinessDepartment::class,
'choice_value' => 'id',
'choice_label' => 'nameDepartment',
])
->add('message', TextareaType::class, [
'label' => 'Votre message'
])
;
}
}
I want to create form, that serve for adding, editing (and removing when url field is empty) menu items. Problem is that the count of rows/items are variable. (As you can see on the first picture)
Questions:
1)How to write a form that has variable number of fields.
2)How to parse data into the fields at this form.
class GalleryType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add(
//...some textType, fileType fields,...etc
);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
//...
//some data/data_class option that parse data into the field
]);
}
Extra Information:
I am working on own simple content management system with Symfony 3 framework. I want to allow user to add menu item with information like: URL, Title and for instance FA icon, background image,..etc .
-There is always one empty row for adding item and the rest of fields are fulfilled with existing data (menu item/s). When you confirm the form, this row is added into the form (and empty row as well).
-There are few different kind of menu: main menu, slider, side menu, that has diferent type of fields. (you can see it on the second picture)
-Main menu has: title, url and some item can have children items (as sub menu)
-Slider has: title, url, color of title, background image
-Side menu has: title, url and Font Awesome Icon
I have already done form for navigation menu (footer) where is just 2 fields(title and link), but I feel this is not propertly way how to programming it... for illustrative purposes here is how I've done navigation
Controller:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\SMBundle\Navigation;
use AppBundle\Entity\Sett;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class SettingsController extends Controller {
//....
/**
* #Route("/admin/menu/navigation", name="navigation")
*/
public function navigationAction(Request $request) {
$set = $this->getDoctrine()->getRepository('AppBundle:Sett')->findOneByName('navigation');
$navigation = $this->deserializeFromStringToObject('navigation');
if (!$navigation) {
$set = new Sett();
$navigation = new Navigation();
}
$form = $this->createFormFromArray($navigation->getLinksArray());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$set->setEditedAt(new \DateTime());
$set->setName('navigation');
$this->brutalHack($navigation, $form);
$set->setContent($this->serializeFromObjectToString($navigation));
// Save
$this->save($set);
return $this->redirect($this->generateUrl('navigation'));
}
return $this->render("viewSM/menu/navigation.html.twig", array('form' => $form->createView()));
}
private function deserializeFromStringToObject($name) {
$object = $this->getDoctrine()->getRepository('AppBundle:Sett')->findOneByName($name);
if (!$object) {
return null;
}
$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new JsonEncoder()));
return $serializer->deserialize($object->getContent(), 'AppBundle\\Entity\\SMBundle\\' . ucfirst($name), 'json');
}
private function serializeFromObjectToString($object) {
$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new JsonEncoder()));
return $serializer->serialize($object, 'json');
}
private function createFormFromArray(array $collection) {
$i = 0;
$formBuilder = $this->createFormBuilder();
foreach ($collection as $key => $value) {
$formBuilder
->add('url' . $i, TextType::class, ['label' => 'URL ', 'data' => '' . $key, 'attr' => ['class' => 'form-control']])
->add('name' . $i, TextType::class, ['label' => 'Titulek ', 'data' => '' . $value, 'attr' => ['class' => 'form-control']]);
$i++;
}
$formBuilder
->add('url' . $i, TextType::class, ['label' => 'URL ', 'attr' => ['class' => 'form-control']])
->add('name' . $i, TextType::class, ['label' => 'Titulek ', 'attr' => ['class' => 'form-control']])
->add('submit', \Symfony\Component\Form\Extension\Core\Type\SubmitType::class, ['label' => 'Uložit', 'attr' => ['class' => 'btn btn-primary']]);
$form = $formBuilder->getForm();
return $form;
}
private function save($set) {
$em = $this->getDoctrine()->getManager();
$em->persist($set);
$em->flush();
}
private function brutalHack($navigation, $form) {
$nav = array();
if (count($navigation->getLinksArray()) == 0) {
$nav[$form['url0']->getData()] = $form['name0']->getData();
}
for ($i = 0; $i < count($navigation->getLinksArray()); $i++) {
$key = $form['url' . $i]->getData();
$value = $form['name' . $i]->getData();
if ($key != NULL && $value != NULL) {
$nav[$key] = $value;
}
}
$key = $form['url' . $i]->getData();
$value = $form['name' . $i]->getData();
if ($key != NULL && $value != NULL) {
$nav[$key] = $value;
}
$navigation->setLinksArray($nav);
}
//...
}
Entity:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="sett")
* #ORM\HasLifecycleCallbacks
*/
class Sett
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="name", length=255)
*/
private $name;
/**
* #ORM\Column(name="content", type="json_array")
*/
private $content;
/**
* #ORM\Column(name="edited_at", type="datetime")
*/
private $editedAt;
/**
* #ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
/**
* #ORM\PrePersist
*/
public function onPrePersist()
{
$this->createdAt = new \DateTime();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Set
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set content
*
* #param array $content
*
* #return Set
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* #return array
*/
public function getContent()
{
return $this->content;
}
/**
* Set editedAt
*
* #param \DateTime $editedAt
*
* #return Set
*/
public function setEditedAt($editedAt)
{
$this->editedAt = $editedAt;
return $this;
}
/**
* Get editedAt
*
* #return \DateTime
*/
public function getEditedAt()
{
return $this->editedAt;
}
/**
* Set createdAt
*
* #param \DateTime $createdAt
*
* #return Set
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt
*
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
}
Data class:
class Navigation
{
private $linksArray;
public function __construct() {
$this->linksArray=array();
}
function getLinksArray() {
return $this->linksArray;
}
function setLinksArray($linksArray) {
$this->linksArray = $linksArray;
}
function add($key,$value){
$this->linksArray[$key]=$value;
}
}
I am not sure if this will work but you should give it a try.
2)How to parse data into the form that has variable number of fields.
You can send the data as form $options.
in your controller
$oForm = $this->createForm(YourFormType::class,
$FormObject, [
'your_options' => [
'Checkbox' => 'FieldName1',
'TextArea' => 'FieldName2'
]);
in your form
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach($options['your_options'] as $key, $option) { //you can name $option as $filedName or whatever you find convenient
$builder->add($option, $key.Type::class);
}
...}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'your_options' => null
])
}
I'm developing a Symfony 2.6.1 application and I have a form I render with the FormTypes and an Entity using annotations as validation. The form is submitted an AJAX POST call to a FOSRestController. The thing is the isValid() function is returning FALSE and I get no error messages...
My FOSRestController looks as follows:
class RestGalleryController extends FOSRestController{
/**
* #Route(requirements={"_format"="json"})
*/
public function postGalleriesAction(\Symfony\Component\HttpFoundation\Request $request){
return $this->processForm(new \Law\AdminBundle\Entity\Gallery());
}
private function processForm(\Law\AdminBundle\Entity\Gallery $gallery){
$response = array('result' => 'Default');
$gallery->setName('TEST'); //Just added this to be sure it was a problem with the validator
$form = $this->createForm(
new \Law\AdminBundle\Form\Type\GalleryType(),
$gallery
);
$form->handleRequest($this->getRequest());
if ($form->isValid()) {
$response['result'] = 'Is Valid!';
}else{
var_dump( $form->getErrorsAsString() );
die;
}
return $response;
}
My Gallery Entity class below:
<?php
namespace Law\AdminBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
/**
* Gallery
*
* #ORM\Table(name="gallery")
* #ORM\Entity
*/
class Gallery{
/**
* #var string
* #Assert\NotBlank()
* #ORM\Column(name="name", type="text", nullable=false)
*/
private $name;
public function __construct(){
$this->images = new ArrayCollection();
}
/**
* Set name
*
* #param string $name
* #return Gallery
*/
public function setName($name){
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName(){
return $this->name;
}
}
The GalleryType, encapsulating the form:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class GalleryType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('name');
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Law\AdminBundle\Entity\Gallery',
'csrf_protection' => false,
));
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'Gallery';
}
}
Finally, In my app/config/config.yml, the validation is set up as follows:
validation: { enable_annotations: true }
To get the validation error I've also tried with the following function, unsuccessfully :
private function getErrorMessages(\Symfony\Component\Form\Form $form) {
$errors = array();
foreach ($form->getErrors() as $key => $error) {
if ($form->isRoot()) {
$errors['#'][] = $error->getMessage();
} else {
$errors[] = $error->getMessage();
}
}
foreach ($form->all() as $child) {
if (!$child->isValid()) {
$errors[$child->getName()] = $this->getErrorMessages($child);
}
}
return $errors;
}
EDIT:
If I manually use a validator, it works:
$formGallery = new Gallery();
$formGallery->setName($this->getRequest()->get('name', NULL));
$validator = $this->get('validator');
$errors = $validator->validate($formGallery);
So it's like somehow my GalleryType wasn't using the validator.
This is because you are using handleRequest with empty submitted data I guess. In such scenario you such call:
// remove form->handleRequest call
// $form->handleRequest($this->getRequest());
$form->submit($request->request->all());
as handleRequest will auto-submit form unless one field is present. When you handle request with empty array form is not being submitted, thats why isValid return false with no errors.
Note: check if you are sending empty POST array or something like:
`Gallery` => []
If you are sending empty Gallery array everything should work as expected.
Could you paste data that you are sending via AJAX request?
I hope you'll understand well...
I have an Entity : 'Models' which contains an attribut 'spokenlangs' format like that : ,es_ES,fr_FR,
I have an Entity : 'Langs' which contains an attribut 'title' (Ex : Español) and an attribute code (Ex : es_ES).
The BDD schema is imposed and non alterable.. (For my bad !). No link exists between this two entities (tables..).
I would like to create an edit form for Models where the field spokenlangs :
is a multiple choice check box
is displayed by title ( attribut title in Langs entity)
is stored in Models like ,es_ES,us_US (etc if user check several languages)
My file ModelsType :
class ModelsType extends \MyProject\AdminBundle\Form\Type\ModerationAbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $spokenlangTransformer = new SpokenLangsTransformer($this->entityManager);
$builder
->add( $this->getLangsField( $builder, 'spokenlangs', array('multiple' => true, 'expanded' => true) ))
->add( 'user', new UserType($this->entityManager) )
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyProject\EntityBundle\Entity\Models'
));
}
/**
* #return string
*/
public function getName()
{
return 'myproject_entitybundle_models';
}
}
My ModerationAbstractType file (where getLangsField() is defined)
namespace MyProject\AdminBundle\Form\Type;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use MyProject\AdminBundle\Form\Transformer\CountryTransformer;
use MyProject\AdminBundle\Form\Transformer\SpokenLangsTransformer;
/**
* Centralyze user form type
*/
abstract class ModerationAbstractType extends AbstractType
{
/** #var EntityManager */
protected $entityManager;
/**
*
* #param EntityManager $entityManager
*/
public function __construct( EntityManager $entityManager )
{
$this->entityManager = $entityManager;
}
/**
* Return a lang field linked to the langs list by code
* #param type $name
* #param array $options
* #return type
*/
public function getLangsField($builder, $name, $options){
$transformer = new SpokenLangsTransformer($this->entityManager);
return $builder->create($name, 'choice', $options)
->addModelTransformer($transformer);
}
}
And my SpokenLangsTransformer file :
namespace MyProject\AdminBundle\Form\Transformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use MyProject\EntityBundle\Entity\Wmlangs;
/**
* Description of SpokenLangsTransformer
*
*
*/
class SpokenLangsTransformer implements DataTransformerInterface {
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om) {
$this->om = $om;
}
/**
* Transforms an object (wmlangs) to a string (code).
*
* #param Wmlangs|null $spokenlangs
* #return string
*/
public function transform($spokenlangs) {
if (null === $spokenlangs) {
return "";
}
$codeArray = array_filter(explode(",", $spokenlangs));
foreach ($codeArray as $code) {
$spokenlangsArray[] = $this->om
->getRepository('MyProject\EntityBundle\Entity\Wmlangs')
->findOneBy(array('code' => $code));
}
foreach($spokenlangsArray as $namelang) {
$namesLangs[] = $namelang->getTitle();
}
return $namesLangs;
}
/**
* Transforms a string (number) to an object (issue).
*
* #param string $number
* #return Wmlangs|null
* #throws TransformationFailedException if object (wmlangs) is not found.
*/
public function reverseTransform($codes) {
if (!$codes) {
return null;
}
$codeArray = array_filter(explode(",", $codes));
foreach ($codeArray as $code) {
$spokenlangs[] = $this->om
->getRepository('MyProject\EntityBundle\Entity\Wmlangs')
->findOneBy(array('code' => $code));
}
if (null === $spokenlangs) {
throw new TransformationFailedException(sprintf(
'Le problème avec le code "%s" ne peut pas être trouvé!', $code
));
}
return $spokenlangs;
}
}
With this actual code the field does not display anything..
Please, how can I do to add what I expect ?
NB : note that when I tried to access the form it passes in transform function (in my datatransformer.. I think it's not right )
Try 'entity' form filed at getLangsField function
I've been trying to set up a form with Symfony 2.
So I followed the tutorial and I've created a special class for creating the form and handling the validation process outside the controller (as shown in the documentation)
But now I need to fill in a field automatically, I've heard that I have to do it in the ProductType.php, where the form (for my product) is created.
But I don't know how to do, here is my buildForm function in ProductType.php :
class QuotesType extends AbstractType
{
private $id;
public function __construct($id){
$this->product_id = $id;
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('user_name', 'text')
->add('user_lastname', 'text')
->add('user_email', 'email')
->add('user_comments', 'textarea')
->add('user_product_id', 'hidden', array(
'data' => $this->product_id,
));
;
}
and it obviously doesnt work since I got a SQL error saying that my field is null.
How can I put a default value to the user_product_id ? should I do it directly to the object ?
EDIT:
Here is a part of the code of my entity :
namespace QN\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* QN\MainBundle\Entity\Quotes
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="QN\MainBundle\Entity\QuotesRepository")
*/
class Quotes
{
public function __construct($p_id)
{
$this->date = new \Datetime('today');
}
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer $user_product_id
*
* #ORM\Column(name="user_product_id", type="integer")
*/
private $user_product_id = "1";
/**
* #var datetime $date
*
* #ORM\Column(name="date", type="datetime")
*/
private $date;
And my controller :
public function requestAction($id)
{
$repository = $this->getDoctrine()
->getEntityManager()
->getRepository('QNMainBundle:Categories');
$categories = $repository->findAll();
$quote = new Quotes($id);
$form = $this->createForm(new QuotesType(), $quote);
$formHandler = new QuotesHandler($form, $this->get('request'), $this->getDoctrine()->getEntityManager());
if( $formHandler->process() )
{
return $this->redirect( $this->generateUrl('QNMain_Product', array('id' => $id)) );
}
return $this->render('QNMainBundle:Main:requestaform.html.twig', array(
'categories' => $categories,
'id' => $id,
'form' => $form->createView(),
));
}
My Handler :
namespace QN\MainBundle\Form;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManager;
use QN\MainBundle\Entity\Quotes;
class QuotesHandler
{
protected $form;
protected $request;
protected $em;
public function __construct(Form $form, Request $request, EntityManager $em)
{
$this->form = $form;
$this->request = $request;
$this->em = $em;
}
public function process()
{
if( $this->request->getMethod() == 'POST' )
{
$this->form->bindRequest($this->request);
if( $this->form->isValid() )
{
$this->onSuccess($this->form->getData());
return true;
}
}
return false;
}
public function onSuccess(Quotes $quote)
{
$this->em->persist($quote);
$this->em->flush();
}
}
I've also put here the Date I try to set up in the entity, I might do something wrong in both case since I can't make it work neither ..Date is not in the buildForm function, I don't know if I should ..
Another way is creating a Form Type Extension:
namespace App\Form\Extension;
// ...
class DefaultValueTypeExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (null !== $default = $options['default']) {
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
static function (FormEvent $event) use ($default) {
if (null === $event->getData()) {
$event->setData($default);
}
}
);
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('default', null);
}
public static function getExtendedTypes(): iterable
{
yield FormType::class;
}
}
Now any possible value can be passed as default to any form field:
$form->add('user', null, ['default' => $this->getUser()]);
$form->add('user_product_id', null, ['default' => 1]);
This method is specially useful when you don't have a chance to hook into the initialization process of the bound object.
What you're trying to do here is creating a security hole: anyone would be able to inject any ID in the user_product_id field and dupe you application. Not mentioning that it's useless to render a field and to not show it.
You can set a default value to user_product_id in your entity:
/**
* #ORM\Annotations...
*/
private $user_product_id = 9000;