symfony / symfony-docs

The Symfony documentation
https://symfony.com/doc
Other
2.15k stars 5.11k forks source link

Remove references to entity generation, replace by proper entity modelling #8893

Closed Majkl578 closed 3 years ago

Majkl578 commented 6 years ago

Hello,

there are lots of users using doctrine:generate:entities nowadays. This must stop.

Symfony should provide its large user base decent article on designing/modelling entities in a proper way, with business methods and as few setters as possible, not by generating them from database. Such generated entities lead to a tediously known anemic domain model, something that does more harm than good: breaks encapsulation and completely undermines the purpose of ORM. It'd probably be even better to not use ORM at all and use DBAL directly, the result would be very similar without runtime overhead.

Note that Doctrine 3.0 will remove support for code generation entirely, so the better we educate the users, the better.

This issue is a direct response to discussion in doctrine/DoctrineBundle#729.

Thanks.

Ocramius commented 6 years ago

@JarJak developers are vicious and lazy animals, so they'll go for the quick win instinctively :-)

Let's show them the right carrot, shall we?

javiereguiluz commented 6 years ago

@Ocramius I'm sure it wasn't ill-intended, but this expression is really unfortunate:

"developers are vicious animals"

As you can see in the list of synonyms of the "vicious" word this can only mean horrible things! Maybe the expression you were looking for was "creatures of habit"?

In any case, could you please reword your comment a bit to remove that word? Thanks a lot!

Ocramius commented 6 years ago

this can only mean horrible things!

Yes? That's exactly what I intended...

tvogt commented 6 years ago

Developers are under pressure to deliver code that works. I have some code that has been in production for almost 20 years. It probably contains every anti-pattern in the world and goes against every established good practice (or carrot, if you will), mostly because it predates them. But it works. Sure, only a handful of people who spent a good amount of time learning the codebase can maintain it, but thousands of people enjoy its benefits and we have over the years refactored a lot of things (mostly as PHP evolved and better coding practices developed), including moving from a self-written very thin DBAL to Doctrine and some Symfony components. For all of these years, the code was running in production, with an uptime upwards of 99.8%. This is half a million lines of code. It will never be refactored entirely. Evolving it so that it can use Doctrine and Symfony components would not have been possible if either had insisted on strictly following specific paradigms. Flexibility and the possibility to be "vicious animals" allowed us to move the codebase into a better direction over the years. There are a hundred places in the code where you can either be "lazy", or not touch the code at all. Not because developers went "for the quick win", but because PHP3 downward compatability was still a thing and most of the patterns of today didn't yet exist. Symfony 1 was six years away. The carrot this project needs is flexibility and a pragmatic approach, because every refactored script is a step in the right direction, and "anemic vs. rich" discussions are philosophical musings when some parts of your code still have SQL statements inside HTML.

Now this might be an outlier in the vast number of Symfony projects, but I hope it serves as a reminder that one size does not fit all. Without the past flexibility and pragmatism of Symfony and Doctrine, this project would still be in the stone age or abandoned. So my gratitude to all you guys for providing these tools, and maybe you now have a better understanding why I cling to them so much.

Ocramius commented 6 years ago

Now this might be an outlier in the vast number of Symfony projects, but I hope it serves as a reminder that one size does not fit all. Without the past flexibility and pragmatism of Symfony and Doctrine, this project would still be in the stone age or abandoned. So my gratitude to all you guys for providing these tools, and maybe you now have a better understanding why I cling to them so much.

I have dozens of projects with anemic domains that are still in production and still run. Heck, I have 2 projects with ~20k sparse PHP files with mixed HTML, PHP, JS and probably hundreds of security issues more important than what we are discussing here: they will need a rewrite if the domain logic changes (strangler pattern, obviously). Why? Because nobody can follow state mutations, nor figure out what the logic behind those state mutations is.

These practices being deprecated/archived and finally removed is just a natural step in the evolution of how we write code. Betterment and improvement is a necessity of required from and by every software developer. You won't be forced to rewrite anything: that's a decision for the stakeholders to take, but if you write something new, make it so that the next generation of developers isn't forced to play the archaeologist role.

kiler129 commented 6 years ago

