Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Form] InvalidArgumentException while using type declaration in entity #43509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Safranil opened this issue Oct 14, 2021 · 11 comments
Closed

[Form] InvalidArgumentException while using type declaration in entity #43509

Safranil opened this issue Oct 14, 2021 · 11 comments

Comments

@Safranil
Copy link

Symfony version(s) affected: 5.3.4

Description
I use PHP 8 and my entities use the type declaration on setter and getter.

When validating a Form with a null value on a field that require non null (eg: method(string $value)), the handle() method throw an InvalidArgumentException and a TypeError.

How to reproduce

Example Entity, Form and Controller

src/Entity/Structure.php

<?php
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

namespace App\Entity;
/**
 * @ORM\Entity()
 */
class Structure {
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBank()
     */
    private $name;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }
}

src/Form/StructureType.php

<?php

namespace App\Form;

use App\Entity\Structure;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class StructureType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Structure::class,
        ]);
    }
}

src/Controller/Structure.php

<?php

namespace App\Controller;

use App\Entity\Structure;
use App\Form\StructureType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class StructureController extends AbstractController
{

    #[Route('/test_form', name: 'test_form')]
    public function create(Request $request): Response
    {
        $structure = new Structure();
        $form = $this->createForm(StructureType::class, $structure);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            /* ... */
        }

        return $this->render('structure/form.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

Possible Solution
Catch these exceptions and add a validation error asking a non null value (or use the existing Validator on the field)

Additional context

The stack trace when $form->handleRequest($request) is called
Symfony\Component\PropertyAccess\Exception\InvalidArgumentException:
Expected argument of type "string", "null" given at property path "name".

  at vendor/symfony/property-access/PropertyAccessor.php:268
  at Symfony\Component\PropertyAccess\PropertyAccessor::throwInvalidArgumentException()
     (vendor/symfony/property-access/PropertyAccessor.php:179)
  at Symfony\Component\PropertyAccess\PropertyAccessor->setValue()
     (vendor/symfony/form/Extension/Core/DataAccessor/PropertyPathAccessor.php:68)
  at Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor->setValue()
     (vendor/symfony/form/Extension/Core/DataAccessor/ChainAccessor.php:54)
  at Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor->setValue()
     (vendor/symfony/form/Extension/Core/DataMapper/DataMapper.php:87)
  at Symfony\Component\Form\Extension\Core\DataMapper\DataMapper->mapFormsToData()
     (vendor/symfony/form/Form.php:640)
  at Symfony\Component\Form\Form->submit()
     (vendor/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php:109)
  at Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler->handleRequest()
     (vendor/symfony/form/Form.php:501)
  at Symfony\Component\Form\Form->handleRequest()
     (src/Controller/StructureController.php:36)
  at App\Controller\StructureController->edit()
     (vendor/symfony/http-kernel/HttpKernel.php:156)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw()
     (vendor/symfony/http-kernel/HttpKernel.php:78)
  at Symfony\Component\HttpKernel\HttpKernel->handle()
     (vendor/symfony/http-kernel/Kernel.php:199)
  at Symfony\Component\HttpKernel\Kernel->handle()
     (public/index.php:20)

TypeError:
Proxies\__CG__\App\Entity\Structure::setName(): Argument #1 ($name) must be of type string, null given, called in /***/vendor/symfony/property-access/PropertyAccessor.php on line 576

  at var/cache/dev/doctrine/orm/Proxies/__CG__AppEntityStructure.php:251
  at Proxies\__CG__\App\Entity\Structure->setName()
     (vendor/symfony/property-access/PropertyAccessor.php:576)
  at Symfony\Component\PropertyAccess\PropertyAccessor->writeProperty()
     (vendor/symfony/property-access/PropertyAccessor.php:175)
  at Symfony\Component\PropertyAccess\PropertyAccessor->setValue()
     (vendor/symfony/form/Extension/Core/DataAccessor/PropertyPathAccessor.php:68)
  at Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor->setValue()
     (vendor/symfony/form/Extension/Core/DataAccessor/ChainAccessor.php:54)
  at Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor->setValue()
     (vendor/symfony/form/Extension/Core/DataMapper/DataMapper.php:87)
  at Symfony\Component\Form\Extension\Core\DataMapper\DataMapper->mapFormsToData()
     (vendor/symfony/form/Form.php:640)
  at Symfony\Component\Form\Form->submit()
     (vendor/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php:109)
  at Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler->handleRequest()
     (vendor/symfony/form/Form.php:501)
  at Symfony\Component\Form\Form->handleRequest()
     (src/Controller/StructureController.php:36)
  at App\Controller\StructureController->edit()
     (vendor/symfony/http-kernel/HttpKernel.php:156)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw()
     (vendor/symfony/http-kernel/HttpKernel.php:78)
  at Symfony\Component\HttpKernel\HttpKernel->handle()
     (vendor/symfony/http-kernel/Kernel.php:199)
  at Symfony\Component\HttpKernel\Kernel->handle()
     (public/index.php:20)   
@derrabus
Copy link
Member

Either use empty_data

$builder
    ->add('name', TextType::class, ['empty_data' => ''])
;

… or make the type declaration nullable.

@ghost
Copy link

ghost commented Oct 14, 2021

This is absolutely valid. You never know what data will be send in a request. Form Component maps the raw data to your model and when some field does not exist null value is passed so it can be validated against NotNull/NotBlank assert. You have to make you setters nullable as derrabus said or workaround with empty_data but then you will not know if a user didn't send this field or sent an empty string (but if you don't care it doesn't make any difference). However this hack with empty_data may work for string typehint but if you typed with some object type you could make it only nullable (ok, in PHP8 we have union types but it's better to avoid them in the userland code, especially in data objects).

Btw. you should not use entities as your models in forms. You should use some kind of DTO so Form Component can populate it, the validate it and then if it is valid you can create your entity from this DTO.

@Safranil
Copy link
Author

Either use empty_data

$builder
    ->add('name', TextType::class, ['empty_data' => ''])
;

This would work only on string and I also have integer and boolean.

… or make the type declaration nullable.

The maker bundle contain an entity generator, I tested and when I ask a field with not nullable string, the entity is generated with string type declaration and not ?string type.

A test entity generated with bin/console make:entity
<?php

namespace App\Entity;

use App\Repository\TestRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=TestRepository::class)
 */
