Sf2: using a service inside an object

I know that this was asked over and over again, I read topics, but he always focused on specific cases, and I generally try to understand why it is not recommended to use the service inside the object.

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

 Class People { private $firstname; private $lastname; private $birthday; } 

From the controller I want to do:

 $som1 = new People('Paul', 'Smith', '1970-01-01'); $som1->getAge(); 

Out of the course, I can rewrite the getAge() function inside my entity, it is not long, but I am very lazy, and since I already wrote all the possible datetime-> diff () that I need in the above service, I can not understand why I shouldn't use it ...

NB: my question is not how to inject a container into my entity, I can understand why this does not make sense, but more than what would be best practice to avoid overwriting the same function in different entities.

Inheritance seems like a bad "good idea" because I could use getAge () inside the BlogArticle class, and I doubt that this BlogArticle class should inherit from the same class as the People class ...

Hope I was clear, but not sure ...

+13
symfony service entities
Aug 26 '14 at 21:30
source share
4 answers

One big confusion for many programmers is to think that the essence of the doctrine "is" a model. This is mistake

  • See Editing this post at the end, including ideas related to CQRS + ES -

Embedding services in your entities doctrines is a symptom of "trying to do more than store data" in your entities. When you see this “anti-pattern”, most likely you are violating the “One Responsibility” principle in SOLID programming.

Symfony is not an MVC environment, but only a VC environment. Part M Essence of the doctrine is missing (I will call them entities later on; see the explanation at the end) - this is the “level of persistence of data” and not the “level of model”. There are many things in SF for views, web controllers, command controllers ... but it doesn’t help with domain modeling ( http://en.wikipedia.org/wiki/Domain_model ) - even the constant level is Doctrine, not Symfony .

Overcome the problem in SF2

When you "need" services at the data level, run an antipattern warning. The repository should only be a "put here - get from there" system. Nothing more.

To overcome this problem, you must implement the services in the “logical level” (model) and separate it from the “clean storage” (data storage level). Following the principle of single responsibility, put the logic in one direction, and getters and setters in mysql.

The solution is to create a missing Model layer that is missing in Symfony2 and make it “logical” for domain objects that are completely separate and separate from the data storage layer, which knows how to store the model in a mysql database with a doctrine, or with redis, or just with a text file.

All of these storage systems should be interchangeable, and your Model should still provide the same public methods without any change to the consumer.

Here's how you do it:

Step 1: Separate the Model from Data Persistence

To do this, in your bundle you can create another directory named Model at the root level of the package (in addition to tests , DependencyInjection , etc.), as in this example game.

Model should be a separated layer different from the persistence layer

  • The name Model is optional, Symfony says nothing about it. You can choose everything you want.
  • If your project is simple (say, one package), you can create this directory inside one package.
  • If your project has many packages, you can consider
    • either placing the model in different bundles, or
    • or -as in the image- example, use a ModelBundle that contains all the "objects" that the project needs (without interfaces, controllers, commands, only the game logic and its tests). In this example, you see a ModelBundle providing logical concepts such as Board , Piece or Tile and many other directory structures for clarity.

Especially for your question

In your example, you could have:

 Entity/People.php Model/People.php 
  • Everything related to the "store" should go inside Entity/People.php - Example: suppose you want to save the date of birth both in the date-time field and in three redundant fields: year, month, day, from- for any tricks, things related to search or indexing that are not related to the domain (that is, are not related to the "logic" of a person).
  • Everything related to “logic” should go inside Model/People.php - Example: how to calculate whether Model/People.php is a person of legal age only now, given the specific date of birth and the country in which he lives (which will determine the minimum age), as you can see, this has nothing to do with constancy.

Step 2: use factories

Then you must remember that consumers of the model should never create model objects using the "new" ones. Instead, they should use a factory that correctly configures the model objects (will link to the appropriate data storage layer). The only exception is unit testing (we will see this later). But besides unitary tests, take it with a fire in your brain and get a tattoo with a laser on the retina: never do the “new” in the controller or team. Use factories instead;)

To do this, you create a service that acts as the “recipient” of your model. You create a getter as a factory accessible through the service. See image:

Use a service as a factory to get your model

You can see BoardManager.php there. This is a factory. He acts as the main earner of everything connected with the boards. In this case, BoardManager has the following methods:

 public function createBoardFromScratch( $width, $height ) public function loadBoardFromJson( $document ) public function loadBoardFromTemplate( $boardTemplate ) public function cloneBoard( $referenceBoard ) 
  • Then, as you see in the image, in services.yml you define this manager and inject a persistence layer into it. In this case, you are an ObjectStorageManager in the BoardManager . ObjectStorageManager , in this example, can store and load objects from a database or from a file; while BoardManager is storage dependent.
  • You can also see the ObjectStorageManager in the image, which in turn enters @doctrine to access mysql .
  • Your managers are the only place new allowed. Never in a controller or team.