@tvogt You touched subject very close to a situation which I have - 20 years old codebase predating any sane frameworks serving large number of users and (I guess) earning very good money for the company. It had probably every possible anti-pattern and sql injection was the least problematic thing.

Two years ago, after familiarizing myself with the codebase, I began to plan how to attack a monolithic monster containing well over 1M LOC to convert it into something more maintainable. The crucial part was where I started. I didn't went to business saying "oh, hey, this is pile of garbage, let's burn it and start over" - that would be asking for a Netscape all over again. Instead I started analyzing where developers spend waste much of their time. It was a simple thing... autoloader and migrating away from PHP 4 leftovers. This task took over 6 months to complete, but in the end we had a Symfony DIC implemented... yes, it wasn't perfect, yes it had hacks to make old stuff working, yes it was a huge productivity boost.

Fast forward 2 years: we use almost every Symfony component, our custom templating engine got on-the-fly transpiler to convert templates to Twig and our database interactions are done via Doctrine DBAL. The project kept its uptime and continued to deliver value and new features. It's still faaar from good architecture, it still has classes freezing PhpStorm containing business logic crucial from the perspective of stakeholders.

I can sign with my both hands under what @Ocramius said:

These practices being deprecated/archived and finally removed is just a natural step in the evolution of how we write code.

That's exactly why you follow best practices available at the time. This creates at least manageable code which can be easily refactored because it's decoupled.

Ultimately our job is to deliver business value and it comes with compromises, however I think Symfony should encourage good practices where possible. It's a very flexible codebase (hey, if current components can be adapted to work with ancient codebase it means a lot) which contains amazing documentation. I think no ones intention here is to force the-only-right-way to do stuff, but rather present alternatives... and that's what Symfony was doing for long time.

Then said I believe we should have documentation talking about both RAD and proper DDD usage of the framework here. It's up to the developer to make a choice between DTO persisted to DB intended for CRUD vs. full fledged DDD. We can provide options, but we cannot force only one - it's just toxic. However I believe some of you here are forgetting about whole ecosystem here:

...and those are just examples. So how can, e.g. I, stand in front of my team and say "hey, maybe we should follow DDD principles, but 90% of our tools will stop working".

Just my 2¢... sorry for such a long comment.

codedmonkey commented 6 years ago

Yes let’s throw deadlines out of the window and work on implementing best practices all day every day! (/trolling)

Like @kiler129 points out, not all tools are compatible with such practices but I would like to add a library that looks promising and that’s the work of @ro0nl over at @msgphp who’s trying to create message driven components to so that any interaction with the domain model can be delegated/dispatched to handlers. This would make a lot of (user) interfaces versataille available for any domain model you may have.

MisatoTremor commented 6 years ago

I think it is important to encourage best practices, but never enforce them (Edit: besides in your own code base of course). It's also important to explain why and when to use them and sometimes even telling people when not to.

Many if not all human beeings strive for freedom and have a habit to develop resistance against applied force, so trying to force something could even have the opposite effect.

dunglas commented 6 years ago

API Platform doesn't make usage of DTOs really simple

It's true, but it has improved since the release of Symfony 4 because event subscribers are now automatically registered (the API Platform docs need to be updated), and will improve a lot in v2.3 (to be released soon).

As a tradeoff between RAD and "best practices", I suggest to update maker bundle to generate public properties by default (with and option to change the default visibility to private). It leaves to the developper the responsibility to switch to non-anemic models, or to use it-as is for CRUD-oriented apps, prototyping... Using public properties is also what is recommended when creating simple CRUD REST APIs when using API Platform.

