Skip to content
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

Documentation about getters/setters to DTOs and Value Objects #6902

Merged
merged 5 commits into from
Mar 18, 2018
Merged

Documentation about getters/setters to DTOs and Value Objects #6902

merged 5 commits into from
Mar 18, 2018

Conversation

Pierstoval
Copy link
Contributor

I recently tweeted about this, but the original questioning comes from @javiereguiluz:

The "Getting started" guide refers to getters and setters as the base of an entity's behavior, but the Doctrine core team and other experts certainly prefer mapping rich accessors and mutators. @Ocramius also gives this advice in his "Doctrine beset practices" conference (slides here).

This PR is not a final proposal, but more a starter about a change to the documentation to suggest using a rich model for our domain instead of relying only on getters and setters.

I thought it would still be nice to bring the getters and setters and then talk about DTOs and rich model, to keep consistency with the 95% (arbitrary number) entities created by developers these days, especially the ones using Symfony (and mostly because the Symfony Form component uses getters and setters a lot, and most docs propose to inject them in our forms rather than relying on a DTO).

WDYT about this?

@Majkl578
Copy link
Contributor

Majkl578 commented Dec 18, 2017

edit: This was a reply to a comment that has been deleted, it suggested to use constructor to set value and keep entity immutable only with a getter.

Agreed on constructor + immutable state being easier to understand in Getting Started. But behavioral methods should be explained too at some later point. For example, changePassword($newPassword) would be good example.

Referencing symfony/symfony-docs#8893 - similar issue for Symfony docs.

@fesor
Copy link

fesor commented Dec 18, 2017

I don't think that information about proper modelling fits to "getting started" section... This is far more complicated topic. You could change examples, like remove setters:

<?php
// src/Product.php
class Product
{
    /**
     * @var int
     */
    private $id;
    /**
     * @var string
     */
    private $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

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

    public function getName()
    {
        return $this->name;
    }
}

and

$product->rename($name);
// instead of
$product->setName($name);

Doctrine doesn't enforce users to use setters, so i don't think why it used in documentation. But with getters there are different story. Since #6719 will not be available to users until 3.0, it will be just to complicated to describe why you still need getId() getter so you could fetch ID of related entity for example without actually preform db query.

Anyway, right now documentation contains something like:

When creating entity classes, all of the fields should be protected or private (not public), with getter and setter methods for each one (except $id).

this literally enforce users to use getters in setters in their models. And the only reason for that is how doctrine use accessors for lazy load. Probably it will be better to replace mutators to accessors.

Also I think that it would be useful to describe concepts of persistence ignorance instead of digging into DTO/UoW stuff in the getting started section. For example some of the devs thinks that Doctrine is calling constructor of entities for each fetch, and many problems comes from the fact that they don't understand that entities this simple concepts.

You could provide links on separate sections which will provide more detailed information. Like "how to use entities with forms" and so on. Something for cookbook rather than "getting started".

@Ocramius
Copy link
Member

Ocramius commented Dec 19, 2017 via email

For example, when having a ``User`` entity, we could foresee
the following kind of optimization.

Before, an anemic model with getters and setters:
Copy link

Choose a reason for hiding this comment

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

there are sections "Getting Started: Database First" and "Getting Started: Model First". I think this sections could cover "anemic vs rich model" cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's a.... rich idea 😎


public function authenticate(string $password, callable $checkHash) : bool
{
return $checkHash($password, $this->passwordHash) && ! $this->hasActiveBans();
Copy link

@fesor fesor Dec 20, 2017

Choose a reason for hiding this comment

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

what about cases with password_needs_rehash?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's an example retrieved from Marco's talk about doctrine best practices, but of course it can be changed :)

Copy link
Member

Choose a reason for hiding this comment

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

@fesor I think this is a good example of how a rich domain model aids in better maintainability later on. Not sure if it would fit a simplified example though. Maybe just skip it for now?

Copy link

@fesor fesor Dec 20, 2017

Choose a reason for hiding this comment

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

Yes, probably this something that could be covered later in some cookbook (like how to deal with dependencies).

Choose a reason for hiding this comment

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

Hi guys.
I appreciate the effort you put in updating the documentation and promote better programming practices.
I agree with @fesor that the "dealing with dependencies" topic must be covered. Yes, it can be in some cookbook, but it must be covered because it will be an issue people will run into very quickly and not finding anything related to this will probably make them to give up and go to the well known "don't put behavior in your entities, just put data and make some services that will operate on your entities through some setters".

