-
-
Notifications
You must be signed in to change notification settings - Fork 417
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
[RFC] Add a form DTO maker for entities #162
Comments
To allow us make a better decision, could you please show an example of how would you run this command and the code it would generate? It'd be great if you could use the Symfony Demo app as an example because that way we can compare "the new way" with "the old way" of doing things. Thanks! |
Yea, @javiereguiluz has a great idea. I'd like to see some proposed code. Definitely not against this :). |
Good suggestion, I tried to make a good example of what I'm thinking of in the original post, let me know if anything is still unclear. |
Does anything moves with this topic? |
I'd suggest using |
Great idea would save us some time. :-). |
It might be easier to have Basically we should have an entity that just defines the variables + getters/setters and a DTO with all the assertions. |
@ckrack , I usually have structure like
On my experience it is pretty convenient in a huge project. |
@vudaltsov While I do agree, it's fairly easy to move the files where one wants them. |
@javiereguiluz @weaverryan I have setup a repo with some example code. I used code from the You can see the result in the repo, too: |
This command is added as a supplement to make:form. To prevent problems with invalid state of entities when used in forms, data transfer objects are a solution. This command generates these from existing entities. It can generate getters/setters and helper methods to fill and extract data from/to entities. Closes symfony#162
Hi, what if I have fields that were generated from an entity class? For example, I have a Book Entity with properties
What do I need to check in the data class for this field? |
@codedmonkey I like the idea but in the meantime, take a look at https://github.com/sensiolabs-de/rich-model-forms-bundle It is absolutely amazing bundle, mostly because it allows you to always have valid entity; dependencies are easier to inject in constructor than using 'empty_data' and TypeError exceptions are converted to validation errors. The problem with DTO is excess code and if you work with entity collections, you are in trouble. I tried, I failed, that's why I am promoting this bundle. And I don't feel bad about that 😄 |
This command is added as a supplement to make:form. To prevent problems with invalid state of entities when used in forms, data transfer objects are a solution. This command generates these from existing entities. It can generate getters/setters and helper methods to fill and extract data from/to entities. Closes symfony#162
I like the idea and think that we should encourage to separate data representation used for example in FormType, from the Entity class which should represent DB model. FormType is not always a 1to1 representation of the model, and this is why we have DTO or dataTransformer. We see some ticket like "#1175", and even if the purpose to be "as friendly as possible" is understandable, it's seems inconsistent that the ORM annotation/attribute claims that a field is not null-able, but the property claims the opposite. Morever, using some tools like phpstan complains with errors: " Property App\Entity\Foo::$name type mapping mismatch: property can contain string|null but database expects string". It's seems a mistake to just "silenced" those error and allowed invalid state for a while. TLDR: Entity should be valid in the context of DB, and create a DTO in the same time as the Entity from the maker:bundle would be a great feature. |
This would work only for simple scalar fields, not for collections or Here is why:
This means that if you don't change anything, Symfony will not even make a single call to adder/remover. Symfony can compare these 2 arrays as Doctrine has identity-map and you will never get 2 different instances representing same row. But: So what would happen, in best-case scenario, are unnecessary calls to adder/remover. Sure: Doctrine would delete old and add new ones, pretty much leaving things as they were. But this is a big problem: plain m2m relations are rare, there are always some extra columns. In above example, imagine that instead of real m2m, you have 2 of one2many relations like described here. So you would have new entity like class ProductCategoryReference
{
public function __construct(
private Product $product,
private Category $category,
private DateTime $joinedAt = new DateTime(),
){}
} Back to entity, fake generics for readability: class Product
{
private Collection<ProductCategoryReference> $categoryReferences;
public function getCategories(): array<Category>
{
return $this->categoryReferences->map(fn($ref)=> $ref->getCategory())->toArray();
}
public function addCategory(Category $category): void
{
$ref = new ProductCategoryReference($this, $category);
$this->categoryReferences->add($ref);
}
} By working with entities directly, you can change only these getters/adders/removers in one place (bound entity) but everything else in the code remain unchanged; I did many of these before but not as simple as shown here. I even have collections having their own collections 😄 Idea of not binding entities to forms make sense, but it is simply not worth pursuing; very little benefits, but big PITA for anything but the most simplest of forms. |
@zmitic Thanks for your detailed answer ! Sure that collection is not trivial, but I'm not convinced that because dealing with collection is tricky, we shoud have a one size fit all Entity that will "represent the DB Model... but in fact not really". I'm in favor that "default" should be "Entity is always valid" and "Entity is only DB", but I don't really care if we can have an option in the maker. As i said, if you use symfony to deliver your frontEnd with formType, at some point you will have some forms field that will not stick to you Entity.
So IMHO, collection is a real thing, but in a medium to large project, this will not be the biggest issue, and the database will drastically diverge from the UI/Entrypoint. At that time, I think DTO can clearly separate the view from the model and allowing more flexibility. |
First: sorry to all subscribers that will get emails. I just want to give real life examples why I think binding DTOs to forms is bad. I use everything you mentioned, but not with default Symfony mapper. Instead, I use my own which is very similar to this one. The difference is my version doesn't have Both of them allows users to not work directly on underlying entity, but use other entities and even some API if needed. I have cases like that but they are kinda tricky to show w/o understanding the reasons for that. But: the comparison of what it was, to what is submitted is still a really big problem; I don't want my Now... there is one thing I didn't mention, but it is good you did. Symfony has the concept of parent forms so this is real code I have (slightly modified for readability): /**
* @extends AbstractType<Patient>
*/
class PatientFactoryType extends AbstractType
{
public function __construct(private PatientFactory $factory) { }
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('factory', $this->factory->create(...));
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('firstName', TextType::class, [ // <----- field name DOES NOT have to match property name
'get_value' => fn(Patient $patient) => $patient->getFirstName(),
'update_value' => fn(string $firstName, Patient $patient) => $patient->setFirstName($firstName),
]);
// other REQUIRED values
} This above form type is the absolute bare minimum for Patient entity to be created: class Patient
{
/**
* @see PatientFactory::create()
*
* @internal
* @psalm-internal \App\Factory\Entity\Strategy
*/
public function __construct(
private string $firstName,
private string $lastName,
private DateTimeInterface $dateOfBirth,
private GenderEnum $gender,
private Brand $brand,
private ?string $phoneNumber = null,
// other fields that are nullable are not shown
){} Those other fields are populated by forms that extends the first one, which is the case you mentioned: /**
* @extends AbstractType<Patient>
*/
class PatientRegistrationType extends AbstractType
{
public function getParent(): string
{
return PatientFactoryType::class; // <---------------- this
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('phone', PrettyTelType::class, [
'get_value' => fn(Patient $patient) => $patient->getPhoneNumber(),
'update_value' => fn(string $phone, Patient $patient) => $patient->setPhoneNumber($phone),
]);
// other fields
} With this Hope it is more clear now. Symfony forms are just crazy powerful, and I yet have to find a case when they can't do something. I even used them for API, no problems there; yes, you read it right, even for API 😄 Anyway, I don't want this issue to become a chat as every other subscriber will get tons of emails. We can continue on /r/symfony , I have much more examples. |
I'm proposing the creation of a
make:form-data
command to create a Data Transfer Object, specifically for use with the Form component, that holds the data of your domain model (e.g. entities) when using a form type to modify it.Some people argue that an entity should never be in an invalid state (for example, required field should always be filled) and this position has been strengthened due to the fact that typehinting in PHP7 can enforce this notion where
public function getUsername(): string
can't returnnull
.This is relevant because when interacting with an entity, the Form component calls
getUsername()
to fill the initial form value of theusername
field, so using a form type directly on a new entity will result in a PHP error thatgetUsername()
cannot return null. A DTO solves this problem by carrying the data for such an entity with seperate typehinting to comply with those interaction requirements. This DTO can be validated before the entity is created, meaning the developer can prevent creating an entity with an invalid state in this way.Using DTOs in forms isn't mentioned in the docs anywhere, so that might need to change first (see symfony/symfony-docs#8893).
Example Interaction
Expected Result
Mainstream
In my own experience, I've rarely seen DTOs being more than carriers of data, which would mean one file will be generated, but other components of the project need to be reworked to fill and extract data in the DTO (like the Controller displaying the form):
Opinionated
Another (likely more opininated) way of handling DTOs is to add the ability to fill and extract data directly into the DTO. In addition to the generated code above, the following code would also be generated:
in this way, the developer can interact with the DTO with just a few calls:
The text was updated successfully, but these errors were encountered: