Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
554 views
in Technique[技术] by (71.8m points)

symfony - Sf2 : using a service inside an entity

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...

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

One major confusion for many coders is to think that doctrine entities "are" the model. That is a mistake.

  • See edit of this post at the end, incorporating ideas related to CQRS+ES -

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 a VC framework only. Lacks the M part. Doctrine 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 (besides tests, DependencyInjection and so), as in this example of a game.

Model should be a separated layer different from the persistence layer

  • The name Model is not mandatory, Symfony does not say anything about it. You can choose whatever you want.
  • If your project is simple (say one bundle), you can create that directory inside the same bundle.
  • If your project is many bundles wide, you could consider
    • either putting the model splitted in the different bundles, or
    • or -as in the example image- use a ModelBundle that contains all the "objects" the project needs (no interfaces, no controllers, no commands, just the logic of the game, and its tests). In the example, you see a ModelBundle, providing logical concepts like Board, Piece or Tile among many others, structures in directories for clarity.

Particularly for your question

In your example, you could have:

Entity/People.php
Model/People.php
  • Anything related to "store" should go inside 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).
  • Anything related to the "logics" should go inside 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:

Use a service as a factory to get your model

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:

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 the services.yml you define that manager, and you inject the persistence layer into it. In this case, you inject the ObjectStorageManager into the BoardManager. The ObjectStorageManager is, for this example, able to store and load objects from a database or from a file; while the BoardManager is storage agnostic.
  • You can see also the ObjectStorageManager in the image, which in turn is injected the @doctrine to be able to access the mysql.
  • Your managers are the only place where a 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 single Person - this can be because you are currently (wrongly) matching the model to the database table name.

So, involved model classes will be:

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

For example (pseudocode! using C++ notation to indicate the return type):

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 with your example, when you do...

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

...you now can mutate to:

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

The PeopleManager will do the newfor you.

At this point, your variable $som1 of type Person, as it was created by the factory, can be pre-populated with the necessary mechanics to store and save to the 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:

  • When the factory creates a model object, (ie when PeopleManager creates a Person) to inject it via either the constructor, either a setter.
  • Proxy the function in the PeopleManager and inject the PeopleManager thru the constructor or a setter.

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.

// 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 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.

// Second example. Using the manager as a proxy.
class PeopleManager
{
    private $externalService = null;

    class PeopleManager( Ser

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...