return $this->username;
}

public function setUsername(string $username) {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 for types, also add void

$this->passwordHash = $hash($password);
}

public function ban()
Copy link
Contributor

Choose a reason for hiding this comment

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

It may be interesting to see some parameters here. I.e. \DateInterval $duration + assert() that it's non-negative. WDYT?

@ostrolucky
Copy link
Member

Some pretty big words unnecessarily used here for getting started guide. I suggest following changes (I focused on getter/setters option, not sure how to change the rest)

 Adding the Entity behaviors
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-You have two options to define the accessors mutators that will
-update the state of your objects.
+You have two options how to define methods in your entity.
 
 **Getters and setters**
 
-The most popular method is to create getter and setter methods
-for each property (except ``$id``). The use of mutators allows
-Doctrine to hook into calls which manipulate the entities in ways
-that it could not if you just directly set the values with ``entity#field = foo;``.
+The most popular option is creating 2 separate methods for reading
+(getter) and updating (setter) for each field in your entity.
+Some fields, such as ``$id`` are undesirable to be modified and as
+such it's ok to not provide setters for them.
 
-The id field has no setter since, generally speaking, your code
-should not set this value since it represents a database id value.
-(Note that Doctrine itself can still set the value using the
-Reflection API instead of a defined setter function)
+This is a convention which makes it possible to expose each field
+in your entity to external objects in automated way, while allowing
+you to keep type safety in place.
+
+Such approach is good choice for RAD (rapid application development),
+but may prove problematic later down the road, because providing such
+easy way to modify any field in your entity means entity itself cannot
+ensure it's in valid state. Having entity in invalid state is dangerous,
+because it's one step away from being implicitly saved in database.
 
 .. note::
 
-    This method, although very common, is **not** the best approach if
-    you want a perfect domain design. It can lead to unexpected behavior
-    when dealing with different steps of logic in between different
-    calls to ``EntityManager::flush()``, for example when you change
-    the state of an object via event listeners or lifecycle callbacks.
+    This method, although very common, is inappropriate for domain driven
+    design, because in such design, methods should be named according to
+    actual business operation and entity should be valid at all times.
 
 **Mutators and DTOs**

@fesor
Copy link

fesor commented Jan 2, 2018

I thought about this a little bit (and asked some of the developers in local community about pit-fails in learning doctrine).

The goal of "getting started" section of every tool is to have it running it as soon as possible (within 10-15 minutes). This is very important to not scare new developers with complexity of the tool. So there should be bare minimal information with only hints about preferred way of designing entities (like remove setters from samples). Al other things should be covered in other sections.

From what I know, getters/setters are mostly used since getting started guides of other tools (like forms, CRUD generators and so on) are provided this examples as most straightforward way to start. later in documentation most of this tools covers how to handle DTO, value objects and other stuff, but in format of cook books. However most of the tools doesn't cover this information (api-platform for example, or sonata admin bundle) which make it hard to developers to start without at-least setters.

So if Doctrine's documentation will push to one direction, and other tools like forms components in to another, and there will be no handy tutorial on how to mix concepts, developers will fall into cognitive dissonance.

Another thought. I faced with fact that most of the developers just don't know how to handle updates in CRUD based application. Basically they need to replace most of the entity data in update operation, with some kind of control. I teach my team to rely in immutable data since in this way we could replace "update" to another "create" operation but in different context. This requires understanding of concepts like value objects, which represented in Doctrine as Embeddable. Unfortunately "Separating Concerns using Embeddables" section is just not contain enough information and many developer event don't know about embeddable available to them.

I think that embeddable should be covered right after entities in basics of the ORM. Also I found structure of documentation not user friendly. Use of embeddables are simplifies understanding of how to better structure business logic into separate objects.

Another "pattern" in behaviour which I found, is how data modeling is happening. For example in some application we have users. And we also have some kind of referral program. And also users could be both merchants and customers. And each has profiles. Most of the devs which I talked with putted all this logic in same User entity eventually will break SRP and will be too big to maintain. And having multiple entities which are referencing same ID and used in different contexts is counterintuitive to people which doesn't want to know anything about DDD and other fancy stuff.

