Symfony2: Entity form field with empty value - php

i have a form definition which uses the so-far great field type entity. With the option query_builder I select my values and the are displayed.
The sad part is, I am required to display a null default value, like all (it's a filter form). I don't like the choices option of entity because I have database values and a FormType shouldn't query the database.
My approach so far was to implement a custom field type which extends entity and adds a null entry to the top of the list. The field type is loaded and used but unfortunately the dummy value is not displayed.
The field definition:
$builder->add('machine', 'first_null_entity', [
'label' => 'label.machine',
'class' => Machine::ident(),
'query_builder' => function (EntityRepository $repo)
{
return $repo->createQueryBuilder('m')
->where('m.mandator = :mandator')
->setParameter('mandator', $this->mandator)
->orderBy('m.name', 'ASC');
}
]);
The form type definition:
class FirstNullEntityType extends AbstractType
{
/**
* #var unknown
*/
private $doctrine;
public function __construct(ContainerInterface $container)
{
$this->doctrine = $container->get('doctrine');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired('query_builder');
$resolver->setRequired('class');
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$class = $options['class'];
$repo = $this->doctrine->getRepository($class);
$builder = $options['query_builder']($repo);
$entities = $builder->getQuery()->execute();
// add dummy entry to start of array
if($entities) {
$dummy = new \stdClass();
$dummy->__toString = function() {
return '';
};
array_unshift($entities, $dummy);
}
$options['choices'] = $entities;
}
public function getName()
{
return 'first_null_entity';
}
public function getParent()
{
return 'entity';
}
}

Here is what works in Symfony 3.0.3
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
$builder->add('example' EntityType::class, array(
'label' => 'Example',
'class' => 'AppBundle:Example',
'placeholder' => 'Please choose',
'empty_data' => null,
'required' => false
));

You can use placeholder from 2.6

An alternative approach would be to use a ChoiceList with choices that are generated from the database and then use that in a custom choice form type that will allow for an empty_value.
Choice List
namespace Acme\YourBundle\Form\ChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
class MachineChoiceList extends LazyChoiceList
{
protected $repository;
protected $mandator;
public function __construct(ObjectManager $manager, $class)
{
$this->repository = $manager->getRepository($class);
}
/**
* Set mandator
*
* #param $mandator
* #return $this
*/
public function setMandator($mandator)
{
$this->mandator = $mandator;
return $this;
}
/**
* Get machine choices from DB and convert to an array
*
* #return array
*/
private function getMachineChoices()
{
$criteria = array();
if (null !== $this->mandator) {
$criteria['mandator'] = $this->mandator;
}
$items = $this->repository->findBy($criteria, array('name', 'ASC'));
$choices = array();
foreach ($items as $item) {
$choices[** db value **] = ** select value **;
}
return $choices;
}
/**
* {#inheritdoc}
*/
protected function loadChoiceList()
{
return new SimpleChoiceList($this->getMachineChoices());
}
}
Choice List Service (YAML)
acme.form.choice_list.machine:
class: Acme\YourBundle\Form\ChoiceList\MachineChoiceList
arguments:
- #doctrine.orm.default_entity_manager
- %acme.model.machine.class%
Custom Form Type
namespace Acme\YourBundle\Form\Type;
use Acme\YourBundle\Form\ChoiceList\MachineChoiceList;
..
class FirstNullEntityType extends AbstractType
{
/**
* #var ChoiceListInterface
*/
private $choiceList;
public function __construct(MachineChoiceList $choiceList)
{
$this->choiceList = $choiceList;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceList = $this->choiceList;
$resolver->setDefault('mandator', null);
$resolver->setDefault('choice_list', function(Options $options) use ($choiceList) {
if (null !== $options['mandator']) {
$choiceList->setMandator($options['mandator']);
}
return $choiceList;
});
}
public function getName()
{
return 'first_null_entity';
}
public function getParent()
{
return 'choice';
}
}
Custom Form Type Service (YAML)
acme.form.type.machine:
class: Acme\YourBundle\Form\Type\FirstNullEntityType
arguments:
- #acme.form.choice_list.machine
tags:
- { name: form.type, alias: first_null_entity }
In Your Form
$builder
->add('machine', 'first_null_entity', [
'empty_value' => 'None Selected',
'label' => 'label.machine',
'required' => false,
])
;

Related

Update Many to Many relationship

I am working on project with symfony 3 and doctrine .
I have a many to many relationship between Pack and Produit :
Pack Entity :
class Pack
{
/**
* #var ArrayCollection | Produit[]
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Produit", inversedBy="packs")
* #ORM\JoinTable(name="link_pack")
*/
private $produits;
public function __construct()
{
$this->produits = new ArrayCollection();
}
/**
* #return Produit[]|ArrayCollection
*/
public function getProduits()
{
return $this->produits;
}
public function addProduit(Produit $produit)
{
if ($this->produits->contains($produit)) {
return;
}
$this->produits[] = $produit;
}
public function removeProduit(Produit $produit)
{
if (! $this->produits->contains($produit)) {
return;
}
return $this->produits->removeElement($produit);
}
}
Produit Entity :
class Produit
{
/**
* #var ArrayCollection | Pack[]
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Pack", mappedBy="produits")
*/
private $packs;
public function __construct()
{
$this->packs = new ArrayCollection();
}
/**
* #return Pack[]|ArrayCollection
*/
public function getPacks()
{
return $this->packs;
}
public function addPack(Pack $pack)
{
if ($this->packs->contains($pack)) {
return;
}
$pack->addProduit($this);
$this->packs[] = $pack;
}
}
I want to assign products to a pack , so I have a form that contains products in a select field. (the pack and products are created before).
The form type :
class PackAffectProduitType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('produits', EntityType::class, array(
'class' => Produit::class,
'choice_label' => 'libelle',
'multiple' => true,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Pack::class,
));
}
}
The controller :
public function affectProduitsAction(Pack $pack, Request $request)
{
$form = $this->createForm(PackAffectProduitType::class, $pack);
$form->handleRequest($request);
dump($pack);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
dump($pack);
$em->flush();
// ...
}
//...
}
The problem is when I select the products and submit, It assigns the selected products to the pack but the products assigned before are deleted. I want them to still assigned, So how to solve this problem ?
If you want your view to add OR remove values, if you manually generate your field view structure, you need not to forget to fill already set values (e.g. the products previously added to the Pack).
For example by adding the selected attribute to the choices of your select. Doing so will pre-fill your field with current values therefore removing at submit only those which have been deliberately unselected.
If you don't want your view to display nor know anything about already set Produit entities on this pack and just blindly perform adds, you can add mapped => false to your field options and handle manually the addition of your products to your pack in your controller.
This will only allow you to add though, since it does not have knowledge of already set values, and you would have to make another case/action to perform deletion. Above way is better in my opinion.
In this last case, your controller would look like :
public function affectProduitsAction(Pack $pack, Request $request)
{
$form = $this->createForm(PackAffectProduitType::class, $pack);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
if(is_array($form->get('produits')->getData()){
foreach($form->get('produits')->getData() as $produit){
$pack->addProduit($produit);
}
}
$em->flush();
// ...
}
//...
}
And your FormType :
class PackAffectProduitType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('produits', EntityType::class, array(
'class' => Produit::class,
'mapped' => false,
'choice_label' => 'libelle',
'multiple' => true,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Pack::class,
));
}
}
Try to add 'by_reference' => false option to your form, it will force to use setters and getters from your entity

