Why doesn't non doctrine-mapped ArrayCollection get initialized? - forms

My Setup is a Symfony 3.4 App with the typical 'ManyToMany'-Relation with additional fields, something like this:
Entity Article
Entity Specialty
Entity ArticleSpecialtyRelation
In a Form for an Article i wanted it to look like as if it were a ManyToMany-Relation rendered as an EntityType with multiple=true and expanded=true, so all entries of Specialty are rendered as checkboxes.
To achieve that i created a non orm-mapped property specialties that is an ArrayCollection, gets initialized in the Constructor and has a Getter, Adder and Remover.
/**
*
* #var ArrayCollection;
*
*/
protected $specialties;
public function __construct()
{
$this->specialties = new ArrayCollection();
}
/**
* #return Collection|Specialty[]
*/
public function getSpecialties()
{
return $this->specialties;
}
/**
* #param Specialty $specialties
*/
public function addSpecialties(Specialty $specialties)
{
$this->specialties->add($specialties);
}
/**
* #param Specialty $specialties
*/
public function removeSpecialties(Specialty $specialties)
{
$this->specialties->removeElement($specialties);
}
This property is used to render the Specialty Entity as checkboxes:
add('specialties', EntityType::class,array(
'class' => Specialty::class,
'expanded'=>true,
'multiple'=>true,
'label'=>'Specialties',
'required' => false,
'mapped'=>true,
));
To populate it with the data from SpecialtyRelation i added a PreSetData Formevent:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$article = $event->getData();
if ($article instanceof Article) {
$form->get('specialties')->setData($article->getUsedSpecialties());
}
});
The used Getter of $artikel just iterates over $article->getArtikelSpecialties and returns a collection of Specialty.
It all works until the submit. Because the formfield is mapped=true, somewhere in handleRequest($form) where the entity is hydrated with the form data, it explodes when the Adder for $specialty is called:
Call to a member function add() on null
Because as i just learned, the Constructor is never called by Doctrine and obviously initializes all ORM-ArrayCollections but not the ArrayCollection for the non-mapped property specialties -
Of course I can check if the ArrayCollection is initialized in the Adder and Remover and initialize it there if it is null, but that just feels a bit hacky in a already at least hacky-felt setup and i am wondering if my setup is completely stupid, especially since i didn't find anybody trying to do that (or getting problems with that) on here or elsewhere.
Is there a better solution to this or should i just check the ArrayCollection in Adder and Remover and live happily ever after?
Also, just curious, is there any other way to initialize the ArrayCollection?
P.S. If there are typos in the names it's because i translated the names into english.
Partial Stacktrace
Symfony\Component\Debug\Exception\FatalThrowableError: Call to a
member function add() on null
at src/Test/Bundle/TestBundle/Entity/Article.php:312 at
Test\Bundle\TestBundle\Entity\Article->addSpecialties(object(Specialty))
(vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:674)
at
Symfony\Component\PropertyAccess\PropertyAccessor->writeCollection(array(object(Article),
object(Article)), 'specialties', object(ArrayCollection),
'addSpecialties', 'removeSpecialties')
(vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:622)
at
Symfony\Component\PropertyAccess\PropertyAccessor->writeProperty(array(object(Article),
object(Article)), 'specialties', object(ArrayCollection))
(vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:216)
at
Symfony\Component\PropertyAccess\PropertyAccessor->setValue(object(Article),
object(PropertyPath), object(ArrayCollection))
(vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php:86)
at
Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper->mapFormsToData(object(RecursiveIteratorIterator),
object(Article))
(vendor/symfony/symfony/src/Symfony/Component/Form/Form.php:636) at Symfony\Component\Form\Form->submit(array(), true)
(vendor/symfony/symfony/src/Symfony/Component/Form/Form.php:580)

Related

Constraint on Value Object in form Symfony

