Symfony3 form: View transformer error suggests using view transfomer - php

I wanted to create a simple HiddenEntityType, so that I can have hidden form fields that represent an entity. The simplest solution by far looked to be to employ a (view) transformer.
However I cannot see to get it to work.
Based on documentation here
...the forward transform needs to return something you can put into HTML, a string(e.g. the id of my entity) and the reverseTransform must transform an id into an entity.
With that in mind here is the simple class I made:
namespace AppBundle\Form\Type;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class HiddenEntityType extends AbstractType
{
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* Constructor
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$repository = $this->entityManager->getRepository($builder->getDataClass());
$builder->addViewTransformer(new CallbackTransformer(
function ($entity) use($repository) { //Forward
dump('Forward');
dump($entity);
if (!is_null($entity)) return $entity->getId();
return $entity;
},
function ($id) use($repository) { //Reverse
dump('Reverse');
dump($id);
return $repository->find($id);
}
));
}
/**
* #inheritDoc
*/
public function getParent()
{
return 'hidden';
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}
I receive the following illogical error:
An exception has been thrown during the rendering of a template ("The form's view data is expected to be an instance of class AppBundle\Entity\MyEntity, but is a(n) integer. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) integer to an instance of AppBundle\Entity\MyEntity.").
When creating the form, based on the dumps you see above, the forward transform is called twice - once with null and once with the AppBundle\Entity\MyEntity as its argument.
What might I be doing wrong?
Sy 3.3.6
PHP 7.1.8

Related

Symfony - How to create entity with a form and DTO

I want to create entity with symfony form and DTO. I tried to do DTO and form like I've seen on symfonycast. But there's something wrong and I can't figure it out.
After sending json file via postman I get an error:
Typed property App\Form\Model\CreateFacilityDTO::$pitchTypes must not be accessed before initialization (500 Internal Server Error)
postman body:
{
"name": "legia",
"pitchTypes": ["basketball"],
"address": "kosynierów"
}
Can You tell me what I'm doing wrong?
<?php
namespace App\Controller;
use App\Entity\Facility;
use App\Form\CreateFacilityFormType;
use App\Form\Model\CreateFacilityDTO;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* #method saveEntities(array $array)
*/
class CreateFacilityAction extends AbstractController
{
/**
* #Route("/api/create/facility", name="create_facility")
* #param Request $request
* #return Response
*/
public function __invoke(Request $request, EntityManagerInterface $em)
{
$form = $this->createForm(CreateFacilityFormType::class);
$data = json_decode($request->getContent(), true);
$form->submit($data);
// if ($form->isSubmitted() && $form->isValid()) {
/** #var CreateFacilityDTO $facilityDto */
$facilityDto = $form->getData();
$createFacility = new Facility($facilityDto->name, $facilityDto->pitchTypes,
$facilityDto->address);
$em = $this->getDoctrine()->getManager();
$em->persist($createFacility);
$em->flush();
// return new Response($data, 201);
// }
return new Response($createFacility, 201);
}
}
<?php
namespace App\Form;
use App\Form\Model\CreateFacilityDTO;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CreateFacilityFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('pitchTypes')
->add('address');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CreateFacilityDTO::class,
]);
}
}
<?php
namespace App\Form\Model;
class CreateFacilityDTO
{
public string $name;
public array $pitchTypes;
public string $address;
}

Custom choices on form with data_class

I have created a FormType that get's a data_class set in the defaults in the configureOptions class. It looks a bit like this
<?php
namespace tzfrs\AppBundle\Form\Type;
use tzfrs\AppBundle\Model\Car;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class CarType
* #package tzfrs\AppBundle\Form\Type
*/
class CarType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('specification', ChoiceType::class, [
'choices' => [1 => 1, 2 => 2, 3 => 3]
]);
}
/**
* #inheritdoc
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Car::class
]);
}
}
When I now create the form like this
$carForm = $this->createForm(CarType::class, $car);
and want to use it I get the following error:
The value of type "object" cannot be converted to a valid array key.
I think the reason for this, is because my Car class has a property specification with getters/setters and therefore returns a Specification object. However, my Specification object has a __toString() method.
Car.php
/**
* #var Specification
*/
protected $specification;
public function getSpecification(): Specification
{
return $this->specification;
}
public function setSpecification(Specification $specification): Car
{
$this->specification = $specfification;
return $this;
}
Specification.php
/**
* Returns a string implementation of the current class
*
* #return string
*/
public function __toString(): string
{
return sprintf(
'HP: %s,CCM: %s',
$this->getHorsePower(),
$this->getCapacity()
);
}
How would I achieve overriding my choices for a form field which uses a custom Model? I can't use the EntityType because it's not a Doctrine Model.
When I build my form like this
$carForm = $this->createForm(Car::class);
then it works, but I need the Car object inside the Form, because of another field I'm adding in the buildForm method

Symfony2 UniqueEntity constraint SQL error instead of message

I'm stuck with creating a user registration form with Symfony2.
I'm trying to define an Unique constraint on the email attribute of my User class.
Acme\APPBundle\Entity\User.php
namespace Acme\APPBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* #ORM\Entity
* #ORM\Entity(repositoryClass="Acme\APPBundle\Entity\UserRepository")
* #ORM\Table("users")
* #UniqueEntity(
* fields={"email"},
* message="email already used"
* )
*/
class User implements UserInterface, \Serializable
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=255, unique=true)
* #Assert\NotBlank()
* #Assert\Email()
*/
protected $email;
[...]
}
Acme\APPBundle\Form\Type\UserType.php
namespace Acme\APPBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('email', 'email');
$builder->add('password', 'repeated', array(
'first_name' => 'password',
'second_name' => 'confirm',
'type' => 'password',
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\APPBundle\Entity\User',
'cascade_validation' => true,
));
}
public function getName()
{
return 'user';
}
}
I've added the constraint following the documentation but I still get an exception like :
An exception occured while executing 'INSERT INTO users ( ... )'
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
It looks like my message value defined in annotations is ignored and the form validation is bypassed since it should fail before attempting to insert row in database.
Can you tell me what am I doing wrong ?
EDIT :
Following Matteo 'Ingannatore' G.'s advice I've noticed that my form is not properly validated.
I forgot to mention that I use a registration class that extends the user form. I've written my code after what is explained in the Symfony Cookbook.
Thus I have :
Acme\APPBundle\Form\Model\Registration.php
namespace Acme\APPBundle\Form\Model;
use Symfony\Component\Validator\Constraints as Assert;
use Acme\APPBundle\Entity\User;
class Registration
{
/**
* #Assert\Type(type="Acme\APPBundle\Entity\User")
*/
protected $user;
/**
* #Assert\NotBlank()
* #Assert\True()
*/
protected $termsAccepted;
public function setUser(User $user)
{
$this->user = $user;
}
public function getUser()
{
return $this->user;
}
public function getTermsAccepted()
{
return $this->termsAccepted;
}
public function setTermsAccepted($termsAccepted)
{
$this->termsAccepted = (Boolean) $termsAccepted;
}
}
Acme\APPBundle\Form\Type\RegistrationType.php
namespace Acme\APPBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class RegistrationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('user', new UserType());
$builder->add('terms', 'checkbox', array('property_path' => 'termsAccepted'));
}
public function getName()
{
return 'registration';
}
}
Acme\APPBundle\Controller\AccountController.php
namespace Acme\APPBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Acme\AccountBundle\Form\Type\RegistrationType;
use Acme\AccountBundle\Form\Model\Registration;
class AccountController extends Controller
{
public function registerAction()
{
$form = $this->createForm(new RegistrationType(), new Registration());
return $this->render('AcmeAPPBundle:Account:register.html.twig', array('form' => $form->createView()));
}
public function createAction()
{
$em = $this->getDoctrine()->getEntityManager();
$form = $this->createForm(new RegistrationType(), new Registration());
$form->handleRequest($this->getRequest());
if ($form->isValid()) { // FIXME !!
$registration = $form->getData();
$em->persist($registration->getUser());
$em->flush();
return $this->redirect($this->generateUrl('home'));
}
return $this->render('AcmeAPPBundle:Account:register.html.twig', array('form' => $form->createView()));
}
}
I guess the error I get might be caused by the fact that the Registration Form is validated, but the User Form within isn't submitted to validation. Am I wrong ?
How can I simply change that behaviour ? I saw there is a cascade_validation option but it seems to be useless here.
I think it's strange that Symfony Cookbook provides both guides to create a user provider and create a registration form but does not explain how to get those work along.
I finally found what the acutal problem was.
The validation was processed only on the RegistrationType instance but not on the UserType within.
To make sure that the validation also checks the constraints for the user I added the following code to my RegistrationType class :
public function setDefaultOptions(Options ResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\APPBundle\Form\Model\Registration',
'cascade_validation' => true,
));
}
What changes everything is the cascade_validation option that must be set to true for this class while this option is set on the UserType class in the CookBook example.
Also, don't forget to :
use Symfony\Component\OptionResolver\OptionsResolverInterface
in the file where you define the setDefaultOptions.

