phpsymfonysymfony-formsphp-8.2

is it possible to use a readonly class as a DTO for symfony forms?


I have a simple symfony form used to create a ThingEntity.

I have a CreateThingType and a CreateThing DTO:

The DTO

namespace App\DataTransfer;

use App\Validator\UniqueNameForParent;
use Symfony\Component\Validator\Constraints\GreaterThan;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;


#[UniqueNameForParent]
readonly class CreateThing
{

    function __construct(
        #[GreaterThan(0)]
        public int    $parent,

        #[NotBlank]
        #[Length(min: 3, max: 6)]
        public string $name,
    )
    {
    }

}

The Type:

namespace App\Form\Type;

use App\DataTransfer\CreateThing;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CreateThingType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {

        $builder->add('name', TextType::class)
            ->add('parent', IntegerType::class)
            ->add('save', SubmitType::class);
    }

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

}

And a simple controller to handle the thing:

namespace App\Controller;

use App\Entity\ThingEntity;
use App\Form\Type\CreateThingType;
use App\ThingRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/create')]
class CreateController extends AbstractController
{
    function __construct(private readonly ThingRepository $thingRepository)     {}

    public function __invoke(Request $request): Response
    {
        $form = $this->createForm(CreateThingType::class);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $thing = $form->getData();
            
            $thingEntity = new ThingEntity(
                id: null,
                parent: $thing->parent,
                name: $thing->name
            );
            $this->thingRepository->save($thingEntity);

            return $this->redirectToRoute('app_thing_list');
        }

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

}

When the form is not submitted, the form displays correctly:

form ok

But when I submit the form, I get an error, and it looks like this:

Too few arguments to function App\DataTransfer\CreateThing::__construct(), 0 passed in /U/a/symfony-form-dto-shared-repro/vendor/symfony/form/Extension/Core/Type/FormType.php on line 134 and exactly 2 expected

I can make it work by removing the readonly keyword and making the arguments/properties optional, by having empty default values (which I would prefer not to, since I'd like the DTO to be clear about that "everything is required" when I reuse it elsewhere)

Is it possible to use a readonly DTO with Symfony forms? How?


Solution

  • There is no way of the PropertyAccessor to know about your constructor. You could have changed the variables names, or apply them differently in the constructor.

    As far as I know symfony uses the PropertyAccessor component to set the properties to the data_class by either using setters or by setting the properties if they are public. Any of which will not be able to set your Dto as readonly.

    I also don't think this would be possible at all with Symfony Form, since you are able to set default values, which are overwritten by your form by using setters or using the public properties of your Dto's which is done by the PropertyAccessor component.

    So in short, skip the readonly part and the constructor and create simple Models with getters and setters.