Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[Form] AddAsFormType attribute to create FormType directly on model classes#60563

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
WedgeSama wants to merge1 commit intosymfony:7.4
base:7.4
Choose a base branch
Loading
fromWedgeSama:form-attribute

Conversation

WedgeSama
Copy link
Contributor

@WedgeSamaWedgeSama commentedMay 27, 2025
edited
Loading

QA
Branch?7.4
Bug fix?no
New feature?yes
Deprecations?no
LicenseMIT

Form Attribute

This PR is about how to use PHP Attribute to build a Symfony Form.

Build your form directly on your data (e.g. on a DTO but not limited to it 😉).

Brainstormed with@Jean-Beru@Neirda24 and@smnandre

Description

What in mind when starting this:

  • less code to build a form (a lot less!)
  • prevent the need to write a dedicatedFormType class
  • Make mandatory to use object (DTO?) as data and not array (data_class mandatory but implicit)
  • "Data first, Form second instead ofForm first, Data second" quoted from@Neirda24 😄
  • keep all existing features and capabilities of a classicFormType (easy because in the background, it is still aFormType 😉)

Here what it looks like with a basic example:

// UserDTO.phpuseSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\Extension\Core\Type\PasswordType;useSymfony\Component\Form\Extension\Core\Type\RepeatedType;useSymfony\Component\Form\Extension\Core\Type\TextareaType;#[AsFormType(options: [// Set default option for your root FormType.'label' =>'FOO MAIN TITLE',])]class UserDTO{    #[Type]// Use of FormTypeGuesser to guess the right type.public ?string$name =null;    #[Type(EmailType::class)]public ?string$email =null;    #[Type(RepeatedType::class, ['type' => PasswordType::class,    ])]public ?string$password =null;    #[Type(options: ['label' =>'More info',    ])]public ?string$info =null;    #[Type(TextareaType::class)]public ?string$description =null;    #[Type(AnotherDTO::class)]// Allow the use of others DTO directly.public ?AnotherDTO$another;}
// Anywhere you nee to call your form, service, controller, etc...$formFactory =/* get here your FromFactory */$user =newUserDTO();$form =$formFactory->create(UserDTO::class,$user);// or get a FormBuilder (or call any other method of the Factory like you'll do with a classic FormType)$formBuilder =$formFactory->createBuilder(UserDTO::class,$user);// Then do what you need to do with your brand-new form :wink:

Basic features (implemented in this PR)

