How should I process data definitions related to BusinessLogic (like state types) in Doctrine 2?

Suppose I have a Booking object and it has a state field that can be set to one of several values ​​- let it do this: NEW , ACCEPTED and REJECTED

I am looking for the “right” way to implement this. So far I have used this approach:

 class Booking { const STATUS_NEW = 0; const STATUS_ACCEPTED = 1; const STATUS_REJECTED = 2; protected $status = self::STATUS_ACTIVE; } 

And this works fine, but I'm really curious about how to behave, and I have a few problems with this approach:

  • It looks a lot like business logic hidden in an entity class - if an entity should be a POJO , then why would it matter what status it could be? So I could put it in a manager class, for example:

     class BookingManager { const STATUS_NEW = 0; const STATUS_ACCEPTED = 1; const STATUS_REJECTED = 2; public function setBookingStatus(Booking $b, $status) { } } 

    but it still does not help with the second problem:

  • It is difficult to reuse this data in the view, for example, take a branch - I will need to create a Twig extension to convert the number to the actual name:

     Status type: {{ booking.status }} Status name: {{ booking.status|statusName }}{# I don't like this approach #} Status name: {{ booking.getStatusName() }} {# This seems even worse #} 

    Therefore, I could add the getStatusName method to BookingManager , but I believe that it does not belong there. I could add the same method to the Booking class, and it will work fine, but again, the business logic. And now also the logic of representation is hidden in essence.

  • If any of my codes depend on MyVendor\MyBundle\Entity\Booking::STATUS_NEW , could this be a problem when unit testing the code? With calling static methods, the problem is obvious - I can’t mock the dependency. Is there any precedent where dependency on static constants can be a problem?

All I could think of would move all this logic to the service level and create a service like BookingStatusManager (let it ignore the fact that this might be mistaken for a subclass of EntityManager) - it might look like this:

 class BookingStatusManager { const STATUS_NEW = 0; const STATUS_ACCEPTED = 1; const STATUS_REJECTED = 2; public function getStatusName($code) { ... } } 

but now I need an extra class for each enum like property for each object, and this is a pain, and it still doesn't seem right - I need to reference the static values ​​from this class whenever I want to deal with Booking .

Please note that this is a general question, and not one specific example of Booking - it could be, for example, a BorderLength object with a lengthUnit field with possible MILES or KILOMETERS ; In addition, status transitions are not limited to what are called by users; they should be possible to execute from code.

What is the “right way” to solve this problem in your experience?

+7
php symfony doctrine2
source share
7 answers

Answers to your numbered questions

1 . Why don't you have business logic in your business model / objects?

Are these relationships and behavior, after all, one of the goals of object orientation? I believe that you may have misunderstood the concept of “ POJO (and I'm not talking about J standing in Java;)), the purpose of which was to prevent frameworks from invading your model, thereby restricting the model to context specific for structure, and makes unit testing or reuse difficult in any other context.

What you're talking about is more like DTOs , which is usually not the one your model should consist of.

2 : Yes, Twig doesn't manipulate numbers very well, which symbolizes meaning. You will probably get all kinds of offers based on things like storage optimization (number of bytes) or database traffic / query time, but for most projects I prefer the priority of human experience - i.e. do not optimize for computers if you do not need it.

Thus, my personal preferences (in most cases) are instantly decryptable fields "enum", but in a key form instead of ordinary words. For example, "status.accepted" as opposed to 1 or "Accepted" . Key-like notations lend themselves well to i18n, using the twing |trans filter, {% trans %} tag or something similar.

3 . Static "enum" links within your model are rarely a problem when unit testing the model itself.

At some point, your model should define its semantics anyway, using the building blocks you have available. Although the ability to abstract from implementations (in particular, services) is useful, the ability to abstract from meaning is rarely (never?) Fruitful. Which reminds me of this story. Do not go there.: - D

If you are still concerned about this, put the constants in an interface that implements the model class; then your tests can only refer to the interface.

Recommended Solution

Model Alternative 1:

 class Booking { const STATUS_NEW = 'status.new'; const STATUS_ACCEPTED = 'status.accepted'; const STATUS_REJECTED = 'status.rejected'; protected $status = self::STATUS_NEW; } 

Model Alternative 2:

 interface BookingInterface { const STATUS_NEW = 'status.new'; const STATUS_ACCEPTED = 'status.accepted'; const STATUS_REJECTED = 'status.rejected'; // ...and possibly methods that you want to expose in the interface. } class Booking implements BookingInterface { protected $status = self::STATUS_NEW; } 

Twig:

 Status name: {{ ("booking."~booking.status)|trans({}, 'mybundle') }} 

(Of course, the booking. Prefix is ​​optional and depends on how you want to structure your keys and i18n files.)

Resources / Translations / mybundle.en.yml:

 booking.status.new: New booking.status.accepted: Accepted booking.status.rejected: Rejected 

Constant-like-objects

In Tomdarkness, the proposal to include these constants in their own model class, I want to emphasize that this should be a solution for the business / domain, and not a question about technical preferences.

If you clearly anticipate the use cases for dynamically adding statuses (provided by users of the system), then, of course, the right choice is the model / entity class. But if the statuses are used for the internal state of the application, which is related to the actual code that you are writing (and therefore will not change until the code changes), you are better off using constants.

Entity constants make working with them much more difficult ("hm, how can I get the primary key of the" accepted ", again?"), It is not so easy to internationalize ("hm, I store possible locales as hard-coded properties in the ReserveStatus entity, or I I’m doing another OrderStatusI18nStrings (id, locale, value)? ") object, as well as the refactoring problem you came up with. In short: don't relearn - and good luck .; -)

+2
source share

I suggest you get rid of the constants and simply create the Many-To-One association on your Booking with the new BookingStatus . What for? Well, for a number of reasons:

  • Code editing is not required if you want to add a new reservation status. This is not only easier for developers, but also allows you to dynamically create statuses.
  • You can easily save additional status information in the new BookingStatus entity, for example, name. This information can also be updated without changing the code.
  • Allows external tools to understand different statuses. For example, you can use an external reporting tool directly in the database. He will not know what some integers mean, but he will be able to understand the many-to-one relationship.
+4
source share

I use a very simple approach. Example:

 class Offer extends Entity implements CrudEntityInterface { const STATUS_CANCELED = -1; const STATUS_IN_PROGRESS = 0; const STATUS_FINISHED = 1; const STATUS_CONFIRMED = 2; protected static $statuses_names = [ self::STATUS_CANCELED => 'canceled', self::STATUS_IN_PROGRESS => 'in_progress', self::STATUS_FINISHED => 'finished', self::STATUS_CONFIRMED => 'confirmed' ]; /** * @var integer * * @ORM\Column(name="status", type="integer") * @Assert\NotBlank */ protected $status = self::STATUS_IN_PROGRESS; public static function getStatuses() { return self::$statuses_names; } /** * Set status * * @param integer $status * @return Offer */ protected function setStatus($status) { if(!array_key_exists($status, self::$statuses_names)){ throw new \InvalidArgumentException('Status doesn\'t exist'); } $this->status = $status; return $this; } /** * Get status * * @return integer */ public function getStatus() { return $this->status; } public function getStatusName() { return self::$statuses_names[$this->status]; } } 

All display names are always translated to maintain separation from the model.

 {{ ('offer.statuses.'~offer.statusName)|trans }} 
+3
source share

While it's not the most elegant, you can get a little pragmatic and just use string keys in a fairly efficient, safe way:

 <?php class Booking { protected $statusMap = array( 0 => 'new', 1 => 'accepted', 2 => 'rejected' ); /** * A premature optimization, trading memory to reduce calls to * array_flip/array_search in a more naive implementation * * @var array */ protected $statusMapReverse; public function __construct(){ $this->statusMapReverse = array_flip($this->statusMap); $this->setStatus('new'); } /** * @ORM\Column(type="integer") */ protected $status; /** * @param string $status Valid values are 'new', 'accepted', and 'rejected' * * @throws InvalidBookingStatusException */ public function setStatus($status){ if (! in_array($status, $this->statusMap)){ throw new InvalidBookingStatusException(); } $this->status = $this->statusMapReverse[$status]; } /** * @return string */ public function getStatus(){ return $this->statusMap[$this->status]; } /** * @return integer */ public function getStatusCode(){ return $this->status; } /** * @return array */ public function getStatusMap(){ return $this->statusMap; } } 

I find that I use this template quite often when I need to model similar data like an enumeration. It has several nice features:

1) The state is saved as an integer in db.

2) The call code should never care about those (integer) values ​​if it does not want to.

3) The call code is protected from typos / invalid status as setStatus checks the lines (can only be checked at runtime, but hey)