Symfony (2.7) -> Building a form -> pass a value from another field in the form to a custom Constraint

I want to pass a value of some field to my custom Constraint for another field(->to use it in the custom Validator)
Form with some fields:
...
->add('BsaKey', new \app\...\fieldTypes\RadioButtonType(), [
'choices' => [
...
],
'expanded' => true,
'multiple' => false,
...
])
->add('MeteringCodes', 'collection', [
'type' => new \app\...\formTypes\MeteringCodeType(),
'allow_add' => true,
'label' => false,
'options' => ['label' => $this->lang->get('MeteringCode.Caption')],
'constraints' => new \app\...\validators\NoIdenticMeteringCodes()
])
...
Now i need to pass the value of BsaKey to my custom Constraint for the MeteringCodeType:
class MeteringCodeType extends \Symfony\Component\Form\AbstractType
{
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, array $options)
{
$builder->add('meteringCode', 'text', [
'...' => '...',
'constraints' => new \app\...\MeteringCodeConstraint(['param' => 'VALUE_OF_BsaKey'])
]);
}
}
How can i achieve this?
P.S. I'm not using Symfony as a whole, just some standalone Components...
EDIT:
Thx, I found the solution:
class MeteringCodeValidator extends \Symfony\Component\Validator\ConstraintValidator
{
public function validate($value, \Symfony\Component\Validator\Constraint $constraint)
{
$BsaKey = $this->context->getRoot()->get('BsaKey')->getData();
...
}
}
Seems to work independently from the option returned by "getTargets()" function.
I have done something simular using a custom constraint.
Mine checks that one field is greater then the other, feel free to change it to your requirements.
Sample code;
<?php
// src/AppBundle/Validator/Constraints/FieldCompare.php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class FieldCompare extends Constraint
{
/**
* Error Message
* #access public
* #var string - with placeholders [field1,field2]
*/
public $message = 'Field field2 must be greater than field1 ';
/**
* Form fields
* #access public
* #var array
*/
public $fields = array();
/**
* Class accessors (getters) for the fields.
* #access public
* #var array
*/
public $properties = array();
/**
* Error Path
* #var string
*/
public $errorPath;
public function __construct($options = null)
{
parent::__construct($options);
// check fields is an array
if (!is_array($this->fields) && !is_string($this->fields)) {
throw new UnexpectedTypeException($this->fields, 'array');
}
// make sure there are two of them
if (2 != count($this->fields)) {
throw new ConstraintDefinitionException("Two fields must be specified.");
}
// make sure they are strings
foreach ($this->fields as $f) {
if (null !== $this->errorPath && !is_int()) {
throw new UnexpectedTypeException($this->errorPath, 'integer or null');
}
}
}
/**
* getTargets()
*
* Set traget (so can be used against the class).
* #access public
* #return type
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
<?php
// src/AppBundle/Validator/Constraints/FieldCompareValidator.php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class FieldCompareValidator extends ConstraintValidator {
public function validate($protocol, Constraint $constraint)
{
$fields = (array) $constraint->fields;
$properties = (array) $constraint->properties;
if ($protocol->$properties[0]() >= $protocol->$properties[1]()) {
$this->context->addViolationAt($fields[1], $constraint->message,
array(
'field1' => $this->prettyField($fields[0]),
'field2' => $this->prettyField($fields[1])
), null);
}
}
private function prettyField($field)
{
if (strstr($field, '_')) {
$pretty = str_replace('_', ' ', $field);
} else {
// is camelCase
$converted = preg_replace('/(?!^)[[:upper:]]+/',' \0', $field);
if (is_array($converted)) {
$pretty = implode(' ', $converted);
} else {
$pretty = $converted;
}
}
return ucwords($pretty);
}
}
Here is how I aplied the validator (yaml format);
AppBundle\Model\Foo:
constraints:
- AppBundle\Validator\Constraints\FieldCompare:
fields: [min_lead_time, max_lead_time]
properties: [getminLeadtime, getmaxLeadtime]
groups: [add]

Update Form with 'choice_list' to Symfony >= 2.8

I want to update a form class to Symfony2.8 (and later to Symfony3). Now the form is converted except one attribute, the choice_list, that is not supported anymore. And I don't know how to do this.
I have the following form type that is defined also as a service:
class ExampleType extends AbstractType
{
/** #var Delegate */
private $delegate;
public function __construct(Delegate $delegate)
{
$this->delegate = $delegate;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('list', ChoiceType::class, array(
'choice_list' => new ExampleChoiceList($this->delegate),
'required'=>false)
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'ExampleClass',
));
}
}
And I have the following class for the choice list:
class ExampleChoiceList extends LazyChoiceList
{
/** #var Delegate */
private $delegate;
public function __construct(Delegate $delegate)
{
$this->delegate = $delegate;
}
/**
* Loads the choice list
* Should be implemented by child classes.
*
* #return ChoiceListInterface The loaded choice list
*/
protected function loadChoiceList()
{
$persons = $this->delegate->getAllPersonsFromDatabase();
$personsList = array();
foreach ($persons as $person) {
$id = $person->getId();
$personsList[$id] = (string) $person->getLastname().', '.$person->getFirstname();
}
return new ArrayChoiceList($personsList);
}
}
The class ExampleChoiceList generate the choice list how I want to have it, and until now it worked. But the attribute choice_list is not supported anymore, and my question is "how do I get this converted without too much work?". I read that I should use simple choice but how I get that what I want (the specific label from the database) in Symfony 2.8. I hope somebody can help me.
By using a ChoiceListInterface you are almost there.
I suggest you change the ExampleChoiceList to implement the Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface instead, it requires you implement 3 methods:
<?php
// src/AppBundle/Form/ChoiceList/Loader/ExampleChoiceLoader.php
namespace AppBundle\Form\ChoiceList\Loader;
use Acme\SomeBundle\Delegate;
use Symfony\Component\Form\ArrayChoiceList;
use Symfony\Component\Form\Loader\ChoiceLoaderInterface;
class ExampleChoiceLoader implements ChoiceLoaderInterface
{
/** $var ArrayChoiceList */
private $choiceList;
/** #var Delegate */
private $delegate;
public function __construct(Delegate $delegate)
{
$this->delegate = $delegate;
}
/**
* Loads the choice list
*
* $value is a callable set by "choice_name" option
*
* #return ArrayChoiceList The loaded choice list
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
$persons = $this->delegate->getAllPersonsFromDatabase();
$personsList = array();
foreach ($persons as $person) {
$label = (string) $person->getLastname().', '.$person->getFirstname();
$personsList[$label] = (string) $person->getId();
// So $label will be displayed and the id will be used as data
// "value" will be ids as strings and used for post
// this is just a suggestion though
}
return $this->choiceList = new ArrayChoiceList($personsList);
}
/**
* {#inheritdoc}
*
* $choices are entities or the underlying data you use in the field
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// optimize when no data is preset
if (empty($choices)) {
return array();
}
$values = array();
foreach ($choices as $person) {
$values[] = (string) $person->getId();
}
return $values;
}
/**
* {#inheritdoc}
*
* $values are the submitted string ids
*
*/
public function loadChoicesForValues(array $values, $value)
{
// optimize when nothing is submitted
if (empty($values)) {
return array();
}
// get the entities from ids and return whatever data you need.
// e.g return $this->delegate->getPersonsByIds($values);
}
}
register both the loader and the type as services so they are injected:
# app/config/services.yml
services:
# ...
app.delegate:
class: Acme\SomeBundle\Delegate
app.form.choice_loader.example:
class: AppBundle\Form\ChoiceList\Loader\ExampleChoiceLoader
arguments: ["#app.delegate"]
app.form.type.example:
class: AppBundle\Form\Type\ExampleType
arguments: ["#app.form.choice_loader.example"]
Then change the form type to use the loader:
<?php
// src/AppBundle/Form/Type/ExampleType.php
namespace AppBundle\Form\Type;
use AppBundle\Form\ChoiceList\Loader\ExampleChoiceLoader;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class ExampleType extends AbstractType
{
/** #var ExampleChoiceLoader */
private $loader;
public function __construct(ExampleChoiceLoader $loader)
{
$this->loader = $loader;
}
public function buildForm(FormBuilderInterface $builder, array $options = array())
{
$builder->add('list', ChoiceType::class, array(
'choice_loader' => $this->loader,
'required' => false,
));
}
// ...
}
Yes, 'choice_list' is deprecated with SYmfony 2.8, but you can use 'choices' instead, which accepts an array as well. From documentation:
The choices option is an array, where the array key is the item's
label and the array value is the item's value.
You must just pay attention than in Symfony 3.0, keys and values are inversed, and in Symfony 2.8, the recommended way is to use the new inversed order, and to specify 'choices_as_values' => true.
So, in the form type:
$builder->add('list', ChoiceType::class, array(
'choices' => new ExampleChoiceList($this->delegate),
'choices_as_values' => true,
'required'=>false));
And in ExampleChoiceList:
protected function loadChoiceList()
{
$persons = $this->delegate->getAllPersonsFromDatabase();
$personsList = array();
foreach ($persons as $person) {
$id = $person->getId();
$personsList[(string) $person->getLastname().', '.$person->getFirstname()] = $id; // <== here
}
return new ArrayChoiceList($personsList);
}
UPDATE:
Ok, so I suggest you don't use a ChoiceType at all, but a EntityType, as you seem to fetch all "Persons" from database. And to display "Last Name, First Name" as a label, use the 'choice_label' option. Assuming your entity is called 'Person':
$builder->add('list', EntityType::class, array(
'class' => 'AppBundle:Person',
'choice_label' => function ($person) {
return $person->getLastName() . ', ' . $person->getFirstName();
}
));