I'm quite new to Symfony and I started digging around Symfony forms.
As described here https://webmozart.io/blog/2015/09/09/value-objects-in-symfony-forms/ I'm using value objects in my subform. A constructor of value object can throw an exception if invalid values are provided. Therefore when I put invalid value to my field I'm getting ugly exception from VO, hence I want to connect a Validator Constraint on this but the validate() function gets already a Value object... Any thoughts on this issue?
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
/....
$builder->add('latitude', LatitudeType::class, [
'label' => false,
'constraints' => [new Valid()],
]);
}
Latitude type
class LatitudeType extends AbstractType implements DataMapperInterface
{
const INPUT_NAME = 'latitude';
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(self::INPUT_NAME, TextType::class, [
'label' => 'FORM.LATITUDE',
'attr' => [
'placeholder' => 'PLACEHOLDER.LATITUDE',
],
'required' => false,
'constraints' => [new LatitudeValidator()],
]);
$builder->setDataMapper($this);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Latitude::class,
'empty_data' => null,
'error_bubbling' => true
]);
}
/**
* #param Latitude $data
* #param FormInterface[]|\Traversable $forms
*/
public function mapDataToForms($data, $forms)
{
$forms = iterator_to_array($forms);
$forms[self::INPUT_NAME]->setData($data);
}
/**
* #param FormInterface[]|\Traversable $forms
* #param mixed $data
*/
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
if ($forms[self::INPUT_NAME]->getData()) {
$data = new Latitude((float)$forms[self::INPUT_NAME]->getData());
}
}
This validation method is receiving already a created VO
class LatitudeValidator extends ConstraintValidator
{
/**
* {#inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (null === $value || '' === $value) {
return;
}
But I want to be able to do something like
try {
new \ValueObject\Latitude((float)$value);
} catch (\InvalidArgumentException $e) {
$this->context->buildViolation($e->getMessage())
->addViolation();
}
You have differents methods to use form with Value Objects but after a lot of troubles by my side I decided to stop this. Symfony have to construct your Value Object even if your VO is invalid. You gave an example on an invalid state but you have also others example when you form doesn't fit well your Domain like when you have not enought fields to complete your required properties on your VOs.
Symfony Forms can be complexe and the use of VOs inside them can bring more complexity whereas the forms should be linked to the interface and not always to the domain objects.
The best solution for me is to use the command pattern. You have a simple example with other reasons to use it here. You can also avoid to put this logic into your controllers and avoid code duplication with a command bus librairy like tactician or now the messenger component of Symfony.
With a command you can simply represent an action by the form. The form can have validators related to the VO or directly to the form.
With the command bus you can create your Value Object in a valid state and throw exceptions in a second layer when you forget a use case.
This approach is more robust and avoid a lot of troubles for my point of view.
The best thing you achieve this, is to accept any kind of value into the ValueObject and then perform validation on it.
This way you're not forced to handle exception due to invalid types passed through constructor.
Moreover remember that creation or "value setting" of the underlying object is performed by the framework before validation (otherwise you'll never have to use VO) so you should leverage on this and let the Form component do his job (as you done correclty with transformers). Then, you can perform any kind of validation on underlying object.

Symfony 3 - Form - CollectionType in Entity without Doctrine

I'm struggling with symfony3 forms and the CollectionType class:
I have a page with several complex forms. I do not use any database (the validated forms are sent to a foreign REST-service)
Now let's say I have an entity object for my request called "ProductService":
class ProductService
{
/** #var string */
private $msg;
/** #var array*/
private $products
}
And a class ProductServiceType to render the form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('products', CollectionType::class, [
'entry_type' => ProductType::class,
])
->add('msg' [...]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ProductService::class,
]);
}
With this setup, everything works like a charm, all products will be added in the products array of my entity.
The problem is, i want $products to be a SplObjectStorage-object:
class ProductService
{
/** #var string */
private $msg;
/** #var SplObjectStorage */
private $products
}
If i set it to this and attach an empty object into it, symfony can't render the form any more. It throws following error:
Warning: SplObjectStorage::offsetExists() expects parameter 1 to be object, string given
So, can anybody tell me, how to handle the collectionType in an entity when NOT using doctrine and orm?
Is the only possibility using arrays, or is there any documentation for this case, i did not find?
(I'm still wondering how symfony calls offsetExists, there must be someting implemented to handle SplObjectStorage, or am i wrong?)
I believe your error is caused because a form collection has not been implemented to handle SplObjectStorage as you would expect. You can create an issue for it at the symfony repository.
The error is caused when symfony is trying to populate the form collection by reading your products from ProductService this way:
$products = $productService->getProducts();
$products->offsetExists(0); //here is the error.
because it expects any storage that implements ArrayAccess will be read this way, but for SplObjectStorage this is not the case.
Form elements have a setting property_path link which you can take advantage to work around your issue.
My solution is to use this setting and return return an array to populate your collection:
$builder
->add('products', CollectionType::class, [
'entry_type' => ProductType::class,
'property_path' => 'productsArray'
])
class ProductService
{
...
public function getProductsArray() {
$prArray= [];
foreach ($this->products as $value) {
$prArray[] = $value;
}
return $prArray;
}
This way you can populate your form collection using the array produced.
Another solution I think would be to use a data transformer. Hope this helps

handle request of incomplete missing fields symfony form

I have created a small Symfony (Sf3.2 + php7) web with a Task Entity. I have my controller where I have a new action to create a new task. Everything is fine. Now, some of the form fields are not necessary to be filled (created date, for example). So I have removed from my taskType:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add->('name')->add('description');
}
Now my form looks exactly as I want. The surprise is that when I submit it my controller pass it to doctrine,and every missing field is write as a NULL. NO!, I don't want that, what I wanted is my default mysql value.
Ok, I read the doc. There seems to be two ways of handling data:
$form->handleRequest($request);
$form->submit($request->request->get($form->getName()));
I've found in the API, (not in the doc) that submit have a second parameter, a boolean:
API says:
bool $clearMissing Whether to set fields to NULL when they are missing in the submitted data.
Great! this is exactly what I need. Lets see it:
public function newAction(Request $request) {
$task = new Task();
$form = $this->createForm('myBundle\Form\TaskType', $task);
$form->submit($request->request->get($form->getName()), false);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($task);
$em->flush();
$response = new Response();
$response->setContent(json_encode(array(
'data' => 'success',
)));
$response->headers->set('Content-Type', 'application/json');
return $response;
return $this->redirectToRoute('task_success');
}
return $this->render('task/new.html.twig', array(
'task' => $task,
'form' => $form->createView(),
));
}
Well, after tried everything I always get NULL in all missing fields. What am I doing wrong.
Thank you, Caterina.
I'm afraid there must be something more. And I was thinking that perhaps is not Doctrine the solution. The Symfony log shows me the insert and it's setting a Null in every empty field, on empty fields and missing fields. In fields like status i could set a default value, at doctrine level, but if I want to set created_at field, I suppose that must be Mysql the responsible is setting current timeStamp.
Anyway this is my ORM property status from Task Entity.
/**
* #var \scrumBundle\Entity\Taskstatus
*
* #ORM\ManyToOne(targetEntity="scrumBundle\Entity\Taskstatus")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="status", referencedColumnName="id")
* })
* #ORM\Column(name="status", type="integer", nullable=false,options={"default":1})
*/
private $status;
And this is the property id from TaskStatus:
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false, options={"default":1})
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
As you see I tried to follow your advise of setting the option default value. I've tried to create to set the form with handleRequest and submit with same result
$task = new Task();
$form = $this->createForm('scrumBundle\Form\TaskType', $task);
//$form->handleRequest($request);
$form->submit($request->request->get($form->getName()), false);
if ($form->isSubmitted() && $form->isValid()) {
I even tried to debug step by step submit function, a real Hell, because submit is a recursive function to much complex for me.
Ok , again thank for you time.
Regards, Cate
PS. Sorry about my poor english.;^)
Short answer - when you persist a PHP object through Doctrine, you must have absolutely every value set that you want. If your PHP object has null fields, Doctrine will manually set them as null in your entity. Doctrine doesn't assume that just because a value is null in your PHP object, that you don't want it included on your INSERT statement. This is because null is a perfectly valid SQL value.
So, whatever your PHP object is at the time of insert is exactly what is going to be inserted into your database, null values and all.
When you are editing an entry, Doctrine will only update the fields that are different, so this isn't a concern. Your concern is when persisting entities.
The easiest solution is to copy your MySQL default value into your PHP entity. Like one of these many ways:
// set default value on the variable itself
public $someVar = 100;
// set default values in the constructor (so when creating a new entry)
public function __construct()
{
$this->createdAt = new \DateTime();
$this->isActive = true;
}
You can also use Lifecycle Callbacks to set when inserting a new entry:
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Task
{
// ...
/**
* #ORM\PrePersist
*/
public function setCreatedAtValue()
{
$this->createdAt = new \DateTime();
}
}

