Description
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);