Symfony2 FOSUserBundle merge resetPasswordform with custom form

I am trying to create a form by merging 2 form types as following.
Teacher Entity that has a one to one relation with User Entity.
I am using FOSUserBundle and i want to merge the ResettingFormType with my custom TeacherFormType, to end up with a form that both fields from my custom & fos reset password form.
1- Teacher Entity:
/**
* #var \User
*
* #ORM\OneToOne(targetEntity="ITJari\UserBundle\Entity\User", fetch="EAGER")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*
*/
private $user;
public function getUser() {
return $this->user;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
2- Extending FOS Reset password:
namespace ITJari\SchoolBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class ResettingFormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
}
public function getParent() {
return 'fos_user_resetting';
}
public function getName() {
return 'ragab';
}
}
3- Teacher Form Type
namespace ITJari\SchoolBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TeacherFormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'ITJari\SchoolBundle\Entity\Teacher',
));
}
public function getParent() {
return 'fos_user_resetting';
}
public function getName() {
return 'teacherform';
}
}
4- Inside controller:
$teacher = new \ITJari\SchoolBundle\Entity\Teacher();
$teacher->setUser($user);
$form = $this->createForm('teacherform', $teacher);
But i got the following error:
Neither the property "plainPassword" nor one of the methods "getPlainPassword()", "isPlainPassword()", "hasPlainPassword()", "__get()" exist and have public access in class "ITJari\SchoolBundle\Entity\Teacher".
500 Internal Server Error - NoSuchPropertyException

Symfony2 Form Validator - Comparing old and new values before flush