Validation with ManyToOne

I have an Entity "Element" with a ManyToOne relationship with List (a list can have multiple elements)
/**
* #ORM\ManyToOne(targetEntity="Liste")
*/
private $list;
How can I validate a form to add a new element, with just passing the id of the list and not the list itself ? (The list has to exist)
in the old days (pre 2.8) we were able to set the cascade_validation flag which would then validate any child objects pre-persist. This was at best hit and miss.
That gone, the correct way is to do the following (note the valid constraint):
from the docs
use use Symfony\Component\Validator\Constraints as Assert;
class stuff
{
// ....
/**
* #ORM\ManyToOne(targetEntity="Liste")
* #Assert\Valid
*/
private $list;
// ....
}
this will force the framework the call any validators that you have on the related entity.
this is available from symfony 2.7
You have to follow by this steps:
1) Assign/Set entity class in Form
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'CoreBundle\Form\Type\YourEntity',
])
}
2) Create custom validation on YourEntity
#Assert\CheckExistingList()
class YourEntity
{
public function __construct()
}
3) Create new validator file CheckExistingListValidator.php and write your validation logic inside below function.
public function validate(Constraint $constraint)
{
// logic here.
}
So whenever your Form will submit then this validation should be called and error message show in Form error list.
You must add the form field with entity type like this:
->add('list', EntityType::class, [
'choice_label' => 'my test title'
])