Especially for your question

In your example, you will have the PeopleManager in the model, capable of receiving people objects as you need.

Also in the model, you must use the correct names in the singular and plural, as they are separate from your level of data persistence. It seems you are using People to represent one Person - this may be because you (erroneously) map the model to the name of the database table.

So, the involved model classes will be:

 PeopleManager -> the factory People -> A collection of persons. Person -> A single person. 

For example (pseudo code! Using C ++ notation to indicate the type of return value):

 PeopleManager { // Examples of getting single objects: Person getPersonById( $personId ); -> Load it from somewhere (mysql, redis, mongo, file...) Person ClonePerson( $referencePerson ); -> Maybe you need or not, depending on the nature the your problem that your program solves. Person CreatePersonFromScratch( $name, $lastName, $birthDate ); -> returns a properly initialized person. // Examples of getting collections of objects: People getPeopleByTown( $townId ); -> returns a collection of people that lives in the given town. } People implements ArrayObject { // You could overload assignment, so you can throw an exception if any non-person object is added, so you can always rely on that People contains only Person objects. } Person { private $firstname; private $lastname; private $birthday; } 

So, continuing your example when you do ...

 // **Never ever** do a new from a controller!!! $som1 = new People('Paul', 'Smith', '1970-01-01'); $som1->getAge(); 

... now you can change:

 // Use factory services instead: $peopleManager = $this->get( 'myproject.people.manager' ); $som1 = $peopleManager->createPersonFromScratch( 'Paul', 'Smith', '1970-01-01' ); $som1->getAge(); 

PeopleManager will do new for you.

At this stage, your variable $som1 type Person , as it was created at the factory, can be pre-filled with the necessary mechanics to save and save in the persistence layer.

myproject.people.manager will be defined in your services.yml and will have access to the doctrine either directly, either through the layer "myproject.persistence.manager", or whatever.

Note. This injection of persistence through the manager has several side effects that can go from "how to make the model accessible to services." See steps 4 and 5 for this.

Step 3: Enter the services you need through the factory.

Now you can add any services you need to people.manager

You, if your model object should access this service, you have 2 options:

  • When the factory creates the model object (that is, when the PeopleManager creates the Person) to embed it through the constructor or setter.
  • Proxy the function in the PeopleManager and implement the PeopleManager through the constructor or installer.

In this example, we provide the PeopleManager service that the model will use. When a new model object is requested from the personnel manager, he adds the service he needs in the new sentence so that the model object can directly access the external service.

 // Example of injecting the low-level service. class PeopleManager { private $externalService = null; class PeopleManager( ServiceType $externalService ) { $this->externalService = $externalService; } public function CreatePersonFromScratch() { $externalService = $this->externalService; $p = new Person( $externalService ); } } class Person { private $externalService = null; class Person( ServiceType $externalService ) { $this->externalService = $externalService; } public function ConsumeTheService() { $this->externalService->nativeCall(); // Use the external API. } } // Using it. $peopleManager = $this->get( 'myproject.people.manager' ); $person = $peopleManager->createPersonFromScratch(); $person->consumeTheService() 

In this example, we provide the PeopleManager service that the model will use. However, when the personnel manager is requested a new model object, he embeds himself in the created object, so the model object can access the external service through the manager, which then hides the API, so if the external service ever changes the API, the manager can do the right transformations for all consumers in the model.

 // Second example. Using the manager as a proxy. class PeopleManager { private $externalService = null; class PeopleManager( ServiceType $externalService ) { $this->externalService = $externalService; } public function createPersonFromScratch() { $externalService = $this->externalService; $p = new Person( $externalService); } public function wrapperCall() { return $this->externalService->nativeCall(); } } class Person { private $peopleManager = null; class Person( PeopleManager $peopleManager ) { $this->peopleManager = $peopleManager ; } public function ConsumeTheService() { $this->peopleManager->wrapperCall(); // Use the manager to call the external API. } } // Using it. $peopleManager = $this->get( 'myproject.people.manager' ); $person = $peopleManager->createPersonFromScratch(); $person->ConsumeTheService() 

Step 4: Throw Events for Everything

At the moment, you can use any service in any model. Everything seems to be done.

However, when you implement it, you will find problems in disconnecting the model from the entity if you really need a SOLID template. This also applies to separating this model from other parts of the model.

The problem clearly arises in places like “when to do flush ()” or “when to decide whether to save something or leave it for later saving” (especially in long-lived PHP processes), as well as in problematic changes in case the doctrine changes its API and the like.

But this is also true when you want to check the Person without checking his House, but the House must “track” whether the Person changes his name in order to change the name in the mailbox. This is especially for long-lived processes.

The solution to this problem is to use the observer pattern ( http://en.wikipedia.org/wiki/Observer_pattern ) so that the objects in your model generate events for almost anything, and the observer decides to cache the data in RAM, fill in the data or save the data to disk.

This greatly enhances the solid / closed principle. You should never change your model if the thing you are changing is not domain related. For example, adding a new storage method to a new type of database should require zero revision in your model classes.

You can see an example of this in the following image. In it, I highlight a package called "TurnBasedBundle", which is like the basic functionality for each game based on a turn-based game, regardless of whether it has a board or not. You can see that only Model and Tests are included.

Each game has a set of rules, players, and during the game, players express a desire for what they want to do.

In the Game object, instances add a rule set (poker "chess" tic-tac-toe "). Warning: what if the rule set that I want to load does not exist?

Upon initialization, someone (possibly the / start controller) will add players . Caution: what if 2 players participate in the game, and I add three?

And during the game, the controller, which receives the movements of the players, will add desires (for example, if he plays chess, "the player wants to move the queen to this tile" -which may be valid, or not-.

In the picture you can see these 3 actions under control thanks to events.

Example of throwing events from the model for virtually everything that can happen

  • You may notice that only Model and Tests are included.
  • In the model, we define our 2 objects: Game and GameManager to get instances of Game objects.
  • We also define interfaces, such as GameObserver, so anyone who wants to receive Game events must be a GameObserver folk.
  • Then you can see that for any action that changes the state of the model (for example, adding a player), I have 2 events: PRE and POST . See how it works:

    1. Someone calls the $ game-> addPlayer ($ player) method.
    2. As soon as we enter the addPlayer () function, the PRE event is raised.
    3. Observers can then catch this event to decide if a player can be added or not.
    4. All PRE events must be accompanied by a cancellation passed by reference. Therefore, if someone decides that this is a game for 2 players and you try to add a 3rd, $ cancel will be set to true.
    5. Then you are again inside the addPlayer function. You can check if someone wanted to cancel the operation.
    6. Perform the operation, if allowed (i.e.: change the state of $ this->).
    7. After the POST state changes, the POST event is used to indicate to the observers that the operation was completed.

In the picture you see three, but, of course, there are many more. As a rule, you will have about 2 events per setter, 2 events per method that can change the state of the model, and 1 event for each “inevitable” action. So if you have 10 methods for a class that work with it, you can expect about 15 or 20 events.

You can easily see this in a typical plain text field of any graphics library of any operating system: Typical events are: gotFocus, lostFocus, keyPress, keyDown, keyUp, mouseDown, mouseMove, etc.

In particular, in your example

The person will have something like preChangeAge, postChangeAge, preChangeName, postChangeName, preChangeLastName, postChangeLastName if you have installers for each of them.

For long-lived activities, such as “man, walk 10 seconds”, you can have 3: preStartWalking, postStartWalking, postStopWalking (in case a stop of 10 seconds cannot be prevented programmatically).

If you want to simplify, you can have two separate preChanged( $what, & $cancel ) and postChanged( $what ) for everything.

If you never hinder changes, you can even change changed() a single, changed() event for everyone and any changes in your model. Then your entity will simply “copy” the properties of the model in the properties of the entity with each change. This is normal for simple classes and projects, or for structures that you are not going to publish for third-party consumers, and saves some coding. If the model class becomes the main class for your project, spending a little time adding the entire list of events will save you time in the future.

Step 5: catch events from the data layer.

It is at this moment that your data packet begins to act !!!

Make your data layer an observer of your model. When the model changes its internal state, then make your entity “copy” this state into the state of the entity.

In this case, the MVC acts as expected: the controller works with the model. The consequences of this are still hidden from the controller (since the controller should not have access to Doctrine). The model "translates" the performed operation, so that everyone who is interested knows that, in turn, leads to the fact that the data layer is aware of the change in the model.

In particular, in your project

The Model/Person object will be created by the PeopleManager . When it is created, the PeopleManager , which is a service and, therefore, can include other services, can have an ObjectStorageManager subsystem under the ObjectStorageManager . This way, PeopleManager can get the Entity/People that you indicated in your question and add that Entity/People as an observer to Model/Person .

In Entity/People you basically replace all setters with event traps.

You read your code as follows: When Model/Person changes its Surname, Entity/People will be notified and copy the data to the internal structure.

Most likely, you are tempted to implement the entity inside the model, so instead of calling an event, you call the installers of the entity.

But with this approach, you "violate" the Open-Closed principle. Therefore, if at some point you want to switch to MongoDb, you need to "change" your "entities" to "documents" in your model. With an observer pattern, this change occurs outside the model, which never knows the nature of the observer, except that it is a PersonObserver.

Step 6: Unit Testing Everything

Finally, you want to conduct unit testing of your software. Since this pattern, which I explained, overcomes the anti-pattern you discovered, you can (and should) modularly test the logic of your model no matter how it is stored.

Following this pattern helps you follow the principles of SOLID, so each "unit of code" is independent of the others. This will allow you to create unit tests that will check the "logic" of your Model without writing to the database, since it will introduce a fake data storage layer as test-double.

Let me use the example game again. I am showing you the game test in the picture. Suppose that all games can last several days, and the starting date and time are stored in a database. In this example, we test only if getStartDate () returns a dateTime object.

enter image description here

It has arrows that represent the flow.

, , : Game ( BoardManager , PieceManager ObjectStorageManager ), GameManager .

  1. -, phpunit, Tests, , XxxTest. textSomething().
  2. setup().
  3. -, " " , . , ObjectStorageManager .
  4. ...
  5. ... GameTest...
  6. ... .
  7. $ sut ( ) new , . , , ? ( ), -, , : . new , ( ). GameId = 1 . , . .
  8. ( Game ) .

"Game id = 1" new . , DateTime. , , , , , "" ObjectStorageManager ( ), , , , , = 1, - 1 2014 , = 2 - 2 2014 . testGetStartDate 2 1 2 .

,

unit Test/Model/PersonTest , , .

, , -, , , . , , postChangeAge , ( ). , .

In short:

  1. . Model , , .
  2. new . . . : unit , new .
  3. , , , services.yml.
  4. . , . , . ? .
  5. , , , , , , "" , .
  6. - . .

, . But this is not so. . "", "" . , . -, .

Edit apr/2016 -

" ", , , .

  • - , .
  • . , , .
  • , DDD , DDD Entity .
  • Domain objects Domain entities ( Doctrine entities ) , Domain objects .
  • , Domain objects :
    • Domain entities ( Doctrine entites ).
    • Domain value objects ( )
    • Domain events ( Symfony events Doctrine events ).
    • Domain commands ( , Symfony command line ).
    • Domain services ( Symfony framework services ).
    • etc.
  • : " ", " ".

Edit /2019 - CQRS + ES

, (, ).

CQRS + ES ( + ) , " " , , . . , , , .

CQRS + ES 3 4 , 5 :

, . .

PRE, , " ". POST " , ".

CQRS , " " . , , , , , .

, "" " X". , , 80 /, 200 .

cancel , "" - .

POST "" , . : " , ", : .

So...

2014 "pre" " " CQRS + ES ( ), "post" " " CQRS + ES ( , , , ).

.

Xavi.

+70
Aug 27 '14 17:55
source share

. Person - , . BlogArticle . PHP 5.4+, , (, , ).

, , . :

  • ( Aging )
  • , ( $birthdate , $createdDate ,...)



Generic

 trait Aging { public function getAge() { return $this->calculate($this->start()); } public function calculate($startDate) { ... } } 

For man

 trait AgingPerson { use Aging; public function start() { return $this->birthDate; } } class Person { use AgingPerson; private $birthDate = '1999-01-01'; } 

 // Use for articles, pages, news items, ... trait AgingContent { use Aging; public function start() { return $this->createdDate; } } class BlogArticle { use AgingContent; private $createDate = '2014-01-01'; } 



.

 echo (new Person())->getAge(); echo (new BlogArticle())->getAge(); 



Finally

, . , , ( - , ).

 interface Ageable { public function getAge(); } class Person implements Ageable { ... } class BlogArticle implements Ageable { ... } function doSomethingWithAgeable(Ageable $object) { ... } 

, .

+2
26 . '14 23:34
source share

, .

 $person = $personRepository->find(1); // How to get the age service injected? 

.

 $ageCalculator = $container('age_service'); $person = $personRepository->find(1); $age = $person->calcAge($ageCalculator); 

, , Person. .

, ? , . getAge .

, , .

+1
26 . '14 22:45
source share

, . , . , ( ) ... - , ?

  • AbstractEntity , . AbstractEntity , .

  • Doctrine , " " , . , .

  • , /, . : , ( ) . : , , .

  • / .

  • , . . AbstractEntity.

, . .

0
26 . '14 22:39
source share



All Articles