Symfony2 DataTransformer after handleRequest

I have an API and I am sending a reference of an entity, I'm using a DataTransformer to get my entity but the DataTransformer is always called before the $form->handleRequest($request) the value is always null and it could't works
My Controller
public function newAction(Request $request)
{
$orderNewFormType = $this->get('competitive_bo.api_bundle.form.type.order_new');
$card = new Card();
try {
$form = $this->createForm($orderNewFormType, $card);
$form->handleRequest($request);
} catch (TransformationFailedException $e) {
return $this->notFoundErrorResponse(
'Business not found'
);
}
if ($form->isValid()) {
return $this->okResponse(array());
}
$validatorErrorFormatter = $this->get('competitive_bo.api_bundle.formatter.validator_error');
$errors = $validatorErrorFormatter->formatFromFormError($form->getErrors(true));
return $this->badRequestErrorResponse(
'Invalid data',
$errors
);
}
The form type
class OrderNewFormType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('customer', 'entity', array(
'class' => 'CompetitiveBOBusinessBundle:Customer',
'property' => 'id'
))
->add('business', 'business', array(
'mapped' => false,
))
;
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false,
'data_class' => Card::class
));
}
public function getName()
{
return null;
}
}
The Business form type
class BusinessReferenceFormType extends AbstractType
{
/**
* #var ReferenceToBusinessTransformer
*/
private $referenceToBusinessTransformer;
public function __construct(ReferenceToBusinessTransformer $referenceToBusinessTransformer)
{
$this->referenceToBusinessTransformer = $referenceToBusinessTransformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer($this->referenceToBusinessTransformer);
}
public function getName()
{
return 'business';
}
public function getParent()
{
return 'text';
}
}
And the DataTransformer
/**
* Class ReferenceToBusinessTransformer
*/
class ReferenceToBusinessTransformer implements DataTransformerInterface
{
/**
* #var BusinessRepository
*/
private $businessRepository;
public function __construct(BusinessRepository $businessRepository)
{
$this->businessRepository = $businessRepository;
}
/**
* {#inheritdoc}
*/
public function transform($reference)
{
var_dump($reference);
$business = $this->businessRepository->findOneBy(array(
'reference' => $reference
));
if (null === $business) {
throw new TransformationFailedException;
}
return $business;
}
/**
* {#inheritdoc}
*/
public function reverseTransform($value)
{
if (!($value instanceof Business)) {
throw new TransformationFailedException;
}
return $value->getReference();
}
}
The var_dump($reference) is always null
And I have my test
public function testNewAction($getParams, $postParam, $responseCode)
{
$client = static::createClient();
$router = $client->getContainer()->get('router');
$route = $router->generate('competitivebo_api_order_new',$getParams);
$client->request('POST', $route, $postParam);
$response = $client->getResponse();
$this->assertJsonResponse($response, $responseCode);
}
With the post params
'customer' => 1,
'business' => LoadBusinessData::REFERENCE_1,
'name' => 'Test',
The exception TransformationFailedException is always thrown during the $this->createForm(...) so the request is not handled
According with the documentation,
When null is passed to the transform() method, your transformer should return an equivalent value of the type it is transforming to (e.g. an empty string, 0 for integers or 0.0 for floats).

