Skip to content

[RFC] Add a form DTO maker for entities #162

Open
@codedmonkey

Description

@codedmonkey

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 return null.

This is relevant because when interacting with an entity, the Form component calls getUsername() to fill the initial form value of the username field, so using a form type directly on a new entity will result in a PHP error that getUsername() 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

$ php bin/console make:form-data

 The name of the form data class (e.g. GentleJellybeanData):
 > UserData

 The name of Entity or custom model class that the new form data class will be bound to (empty for none):
 > User

 created: src/Form/UserData.php
// src/Entity/User.php

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

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

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

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

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

    public function getUsername(): string
    {
        return $this->username;
    }

    public function setUsername(string $username): self
    {
        $this->username = $username;

        return $this;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;

        return $this;
    }

    public function getPassword(): string
    {
        return $this->password;
    }

    public function setPassword(string $password): self
    {
        $this->password = $password;

        return $this;
    }
}

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):

<?php

namespace App\Form;

class UserData
{
    /**
     * @var string|null
     */
    private $username;

    /**
     * @var string|null
     */
    private $email;

    /**
     * @var string|null
     */
    private $password;

    public function getUsername(): ?string
    {
        return $this->username;
    }

    public function setUsername(?string $username): self
    {
        $this->username = $username;

        return $this;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(?string $email): self
    {
        $this->email = $email;

        return $this;
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

    public function setPassword(?string $password): self
    {
        $this->password = $password;

        return $this;
    }
}

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:

<?php

namespace App\Form;

use App\Entity\User;

class UserData
{
    // fields
    
    public function __construct(User $user = null)
    {
        if ($user) {
            $this->extract($user);
        }
    }

    // getters / setters
    
    public function fill(User $user)
    {
        $user->setUsername($this->getUsername());
        $user->setEmail($this->getEmail());
        $user->setPassword($this->getPassword());
    }
    
    private function extract(User $user)
    {
        $this->setUsername($user->getUsername());
        $this->setEmail($user->getEmail());
        $this->setPassword($user->getPassword());
    }
}

in this way, the developer can interact with the DTO with just a few calls:

// New user form
$data = new UserData();

$user = new User();
$data->fill($user);

// Edit user form
$data = new UserData($user);

$data->fill($user);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions