ChoiceType Not Mapping to Entity Field - php

I have an Announcement Entity that where a EditAnnouncementType form is mapped to. I have two other CheckBoxType forms that automatically update their respective fields, but the ChoiceType form isn't working.
$builder
->add('edit', SubmitType::class,
array
(
'label' => 'Save changes',
'attr' => ['class' => 'btn btn-primary']
))
->add('type', ChoiceType::class,
[
'choices' => [
'info_type' => 1,
'star_type' => 2,
'alert_type' => 3,
'lightbulb_type' => 3,
'event_type' => 4,
'statement_type' => 5,
'cat_type' => 6,
'hands_type' => 7
],
'mapped' => true,
'expanded' => true,
'required' => true,
])
The Announcement entity and type field
class Announcement
{
/**
* #var int
*/
private $type;
/**
* #return string
*/
public function getType(): string
{
return $this->type;
}
/**
* #param int $type
*
* #return $this
*/
public function setType(int $type)
{
$this->type = $type;
return $this;
}

My suspicion would be that Symfony somehow strict checks the value (using ===).
And since your getter returns a string, the mapping doesn't happen properly.
You should try fixing your getter:
/**
* #return int
*/
public function getType(): int
{
return $this->type;
}
Also mind that you might have a problem in your choice array:
// be careful: those two have the same value
'alert_type' => 3,
'lightbulb_type' => 3,
This would surely cause an issue to Symfony, especially if it uses array_values in order to select the right choice out of the value of your entity.

Related

Zend3 Form Filter - ParamConverter could not generate valid form

I'm really confused about my Form Filter.
My Test-Project contains 2 Models.
class Category extends AbstractEntity
{
use Nameable; // just property name and getter and setter
/**
* #var boolean
* #ORM\Column(name="issue", type="boolean")
*/
private $issue;
/**
* #var Collection|ArrayCollection|Entry[]
*
* #ORM\OneToMany(targetEntity="CashJournal\Model\Entry", mappedBy="category", fetch="EAGER", orphanRemoval=true, cascade={"persist", "remove"})
*/
private $entries;
}
the entry
class Entry extends AbstractEntity
{
use Nameable;
/**
* #var null|float
*
* #ORM\Column(name="amount", type="decimal")
*/
private $amount;
/**
* #var null|Category
*
* #ORM\ManyToOne(targetEntity="CashJournal\Model\Category", inversedBy="entries", fetch="EAGER")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
*/
protected $category;
/**
* #var null|DateTime
*
* #ORM\Column(name="date_of_entry", type="datetime")
*/
private $dateOfEntry;
}
And if someone needed the AbstractEntity
abstract class AbstractEntity implements EntityInterface
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
}
Every Category can have many Entries. I'm using Doctrine for this relation. And this works fine.
I have a Form based on this FieldSet:
$this->add([
'name' => 'id',
'type' => Hidden::class
]);
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name'
]
]);
$this->add([
'name' => 'amount',
'type' => Number::class,
'options' => [
'label' => 'Summe'
]
]);
$this->add([
'name' => 'date_of_entry',
'type' => Date::class,
'options' => [
'label' => 'Datum'
]
]);
$this->add([
'name' => 'category',
'type' => ObjectSelect::class,
'options' => [
'target_class' => Category::class,
]
]);
So my Form displays a dropdown with my categories. Yeah fine.
To load the Category for my Entry Entity i use a filter.
$this->add([
'name' => 'category',
'required' => true,
'filters' => [
[
'name' => Callback::class,
'options' => [
'callback' => [$this, 'loadCategory']
]
]
]
]);
And the callback:
public function loadCategory(string $categoryId)
{
return $this->mapper->find($categoryId);
}
The mapper loads the category fine. great. But the form is invalid because:
Object of class CashJournal\Model\Category could not be converted to int
Ok, so i'm removing the Filter, but now it failed to set the attributes to the Entry Entity, because the setter needs a Category. The Form error says:
The input is not a valid step
In Symfony i can create a ParamConverter, which converts the category_id to an valid Category Entity.
Question
How i can use the filter as my ParamConver?
Update
Also when i cast the category_id to int, i will get the error from the form.
Update 2
I changed my FieldSet to:
class EntryFieldSet extends Fieldset implements ObjectManagerAwareInterface
{
use ObjectManagerTrait;
/**
* {#inheritDoc}
*/
public function init()
{
$this->add([
'name' => 'id',
'type' => Hidden::class
]);
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name'
]
]);
$this->add([
'name' => 'amount',
'type' => Number::class,
'options' => [
'label' => 'Summe'
]
]);
$this->add([
'name' => 'date_of_entry',
'type' => Date::class,
'options' => [
'label' => 'Datum'
]
]);
$this->add([
'name' => 'category',
'required' => false,
'type' => ObjectSelect::class,
'options' => [
'target_class' => Category::class,
'object_manager' => $this->getObjectManager(),
'property' => 'id',
'display_empty_item' => true,
'empty_item_label' => '---',
'label_generator' => function ($targetEntity) {
return $targetEntity->getName();
},
]
]);
parent::init();
}
}
But this will be quit with the error message:
Entry::setDateOfEntry() must be an instance of DateTime, string given
Have you checked the documentation for ObjectSelect? You appear to be missing a few options, namely which hydrator (EntityManager) and identifying property (id) to use. Have a look here.
Example:
$this->add([
'type' => ObjectSelect::class,
'name' => 'category', // Name of property, 'category' in your question
'options' => [
'object_manager' => $this->getObjectManager(), // Make sure you provided the EntityManager to this Fieldset/Form
'target_class' => Category::class, // Entity to target
'property' => 'id', // Identifying property
],
]);
To validate selected Element, add in your InputFilter:
$this->add([
'name' => 'category',
'required' => true,
]);
No more is needed for the InputFilter. A Category already exist and as such has been validated before. So, you should just be able to select it.
You'd only need additional filters/validators if you have special requirements, for example: "A Category may only be used once in Entries", making it so that you need to use a NoObjectExists validator. But that does not seem to be the case here.
UPDATE BASED ON COMMENTS & PAST QUESTIONS
I think you're over complicating a lot of things in what you're trying to do. It seems you want to simply populate a Form before you load it client-side. On receiving a POST (from client) you wish to put the received data in the Form, validate it and store it. Correct?
Based on that, please find a complete controller for User that I have in one of my projects. Hope you find it helpful. Providing it because updates are veering away from your original question and this might help you out.
I've removed some additional checking and error throwing, but otherwise is in complete working fashion.
(Please note that I'm using my own abstract controller, make sure to replace it with your own and/or recreate and match requirements)
I've also placed additional comments throughout this code to help you out
<?php
namespace User\Controller\User;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\ORMException;
use Exception;
use Keet\Mvc\Controller\AbstractDoctrineActionController;
use User\Entity\User;
use User\Form\UserForm;
use Zend\Http\Request;
use Zend\Http\Response;
class EditController extends AbstractDoctrineActionController
{
/**
* #var UserForm
*/
protected $userEditForm; // Provide this
public function __construct(ObjectManager $objectManager, UserForm $userEditForm)
{
parent::__construct($objectManager); // Require this in this class or your own abstract class
$this->setUserEditForm($userEditForm);
}
/**
* #return array|Response
* #throws ORMException|Exception
*/
public function editAction()
{
$id = $this->params()->fromRoute('id', null);
// check if id set -> else error/redirect
/** #var User $entity */
$entity = $this->getObjectManager()->getRepository(User::class)->find($id);
// check if entity -> else error/redirect
/** #var UserForm $form */
$form = $this->getUserEditForm(); // GET THE FORM
$form->bind($entity); // Bind the Entity (object) on the Form
// Only go into the belof if() on POST, else return Form. Above the data is set on the Form, so good to go (pre-filled with existing data)
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost()); // Set received POST data on Form
if ($form->isValid()) { // Validates Form. This also updates the Entity (object) with the received POST data
/** #var User $user */
$user = $form->getObject(); // Gets updated Entity (User object)
$this->getObjectManager()->persist($user); // Persist it
try {
$this->getObjectManager()->flush(); // Store in DB
} catch (Exception $e) {
throw new Exception('Could not save. Error was thrown, details: ', $e->getMessage());
}
return $this->redirectToRoute('users/view', ['id' => $user->getId()]);
}
}
// Returns the Form with bound Entity (object).
// Print magically in view with `<?= $this->form($form) ?>` (prints whole Form!!!)
return [
'form' => $form,
];
}
/**
* #return UserForm
*/
public function getUserEditForm() : UserForm
{
return $this->userEditForm;
}
/**
* #param UserForm $userEditForm
*
* #return EditController
*/
public function setUserEditForm(UserForm $userEditForm) : EditController
{
$this->userEditForm = $userEditForm;
return $this;
}
}
Hope that helps...