Basic form
| - With Attributes
namespaceApp\DTO;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\Extension\Core\Type\PasswordType;useSymfony\Component\Form\Extension\Core\Type\RepeatedType;useSymfony\Component\Form\Extension\Core\Type\TextareaType;#[AsFormType(options: ['label' =>'FOO MAIN TITLE',])]class UserDTO{    #[Type]publicstring$name;    #[Type(EmailType::class)]publicstring$email;    #[Type(RepeatedType::class, ['type' => PasswordType::class,    ])]publicstring$password;    #[Type(options: ['label' =>'More info',    ])]publicstring$info;    #[Type(type: TextareaType::class)]publicstring$description;}
namespaceApp\Controller;useApp\DTO\UserDTO;class HomeControllerextends AbstractController{    #[Route('/', name:'app_home')]publicfunctionindex():Response    {// Same `createForm` method for existing form.$form =$this->createForm(UserDTO::class);// ...    }}
| - Classic equivalence (without attribute)
namespaceApp\DTO;class UserDTO{publicstring$name;publicstring$email;publicstring$password;publicstring$info;publicstring$description;}
namespaceApp\Controller;useApp\Form\UserDTOType;class HomeControllerextends AbstractController{    #[Route('/', name:'app_home')]publicfunctionindex():Response    {$form =$this->createForm(UserDTOType::class);// ...    }}
namespaceApp\Form;useApp\DTO\UserDTO;useSymfony\Component\Form\AbstractType;useSymfony\Component\Form\Extension\Core\Type\EmailType;useSymfony\Component\Form\Extension\Core\Type\PasswordType;useSymfony\Component\Form\Extension\Core\Type\RepeatedType;useSymfony\Component\Form\Extension\Core\Type\TextareaType;useSymfony\Component\Form\FormBuilderInterface;useSymfony\Component\OptionsResolver\OptionsResolver;class UserDTOTypeextends AbstractType{publicfunctionbuildForm(FormBuilderInterface$builder,array$options)    {$builder            ->add('name')            ->add('email', EmailType::class)            ->add('password', RepeatedType::class, ['type' => PasswordType::class,            ])            ->add('info',null, ['label' =>'More info',            ])            ->add('description', TextareaType::class)        ;    }publicfunctionconfigureOptions(OptionsResolver$resolver)    {$resolver->setDefaults(['data_class' => UserDTO::class;        ]);    }}
| - Variant 1, with one attribute per FormType (Not implemented)

Thought of that too, I like the readability but maybe too much work to maintain.

Still need a 'GenericType' to work with type that does not have attribute equivalence.

namespaceApp\DTO;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\Extension\Core\Type\PasswordType;useSymfony\Component\Form\Extension\Core\Type\TextareaType;#[AsFormType]class UserDTO{    #[Type\TextType]publicstring$name;    #[Type\EmailType]publicstring$email;    #[Type\RepeatedType(options: ['type' => PasswordType::class,    ])]publicstring$password;    #[Type\TextType(options: ['label' =>'More info',    ])]publicstring$info;// Still got a Generic type    #[Type\FormType(type: TextareaType::class)]publicstring$description;}
With validation

Work like a charm, without to do anything more to support it

namespaceApp\DTO;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\Extension\Core\Type\PasswordType;useSymfony\Component\Form\Extension\Core\Type\RepeatedType;useSymfony\Component\Form\Extension\Core\Type\TextareaType;useSymfony\Component\Validator\ConstraintsasAssert;#[AsFormType]class UserDTO{    #[Type]    #[Assert\NotBlank]publicstring$name;    #[Type(EmailType::class)]    #[Assert\NotBlank]    #[Assert\Email]public ?string$email;    #[Type(RepeatedType::class, ['type' => PasswordType::class,    ])]publicstring$password;    #[Type(options: ['label' =>'More info',    ])]    #[Assert\Length(min:5, max:511)]publicstring$info;    #[Type(type: TextareaType::class)]    #[Assert\Length(min:5, max:511)]publicstring$description;}
Class inheritance
| - With attribute
// ParentDTO.php#[AsFormType]class ParentDTO{    #[Type]publicstring$name;}
// ChildDTO.php#[AsFormType]class ChildDTOextends ParentDTO{    #[Type]publicstring$anotherProp;}
| - Classic equivalence (without attribute)
// ParentDTO.phpclass ParentDTO{publicstring$name;}
// ChildDTO.phpclass ChildDTOextends ParentDTO{publicstring$anotherProp;}
// ParentType.phpuseSymfony\Component\Form\AbstractType;useSymfony\Component\Form\FormBuilderInterface;useSymfony\Component\OptionsResolver\OptionsResolver;class ParentTypeextends AbstractType{publicfunctionbuildForm(FormBuilderInterface$builder,array$options)    {$builder->add('name');    }publicfunctionconfigureOptions(OptionsResolver$resolver)    {$resolver->setDefaults(['data_class' => ParentDTO::class;        ]);    }}
// ChildType.phpclass ChildTypeextends AbstractType{publicfunctionbuildForm(FormBuilderInterface$builder,array$options)    {$builder->add('anotherProp');    }publicfunctionconfigureOptions(OptionsResolver$resolver)    {$resolver->setDefaults(['data_class' => ChildDTO::class;        ]);    }publicfunctiongetParent():string    {return ParentType::class;    }}

Next features (WIP)

Here some DX example of how to implement next features from form component with attributes.

For Form Event and Data Transformer, can be more powerful withPHP 8.5 closures_in_const_expr in the future \o/

Form Event
| - Event on the root FormType
    | - With Attributes
namespaceApp\DTO;useSymfony\Component\Form\Attribute\ApplyFormEvent;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\FormEvent;useSymfony\Component\Form\FormEvents;#[AsFormType]#[ApplyFormEvent]class UserDTO{    #[Type]publicstring$name;publicstaticfunctiononPreSetData(FormEvent$event):void    {// Do stuff    }    #[ApplyFormEvent(FormEvents::POST_SET_DATA)]publicstaticfunctionanotherMethod(FormEvent$event):void    {// Do stuff    }}
    | - Classic equivalence (without attribute)
namespaceApp\DTO;class UserDTO{publicstring$name;}
namespaceApp\Form;useApp\DTO\UserDTO;useSymfony\Component\Form\AbstractType;useSymfony\Component\Form\FormBuilderInterface;useSymfony\Component\Form\FormEvent;useSymfony\Component\Form\FormEvents;useSymfony\Component\OptionsResolver\OptionsResolver;class UserDTOTypeextends AbstractType{publicfunctionbuildForm(FormBuilderInterface$builder,array$options)    {$builder->add('name');$builder->addEventListener(FormEvents::PRE_SET_DATA,function (FormEvent$event):void {// Do stuff        });$builder->addEventListener(FormEvents::POST_SET_DATA,function (FormEvent$event):void {// Do stuff        });    }publicfunctionconfigureOptions(OptionsResolver$resolver)    {$resolver->setDefaults(['data_class' => UserDTO::class;        ]);    }}
| - Event on a FormType's field
    | - With Attributes
namespaceApp\DTO;useSymfony\Component\Form\Attribute\ApplyFormEvent;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\FormEvent;useSymfony\Component\Form\FormEvents;#[AsFormType]class UserDTO{    #[Type]    #[ApplyFormEvent(FormEvents::PRE_SET_DATA,'myStaticMethod')]//    #[ApplyFormEvent(FormEvents::PRE_SET_DATA, [AnotherClass::class, 'myStaticMethod'])] // Maybe callable too?publicstring$name;publicstaticfunctionmyStaticMethod(FormEvent$event):void    {// Do stuff    }}
    | - Classic equivalence (without attribute)
namespaceApp\DTO;class UserDTO{publicstring$name;}
namespaceApp\Form;useApp\DTO\UserDTO;useSymfony\Component\Form\AbstractType;useSymfony\Component\Form\FormBuilderInterface;useSymfony\Component\Form\FormEvent;useSymfony\Component\Form\FormEvents;useSymfony\Component\OptionsResolver\OptionsResolver;class UserDTOTypeextends AbstractType{publicfunctionbuildForm(FormBuilderInterface$builder,array$options)    {$builder->add('name');$builder->get('add')->addEventListener(FormEvents::PRE_SET_DATA,function (FormEvent$event):void {// Do stuff        });    }publicfunctionconfigureOptions(OptionsResolver$resolver)    {$resolver->setDefaults(['data_class' => UserDTO::class;        ]);    }}
Data Transformer
| - DT on the root FormType
    | - With attribute
namespaceApp\DTO;useSymfony\Component\Form\Attribute\ApplyDataTransformer;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;#[AsFormType]#[ApplyDataTransformer(newMyTransformer())]// `model` by default?//#[ApplyDataTransformer(new MyTransformer(), 'view')]class UserDTO{    #[Type]publicstring$name;}
    | - Classic equivalence (without attribute)
namespaceApp\DTO;class UserDTO{publicstring$name;}
namespaceApp\Form;useApp\DTO\UserDTO;useSymfony\Component\Form\AbstractType;useSymfony\Component\Form\FormBuilderInterface;useSymfony\Component\OptionsResolver\OptionsResolver;class UserDTOTypeextends AbstractType{publicfunctionbuildForm(FormBuilderInterface$builder,array$options)    {$builder->addModelTransformer(newMyTransformer());//        $builder->addViewTransformer(new MyTransformer());$builder->add('name');    }publicfunctionconfigureOptions(OptionsResolver$resolver)    {$resolver->setDefaults(['data_class' => UserDTO::class;        ]);    }}
| - DT on a FormType's field
    | - With attribute
namespaceApp\DTO;useSymfony\Component\Form\Attribute\ApplyDataTransformer;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;#[AsFormType]class UserDTO{    #[Type]    #[ApplyDataTransformer(newMyTransformer())]// `model` by default?//    #[ApplyDataTransformer(new MyTransformer(), 'view')]publicstring$name;}
    | - Classic equivalence (without attribute)
namespaceApp\DTO;class UserDTO{publicstring$name;}
namespaceApp\Form;useApp\DTO\UserDTO;useSymfony\Component\Form\AbstractType;useSymfony\Component\Form\FormBuilderInterface;useSymfony\Component\OptionsResolver\OptionsResolver;class UserDTOTypeextends AbstractType{publicfunctionbuildForm(FormBuilderInterface$builder,array$options)    {$builder->add('name');$builder->get('name')->addModelTransformer(newMyTransformer())//        $builder->get('name')->addViewTransformer(new MyTransformer())        ;    }publicfunctionconfigureOptions(OptionsResolver$resolver)    {$resolver->setDefaults(['data_class' => UserDTO::class;        ]);    }}
| - Variant 1 with DataTransformerInterface

Not a fan but.

namespaceApp\DTO;useSymfony\Component\Form\Attribute\ApplyDataTransformer;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\DataTransformerInterface;#[AsFormType]#[ApplyDataTransformer]//#[ApplyDataTransformer(type: 'view')]class UserDTOimplements DataTransformerInterface{    #[Type]publicstring$name;publicfunctiontransform(mixed$value):mixed {}publicfunctionreverseTransform(mixed$value):mixed {}}
| - Variant 2, with 2 attributes

Use 2 distinct attribute, one for model transformer, another for view transformer.

namespaceApp\DTO;useSymfony\Component\Form\Attribute\ApplyModelTransformer;useSymfony\Component\Form\Attribute\ApplyViewTransformer;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\DataTransformerInterface;#[AsFormType]#[ApplyModelTransformer]//#[ApplyViewTransformer]class UserDTO{    #[Type]publicstring$name;}
Form Type Extension
namespaceApp\Form\Extension;useSymfony\Component\Form\AbstractTypeExtension;useSymfony\Component\Form\FormBuilderInterface;class UserDTOExtensionextends AbstractTypeExtension{publicstaticfunctiongetExtendedTypes():iterable    {yield UserDTO::class;    }publicfunctionbuildForm(FormBuilderInterface$builder,array$options):void    {// do stuff, like add mapped false fields.    }}
Auto Form \o/
namespaceApp\DTO;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Exclude;#[AsFormType(auto:true)]class UserDTO{publicstring$name;publicstring$email;publicstring$password;publicAnotherDTO$another;    #[Exclude]publicstring$description;}

Need to be discussed / Possible evolution

Here stuff that I am no satisfy for now and need to be discuss to find a better way to do it.

Autowire
| - In options
namespaceApp\DTO;useSymfony\Component\DependencyInjection\Attribute\Autowire;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\Extension\Core\Type\ChoiceType;#[AsFormType(options: ['label' =>newAutowire(param:'the_foo'),])]class UserDTO{    #[Type]publicstring$name;        #[Type(ChoiceType::class, options: ['choice_loader' =>newAutowire(service: MyChoiceLoader::class),'multiple' =>true,    ])]publicarray$tags = [];    #[Type(ChoiceType::class, options: ['choice_loader' =>newAutowire(expression:'service("App\\Form\\ChoiceLoader\\TypeLoader")'),//        'choice_label' => new Expression('choice.getIcon()'),'choice_label' => [newAutowire(service: MyChoiceLoader::class),'choiceLabel'],    ])]publicstring$type;}
| - Event
namespaceApp\DTO;useSymfony\Component\DependencyInjection\Attribute\Autowire;useSymfony\Component\Form\Attribute\ApplyFormEvent;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\FormEvents;#[AsFormType]#[ApplyFormEvent(FormEvents::PRE_SET_DATA,newAutowire(service: MyChoiceLoader::class))]class UserDTO{    #[Type]    #[ApplyFormEvent(FormEvents::POST_SET_DATA,newAutowire(service: MyChoiceLoader::class))]publicstring$name;}
| - Data Transformer
namespaceApp\DTO;useSymfony\Component\Form\Attribute\ApplyDataTransformer;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;#[AsFormType]#[ApplyDataTransformer(newAutowire(service: MyTransformer::class))]class UserDTO{    #[Type]    #[ApplyDataTransformer(newAutowire(service: MyTransformer::class))]publicstring$name;}
configureOption method
| - Varaint 1 (not fan of it)
namespaceApp\DTO;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\OptionsResolver\OptionsResolver;#[AsFormType]class UserDTO{publicstring$name;publicstaticfunctionconfigureOptions(OptionsResolver$resolver):void    {// Do stuff    }}
| - Varaint 2 (love it but PHP 8.5)
namespaceApp\DTO;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\OptionsResolver\OptionsResolver;#[AsFormType(configureOptions:staticfunction (OptionsResolver$resolver):void {// Do stuff})]class UserDTO{publicstring$name;}
Override getParent

Not sure if this means something, maybe not needed. What do you think?

namespaceApp\DTO;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\OptionsResolver\OptionsResolver;#[AsFormType(parent: AnotherFormType::class)]class UserDTO{publicstring$name;}

Symfony Demo Application with Form Attribute

You can try the basic examples ofAsFormType in theSymfony Demo Application fork:
https://github.com/WedgeSama/demo-with-form-attribute/tree/demo-with-form-attribute

List ofFormType replaced (examples on both DTO and entities):

  • Form\ChangePasswordType => created DTODTO\UserChangePasswordDTO
  • Form\UserType => created DTODTO\UserProfileDTO
  • Form\CommentType => on existing entityEntity\Comment
  • Form\PostType => on existing entityEntity\Post

It use git submodule to require the form component with form attribute.

PS: any suggestion are welcome 😉

Neirda24, Jean-Beru, daFish, PierreCapel, javiereguiluz, OskarStark, and IndraGunawan reacted with heart emoji
@carsonbot
Copy link

It looks like you unchecked the "Allow edits from maintainer" box. That is fine, but please note that if you have multiple commits, you'll need to squash your commits into one before this can be merged. Or, you can check the "Allow edits from maintainers" box and the maintainer can squash for you.

Cheers!

Carsonbot

Comment on lines +78 to +86
$commandDefinition = new Definition(DebugCommand::class, [
new Reference('form.registry'),
[],
[],
[],
[],
null,
[],
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Why this change ?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

If I dont, I got this error on tests:

1) Symfony\Component\Form\Tests\DependencyInjection\FormPassTest::testAddTaggedTypesToDebugCommandSymfony\Component\DependencyInjection\Exception\RuntimeException: Invalid constructor argument 7 for service "console.command.form_debug": argument 4 must be defined before. Check your service definition./app/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php:48/app/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php:73/app/src/Symfony/Component/DependencyInjection/ContainerBuilder.php:813/app/src/Symfony/Component/Form/Tests/DependencyInjection/FormPassTest.php:86

Do you have an alternative for that?

@Tiriel
Copy link
Contributor

I do like the feature, but thinking about an extreme example of going all-in on attributes, I wonder were it goes.

Small simple example of building an app where we store informations about Events sent by users with our own schema, and we also receive informations from APIs with their own structure (you see where this is going). So we need the usual ORM attributes, some Validation, Object-Mapper, and these Form attributes. The entity becomes quite heavy on attributes.

useApp\Dto\EventasEventDTO;useApp\Repository\EventRepository;useDoctrine\DBAL\Types\Types;useDoctrine\ORM\MappingasORM;useSymfony\Component\Form\Attribute\AsFormType;useSymfony\Component\Form\Attribute\Type;useSymfony\Component\Form\Extension\Core\TypeasFormType;useSymfony\Component\ObjectMapper\Attribute\Map;useSymfony\Component\Validator\ConstraintsasAssert;#[AsFormType]#[Map(target: EventDTO::class)]#[ORM\Entity(repositoryClass: EventRepository::class)]class Event{    #[ORM\Id]    #[ORM\GeneratedValue]    #[ORM\Column]private ?int$id =null;    #[Map(target:'eventName')]    #[Assert\Length(min:15)]    #[Assert\NotBlank()]    #[ORM\Column(length:255)]    #[Type]private ?string$name =null;    #[Map(target:'synopsis')]    #[Assert\NotBlank()]    #[ORM\Column(type: Types::TEXT)]    #[Type(FormType\TextareaType::class)]private ?string$description =null;// ...

As you can see, the business need is small, there's not much in terms of validation or mapping, and I've included only few properties.

I don't know if that's a problem honestly, it's not that unreadable to me, by I do think the question should be raised.

That being said, once again, this is a really nice and neat feature, great job!

@Neirda24
Copy link
Contributor

I do like the feature, but thinking about an extreme example of going all-in on attributes, I wonder were it goes.

Small simple example of building an app where we store informations about Events sent by users with our own schema, and we also receive informations from APIs with their own structure (you see where this is going). So we need the usual ORM attributes, some Validation, Object-Mapper, and these Form attributes. The entity becomes quite heavy on attributes.

use App\Dto\Event as EventDTO;use App\Repository\EventRepository;use Doctrine\DBAL\Types\Types;use Doctrine\ORM\Mapping as ORM;use Symfony\Component\Form\Attribute\AsFormType;use Symfony\Component\Form\Attribute\Type;use Symfony\Component\Form\Extension\Core\Type as FormType;use Symfony\Component\ObjectMapper\Attribute\Map;use Symfony\Component\Validator\Constraints as Assert;#[AsFormType]#[Map(target: EventDTO::class)]#[ORM\Entity(repositoryClass: EventRepository::class)]class Event{    #[ORM\Id]    #[ORM\GeneratedValue]    #[ORM\Column]    private ?int $id = null;    #[Map(target: 'eventName')]    #[Assert\Length(min: 15)]    #[Assert\NotBlank()]    #[ORM\Column(length: 255)]    #[Type]    private ?string $name = null;    #[Map(target: 'synopsis')]    #[Assert\NotBlank()]    #[ORM\Column(type: Types::TEXT)]    #[Type(FormType\TextareaType::class)]    private ?string $description = null;    // ...

As you can see, the business need is small, there's not much in terms of validation or mapping, and I've included only few properties.

I don't know if that's a problem honestly, it's not that unreadable to me, by I do think the question should be raised.

That being said, once again, this is a really nice and neat feature, great job!

I think in your usecase if you map object to a dto then the form should probably be on said dto. WDYT ?

@Tiriel
Copy link
Contributor

I think in your usecase if you map object to a dto then the form should probably be on said dto. WDYT ?

I know you love teasing me 😉 . Just to keep you happy:

  1. It's a fictitious example made just for testing
  2. In this case the DTO is intended to receive data from an API and allow mapping to the entity, when the entity is the base for the frontend form.

I don't pretend this usecase is universale though. Once again, it's designed to create a fake extreme example, to show what it could lead. It's more of a "let's pretend there's a need like that".

And then again, I don't say I'm definitely against this, I love the feature. But I also like to raise questions ;)

@yceruto
Copy link
Member

yceruto commentedMay 28, 2025
edited
Loading

This look like a fantastic addition for simple forms, but honestly I'm not 100% sure for mid-level and complex forms.

PHP attributes are excellent for declaratively adding metadata, but there are limitations and cases where attributes are not the best fit. For instance, dynamic field definition, custom business logic, injecting services, or conditional configuration. In those cases, the traditional FormType classes may still be preferable (as they are services by definition).

I don’t think we should replicate all form type capabilities with attributes (because the scope and dynamic limitations), but they can certainly be useful for building simple forms quickly, and then easily switch to FormType classes when things get more complex.

mtarld reacted with thumbs up emoji

@Neirda24
Copy link
Contributor

@yceruto : Yes this is a totally valid point. I don't think attributes are meant to replace Forms. But should try to cover most common cases without the need to refactor it to a dedicated class. Might be worth adding a warning to the documentation of this feature the list of known limitations.

@yceruto
Copy link
Member

yceruto commentedMay 28, 2025
edited
Loading

$form =$formFactory->create(UserDTO::class,$user);

It looks strange from the design PoV passing the DTO class as form type. I’m wondering if, instead of passing the data class, we can leave the form type param asnull and let the type be guessed from the data (if AsFormType)?

$form =$formFactory->create(null,$user);// the form type will be guessed and defined from the DTO

Maybe the same guesser mechanism could help with this? It'd be aligned with the null form type concept.

@WedgeSama
Copy link
ContributorAuthor

@Tiriel Yes it can be used directly on entities, but even with your example, to make it more readable, a dedicated DTO for the form can still be done 😉

@yceruto I think its a good start to keep it simple yes. We just need to set the limit to which capabilities we want to replicate. I also thought of a command that can "convert" an attribute form to a traditionalFormType, something likemake:form:from-metadata. WDYT?

And Im totally agree to something like :

$form =$formFactory->create(null,$user);// or even?$form =$formFactory->create($user);

* @author Benjamin Georgeault <git@wedgesama.fr>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final readonly class Type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

To stay consistent with the purpose of this attribute, as described in the class description, it represents a form field, so I’d prefer to name this classField ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I thinkField is wrong. Because it's biased by how it displays. But a "Field" could be an entire form. so I think we should keepType but maybe rename other occurences of Field's to Type's. WDYT ?

*/
public function __construct(
private ?string $type = null,
private array $options = [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

What about also adding?string $name = null, which defaults to the property name if null? If it’s set, it would use property_path for mapping. This could be convenient in cases where you need to change the field name in the view

WedgeSama and smnandre reacted with thumbs up emoji
@Tiriel
Copy link
Contributor

@Tiriel Yes it can be used directly on entities, but even with your example, to make it more readable, a dedicated DTO for the form can still be done 😉

In that case, what would be the benefit of using a new dedicated DTO+ attribute over a regular FormType class?

@yceruto
Copy link
Member

I think its a good start to keep it simple yes. We just need to set the limit to which capabilities we want to replicate.

There are implicit limitations with options and Closures, which may be mitigated in PHP 8.5, but still, adding large blocks of logic in attributes feels a bit messy to me.

I think advanced features like data mappers, form events, and model/view transformers carry special complexity and responsibility tied to the form type itself, but I’m open to making them configurable using attributes, as long as we can also implement them outside the DTO.

@yceruto
Copy link
Member

I also thought of a command that can "convert" an attribute form to a traditional FormType, something like make:form:from-metadata. WDYT?

Yes, but what about just creating a command that generate the form type from a DTO 😅?
It's already possible from an Entity classhttps://github.com/symfony/maker-bundle/blob/1.x/src/Maker/MakeForm.php

@yceruto
Copy link
Member

yceruto commentedMay 28, 2025
edited
Loading

And Im totally agree to something like :
$form = $formFactory->create(null, $user);
// or even?
$form = $formFactory->create($user);

even$form = $formFactory->create(data: $user) is fine currently

Comment on lines +42 to +47
($resolver =$this->createMock(OptionsResolver::class))
->expects($this->once())
->method('setDefaults')
->with([
'label' =>'Foo',
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We could create an instance ofOptionResolver and make assertions later instead. It will ensure that onlysetDefaults is called.

// Arrange$resolver =newOptionResolver();// Act(newMetadataType($metadata))->configureOptions($resolver);// Assert$this->assertSame(['label' =>'Foo'],$resolver->resolve());

Comment on lines +90 to +93
($metadata =$this->createMock(FormMetadataInterface::class))
->expects($this->once())
->method('getBlockPrefix')
->willReturn('Foo');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

What about declaring an anonymous class implementingFormMetadataInterface insetUp to make these tests easier to read?

@Neirda24
Copy link
Contributor

And Im totally agree to something like :
$form = $formFactory->create(null, $user);
// or even?
$form = $formFactory->create($user);

even$form = $formFactory->create(data: $user) is fine currently

Symfony does not guarantee named arguments on methods at the moment. So maybe better not show this on documentation / example ?

@WedgeSama
Copy link
ContributorAuthor

@Tiriel Still one class less, less code:

  • Traditional (with DTO): Entity + DTO + FormType
  • Attributes (with DTO) : Entity + DTO

@yceruto

which may be mitigated in PHP 8.5

Yes, using the closure inside attributes in 8.5 can remove some limitations.

I think advanced features like data mappers, form events, and model/view transformers carry special complexity and responsibility tied to the form type itself, but I’m open to making them configurable using attributes, as long as we can also implement them outside the DTO.

Agreed

Yes, but what about just creating a command that generate the form type from a DTO 😅?

Can still be done, those 2 features are not mutually exclusive 😄

yceruto reacted with thumbs up emoji

@Tiriel
Copy link
Contributor

@Tiriel Still one class less, less code:

  • Traditional (with DTO): Entity + DTO + FormType
  • Attributes (with DTO) : Entity + DTO

I don't think we're talking about the same thing and we're going deep into things not strictly related to this feature. I don't see the point of the DTO in the first traditional case. And so, the number of classes is the same.

But that's beside the point. I'm not against you or the feature, I'm just saying that there are use cases where this could lead to bloated entities. And I'm not sure that advising user to put the attributes on another unnecessary class is the best solution.

But then again I seem to be the only one fearing that so maybe it's not that much of a concern.

@yceruto
Copy link
Member

Still one class less, less code:

Traditional (with DTO): Entity + DTO + FormType
Attributes (with DTO) : Entity + DTO

In terms of number of classes, yes. But to be fair, it’s not significantly less code. We’re going to inline the form type definition within the DTO class using attributes instead, so you’ll have to write extra code for that purpose.

The comparison should be in terms of coding:Attributes (with DTO): Entity + DTO + Attributes. It reduces the boilerplate of defining form fields and having an extra class. It makes things simpler for simple forms. Yes!

@yceruto
Copy link
Member

I loved the auto form example, but I’m afraid it would work only as a sample. In practice, you’ll need to define at least the field type to match the property’s purpose.

@smnandre
Copy link
Member

I loved the auto form example, but I’m afraid it would work only as a sample. In practice, you’ll need to define at least the field type to match the property’s purpose.

I'm not sure to be honest.. or, to be more precise, it happens in simple cases, and at least for these it's a real DX improvement.

@smnandre
Copy link
Member

The entity becomes quite heavy on attributes.

A fair point—and one we fully anticipated! 🙂

That said, I don’t believe it’s our role to prevent users from adding attributes here… but neither should we encourage using entities directly in forms. At the end of the day, everyone will adopt the approach that fits their project and philosophy best.

Ultimately, this is one of the original goals of this work: to provide anunopinionated way to define forms directly on the data class... whatever class that may be.

WedgeSama reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@ycerutoycerutoyceruto left review comments

@Jean-BeruJean-BeruJean-Beru left review comments

@Neirda24Neirda24Neirda24 left review comments

@xabbuhxabbuhAwaiting requested review from xabbuhxabbuh is a code owner

Assignees
No one assigned
Projects
None yet
Milestone
7.4
Development

Successfully merging this pull request may close these issues.

7 participants
@WedgeSama@carsonbot@Tiriel@Neirda24@yceruto@smnandre@Jean-Beru

[8]ページ先頭

©2009-2025 Movatter.jp