I was wondering if there is a way to compare old and new values in a validator within an entity prior to a flush.
I have a Server entity which renders to a form fine. The entity has a relationship to status (N->1) which, when the status is changed from Unracked to Racked, needs to check for SSH and FTP access to the server. If access is not achieved, the validator should fail.
I have mapped a validator callback to the method isServerValid() within the Server entity as described here
http://symfony.com/doc/current/reference/constraints/Callback.html. I can obviously access the 'new' values via $this->status, but how can I get the original value?
In pseudo code, something like this:
public function isAuthorValid(ExecutionContextInterface $context)
{
$original = ... ; // get old values
if( $this->status !== $original->status && $this->status === 'Racked' && $original->status === 'Unracked' )
{
// check ftp and ssh connection
// $context->addViolationAt('status', 'Unable to connect etc etc');
}
}
Thanks in advance!
A complete example for Symfony 2.5 (http://symfony.com/doc/current/cookbook/validation/custom_constraint.html)
In this example, the new value for the field "integerField" of the entity "NoDecreasingInteger" must be higher of the stored value.
Creating the constraint:
// src/Acme/AcmeBundle/Validator/Constraints/IncrementOnly.php;
<?php
namespace Acme\AcmeBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class IncrementOnly extends Constraint
{
public $message = 'The new value %new% is least than the old %old%';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return 'increment_only';
}
}
Creating the constraint validator:
// src/Acme/AcmeBundle/Validator/Constraints/IncrementOnlyValidator.php
<?php
namespace Acme\AcmeBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Doctrine\ORM\EntityManager;
class IncrementOnlyValidator extends ConstraintValidator
{
protected $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function validate($object, Constraint $constraint)
{
$new_value = $object->getIntegerField();
$old_data = $this->em
->getUnitOfWork()
->getOriginalEntityData($object);
// $old_data is empty if we create a new NoDecreasingInteger object.
if (is_array($old_data) and !empty($old_data))
{
$old_value = $old_data['integerField'];
if ($new_value < $old_value)
{
$this->context->buildViolation($constraint->message)
->setParameter("%new%", $new_value)
->setParameter('%old%', $old_value)
->addViolation();
}
}
}
}
Binding the validator to entity:
// src/Acme/AcmeBundle/Resources/config/validator.yml
Acme\AcmeBundle\Entity\NoDecreasingInteger:
constraints:
- Acme\AcmeBundle\Validator\Constraints\IncrementOnly: ~
Injecting the EntityManager to IncrementOnlyValidator:
// src/Acme/AcmeBundle/Resources/config/services.yml
services:
validator.increment_only:
class: Acme\AcmeBundle\Validator\Constraints\IncrementOnlyValidator
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: validator.constraint_validator, alias: increment_only }
Accessing the EntityManager inside a custom validator in symfony2
you could check for the previous value inside your controller action ... but that would not really be a clean solution!
normal form-validation will only access the data bound to the form ... no "previous" data accessible by default.
The callback constraint you're trying to use does not have access to the container or any other service ... therefore you cant easily access the entity-manager (or whatever previous-data provider) to check for the previous value.
What you need is a custom validator on class level. class-level is needed because you need to access the whole object not only a single value if you want to fetch the entity.
The validator itself might look like this:
namespace Vendor\YourBundle\Validation\Constraints;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class StatusValidator extends ConstraintValidator
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function validate($status, Constraint $constraint)
{
$em = $this->container->get('doctrine')->getEntityManager('default');
$previousStatus = $em->getRepository('YourBundle:Status')->findOneBy(array('id' => $status->getId()));
// ... do something with the previous status here
if ( $previousStatus->getValue() != $status->getValue() ) {
$this->context->addViolationAt('whatever', $constraint->message, array(), null);
}
}
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return 'previous_value';
}
}
... afterwards register the validator as a service and tag it as validator
services:
validator.previous_value:
class: Vendor\YourBundle\Validation\Constraints\StatusValidator
# example! better inject only the services you need ...
# i.e. ... #doctrine.orm.entity_manager
arguments: [ #service_container ]
tags:
- { name: validator.constraint_validator, alias: previous_value }
finally use the constraint for your status entity ( i.e. using annotations )
use Vendor\YourBundle\Validation\Constraints as MyValidation;
/**
* #MyValidation\StatusValidator
*/
class Status
{
For the record, here is the way to do it with Symfony5.
First, you need to inject your EntityManagerInterface service in the constructor of your validator.
Then, use it to retrieve the original entity.
/** #var EntityManagerInterface */
private $entityManager;
/**
* MyValidator constructor.
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #param string $value
* #param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
{
$originalEntity = $this->entityManager
->getUnitOfWork()
->getOriginalEntityData($this->context->getObject());
// ...
}
Previous answers are perfectly valid, and may fit your use case.
For "simple" use case, it may fill heavy though.
In the case of an entity editable through (only) a form, you can simply add the constraint on the FormBuilder:
<?php
namespace AppBundle\Form\Type;
// ...
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
/**
* Class MyFormType
*/
class MyFormType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('fooField', IntegerType::class, [
'constraints' => [
new GreaterThanOrEqual(['value' => $builder->getData()->getFooField()])
]
])
;
}
}
This is valid for any Symfony 2+ version.

Categories