4) Booking :: getStatusMap () simplifies the creation of selection blocks, etc.

You can expand setStatus to accept a string or an integer (status code) and still check if you want to set the status by code.

Disadvantages (compared to using constants) are mainly:

1) A status request becomes a bit of a hassle. The client code should get getStatusMap () and find the codes.

2) IDEs will not be so useful when transferring a call code, what are the actual statuses (although phpdoc annotation on setStatus () can help if you keep it up to date.

3) Invalid status throws an exception at runtime, but with constants that you detect at compile time.

-

Final note, I do not see the need for BookingManager at all. Yes, objects are just simple objects, but that does not mean that they cannot contain logic to control their own state.

+1
source share

I like to handle such problems in a general way (a little superfluous if all you need is status, but for which project has you ever needed only one enum-like field?);

First I create a "GenericOptionEntity" with the following fields:

  • ID
  • Tag
  • Label
  • Entity
  • Field

And a one-to-many relationship to associate an entity (or a basic object, in the case of a common "status") with the corresponding field.

The entity and field fields help determine the correct parameters for form types.

GenericOptionRepository should implement "getOptionByTag", so you won’t need to use identifiers to get the right entity. In addition, the general method of obtaining the appropriate parameters for the calling \ field method.

Finally, (depending on the case), many interfaces are sometimes useful, especially for creating "Statusable" entities when you need them.