This probably something that should be at lest slightly covered in "Getting Started: Model First". Or maybe instead of two sections there should be one - "Getting Started: Entity Design Approaches" which will have high-level description of different approaches with list of pros/cons of each.

Also I think that restructuring welcome page will have much more benefits to simplify learning projects. Documentation structure should provide much more cleaners way of learning Doctrine.

@Pierstoval
Copy link
Contributor Author

I'm not sure we should document Embeddables right after the "Getting started" guide. It is related only to specific needs which are not very common, as most of the times, having entities and relations is a better option, could it be for the DBMS or for your domain.

For the rest, I take all your comments in account and will make updates on this subject in the next days/weeks.

In the meantime, thanks everyone for participating in this very interesting discussion, happy new year, and may 2018 give you more free time for OSS 😄 🎉

@fesor
Copy link

fesor commented Jan 2, 2018

right after the "Getting started" guide

not right after "getting started". But at least in "Mapping Objects onto a Database"

It is related only to specific needs which are not very common

mostly since many developers even don't know about it. That's why I think it would be better to start from restructuring links on welcome page instead of rewriting getting started. The only thing is to remove obsolete information like "all properties should be protected (private)".

@@ -103,7 +103,7 @@ Install Doctrine using the Composer Dependency Management tool, by calling:
$ composer install

This will install the packages Doctrine Common, Doctrine DBAL, Doctrine ORM,
Symfony YAML and Symfony Console into the `vendor` directory. The Symfony
Symfony YAML and Symfony Console into the `vendor` directory. The Symfony
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should remove Symfony YAML from this page? I mean v3 already removed support for it, v2.7 deprecates it, and I think we should start phasing it out ASAP.

Can you please also update the version in L90?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I will, I'm just waiting for a moment when I'll have more time for this PR, it's hard work 😉

}

public function setName($name)
Additionnally, our entities should never see their state change

Choose a reason for hiding this comment

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

Additionnally -> Additionally

Entities should represent **behavior**, not **data**, therefore
they should be valid even after a ``__construct()``.

To help creating such objects, we can rely on ``DTO``s, and/or make

Choose a reason for hiding this comment

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

make our entities Value Objects.
Can we rephrase it? From DDD point of view VOs and entities are different concepts.

.. code-block:: php

<?php
class User {
Copy link
Member

@greg0ire greg0ire Jan 19, 2018

Choose a reason for hiding this comment

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

This is not PSR compliant, neither is the code below

* DTOs can be reused in other components, like when deserializing
mixed content, using forms...
* Anemic model tends to isolate the entity from the logic, and a
rich model fixes that because entity's logic can be put in the
Copy link
Member

@greg0ire greg0ire Jan 19, 2018

Choose a reason for hiding this comment

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

"the entity's logic" I think

@Pierstoval
Copy link
Contributor Author

Fixed all your review @greg0ire, but next time, could you add them in a Github review so we can hide them when fixed? 😅

@Pierstoval
Copy link
Contributor Author

By the way, for reviewers, I just made some changes to the text, if you could review again 😉

@greg0ire
Copy link
Member

@Pierstoval that's what you get for mentioning me in a commit message :P. You can still squash if you want them to disappear though


public function getId()
When creating entity classes, all of the fields should be protected or private (not public).
Copy link
Member

Choose a reason for hiding this comment

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

We should probably rephrase to something like:

should be private, protected when strictly needed and very rarely if not ever public

I can also add that protected and public have some performance overhead (minimal, but it is there). Unsure if we can pack it into the article.

Copy link

Choose a reason for hiding this comment

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

I don't think that we need to introduce any limitations in getting started at all. Only one that are really limitations of Doctrine. Proper entity modeling could be covered elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure the performance overhead might be interesting for end-users to know.

Copy link
Member

@greg0ire greg0ire Mar 18, 2018

Choose a reason for hiding this comment

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

@fesor it's not a limitation (of Doctrine) is it? Just a good recommendation. There's no explanation though, and the performance concerns are additional to something else.

How about

When creating entity classes, fields should be private, protected when strictly needed and very rarely if not ever public, to make sure encapsulation is not broken by needless exposition.

public function getId()
When creating entity classes, all of the fields should be protected or private (not public).

Adding the Entity behaviors
Copy link
Member

Choose a reason for hiding this comment

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

Adding behavior to Entities?

Adding the Entity behaviors
~~~~~~~~~~~~~~~~~~~~~~~~~~~

You have two options to define to define methods in your entity:
Copy link
Member

Choose a reason for hiding this comment

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

It is a good idea to keep things very impersonal.

There are two options and in entities

Also, to define is used twice here

The most popular method is to create two kinds of methods to
**read** (getter) and **update** (setter) the object's properties.

Some fields such as ``$id`` are unlikely to be changed, so it's ok to
Copy link
Member

Choose a reason for hiding this comment

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

overall, try not abbreviating it's - keep it is


.. note::

Setters are unrelated to Doctrine itself, because the ORM does not
Copy link
Member

Choose a reason for hiding this comment

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

It is true that they are unrelated, but that's not how it should be explained to the reader.

I think a better approach is to say something like:

Doctrine ORM does not use any of the methods you defined: it uses reflection to read and write values to your objects, and will never call methods, not even __construct

Reflection API instead of a defined setter function)
There are several advantages to using such model:

* Entity **state is always valid**. No setters means that we only
Copy link
Member

Choose a reason for hiding this comment

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

highlight Entity as part of the bold section

Copy link
Member

Choose a reason for hiding this comment

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

s/No setters means that/Since no setters exist,

* Entity **state is always valid**. No setters means that we only
update portions of the entity that should already be valid.
* Instead of having plain getters and setters, our entity now has
**real behaviors**, meaning it is much easier to determine the
Copy link
Member

Choose a reason for hiding this comment

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

s/behaviors/behavior

Copy link
Member

Choose a reason for hiding this comment

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

Also: you really like the word "meaning" :D

Copy link
Member

Choose a reason for hiding this comment

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

Can be simplified: **real behaviors**: it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I need to improve my english language a bit 🤣

* Instead of having plain getters and setters, our entity now has
**real behaviors**, meaning it is much easier to determine the
logic in the domain.
* DTOs can be reused in other components, like when deserializing
Copy link
Member

Choose a reason for hiding this comment

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

s/like when/for example

* DTOs can be reused in other components, like when deserializing
mixed content, using forms...
* Classic and static constructors can be used to manage different
ways to create our objects, and they can also use DTOs.
Copy link
Member

Choose a reason for hiding this comment

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

s/use/rely on

* Classic and static constructors can be used to manage different
ways to create our objects, and they can also use DTOs.
* Anemic models tend to isolate the entity from logic, whereas
rich models fix that because logic related to the entity can be
Copy link
Member

Choose a reason for hiding this comment

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

fix that because

"fix that" is strange to read in this context, because it is not immediately obvious that this is a problem. We only briefly explained that anemic models lead to some issues, but we didn't explain that logic outside entities versus inside is worse.

@greg0ire
Copy link
Member

@Pierstoval I have access to a computer again and deleted my comments to keep this PR relatively clean

**Anemic models: Getters and setters**

The most popular method is to create two kinds of methods to
**read** (getter) and **update** (setter) the object's properties.
Copy link
Member

Choose a reason for hiding this comment

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

"read" and "update" sounds weird, how about:

  • "read" and "write"?
  • "share" and "mutate"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought read & update were nicer. By adding "write", I'm confused with the fact that we read something but we write in something, and I didn't want to bother with my imperfect english 😆

"share" and "mutate" are closer to the DDD & rich entities approach, and these are words that are possibly unknown by many developers, so I didn't want to add them in the common anemic design

of your entity to external services, while allowing you to keep type
safety in place.

Such approach is a good choice for RAD (rapid application development),
Copy link
Member

Choose a reason for hiding this comment

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

"Such an approach"

but may bring problems later down the road, because providing such an
easy way to modify any field in your entity means the entity itself cannot
ensure it's in valid state. Having entity in invalid state is dangerous,
because it's one step away from being implicitly saved in database, thereby
Copy link
Member

Choose a reason for hiding this comment

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

How about:

Having any object in an invalid state can lead to bugs that will be even harder to troubleshoot and fix if that broken state is persisted to a database and detected later on when the corrupted data is loaded.

}

public function getName()
Here, we avoid all possible logic from the model and only care about injecting
data into it without validation nor consideration about the object's state.
Copy link
Member

Choose a reason for hiding this comment

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

"without any validation but the one provided by the type hints"