Form edition for a N:M relationship with extra fields is not working

I'm working on a form that handle N:M relationship with an extra parameter (extra field/column). This is what I've done until now:
In OrdersType.php form:
class OrdersType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder fields
// $builder fields only need on edit form not in create
if ($options['curr_action'] !== NULL)
{
$builder
// other $builder fields
->add("orderProducts", "collection", array(
'type' => new OrdersHasProductType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
));
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\Orders',
'render_fieldset' => FALSE,
'show_legend' => FALSE,
'intention' => 'orders_form',
'curr_action' => NULL
));
}
public function getName()
{
return 'orders';
}
}
In OrderHasProductType.php:
class OrdersHasProductType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', 'text', array(
'required' => FALSE,
'label' => FALSE
))
->add('amount', 'text', array(
'required' => TRUE,
'label' => FALSE
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\OrderHasProduct',
'intention' => 'order_has_product'
));
}
public function getName()
{
return 'order_has_product';
}
}
And finally this is Orders.php and OrdersHasProduct.php entities:
class Orders {
use IdentifiedAutogeneratedEntityTrait;
// rest of fields for the entity
/**
* #ORM\OneToMany(targetEntity="OrderHasProduct", mappedBy="order", cascade={"all"})
*/
protected $orderProducts;
protected $products;
/**
* #ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
protected $deletedAt;
public function __construct()
{
$this->orderProducts = new ArrayCollection();
$this->products = new ArrayCollection();
}
public function getOrderProducts()
{
return $this->orderProducts;
}
public function getDeletedAt()
{
return $this->deletedAt;
}
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
}
public function getProduct()
{
$products = new ArrayCollection();
foreach ($this->orderProducts as $op)
{
$products[] = $op->getProduct();
}
return $products;
}
public function setProduct($products)
{
foreach ($products as $p)
{
$ohp = new OrderHasProduct();
$ohp->setOrder($this);
$ohp->setProduct($p);
$this->addPo($ohp);
}
}
public function getOrder()
{
return $this;
}
public function addPo($ProductOrder)
{
$this->orderProducts[] = $ProductOrder;
}
public function removePo($ProductOrder)
{
return $this->orderProducts->removeElement($ProductOrder);
}
}
/**
* #ORM\Entity
* #ORM\Table(name="order_has_product")
* #Gedmo\SoftDeleteable(fieldName="deletedAt")
* #UniqueEntity(fields={"order", "product"})
*/
class OrderHasProduct {
use IdentifiedAutogeneratedEntityTrait;
/**
* Hook timestampable behavior
* updates createdAt, updatedAt fields
*/
use TimestampableEntity;
/**
* #ORM\ManyToOne(targetEntity="\Tanane\FrontendBundle\Entity\Orders", inversedBy="orderProducts")
* #ORM\JoinColumn(name="general_orders_id", referencedColumnName="id")
*/
protected $order;
/**
* #ORM\ManyToOne(targetEntity="\Tanane\ProductBundle\Entity\Product", inversedBy="orderProducts")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
protected $product;
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected $amount;
/**
* #ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
protected $deletedAt;
public function setOrder(\Tanane\FrontendBundle\Entity\Orders $order)
{
$this->order = $order;
}
public function getOrder()
{
return $this->order;
}
public function setProduct(\Tanane\ProductBundle\Entity\Product $product)
{
$this->product = $product;
}
public function getProduct()
{
return $this->product;
}
public function setAmount($amount)
{
$this->amount = $amount;
}
public function getAmount()
{
return $this->amount;
}
public function getDeletedAt()
{
return $this->deletedAt;
}
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
}
}
But when I try to edit a order with this code in my controller:
public function editAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('FrontendBundle:Orders')->find($id);
$type = $order->getPerson()->getPersonType() === 1 ? "natural" : "legal";
$params = explode('::', $request->attributes->get('_controller'));
$actionName = substr($params[1], 0, -6);
$orderForm = $this->createForm(new OrdersType(), $order, array('action' => '#', 'method' => 'POST', 'register_type' => $type, 'curr_action' => $actionName));
return array(
"form" => $orderForm->createView(),
'id' => $id,
'entity' => $order
);
}
I get this error:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
Proxies__CG__\Tanane\ProductBundle\Entity\Product. You can avoid this
error by setting the "data_class" option to
"Proxies__CG__\Tanane\ProductBundle\Entity\Product" or by adding a
view transformer that transforms an instance of class
Proxies__CG__\Tanane\ProductBundle\Entity\Product to scalar, array or
an instance of \ArrayAccess.
And I don't find or know how to fix it, can any give me some help?
I think this is happening because this code
class OrdersHasProductType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', 'text', array(
'required' => FALSE,
'label' => FALSE
))
));
}
//...
}
means that Symfony is expecting "product" to be a field of type "text", but when it calls getProduct() on the OrderHasProduct it gets a Product object (or a Doctrine Proxy to a Product, since it's not been loaded at that point). Symfony Fields inherit from Form/AbstractType, so they're essentially Forms in their own right, with just one field, hence the error message.
The solution is either to make that field of type "entity", or to create a different method which only gives the name of the Product, e.g. getProductName() on OrderHasProduct, and then use that as the data behind the field.

Categories