I really like this approach, mainly because:

  • It makes sense:)
  • Adding and removing parameters can be performed by the administrator through the GUI
  • Easily filter appropriate parameters for presentation / controller levels.
  • Allows reuse of all necessary helper methods
  • Doesn't clutter up your project with mini classes created for 4 DB rows.
+1
source share

I would go with Tom's approach and make a status table as follows:

  • ID
  • description
  • isAccepted (bool)
  • isRejected (bool)

And values ​​like this:

  • id: 1
  • Description: New
  • isAccepted: 0
  • isRejected: 0

  • id: 2

  • Description: Accepted
  • isAccepted: 1
  • isRejected: 0

  • id: 3

  • Description: Rejected
  • isAccepted: 0
  • isRejected: 1

Then you query with isAccepted or isRejected without having to worry about the identifier.

+1
source share

I recently developed something similar in a MenuItem object and three persistent “types”.

The approach that I like includes the addition of additional 'is' methods. I put them right in the essence, but this approach is up to you. I am not sure there is one “right way”.

 <?php class Booking { const STATUS_NEW = 0; const STATUS_ACCEPTED = 1; const STATUS_REJECTED = 2; protected $status; public function setBookingStatus($status) { $this->status = $status; } public function getStatus() { return $this->status; } public function isStatusNew() { return $this->status === self::STATUS_NEW; } public function isStatusAccepted() { return $this->status === self::STATUS_ACCEPTED; } public function isStatusRejected() { return $this->status === self::STATUS_REJECTED; } } 

This allows you to use lighter logic in Twig patterns (YMMV):

 {% if booking.statusAccepted %} {% elseif booking.statusRejected %} {% else %} 

This way you can keep the same Twig syntax for your constants.

0
source share

All Articles