Regardless of the anemic vs non-anemic debate, I think we can all agree that getters and setters bring nothing except complexity compared to public props (see for instance https://dev.to/scottshipp/avoid-getters-and-setters-whenever-possible-c8m).

Edit: actually they can bring a kind of type safety, but Symfony (Serializer / PropertyInfo) deals with PHPDoc and Doctrine to ensure it even with public properties, and the validator component can check types of public props too.

JarJak commented 6 years ago

@dunglas

we can all agree that getters and setters bring nothing except complexity compared to public props

This is valid for statically typed languages like Java in the link. In PHP you can't define property type, so it's better to generate those dumb setters just to ensure basic data integrity.

But again, this is valid only to RAD.

JarJak commented 6 years ago

My previous comment would not be valid anymore: https://wiki.php.net/rfc/typed_properties_v2

dunglas commented 6 years ago

@JarJak this RFC is awesome, but in Symfony/API Platform we already have everything needed to ensure type safety using PHPDoc annotations (or other sources of metadata, including Doctrine mappings):

Majkl578 commented 6 years ago

but in Symfony/API Platform we already have everything needed to ensure type safety using PHPDoc annotations

phpDoc is not a type safety, it's merely just a documentation/hint that is usually relied upon as there is no better way (same applies to unions for example).

dunglas commented 6 years ago

@Majkl578 of course a native solution would be way better, but please read the links I posted...

Levure commented 6 years ago

Le mar. 12 juin 2018 à 13:29, Steffen Roßkamp notifications@github.com a écrit :

I think it is important to encourage best practices, but never enforce them. It's also important to explain why and when to use them and sometimes even telling people when not to.

Many if not all human beeings strive for freedom and have a habit to develop resistance against applied force, so trying to force something could even have the opposite effect.

I fully agree.

Explain, encourage best practices, but not enforce (and so, not removing entity génération).

My first experiences (when discovering Symfony) was via entity generation. I now write my entities by hand.

Never forget that Symfony is known to be hard to learn.

Regards,

Ocramius commented 6 years ago

Here's the news: software design is hard! Yes, it really is!

Believe it or not, but making state cross your application layers via state mutations rather than explicit interactions makes your software even harder to reason about.

Levure commented 6 years ago

Le 21/06/18 à 21:52, Marco Pivetta a écrit :

Here's the news: software design is hard! Yes, it really is!

Believe it or not, but making state cross your application layers via state mutations rather than explicit interactions makes your software even harder to reason about.

@Ocramius : You don't need to be rude.

We are here to exchange - with respect of other opinions - to build a nice and friendly documentation for everyone : starters, intermediate and experts.

Regards,

Ocramius commented 6 years ago

You don't need to be rude

Probably confusing sarcasm with rudeness here.

for everyone : starters, intermediate and experts.

Right, so steer everyone off the quick way to complexity.

Unsubscribing from the thread BTW: feel free to leave the "marketing features" well exposed.

donquixote commented 6 years ago

Do we have code examples (entire project, or some snippet) for the "recommended way" of entity modeling?

Ocramius commented 6 years ago

Try with https://github.com/ShittySoft/fwdays-2018-doctrine-tutorial/tree/feature/specification-testing?files=1 - that's the material of my current workshop.

donquixote commented 6 years ago

@Ocramius interesting. I find only one entity class, User. Or am I not looking in the right place?

The User class is interesting:

The relation of persistence record vs runtime entity object is one of the main design challenges for entity modeling with PHP.

Perhaps some of the above points are by design, and others are because this is a "stub project"?

donquixote commented 6 years ago

The existing methods have mostly no effect on the "outside world".

The "register" method does send an email, but only through a behavior object injected into the method itself. This behavior object is not stored in a property, it is only used in the operation itself. This is a special case because the method is a static factory.

Which leads to the question: If we wanted to add a similar behavior in the "login()" operation, would we pass the behavior object as a parameter to the login() method, or would we keep it as a property?

Ocramius commented 6 years ago

Besides this bit:

The existing methods have mostly no effect on the "outside world". E.g. the login method does not set a session cookie, neither by itself nor through an injected service. It only tests if the password is correct.

The rest is by design. This is for a full day of challenging (dangerous) assumptions in software design, where attendees slowly revolve to this kind of implementation.

There are optional bits such as "logging a login" or "preventing brute-force", which are trivially implemented if the state is correctly handled ONLY by the aggregate root (the User here), but it is a demonstration that state mutation encapsulation keeps things manageable. The entire specification of the features of the software can even be tested with just the aggregate root as SUT.

not something that instantiates a runtime User object.

The word register means something very specific. If you are instantiating this object from stored state, then you may unserialize it, which is again something very specific. This object cannot be instantiated (in the domain or its public API) by design. This conveys the clear purpose of it to anyone wanting to use it.

The "register" function behaves as a constructor. Imo this is unrealistic or misleading for a PHP application

Built already a few applications with these approaches: they work fine, and it is very hard for new developers to misunderstand how to use the domain components. The words in the API match the ubiquitous language as much as possible too.

Which leads to the question: If we wanted to add a similar behavior in the "login()" operation, would we pass the behavior object as a parameter to the login() method, or would we keep it as a property?

Only retain state that you are responsible for. The lifecycle of services and User instances are different, so a User should never have a state reference to a service. Yes, you'd pass a service at call time (note that this is maybe a bit weird for people that are too used to "OOP", but it is the normal way to do it in FP).

See also http://ocramius.github.io/blog/on-aggregates-and-external-context-interactions/

donquixote commented 6 years ago

I'm afraid the example is simplified to a level where the questions from this thread are not really touched. Or again I might not be looking in the right place.

A good example would demonstrate how these steps are not really necessary, and the problems can be solved in other ways. Unless you actually agree with the above :)