How to preset data on forms 'HiddenType::class' field with relational data?

I just want to preset hidden "fkCar" field with Car object with the id matching the one in the url pattern. So that when a user clicks on a link next to a car entry, he can directly add history to that car, without the need to select that car id from a drop down list. I can preset data on the dropdown list but whenever I try to use HiddenType in my FormType I get this error message:
"Expected argument of type "AdminBundle\Entity\Car", "string" given"
From what I have noticed it's like that because the instance of the Car Object is converted by __toString() magic method which returns "string" and not Car object anymore. On the other hand, the same thing happens on the dropdown choice field but no error are thrown and it works fine...
When I use ->add('fkCar') in my FormType instead, it works fine but I have a dropdown list which I don't want
when I use HiddenType like so:
->add('fkCar',HiddenType::class, [
// ...
])
I get quoted error message.
This is my code:
My FormType
<?php
namespace AdminBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Intl\DateFormatter\IntlDateFormatter;
class CarHistoryCarIdType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('issueDate', DateType::class,[
'format'=> IntlDateFormatter::LONG,
])
->add('invoiceNum')
->add('invoiceTotal')
->add('quoteNum')
->add('mileage')
->add('description')
// ->add('fkCar') // works - but dropdown choice field is there able to be edited
->add('fkCar',HiddenType::class, [ // generate above error message
// ...
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AdminBundle\Entity\CarHistory'
));
}
}
My controller action:
use AdminBundle\Form\CarHistoryCarIdType;
use AdminBundle\Entity\Car;
use AdminBundle\Entity\CarHistory;
...
/**
* Creates a new History for selected car.
*
* #Route("/new/history/{carId}", name="car_new_history")
* #Method({"GET", "POST"})
*/
public function newHistoryAction(Request $request, $carId)
{
// get car data
$car = $this->getDoctrine()->getRepository(Car::class)->find($carId);
dump($car); // test
dump(get_class($car)); // test
// create History Entity Object
$history = new CarHistory();
// set History Entity fkCar to Car Entity Object with id == $carId
$history->setFkCar($car);
$history->setIssueDate(new \DateTime('now'));
// build form and set data
$form = $this->createForm(CarHistoryCarIdType::class, $history);
dump($request->request->all()); // test
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($history);
$em->flush();
return $this->redirectToRoute('full_car_history', array('id' => $carId));
}
return $this->render('car/newCarHistory.html.twig', array(
'carId' => $carId,
'form' => $form->createView(),
));
}
This is my dump test result. As you can see, the doctrine returns the car object as expected but form 'fkCar' stays empty:
the problem is the hidden type in the form won't allow edit the data inside this. you must change the type and in the view you must hide the field using css. usually I do this:
<div style="display:none">{{form_widget(form.fkCar)}}</div>
change the type in your form class and use the default text type