public function ban(\DateInterval $duration): void
{
assert($duration->invert !== 1);
Copy link
Member

Choose a reason for hiding this comment

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

TIL 💡

validated before being sent to the object for update.

By using DTOs, if we take our previous ``User`` example, we could create
a ``ProfileFormUser`` DTO object that will be a plain model, totally
Copy link
Member

Choose a reason for hiding this comment

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

DTO object expands to "data transfer object object"

should not set this value since it represents a database id value.
(Note that Doctrine itself can still set the value using the
Reflection API instead of a defined setter function)
There are several advantages to using such model:
Copy link
Member

Choose a reason for hiding this comment

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

should be "such a model", BTW

@Pierstoval
Copy link
Contributor Author

Pierstoval commented Mar 18, 2018

I made another big update, thanks a lot to all reviewers, it makes me both improve my english and my understanding of how we explain such a complex design as DTOs and rich entities 😄

Copy link
Member

@Ocramius Ocramius left a comment

Choose a reason for hiding this comment

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

🚢 this is good as-is IMO (besides more nitpicking that I will avoid here).

If anybody has additions/suggestions, please send more PRs :-)

@Ocramius Ocramius added this to the 3.0 milestone Mar 18, 2018
@Ocramius Ocramius merged commit 8836954 into doctrine:master Mar 18, 2018
@Pierstoval Pierstoval deleted the dto-and-vo branch March 18, 2018 17:20
become much harder, and you might be aware of the bug too late to fix it in a
proper manner.

implicitly saved in database, thereby leading to corrupted or inconsistent
Copy link
Contributor

Choose a reason for hiding this comment

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

@Pierstoval this phrase looks a missed leftover. Is it intended?

Copy link
Member

Choose a reason for hiding this comment

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

Dropped in #7137

language. The metadata language describes how entities, their
properties and references should be persisted and what constraints
should be applied to them.
There are several advantages to using such a model:
Copy link
Contributor

Choose a reason for hiding this comment

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

For the previous "anemic entities" you mentioned 1 advantage (RAD) and 4 drawbacks ... here you mention 4 advantages and 0 drawbacks. Do these "rich entities" really have no drawbacks? Thanks!

Copy link
Member

Choose a reason for hiding this comment

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

Not really: being able to have the database as an implementation detail is primary the reason for using a data mapper over other types of persistence layers, so this approach goes much much closer to the business requirements. It still has the flaws of mutable state management, but that's inherent from RDBMS-backed applications in general.

@@ -228,39 +227,235 @@ entity definition:
* @var string
*/
protected $name;

Choose a reason for hiding this comment

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

The vairable is protected.

When creating entity classes, all of the fields should be private.

Rich entities should represent **behavior**, not **data**, therefore
they should be valid even after a ``__construct()`` call.

To help creating such objects, we can rely on ``DTO``s, and/or make

Choose a reason for hiding this comment

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

image


Example of a rich entity with proper accessors and mutators:

.. configuration-block::

Choose a reason for hiding this comment

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

This does not render!

image

Copy link

Choose a reason for hiding this comment

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

There needs to a an empty line after the .. code-block:: php

@Ocramius
Copy link
Member

@tomasfejfar this patch is already merged: send further PRs to fix pending issues 👍

should be applied to them.
There are several advantages to using such a model:

* **Entity state is always valid**. Since no setters exist, this means that we

Choose a reason for hiding this comment

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

image

The next step for persistence with Doctrine is to describe the structure of
the ``Product`` entity to Doctrine using a metadata language. The metadata
language describes how entities, their properties and references should be
persisted and what constraints should be applied to them.

Choose a reason for hiding this comment

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

The section following this edit mentions again setters and getters. That's IMO confusing.

@Ocramius Ocramius removed the WIP label Mar 18, 2018
{
$this->name = $name;
public function updateFromProfile(ProfileEditingForm $profileForm): void
Copy link
Member

Choose a reason for hiding this comment

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

While the ProfileEditingForm is defined above as a DTO, the code itself reads as the domain object (user) depending on the application logic (form). This way, it violates the Open/closed principle: every time a new form is added, the User class needs to be updated to enable its mutation by the form. Does the user really have to accept multiple types of DTO or at least, could all forms be represented by one DTO type?

BTW, this code is not rendered on the website:
screenshot from 2018-03-21 12-21-36

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.