I am writing this because I think the current discussion might be too theoretical. So if you or anyone tells a person that ORM code generation is bad, people might have very different ideas what should be the alternative.


I personally have implemented different domain model architectures for different purposes, and I have not found the ideal way yet.

But most of the time, I follow the entity model of the CMS I work with (Drupal 7, Drupal 8), which you might like or dislike for various reasons. Some observations from this:

A lot can be said about this, and most of it would be out of scope for this issue.

But I imagine one main reason for the really heavy entity objects in Drupal 8, and their dependency on services, was the implicit or explicit desire to make them not anemic.

Ocramius commented 6 years ago

People might want to add a $user->getName(), to show the username somewhere on a page.

In "real world" apps, this is where the concepts of "read model" and "view model" come in. Aggregates are designed to keep all the state private, and never really tell anything to the outside, which gives you a degree of freedom in changing logic. Read models allow for aggressively optimising read operations via low-level operations (caching, memoising, SQL optimisations, async parallel execution) and for keeping the results on-point with what is expected. I don't expect people to split reads/writes from this discussion, but be aware that valid and battle-tested solutions do exist here. I'd already be very happy to get rid of the concept of "setters" here.

People might want to add setter methods so the object can be manipulated when a form is submitted.

Terrible idea to bind entities to forms. https://webmozart.io/blog/2015/09/09/value-objects-in-symfony-forms/ instead. I personally don't use symfony forms, but all my forms work with arrays or DTOs these days.

People might want to reference the user object from a different entity, e.g. a blog post, to specify the author. I wonder if the email is meant as the primary key for this purpose, or if there is an id which is just not part of the object.

We're probably going too deep into tactical DDD concepts, but:

People might want to add mechanisms to load and save user objects.

Yes, Doctrine ORM is a mapper like any other! You should be able to write trivial reflection-based mappers for any kind of aggregate. The referenced repository includes some primitive implementations that are sufficient to save/load entities: https://github.com/ShittySoft/fwdays-2018-doctrine-tutorial/blob/feature/registration/src/Infrastructure/Authentication/Repository/FileUsers.php

People might want to inject services or behavior objects into the user object, which help with tasks like saving to the database, or for side effects of specific operations.

Please don't: dependency injection is only applicable when lifecycles are compatible. This is why it is usually only valid for long-lived instances (services).

I am writing this because I think the current discussion might be too theoretical. So if you or anyone tells a person that ORM code generation is bad, people might have very different ideas what should be the alternative.

I was asked for a clean code example: well here it is. I think this is the way to write software in the PHP language and Doctrine ORM (at the best of my current knowledge, as of today, 2018-06-22). Whether this can be interpreted differently is another question, but I demonstrate widely enough that getters/setters can be easily avoided by gaining expressiveness, reduced amount of code, maintainability and ease of use (all of them together). This is what the "designing software" is all about (instead of c&p from tutorials).

I think this quote is strictly related and very much fitting the thread.

A: Please help all my code is shit. B: Here’s a different way to program. A: Fuck you that’s absurd. https://twitter.com/PttPrgrmmr/status/1006431230533959680 @PttPrgrmmr