How to implement a group of elements in the drop-down list ObjectSelect

How to implement a group of elements in the drop-down list ObjectSelect 'optgroup_identifier'
Form\CategoryForm.php
$this->add([
'type' => ObjectSelect::class,
'name' => 'category',
'options' => [
'label' => 'Категория',
'object_manager' => $this->getObjectManager(),
'target_class' => Category::class,
'property' => 'name',
'optgroup_identifier' => '???',
'optgroup_default' => 'Главная',
'empty_option' => '== Категория ==',
'is_method' => true,
'find_method' => [
'name' => 'findAllChildCategories',
'params' => [
],
],
],
]);
Category Table is relevant Self-referencing
Entity\Category.php
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\OneToMany(targetEntity="Application\Entity\Category", mappedBy="parent", cascade={"remove"})
*/
private $children;
/**
* #var \Application\Entity\Category
*
* #ORM\ManyToOne(targetEntity="Application\Entity\Category", inversedBy="children")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="parent", referencedColumnName="id", nullable=true)
* })
*/
private $parent;
Group name must be the parent category
$category->getParent()->getName()
Thankfully for this case, Doctrine does not do any queries to get groupings; it does it internally. optgroup_identifier is just name of a getter it uses to get group names, therefore that getter can return anything you want.
In Entity\Category, add a method dedicated to returning parent name of a category. Ensure it does not coincide with any fields so Doctrine does not return a whole object proxy into a form. For example:
public function getParentName() {
if(!$this->parent) return '';
return $this->parent->getName();
}
Since root categories will not have a parent, $this->parent will be null. Look out for that case to avoid script crashing and return empty string as a designation for it.
Then, put this getter name in optgroup_identifier of the form. Final result will be as in screenshot with sample data.

