i know this has been asked over and over again, i read the topics, but it's always focused on specific cases and i generally try to understand why its not best practise to use a service inside an entity.
Given a very simple service :
Class Age
{
private $date1;
private $date2;
private $format;
const ym = "%y years and %m month"
const ...
// some DateTime()->diff() methods, checking, formating the entry formats, returning different period formats for eg.
}
and a simple entity :
Class People
{
private $firstname;
private $lastname;
private $birthday;
}
From a controller, i want to do :
$som1 = new People('Paul', 'Smith', '1970-01-01');
$som1->getAge();
Off course i can rewrite the getAge()
function inside my entity, its not long, but im very lazy and as i've already written all the possible datetime->diff() i need in the above service, i dont understand why i shouldnt use'em...
NB : my question isnt about how to inject the container in my entity, i can understand why this doesnt make sense, but more what wld be the best practise to avoid to rewrite the same function in different entities.
Inheritance seems to be a bad "good idea" as i could use the getAge() inside a class BlogArticle and i doubt that this BlogArticle Class should be inheriting from the same class as a People class...
Hope i was clear, but not sure...
You already mentioned a very good point. Instances of class
Person
are not the only thing that can have an age.BlogArticle
s can also age along with many other types. If you're using PHP 5.4+ you can utilize traits to add little pieces of functionality instead of having service objects from the container (or maybe you can combine them).Here is a quick mockup of what you could do to make it very flexible. This is the basic idea:
Aging
)$birthdate
,$createdDate
, ...)Generic
For person
For blog article
Now you can ask any instance of the above classes for their age.
Finally
If you need type hinting traits won't do you any favors. In that case you will need to provide an interface and let every class that uses the trait implement it (the actual implementation is the trait but the interface enables type hinting).
This may seem like a lot of hassle when in reality it's much easier to maintain and extend this way.
A big part is that there is no easy way to inject dependencies when using the database.
One solution might be to pass the age service as an argument.
But really, you would probably be better off just adding the age stuff to your Person class. Easier to test and all that.
It sounds like you might have some output formatting going on? That sort of thing should probably be done in twig. getAge should really just return a number.
Likewise, your date of birth really should be a date object and not a string.
One major confusion for many coders is to think that doctrine entities "are" the model. That is a mistake.
Injecting services into your doctrine entities is a symptom of "trying to do more things than storing data" into your entities. When you see that "anti-pattern" most probably you are violating the "Single Responsability" principle in SOLID programming.
Symfony is not an
MVC
framework, it is aVC
framework only. Lacks theM
part. Doctine entities (I'll call them entities from now on, see clarification at the end) are a "data persistence layer", not a "model layer". SF has lots of things for views, web controllers, command controllers... but has no help for domain modelling ( http://en.wikipedia.org/wiki/Domain_model ) - even the persistence layer is Doctrine, not Symfony.Overcoming the problem in SF2
When you "need" services in a data-layer, trigger an antipattern alert. Storage should be only a "put here - get from there" system. Nothing else.
To overcome this problem, you should inject the services into a "logic layer" (Model) and separate it from "pure storage" (data-persistence layer). Following the single responsibility principle, put the logics in one side, put the getters and setters to mysql in another.
The solution is to create the missing
Model
layer, not present in Symfony2, and make it to give "the logic" of the domain objects, completely separated and decoupled from the layer of data-persistence which knows "how to store" the model into a mysql database with doctrine, or to a redis, or simply to a text file.All those storage systems should be interchangeable and your
Model
should still expose the very same public methods with absolutely no change to the consumer.Here's how you do it:
Step 1: Separate the model from the data-persistence
To do so, in your bundle, you can create another directory named
Model
at the bundle-root level (besidestests
,DependencyInjection
and so), as in this example of a game.Model
is not mandatory, Symfony does not say anything about it. You can choose whatever you want.ModelBundle
, providing logical concepts likeBoard
,Piece
orTile
among many others, structures in directories for clarity.Particularly for your question
In your example, you could have:
Entity/People.php
- Ex: suppose you want to store the birthdate both in a date-time field, as well as in three redundant fields: year, month, day, because of any tricky things related to search or indexing, that are not domain-related (ie not related withe lo 'logics' of a person).Model/People.php
- Ex: how to calculate if a person is over the majority of age just now, given a certain birthdate and the country he lives (which will determine the minumum age). As you can see, this has nothing to do on the persistence.Step 2: Use factories
Then, you must remember that the consumers of the model, should never ever create model objects using "new". They should use a factory instead, that will setup the model objects properly (will bind to the proper data-storage layer). The only exception is in unit-testing (we'll see it later). But apart from unitary tests, grab this with fire in your brain, and tattoo it with a laser in your retina: Never do a 'new' in a controller or a command. Use the factories instead ;)
To do so, you create a service that acts as the "getter" of your model. You create the getter as a factory accessible thru a service. See the image:
You can see a BoardManager.php there. It is the factory. It acts as the main getter for anything related to boards. In this case, the BoardManager has methods like the following:
ObjectStorageManager
into theBoardManager
. TheObjectStorageManager
is, for this example, able to store and load objects from a database or from a file; while theBoardManager
is storage agnostic.ObjectStorageManager
in the image, which in turn is injected the @doctrine to be able to access themysql
.new
is allowed. Never in a controller or command.Particularly for your question
In your example, you would have a
PeopleManager
in the model, able to get the people objects as you need.Also in the Model, you should use the proper singular-plural names, as this is decoupled from your data-persistence layer. Seems you are currently using
People
to represent a singlePerson
- this can be because you are currently (wrongly) matching the model to the database table name.So, involved model classes will be:
For example (pseudocode! using C++ notation to indicate the return type):
So, continuing with your example, when you do...
...you now can mutate to:
The PeopleManager will do the
new
for you.At this point, you variable
$som1
of typePerson
, as it was created by the factory, can be pre-populated with the necessary mechanics to store and save to persistence layer.The
myproject.people.manager
will be defined in your services.yml and will have access to the doctrine either directly, either via a 'myproject.persistence.manager` layer or whatever.Note: This injection of the persistence layer via the manager, has several side effects, that would side track from "how to make the model have access to services". See steps 4 and 5 for that.
Step 3: Inject the services you need via the factory.
Now you can inject any services you need into the people.manager
You, if your model object needs to access that service, you have now 2 choices:
In this example, we provide the PeopleManager with the service to be consumed by the model. When the people manager is requested a new model object, it injects the service needed to it in the
new
sentence, so the model object can access the external service directly.In this example, we provide the PeopleManager with the service to be consumed by the model. Nevertheless, when the people manager is requested a new model object, it injects itself to the object created, so the model object can access the external service via the manager, which then hides the API, so if ever the external service changes the API, the manager can do the proper conversions for all the consumers in the model.
Step 4: Throw events for everything
At this point, you can use any service in any model. Seems all is done.
Nevertheless, when you implement it, you will find problems at decoupling the model with the entity, if you want a truly SOLID pattern. This also applies to decoupling this model from other parts of the model.
The problem clearly arises at places like "when to do a flush()" or "when to decide if something must be saved or left to be saved later" (specially in long-living PHP processes), as well as the problematic changes in case the doctrine changes its API and things like this.
But is also true when you want to test a Person without testing its House, but the House must "monitor" if the Person changes its name to change the name in the mailbox. This is specially try for long-living processes.
The solution to this is to use the observer pattern ( http://en.wikipedia.org/wiki/Observer_pattern ) so your model objects throw events nearly for anything and an observer decides to cache data to RAM, to fill data or to store data to the disk.
This strongly enhances the solid/closed principle. You should never change your model if the thing you change is not domain-related. For example adding a new way of storing to a new type of database, should require zero edition on your model classes.
You can see an example of this in the following image. In it, I highlight a bundle named "TurnBasedBundle" that is like the core functionality for every game that is turn-based, despite if it has a board or not. You can see that the bundle only has Model and Tests.
Every game has a ruleset, players, and during the game, the players express the desires of what they want to do.
In the
Game
object, the instantiators will add the ruleset (poker? chess? tic-tac-toe?). Caution: what if the ruleset I want to load does not exist?When initializing, someone (maybe the /start controller) will add players. Caution: what if the game is 2-players and I add three?
And during the game the controller that receives the players movements will add desires (for example, if playing chess, "the player wants to move queen to this tile" -which may be a valid, or not-.
In the picture you can see those 3 actions under control thanks to the events.
Then you can see that for any action that modifies the state of the model (for example adding a player), I have 2 events:
PRE
andPOST
. See how it works:PRE
event is raised.PRE
events should come with a cancel passed by reference. So if someone decides this is a game for 2 players and you try to add a 3rd one, the $cancel will be set to true.POST
event to indicate the observers that the operation has been completed.In the picture you see three, but of course it has a lot lot more. As a rule of thumb, you will have nearly 2 events per setter, 2 events per method that can modify the state of the model and 1 event for each "unavoidable" action. So if you have 10 methods on a class that operate on it, you can expect to have about 15 or 20 events.
You can easily see this in the typical simple text box of any graphyc library of any operating system: Typical events will be: gotFocus, lostFocus, keyPress, keyDown, keyUp, mouseDown, mouseMove, etc...
Particularly, in your example
The Person will have something like preChangeAge, postChangeAge, preChangeName, postChangeName, preChangeLastName, postChangeLastName, in case you have setters for each of them.
For long-living actions like "person, do walk for 10 seconds" you maybe have 3: preStartWalking, postStartWalking, postStopWalking (in case a stop of 10 seconds cannot be programatically prevented).
If you want to simplify, you can have two single
preChanged( $what, & $cancel )
andpostChanged( $what )
events for everything.If you never prevent your changes to happen, you can even just have one single event
changed()
for all and any change to your model. Then your entity will just "copy" the model properties in the entity properties at every change. This is OK for simple classes and projects or for structures you are not going to publish for third-party consumers, and saves some coding. If the model class becomes a core class to your project, spending a bit of time adding all the events list will save you time in the future.Step 5: Catch the events from the data layer.
It is at this point that your data-layer bundle enters in action!!!
Make your data layer an oberver of your model. When the model Changes its internal state then make your Entity to "copy" that state into the entity state.
In this case, the MVC acts as expected: The Controller, operates on the Model. The consequences of this are still hidden from the controller (as the controller shoult not have access to Doctrine). The model "broadcasts" the operation made, so anyone interested knows, which in turn triggers that the data-layer knows about the model change.
Particularly, in your project
The
Model/Person
object will have been created by thePeopleManager
. When creating it, thePeopleManager
, which is a service, and therefore can have other services injected, can have theObjectStorageManager
subsystem handy. So thePeopleManager
can get theEntity/People
that you reference in your question and add theEntity/People
as an observer toModel/Person
.In the
Entity/People
mainly you substitute all the setters by event catchers.You read your code like this: When the
Model/Person
changes its LastName, theEntity/People
will be notified and will copy the data into its internal structure.Most probably, you are tempted to inject the entity inside the model, so instead of throwing an event, you call the setters of the Entity.
But with that approach, you 'break' the Open-Closed principle. So if at any given point you want to migrate to MongoDb, you need to "change" your "entities" by "documents" in your model. With the observer-pattern, this change occurs outside the model, who never knows the nature of the observer beyond that is its a PersonObserver.
Step 6: Unit test everything
Finally, you want to unit test your software. As this pattern I have explained overcomes the anti-pattern that you discovered, you can (and you should) unit-test the logics of your model independently of how that is stored.
Following this pattern, helps you to go towards the SOLID principles, so each "unit of code" is independent on the others. This will allow you to create unit-tests that will test the "logics" of your
Model
without writing to the database, as it will inject a fake data-storage layer as a test-double.Let me use the game example again. I show you in the image the Game test. Assume all games can last several days and the starting datetime is stored in the database. We in the example currently test only if getStartDate() returns a dateTime object.
There are some arrows in it, that represent the flow.
In this example, from the two injecting strategies I told you, I choose the first one: To inject into the
Game
model object the services it needs (in this case aBoardManager
,PieceManager
andObjectStorageManager
) and not to inject theGameManager
itself.ObjectStorageManager
.new
command, not via a manager. Do you remember that I said that tests were an exception? If you use the manager (you still can) here it is not a unit-test, it's an integration test because tests two classes: the manager and the game. In thenew
command we fake all the dependencies that the model has (like a board manager, and like a piece manager). I am hardcoding GameId = 1 here. This relates to data-persistance, see below.Game
model object) to test its internals.I am hardcoding "Game id = 1" in the
new
. In this case we are only testing that the returned type is a DateTime object. But in case we want to test also that the date that it gets is the proper one, we can "tune" the ObjectStorageManager (data-persistance layer) mock to return whatever we want in the internal call, so we could test that for example when I request the date to the data-layer for game=1 the date is 1st-jun-2014 and for game=2 the date is 2nd-jun-2014. Then in the testGetStartDate I would create 2 new instances, with Ids 1 and 2 and check the content of the result.Particularly, in your project
You will have a
Test/Model/PersonTest
unit test that will be able to play with the logics of the person, and in case of needing a person from the database, you will fake it thru the mock.In case you want to test the storing of the person to the database, it is enough that you unit-test that the event is thrown, no matter who listens to it. You can create a fake listener, attach to the event, and when the
postChangeAge
happens mark a flag and do nothing (no real database storage). Then you assert that the flag is set.In short:
Model
that has nothing to do with entities, and put all the logics in it.new
to get your models from any consumer. Use factory services instead. Special attention to avoid news in controllers and commands. Execption: The unit-test is the only consumer that can use anew
Seems a lot of work. But it is not. It is a matter of getting used to it. Just think about the "objects" you need, create them and make the data-layer be "monitors" of your objects. Then your objects are free to run, decoupled. If you create the model from a factory, inject any needed service in the model to the model, and leave the data alone.
Edit apr/2016
All occurences of the word entity in this answer are referring to the "doctrine entities" which is what causes confusion to the majority of coders, between the model layer and the persistance layer which should be always different.
DDD
building blocks makes a need to clarify even more my answer, asDDD
uses the wordEntity
within the model too.Domain entities
(notDoctrine entities
) are similar to what I refer in this answer toDomain objects
.Domain objects
:Domain entities
(different from theDoctrine entites
).Domain value objects
(could be thought similar to basic types, with logic).Domain events
(also distinct from thoseSymfony events
and also different from theDoctrine events
).Domain commands
(different from thoseSymfony command line
controller-like helpers).Domain services
(different from theSymfony framework services
).Hope to help.
Xavi.
You are right, it's generally discouraged. However, there are several approaches how you can extend the functionality of an entity beyond the purpose of a data container. Of course, all of them can be considered (more or less) bad practice … but somehow you gotta do the job, right?
You can indeed create an
AbstractEntity
super class, from which all other entities inherit. This AbstractEntity would contain helper methods that other entities may need.You can work with custom Doctrine repositories, if you need an entity context to work with an entity manager and return “more special” results than what the common getters would give you. As you have access to the entity manager in a repository, you can perform all kinds of special queries.
You can write a service that is in charge of the entity/entities in question. Downside: you cannot control that other parts of your code (or other developers) know of this service. Advantage: There's no limit to what you can do, and it's all nicely encapsuled.
You can work with Lifecycle Events/Callbacks.
If you really need to inject services into entities, you could consider setting a static property on the entity and only set it once in a controller or a dedicated service. Then you don't need to take care on each initialization of an object. Could be combined with the AbstractEntity approach.
As mentioned before, all of these are have their advantages and disadvantages. Pick your poison.