As for the Drupal bit: I can't comment, as I don't remotely know how Drupal works, and I strive to stay independent from the tooling in order to keep the language close to the business and the upgrades or migrations manageable. My tip: it shouldn't matter if it is in the context of Drupal or not.

Guikingone commented 6 years ago

If I can give a position about this issue, here's what I'm thinking about.

Should Symfony have a position about model design?

No.

If we look at the main goal of Symfony, the main goal is to manage HTTP requests/response, what we do with this requests/response and all the logic which is involved between the request and response is up to us.

So, should Symfony know about our lower-level policies?

No.

Symfony isn't designed for this, even if the Security component use a User model, is core design isn't prepared to handle model, that up to us to include and call model, we can easily tweak the internal logic to use DTO, Value Object or even array, the choice is up to us.

I know that the debate is about rich vs anemic models but well, should Symfony care about this?

No.

Symfony can easily work without a single model and you know what ? That's probably the best idea we can have, our business logic isn't tied to Symfony, we use Symfony in order to have a "set of tools" that help us to build modern, reliable and evolutive applications, no more.

I'm fully committed to the vision of @Ocramius when it comes to the Form component, using DTO or value object is probably the best idea we can have, why ? Simply because depending on a Symfony component is bad, why ? Simply because the framework can evolve and the component can change, what if some functionality disappear and our logic no longer work? Symfony is a tool and we need to use it as it was designed by the core team, we could use some components but we're not forced to do so, I'm pretty happy with this vision, and you know why ? Simply because sometimes, I don't need all the framework logic to display some data or to handle some user action.

Same things go when it comes to API-Platform and what @dunglas has designed, we can use the internal library but what if we need to design our own logic and adapt API-Platform to our lower-level policies? Well, Dunglas and the core team do a pretty solid work and we can easily do it, we're not tied to his main vision when it comes to handling operations and other internal logic.

For me, Symfony shouldn't give a "sterile" and "final" vision, the developer should be in control of what he do when it comes to lower-level policies, the developer should be able to adapt his logic to the user need and no matter what it does with it, not a single library should say :

You can't do this, that not how we work !

So, what about the idea of bringing a rich model to the documentation ? I'm not fully okay for it, Symfony could easily say :

Here's a way to design your internal logic, keep in mind that you could adapt it to your needs.

Symfony doesn't care about our model logic/design and it shouldn't IMO but well, like said before in this thread, the final choice come to the developer, not Symfony.

donquixote commented 6 years ago

In "real world" apps, this is where the concepts of "read model" and "view model" come in. [..] all my forms work with arrays or DTOs these days.

It sounds interesting, but am not sure I am following. I assume that "read model", "view model" and "DTO" (data transfer object) are all different classes / objects that contain information about the entity (e.g. the user), just for different purposes, with different roles in the application. This would mean you have to write more than one class per entity type.

But perhaps I got this all wrong? Perhaps a DTO in your sense is not even specific to an entity type, and does not do any validation by itself? E.g. an \stdClass?

I could read about "read model" and "write model" and "DTO", but this does not guarantee that I get the same idea about it as you do.

Terrible idea to bind entities to forms.

Again I am not sure I understand what you mean. You have a form where a user can create or modify a blog post, or edit their user account. You need to load and save the entity, and map form elements to entity values, and you need to validate and normalize the data before it goes into the database.

Of course not every form maps 1:1 to an entity type, but for sure such forms do exist and are quite common.

The entity object itself should be agnostic about forms. But it still needs an API that allows a form to interact with it. Or, if you use a data transfer object, than this DTO needs an API for forms to interact with it, which only shifts the problem. Or, if the DTO is sth unstructured like \stdClass, then perhaps the entity class needs an API to interact with DTOs.

People might want to add mechanisms to load and save user objects.

Yes, Doctrine ORM is a mapper like any other! You should be able to write trivial reflection-based mappers for any kind of aggregate. The referenced repository includes some primitive implementations that are sufficient to save/load entities: https://github.com/ShittySoft/fwdays-2018-doctrine-tutorial/blob/feature/registration/src/Infrastructure/Authentication/Repository/FileUsers.php