class Test
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }
}

I think this should be managed somehow by the form since the generator do the typed declaration (my composer.json as the PHP 8 version requirement).

Btw. you should not use entities as your models in forms. You should use some kind of DTO so Form Component can populate it, the validate it and then if it is valid you can create your entity from this DTO.

I use Api Platform on the entity, and I don't want to duplicate my code (validation annotation with @Assert for example) to make DTO specifically for the form (or there is a bundle that do that automatically ?).

@Nek-
Copy link
Contributor

Nek- commented Oct 15, 2021

Hello, I just want to link this PR because it's somehow the same work to achieve but on the form component. #42502

@ghost
Copy link

ghost commented Oct 15, 2021

This is not a duplication. This is a separation between your business logic represented by entity and raw data on data transfer objects that can be everything. Entities are part of your domain logic and should be in a valid state always while form data is just an input to the infrastructure layer which should be validated and if valid then your entity can be updated.
What's more, sooner oraz later you will have a case when the input data will not be 1:1 with the entity data.

I did use Api Platform a few years ago in few small applications and IIRC this is pretty simple to setup up a crud application using entities as Api Resource. Moving resources to models and writing custom persisters is the next level that also takes more time but solves many other issues and is just a better design.

@danieldenbraven
Copy link

@javaDeveloperKid I'm sorry, but this is just not reasonable, no one uses Symfony like this and you can't expect people to duplicate their code just for this (look at the code examples and the Symfony documentation on how to use Forms and Entities). This is definitely a design issue of how the Forms and the Validator components work with the latest versions of PHP. We're struggling with this right now, and this just invalidates the entire point of using types, if we need to declare them as nullable.

@ghost
Copy link

ghost commented Oct 20, 2021

@danieldenbraven
I think you don't understand this issue. What latest versions of PHP have in common with this problem? The OP doesn't even use property typehints in his example. For years you must have had the getters and setters nullable (so as a consequence corresponding fields were nullable also) if you map an entity to form's data_class. This is just how the Form Component works.

  • get raw data from request
  • apply data transformers if exist
  • set data on the model (Property Accessor Component is used here)
  • validate model

As you see in step no. 3 the setters must be nullable because you never know what data will come from outside world.

About the rest of your comment

No one uses Symfony like this? You mean no junior developers, right? I explained why this is not considered a duplication. No senior developer will map their entities to form models. One first must validate the input data and then, when valid, pass this data further in process chain.
Symfony docs - docs are just to show basic usage, concepts of the component. Framework docs will not tell you how to design the architecture or what patterns are appropriate to follow.

@Nek-
Copy link
Contributor

Nek- commented Oct 20, 2021

The OP doesn't even use property typehints in his example.

Actually he does, and it's the error (TypeError).

About your idea of just stopping using forms but doing it with API Platform, I really do not understand your point of view for 3 reasons:

  1. Symfony is supposed to be RAD, and so we should be able to use entities in forms and it should not trigger errors (I mean, after all, it's documented!).
  2. How using an API instead of a simple form can be a solution ?! You can't challenge the whole architecture of a project because of an issue inside the form component.
  3. As I stated in my previous comment, this issue actually exists also in the serializer and will be fixed in 5.4 [Serializer] Add support for collecting type error during denormalization #42502

Besides, as a reminder, the quick fix has been given: use the empty_data option. 👍 I think this issue is absolutely valid (and I didn't check but I bet it's a duplicate because it's also old 😅 ).

@derrabus
Copy link
Member

I think this should be managed somehow by the form

I agree that the DX could be nicer here. But somebody has to build that. Any volunteers? 🙂

@xabbuh
Copy link
Member

xabbuh commented Oct 20, 2021

In the RichModelFormsBundle we among other use cases also deal with type errors when mapping user input from the input data to the model. Anyone wanting to improve DX here please feel free to take inspiration from the code there.

@xabbuh
Copy link
Member

xabbuh commented Apr 12, 2022

Closing here as this is not a bug, but related to how the Form component works. As @derrabus said we are nonetheless open to review PRs improving DX around this.

@xabbuh xabbuh closed this as completed Apr 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants