I'm experiencing an odd error with my form validation in Symfony 4. It is a simple contact form represented by this entity:
class ContactRequest
{
/** #var int */
private $id;
/** #var string */
private $fullName;
//...
/**
* #return string
*/
public function getFullName() : string
{
return $this->fullName;
}
In my controller I'm handling the submission as per Symfony website but there is something I'm missing for I'm getting the following error:
Type error: Return value of App\Entity\ContactRequest::getFullName() must be of the type string, null returned
Now, I know what is the meaning of that: it expects a string to be returned by the method getFullName whereas null is actually returned but I don't understand why.
This is my controller
public function contactSubmit(Request $request, ValidatorInterface $validator)
{
$form = $this->createForm(ContactType::class);
$form->handleRequest($request);
if($form->isValid()){
//...
}
$errors = $validator->validate($form);
Shouldn't the handleRequest() method set the values in the entity for me?
To my surprise, when I have initialised the entity before, it worked well notwithstanding the entity is already set in the configureOptions() method in the form:
$contact = new ContactRequest;
$contact
->setFullName($request->request->get('contact')['fullName'])
//...
$form = $this->createForm(
ContactType::class
$contact
);
$form->setData($contact);
$form->handleRequest($request);
However, shouldn't the scope of using the handleRequest() be to avoid setting the entity's values manually? Shouldn't the handleRequest() method take care of setting those values?
I know I could validate the submitted data against the entity too (thing that I've successfully tried), without using the handleRequest() at all but it would piss me off a little. Why would I need to set a form in such a case?
This is the ContactType form:
//...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('fullName', TextType::class, [
'required' => true,
'empty_data' => 'a',
'attr' => [
'placeholder' => 'Full Name',
'class' => 'form-control'
]
])
//...
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ContactRequest::class
]);
}
In order to find out where your getFullName gets called you could (atleast in dev environment) print the backtrace on the call:
/**
* #return string
*/
public function getFullName() : string
{
if ($this->fullName === null)
{
echo "<pre>getFullName on uninitialized entity:\n";
debug_print_backtrace();
die();
}
return $this->fullName;
}
But as said in the comments: Initializing the entity with a null value in that field and not allowing the getter to return a null value seems kinda odd to me, so : ?string to allow for nullable return values (as of PHP 7.1) seems to be the next best option.
Related
I have a ManyToMany relation in my Symfony 4.2.6 application and I would like for it to be possible to have this to be null.
So my first entity SpecialOffers is as follows :
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\SpecialOfferRepository")
*/
class SpecialOffer
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Neighbourhood", inversedBy="specialOffers")
*/
private $neighbourhood;
public function __construct()
{
$this->neighbourhood = new ArrayCollection();
}
/**
* #return Collection|Neighbourhood[]
*/
public function getNeighbourhood(): Collection
{
return $this->neighbourhood;
}
public function addNeighbourhood(Neighbourhood $neighbourhood): self
{
if (!$this->neighbourhood->contains($neighbourhood)) {
$this->neighbourhood[] = $neighbourhood;
}
return $this;
}
public function removeNeighbourhood(Neighbourhood $neighbourhood): self
{
if ($this->neighbourhood->contains($neighbourhood)) {
$this->neighbourhood->removeElement($neighbourhood);
}
return $this;
}
}
It is related to the neighbourhood class :
/**
* #ORM\Entity(repositoryClass="App\Repository\NeighbourhoodRepository")
*/
class Neighbourhood implements ResourceInterface
{
/**
* #ORM\ManyToMany(targetEntity="App\Entity\SpecialOffer", mappedBy="neighbourhood")
*/
private $specialOffers;
public function __construct()
{
$this->specialOffers = new ArrayCollection();
}
/**
* #return Collection|SpecialOffer[]
*/
public function getSpecialOffers(): Collection
{
return $this->specialOffers;
}
public function addSpecialOffer(SpecialOffer $specialOffer): self
{
if (!$this->specialOffers->contains($specialOffer)) {
$this->specialOffers[] = $specialOffer;
$specialOffer->addNeighbourhood($this);
}
return $this;
}
public function removeSpecialOffer(SpecialOffer $specialOffer): self
{
if ($this->specialOffers->contains($specialOffer)) {
$this->specialOffers->removeElement($specialOffer);
$specialOffer->removeNeighbourhood($this);
}
return $this;
}
}
And finally the form is
class SpecialOfferType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'neighbourhood',
EntityType::class,
[
'class' => Neighbourhood::class,
'label' => 'form.neighbourhood.label',
'translation_domain' => 'Default',
'required' => false,
'placeholder' => 'form.neighbourhood.all'
]
);
}
}
But when I don't select a specific neighbourhood for the Special offer in my form I get the following error :
Could not determine access type for property "neighbourhood" in class "App\Entity\SpecialOffer": The property "neighbourhood" in class "App\Entity\SpecialOffer" can be defined with the methods "addNeighbourhood()", "removeNeighbourhood()" but the new value must be an array or an instance of \Traversable, "NULL" given.
Is there anyway I can make it so that my special offer either contains and array of neighbourhoods or just null ?
I feel like I'm overlooking something really obvious, any help would be greatly appreciated
Test =>
$builder
->add(
'neighbourhood',
EntityType::class,
[
'class' => Neighbourhood::class,
'label' => 'form.neighbourhood.label',
'translation_domain' => 'Default',
'required' => false,
'multiple' => true,
'placeholder' => 'form.neighbourhood.all'
]
);
Since your fields on the entities are both many-to-many, thus expecting an array (or similar) and the form field is of EntityType, which will return one Entity of the expected type or null, I feel like there is some form of asymmetry.
I would consider using the CollectionType from the start or at least setting the multiple option on the form to true, so that the return value is an array.
Another option would be to add a DataTransformer to the form field, which turns null into an empty array and one entity into an array of one entity, and vice-versa.
I have an entity named HoursSpecial with a foreign key relationship to an entity called HoursArea. Each HoursSpecial belongs to an HoursArea. When I create a new HoursSpecial via my HoursSpecialType, I want the form field to automatically populate the HoursArea field.
I know what you're thinking, just do something like this in my controller's method:
$form->add('area', 'hidden', array('data'=>$area));
That would be fine except I need to make a DataTransformer to switch between the area's ID and the actual area entity. So I have to declare my HoursArea field within my HoursSpecialType with the transformer:
$builder
...
->add('area', 'hidden')
;
$builder->get('area')->addModelTransformer(new HoursAreaToIntTransformer($this->manager));
Now, I can't simply feed my HoursArea entity into the form. Is there an effective way to make this happen?
I've thumbed through Symfony's documentation on How to Dynamically Modify Forms Using Form Events, but I can't make heads or tails of how I would pass in that HoursArea entity dynamically from outside of the form builder. Maybe I'm just missing something?
UPDATE
Following the recommendation of the answer (Recommendation #1) below from #Ryan, I have created the custom type HiddenHoursAreaType:
// AppBundle\Form\Type\HideenHoursAreaType.php
class HiddenHoursAreaType extends AbstractType
{
//need to instantiate HoursAreaToIntTransformer
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new HoursAreaToIntTransformer($this->manager);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => null,
'compound' => true //this should be FALSE as there are no children
));
}
/**
*
* the return value of the getParent function indicates that you're extending the choice field type.
* This means that, by default, you inherit all of the logic and rendering of that field type.
*/
public function getParent()
{
return 'hidden';
}
public function getName()
{
return 'app_hoursArea';
}
I have added my transformer into the custom type class. Here is the transformer class:
// AppBundle\Form\DataTransformer;
class HoursAreaToIntTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* Transforms an object (HoursArea) to a string (number).
*
* #param Issue|null $issue
* #return string
*/
public function transform($area)
{
if (null === $area) {
return '';
}
return $area->getId();
}
/**
* Transforms a string (number) to an object (HoursArea).
*
* #param string $areaId
* #return HoursArea|null
* #throws TransformationFailedException if object (HoursArea) is not found.
*/
public function reverseTransform($areaId)
{
// no area number? It's optional, so that's ok
if (!$areaId) {
return;
}
$area = $this->manager
->getRepository('AppBundle:HoursArea')
// query for the issue with this id
->find($areaId)
;
if (null === $area) {
// causes a validation error
// this message is not shown to the user
// see the invalid_message option
throw new TransformationFailedException(sprintf(
'An area with number "%s" does not exist!',
$areaId
));
}
return $area;
}
}
Within my controller I create the form with the custom type field:
$form = $this->createForm(new HoursSpecialType($this->getDoctrine()->getManager()), $entity, array(
'action' => $this->generateUrl('hoursspecial_postcreate'),
'method' => 'POST',
));
$form->add('eventDate', 'hidden', array('data'=>$dateString));
$form->add('area', new \AppBundle\Form\Type\HiddenHoursAreaType($this->getDoctrine()->getManager()), array(
'data'=>$area,
'invalid_message'=>'Area field not converted proerly'
));
$form->add('submit', 'submit', array('label' => 'Create'));
Thanks to the transformer and the custom type, the form now correctly converts the HoursArea entity to an integer for population in the hidden field.
The problem now is that upon form submission, the integer is not converted back into an HoursArea object. I know this because I get the 'invalid_message' upon submission.
Final Update
The reason the HoursArea id wasn't being inserted properly had something to do with the
'compound' => true
setting I had in my custom type. I assume it was looking for child fields and wasn't finding any...which it shouldn't have because there were none!
You could create a custom type for it and add the addModelTransformer() call in the buildForm() of your custom type, but still pass the data in explicitly. So your $form->add('area', 'hidden', array('data'=>$area)) would become $form->add('area', new HiddenHoursAreaType(), array('data'=>$area)) where HiddenHoursAreaType::getParent() would be the hidden type.
You could set the data in a POST_SET_DATA listener.
You could get the $options['data'] value in buildForm() and explicitly pass in the HoursArea ID.
/** #var HoursSpecial $hoursSpecial Prepopulated in controller */
$hoursSpecial = $options['data']
$builder->add('area', 'hidden', ['data' => $hoursSpecial->getHoursArea()->getId()])
I have a form responsible of creating and updating users. A user can (or not) have an address (OneToOne unidirectional relation from user).
When I create a user, no problem.
When I update a user, usually no problem.
Problems come up when i update a user which already has an address and try to unset all the address fields. There is then a validation error.
The wanted behavior would be to have the user->address relation set to null (and delete the previously set address on the DB).
There is a cascade_validation, the addess field in form (nested form) is set to not be required and the user entity allow the address to be null.
UPDATE
Relevant entities and forms :
User entity (Getters & Setters are classical, Symfony generated):
class User
{
[...]
/**
* #var \Address
*
* #ORM\OneToOne(targetEntity="Address", cascade="persist")
* #ORM\JoinColumn(
* name="address_id", referencedColumnName="id"
* )
*/
private $address;
[...]
}
The address entity is classical, no bidirectionnal relation to user.
User form
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
[...]
->add('address', new AddressType(), array('required' => false))
[...]
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Xentia\FuneoBundle\Entity\User',
'cascade_validation' => true
));
}
public function getName()
{
return 'user';
}
}
The address nested form is classical
As you can see, the is a quite classical and straightforward code. The only particular case is that address is optional. Leading to an validation error only in the case that the address was previously set (and, thus, exist in the DB and as a not null relation with the user) and the user want to unset it (all address fields are left empty).
It seems that if the related address has not an actual instance it can still be optional. But, if an instance of the address exist and is linked with the user, it can not be optional anymore.
UPDATE 2
namespace Xentia\FuneoBundle\Form\Type;
use Doctrine\Common\Util\Debug;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add("suggestLocality", null, array(
'mapped' => false,
'label' => 'Locality'
))
->add("suggestStreet", null, array(
'mapped' => false,
'label' => 'Street'
))
->add('street')
->add('locality')
->add('postalCode')
->add('country', null, array(
'label' => false,
))
->add('latitude', 'hidden')
->add('longitude', 'hidden');
$builder->addEventListener(FormEvents::PRE_SUBMIT,
function(FormEvent $event) {
$address = $event->getData();
if (!empty($address)) {
$addressLocality = $address['locality'];
if (empty($addressLocality)) {
$event->setData(null);
}
}
}
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Xentia\FuneoBundle\Entity\Address',
'validation_groups' => array('Default'),
));
}
public function getName()
{
return 'address';
}
}
Try setting orphanRemoval on your relation
/** #OneToOne(targetEntity="...", orphanRemoval=true) */
$address
EDIT
I see now, you have placed the wrong listener. First of all it should be POST_SUBMIT, PRE_SUBMIT is to process request data and modify form. On POST SUBMIT you can modify the object.
I am learning to use Symfony2 and in the documentation I have read, all entities being used with Symfony forms have empty constructors, or none at all. (examples)
http://symfony.com/doc/current/book/index.html Chapter 12
http://symfony.com/doc/current/cookbook/doctrine/registration_form.html
I have parametrized constructors in order to require certain information at time of creation. It seems that Symfony's approach is to leave that enforcement to the validation process, essentially relying on metadata assertions and database constraints to ensure that the object is properly initialized, forgoing constructor constraints to ensure state.
Consider:
Class Employee {
private $id;
private $first;
private $last;
public function __construct($first, $last)
{ .... }
}
...
class DefaultController extends Controller
{
public function newAction(Request $request)
{
$employee = new Employee(); // Obviously not going to work, KABOOM!
$form = $this->createFormBuilder($employee)
->add('last', 'text')
->add('first', 'text')
->add('save', 'submit')
->getForm();
return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}
Should I not be using constructor arguments to do this?
Thanks
EDIT : Answered Below
Found a solution:
Looking into the API for the Controllers "createForm()" method I found something that is not obvious from the examples. It seems that the second argument is not necessarily an object:
**Parameters**
string|FormTypeInterface $type The built type of the form
mixed $data The initial data for the form
array $options Options for the form
So rather than pass in an instance of the Entity, you can simply pass in an Array with the appropriate field values:
$data = array(
'first' => 'John',
'last' => 'Doe',
);
$form = $this->createFormBuilder($data)
->add('first','text')
->add('last', 'text')
->getForm();
Another option (which may be better), is to create an empty data set as a default option in your Form Class.
Explanations here and here
class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('first');
$builder->add('last');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'empty_data' => new Employee('John', 'Doe'),
));
}
//......
}
class EmployeeFormController extends Controller
{
public function newAction(Request $request)
{
$form = $this->createForm(new EmployeeType());
}
//.........
}
Hope this saves others the head scratching.
I have entity Message with ManyToOne relation with entity User:
class Message
{
...
/**
* #var User $sender
*
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User")
* #ORM\JoinColumn(name="sender_id", referencedColumnName="id")
*
**/
private $sender;
...
}
If $sender doesn't have email value i need to create new field for my form, so i create form for Message entity in Contoller:
$user = $this->getUser();
$message = new Message();
$message->setSender($user);
$formBuilder = $this->createFormBuilder($message, array(
'cascade_validation' => true
));
$formBuilder->add('body', 'textarea');
if (!$user->getEmail()) {
$formBuilder->add('email', 'email', array(
'property_path' => 'sender.email'
));
}
And i have some validation rules in validation.yml for entity User. Can i automatically validate this field by my validation rules for User entity in another entity's form? I don't know how to do it.
I found workaround right now: create new MissingEmailType:
class MissingEmailType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\UserBundle\Entity\User',
'validation_groups' => array(
'MissingEmail'
),
));
}
public function getName()
{
return 'missing_email';
}
}
But it looks complicated. Is there any better solutions?
You could redirect the page to the user profile page instead of loading the message form and state that the user needs to add an email prior to adding the message. If you redirect quickly or create a popup, the user might not be turned off as long as they can return to the original page after adding their email. Then validtion is simple since you only need to validate the user entity.