This is interesting. The solution uses reflection to access private properties, so you don't need any getters. This looks like a hack to me. I am open to unorthodox solutions, but I would need some convincing why this is a good idea, e.g. why this is better than public properties.

I guess the idea is that only some selected and trusted classes are supposed to access those private properties. So you are emulating something like a "friend class" mechanic, except that without a native "friend class" feature, it is not documented which classes are allowed to access the private properties.

You are then using serialize/unserialize to store the object to disk, which again frees you of the need for setters and getters. I don't really like serialized objects, I think that the persistence model should be independent of the runtime model. Perhaps I am wrong.

People might want to inject services or behavior objects into the user object, which help with tasks like saving to the database, or for side effects of specific operations.

Please don't: dependency injection is only applicable when lifecycles are compatible. This is why it is usually only valid for long-lived instances (services).

I am not talking about dependency injection containers here, just about injecting services into a component which uses them, instead of letting this component access the global container.

We can assume the life cycle of an entity object to be shorter than that of a service. A service should not get an entity injected, but an entity can be injected with a service on construction time. Or, which is perhaps better, avoid having entities depend on services.

There are practical reasons why Drupal uses the global \Drupal::service(..) instead. https://github.com/drupal/drupal/blob/8.6.x/core/lib/Drupal/Core/Entity/Entity.php#L82

The advantage is that the entity does not carry services around. The disadvantage is that testing the entity requires a global \Drupal::service(..) to be wired up correctly.

I was asked for a clean code example: well here it is. [..] getters/setters can be easily avoided

Yes, this is what I am looking for. An example that eliminates the common reasons why people add getters and setters to their models.

I am not yet convinced that your example sufficiently shows this, but we may be getting there. I hope that my questions are somewhat helpful to others and to the general direction of this thread, and not just to entertain my own confusion.

Ocramius commented 6 years ago

TIL answering to a thread re-subscribes you to it.

Gonna try to keep this short:

In "real world" apps, this is where the concepts of "read model" and "view model" come in. [..] all my forms work with arrays or DTOs these days.

It sounds interesting, but am not sure I am following. I assume that "read model", "view model" and "DTO" (data transfer object) are all different classes / objects that contain information about the entity (e.g. the user), just for different purposes, with different roles in the application. This would mean you have to write more than one class per entity type.

A read model is an abstraction on top of a read-only data store. It could be a class containing an SQL query, for example.

But perhaps I got this all wrong? Perhaps a DTO in your sense is not even specific to an entity type, and does not do any validation by itself? E.g. an \stdClass?

Usually array only

I could read about "read model" and "write model" and "DTO", but this does not guarantee that I get the same idea about it as you do.

Terrible idea to bind entities to forms.

Again I am not sure I understand what you mean.

Simple: don't let forms talk to entities directly, as you are mutating state without checking business invariants. Just because a field called active can be bool, doesn't mean that all permutations of a checkbox are applicable. The example I do for Employee are interactions such as Employee#hire(), Employee#fire(), Employee#expireContract(), Employee#renewContract() Employee#suspend() and Employee#markAsLost(). These are NOT about slamming state from one layer (form) into the next one. The form abstracts an interaction into a payload of "validated" intent by the frontend user, not state to be copy-pasted. What sort of state mutations are caused by said payload is a completely different story that the form should not know about.

You have a form where a user can create or modify a blog post, or edit their user account. You need to load and save the entity, and map form elements to entity values, and you need to validate and normalize the data before it goes into the database

Mutating a blogpost is not an atomic operation. You can:

Some of these can succeed, some of these can fail depending on what you enforce as "valid" inside your concept of BlogPost (which is a terrible example BTW, because it's the typical developer over-simplification of business domains).

The above ones are either 7 different forms or 7 different interactions to be piped in a queue of operations to be executed all at once (UX decision).

Don't represent state in the UI and then slam it back into the entities à la foie gras, because you end up duplicating biz constraints everywhere. The single "migrate the URI" scenario is already a nightmare once anemic models are in play. Also, do all other scenarios lead to an immediately published post?

Of course not every form maps 1:1 to an entity type, but for sure such forms do exist and are quite common.

The entity object itself should be agnostic about forms. But it still needs an API that allows a form to interact with it.

No, it doesn't - it is told what should happen once the interaction is translated from a form into a method call (Employee#renewContract(... data from the form here ...), for example).

Or, if you use a data transfer object, than this DTO needs an API for forms to interact with it, which only shifts the problem. Or, if the DTO is sth unstructured like \stdClass, then perhaps the entity class needs an API to interact with DTOs.

The only shifts the problem bit is what I think isn't getting through: state should not be mutated outside the entity, but should be owned by the entity, and mutations should be controlled. We are removing state mutation problems that lead to extremely complex head-scratchers later on (Employee#fire(), Employee#renewContract() and BlogPost#migrateToUri() are simple interactions that become extremely complex once you let forms talk to entities). The translation of HTTP-to-domain-call is what you'd usually put in a controller, here's some pseudo-code c&p'd from one of my other examples:

    public function purchaseAction()
    {
        $this->securityCheck->assertValidUser($this->request);

        if (! $this->form->isValid($this->request)) {
            return new ViewModel(['form' => $this->form]);
        }

        $data    = $this->form->getData();
        $user    = $this->authentication->requireUser();
        $product = $this->products->get($data['product']);

        // actual domain logic call. 'amount' was already validated
        $product->purchaseWithUser($user, $data['amount']);

        $this->notifications->send($user, 'Purchase completed');

        return new ViewModel(['success' => true]);
    }

People might want to add mechanisms to load and save user objects.

Yes, Doctrine ORM is a mapper like any other! You should be able to write trivial reflection-based mappers for any kind of aggregate. The referenced repository includes some primitive implementations that are sufficient to save/load entities: https://github.com/ShittySoft/fwdays-2018-doctrine-tutorial/blob/feature/registration/src/Infrastructure/Authentication/Repository/FileUsers.php

This is interesting. The solution uses reflection to access private properties, so you don't need any getters. This looks like a hack to me. I am open to unorthodox solutions, but I would need some convincing why this is a good idea, e.g. why this is better than public properties.

It doesn't matter if you serialize it, json-encode it, gRPC it, transform it to YAML and back (good luck) and then finally push it through memcached served through MySQL. Reflection is fine here, we are making a snapshot of an object, freezing it in time and then re-constructing it exactly like it was before. As soon as we change the given entity data, we are making things extremely complex for everyone, which is why things like entity listeners and SQL triggers that modify entity data are a big no-no.

Mapping is not an OO concept, but an FP and procedural concept (when persistence is involved). Here we are effectively copying data (very important -> WITHOUT MUTATING IT <- important, really, I MEAN IT!) from somewhere to somewhere. As long as it's just about copying, you can even use a dump of the low-level PHP process to achieve that: that's not important.

I guess the idea is that only some selected and trusted classes are supposed to access those private properties. So you are emulating something like a "friend class" mechanic, except that without a native "friend class" feature, it is not documented which classes are allowed to access the private properties.

Just use reflection there.

You are then using serialize/unserialize to store the object to disk, which again frees you of the need for setters and getters. I don't really like serialized objects, I think that the persistence model should be independent of the runtime model. Perhaps I am wrong.

Yes, it's an example aimed at showing that persistence can be easily achieved, and that "thinking in tables" is easily avoided. People should use whichever persistence layer fits their needs best, and not take Doctrine ORM and its semantics for granted.

People might want to inject services or behavior objects into the user object, which help with tasks like saving to the database, or for side effects of specific operations. Please don't: dependency injection is only applicable when lifecycles are compatible. This is why it is usually only valid for long-lived instances (services). I am not talking about dependency injection containers here, just about injecting services into a component which uses them, instead of letting this component access the global container.

injecting services into a component which uses them is dependency injection. No containers (DIC) involved. Do not retain dependencies inside objects with different scopes. Services should be given at call-time and then discarded.

About the \Drupal::service(): that is properly horrible, but it takes time to migrate away from it. Magento 2 is an example of a complex piece of software that managed to migrate away from such patterns: yes, some of their constructors are terrible, but still better than the implicit service location happening everywhere at runtime

carsonbot commented 3 years ago

Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?

wouterj commented 3 years ago

Seems like we've removed all mentions of doctrine:generate:entities already. Let's close this issue.