Symfonys: Model with array field is not possible to show in Twig

I have a Model:
class SearchOfferFacet
{
/**
* #var string
*/
protected $id;
/**
* #var array
*/
protected $country;
//getters ans setters....
And I have a FormType
...
$builder
->add('country', ChoiceType::class, array(
'expanded' => true,
'multiple' => false,
'choices_as_values' => true,
'choices' => array('US' => "USA", "IT" => "Italia"),
))
When I create the form in my controller with
$searchOfferFacet = new SearchOfferFacet();
$searchOfferFacet->setCountry(
array(
'US' => 'US',
'MV' => 'MV'
)
);
$form = $this->createForm(SearchOfferFacetType::class, $searchOfferFacet, array('method' => 'POST'));
I get the following Exception:
An exception has been thrown during the rendering of a template ("Notice: Array to string conversion") in form_div_layout.html.twig at line 13.
But when I change the variable in my model to String, and I set a string as Country, for example "US", then the exception appears:
Expected an array.
In my TWIG there is nothing special, just:
{{ form_widget(form.country) }}
Does anyone has an idea what is wrong?
Can you try setting 'multiple' => true,? My suspicion here is that you the member is an array, but since you set 'multiple' => false,, it only needs to save a string.
On the other hand, you can then also just do
/**
* #var string
*/
protected $country;
and the setCountry(...) function should only save a string.

Symfony2 Change checkbox values from 0/1 to 'no'/'yes'

I created a form with one checkbox.
UserSettingsType.php:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('newsletter', 'checkbox', array(
'label' => 'Newsletter erhalten',
'attr' => array(
'class' => 'form-control',
),
'required' => false,
));
}
In the UserSettings.php Entity:
/**
* #ORM\Column(name="newsletter", type="boolean")
*/
protected $newsletter;
In the User.php:
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $user_settings_id;
/**
* #ORM\OneToOne(targetEntity="UserSettings", cascade={"persist"})
* #ORM\JoinColumn(name="user_settings_id", referencedColumnName="id")
*/
protected $settings;
In the PageController.php i handle the settings action:
public function settingsAction() {
$user = $this->getUser();
if ($user->getSettings() !== null) {
$settings = $user->getSettings();
} else {
$settings = new UserSettings($user);
}
$settings_form = $this->createForm(new UserSettingsType(), $settings);
$request = $this->getRequest();
if ($request->getMethod() == 'POST') {
$em = $this->getDoctrine()->getManager();
$settings_form->bind($request);
if ($settings_form->isValid()) {
$user->setSettings($settings);
$em->persist($user);
$em->flush();
}
}
return $this->render('MyCompMyAppBundle:Page:settings.html.twig', array(
'settings_form' => $settings_form->createView(),
));
}
I want to change the checkbox values from false (unchecked) / true (checked) to 'no' / 'yes' and change the definition of the newsletter field to: * #ORM\Column(name="newsletter", type="string", columnDefinition="ENUM('yes', 'no')")
It would be nice if there would be 'yes' and 'no' enum values in the database.
Please correct me if i am wrong: There is no way to change this via form element attributes, right?
I heard something about a DataTransformer:. But is there any easier way to realize this?
do you want checkbox or radio button? for checkbox in peresentation use:
$builder->add('newsletter', 'choice', array(
'label' => 'Newsletter erhalten',
'attr' => array(
'class' => 'form-control',
),
'choices' => array(array('yes' => 'yes'), array('no' => 'no')),
'expanded' => true,
'multiple' => true,
'required' => false,
));
Don't use ENUM for this!
In MySQL, use either data type BIT(1) or TINYINT(1) (=same as BOOLEAN). See here: Which MySQL data type to use for storing boolean values
In PostgreSQL, there is a true BOOLEAN type. But no ENUM. So if you're ever thinking about migrating, better get rid of ENUM ;-)

