How should I handle BusinessLogic-related data definitions (like status types) in Doctrine 2? How should I handle BusinessLogic-related data definitions (like status types) in Doctrine 2? symfony symfony

How should I handle BusinessLogic-related data definitions (like status types) in Doctrine 2?


I'd propose you get rid of of the constants and just create a Many-To-One association on your Booking entity with a new BookingStatus entity. Why? Well for a number of reasons:

  • Does not require editing of code if you wish to add a new booking status. Not only is this easier for developers but also allows the possibility of statuses to be dynamically created.
  • You can easily store additional information about the status in the new BookingStatus entity, e.g name. This information can also be updated without altering code.
  • Allows external tools to understand different statuses. For example you might want to use a external reporting tool directly on the database. It won't know what some integers mean but it will be able to understand a Many-To-One association.


I use 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, in order to keep separation from model

{{ ('offer.statuses.'~offer.statusName)|trans }}


Answers to your numbered questions

1: Why wouldn't you have business logic in your business model/objects?

Coupling data and behaviour is, after all, one of the very purposes of object orientation? I believe you may have misunderstood the "POJO" concept (and I'm not talking about the J standing for Java ;)), whose purpose was to not let frameworks invade your model, thus limiting the model to a framework-specific context and making unit testing or re-use in any other context difficult.

What you're talking about sounds more like DTOs, which generally is not what your model should consist of.

2: Yes, Twig is not terribly good at manipulating numbers-that-symbolizes-meaning. You'll probably get all kinds of suggestions based on things like optimization of storage (number of bytes) or database traffic/query time, but for most projects I prefer prioritizing the human experience - i.e. don't optimize for computers unless you need it.

Thus, my personal preference (in most circumstances) is instantly dev-readable "enum" fields, but in a key-like notation instead of regular words. For example, "status.accepted" as opposed to 1 or "Accepted". Key-like notations lend themselves well to i18n, using twig's |trans filter, {% trans %} tag or something similar.

3: Static "enum" references within your model is rarely a problem while unit testing the model itself.

At some point your model needs to define its semantics anyway, through the use of the building blocks you have available. While being able to abstract away implementations (particularly of services) is useful, being able to abstract away meaning is rarely (never?) fruitful. Which reminds me of this story. Don't go there. :-D

If you're still concerned about it, put the constants in an interface that the model class implements; then your tests can reference only the interface.

 

Suggested 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 the way you want to structure your i18n keys and files.)

Resources/translations/mybundle.en.yml:

booking.status.new:      Newbooking.status.accepted: Acceptedbooking.status.rejected: Rejected

 

Constants-as-entities

On Tomdarkness' suggestion of turning these constants into their own model class, I want to stress that this should be a business/domain decision, and not a question of technical preference.

If you clearly foresee the use cases for dynamically adding statuses (supplied by the system's users), then by all means a new model/entity class is the right choice. But if the statuses are used for internal state in the application, which is coupled to the actual code you're writing (and thus won't change until the code changes too), then you're better off using constants.

Constants-as-entities makes working with them much harder ("hm, how do I get the primary key of 'accepted', again?"), isn't as easily internationalizable ("hm, do I store the possible locales as hard-coded properties on the BookingStatus entity or do I make another BookingStatusI18nStrings(id, locale, value) entity?"), plus the refactoring issue you brought up yourself. In short: don't overengineer - and good luck. ;-)