Doctrine 2: Understand how entities works with doctrine 2

I have 3 Entities: Person, Affiliation and PersonAffiliation.
In my form, I will display each affiliation as a checked checkbox.
Nowm when the user uncecks the checkbox and click submit, this affiliation should be removed from the PersonAffiliation table.
The problem is that when I submit without unchecking, my data are duplicated.
Example: aff1 and aff2. When both checked and submit, I will then get aff1 aff1 aff2 aff2.
The same, If I uncheck aff2, I will then have aff1 aff1.
The error is probably somewhere in using the doctrine:
Here is how I am managing that:
Entity Persom
#ORM\OneToMany(targetEntity="PersonAffiliation", mappedBy="person", cascade={"persist", "remove"})
protected $affiliations;
Entity Affiliation:
#ORM\OneToMany(targetEntity="PersonAffiliation", mappedBy="affiliation")
protected $person_affiliations;
Entity PersonAffiliation
#ORM\ManyToOne(targetEntity="Person", inversedBy="affiliations")
#ORM\JoinColumn(name="person_id", referencedColumnName="id")
protected $person;
#ORM\ManyToOne(targetEntity="Affiliation", inversedBy="person_affiliations")
#ORM\JoinColumn(name="affiliation_id", referencedColumnName="id")
protected $affiliation;
An idea on how to resolve that?
Thank you.
EDIT:
Cotroller part:
foreach( $enquiry->getAffiliations() as $aff )
{
$pAff = new PersonAffiliation();
$pAff->setPersonId( $person->getId() );
$pAff->setAffiliationId( $aff->getAffiliation()->getId() );
$pAff->setPerson( $person );
$pAff->setAffiliation( $aff->getAffiliation() );
$em->persist($pAff);
$em->flush();
}
Form Part:
public function buildForm(FormBuilder $builder, array $options)
{
$person = $this->person;
$user = $this->user;
$builder->add('firstname', 'text');
$builder->add('middlename', 'text', array('required'=>false));
$builder->add('lastname', 'text');
$builder->add('sex', 'choice', array( 'choices' => array('m' => 'Male', 'f' => 'Female'),
'required' => true,
'multiple' => false,
'expanded' => true));
$builder->add('email', 'text', array('required'=>false));
if( $this->addAffiliations ) {
$builder->add('affiliations', 'entity', array(
'label' => 'Athor\'s affiliations',
'class' => 'SciForumVersion2Bundle:PersonAffiliation',
'query_builder' => function($em) use ($person, $user){
return $em->createQueryBuilder('pa')
->where('pa.person_id = :pid')
->setParameter('pid', $person->getId());
},
'property' => 'affiliation',
'multiple' => true,
'expanded' => true,
));
}
}
In the Person entity :
/**
* #ORM\ManyToMany(targetEntity="Affiliation", inversedBy="people")
* #ORM\JoinTable(name="PersonAffiliation")
*/
protected $affiliations;
And in the Affiliation entity :
/**
* #ORM\ManyToMany(targetEntity="Person", mappedBy="affiliations")
*/
protected $people;
You made a $em->flush() on each iteration of the foreach. It should be done after the end of foreach isnt'it ?
Moreover, you may use a ManyToMany relation.

Categories