Constants in Doctrine 2 entities

Viewed 15585

Suppose i'm having the following Doctrine 2 entity:

/**
 * @ORM\Entity
 * @ORM\Table(name="users")
 */
class User {

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     *
     * @var int
     */
    protected $id;

    /**
     * @ORM\Column(length=100)
     *
     * @var string
     */
    protected $name;

    /**
     * @ORM\Column(type="integer")
     *
     * @var int
     */
    protected $status;
}

The User can have several statuses, for example: Pending, Active, Suspended. These statuses are needed throughout the code (services, repositories, etc.) and also in the UI layer (a User edit form would display them in a dropdown).

In order to avoid defining them in multiple places, what i've done so far was to use a class to hold them all (all the application's constants), and it looks somewhat like this:

class App_Constants extends Zrzr_Constants
{

    protected static $_constants = array( 
        'users' => array( 
            'status' => array( 
                0 => 'Pending', 
                1 => 'Active',
                2 => 'Suspended' ) ) );

}

The base class (Zrzr_Constants) would offer some methods to retrieve them, and it looks like this:

class Zrzr_Constants
{
    protected static $_constants = array();

    public static function getConstantValues( $key, $subkey )
    {
        // ...
    }

    public static function getConstantByName( $name )
    {
        // ...
    }
}

Common usage would be:

// example of retrieval by constant name ... it would return an integer
$pendingStatus = App_Constants::getConstantByName( 'USERS.STATUS.PENDING' );

// example of retrieval for UI display purposes ... would return an array
$statuses = App_Constants::getConstantValues('users', 'status');

Of course this means that there are some limitations in that the constant labels cannot contain dots, but i can live with it.

Using Doctrine 2 and going the DDD way however, tells me that the 'status' field should be in fact a 'value object' (but Doctrine 2 does not support value objects yet), or at least that i should have the constants defined within the entity (using const).

My question is how would i do this so that i avoid constant redefinition for the UI layer? I need to have access to the constant by name (in the code) and to have all the possible values for such a field in the case of a UI dropdown (for example).

3 Answers

Several years later and some more experience, what I consider to be the proper answer has changed. The initial question is about domain constants used in the UI layer, but the given example and the discussions actually refer to the following concepts: enums, enum maps and value objects. I did not have these concepts back then and the answers to my question did not provide them.

When you see or think of constants like STATUS_PENDING, STATUS_ACTIVE, STATUS_SUSPENDED you should be thinking of an enum. The standard PHP enum is insufficient so I like to use a third party library like marc-mabe/php-enum. Here's how it would look like:

use MabeEnum\Enum;

/**
 * @method static UserStatus PENDING()
 * @method static UserStatus ACTIVE()
 * @method static UserStatus SUSPENDED()
 */
class UserStatus extends Enum
{
    const PENDING = 0;
    const ACTIVE = 1;
    const SUSPENDED = 2;
}

It's easy to turn this into a value object if you need to add functionality to it (I recommend doing it through composition, not inheritance). Coming back to the User entity, using the above enum the entity would end up like this:

/**
 * @ORM\Entity
 * @ORM\Table(name="users")
 */
class User {

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     *
     * @var int
     */
    protected $id;

    /**
     * @ORM\Column(length=100)
     *
     * @var string
     */
    protected $name;

    /**
     * @ORM\Column(type="user_status")
     *
     * @var UserStatus
     */
    protected $status;
}

Notice the column type is "user_status". To get this to work you need to define a custom Doctrine type and register it with Doctrine. Such a type would look like this:

/**
 * Field type mapping for the Doctrine Database Abstraction Layer (DBAL).
 *
 * UserStatus fields will be stored as an integer in the database and converted back to
 * the UserStatus value object when querying.
 */
class UserStatusType extends Type
{
    /**
     * @var string
     */
    const NAME = 'user_status';

    /**
     * {@inheritdoc}
     *
     * @param array            $fieldDeclaration
     * @param AbstractPlatform $platform
     */
    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return $platform->getIntegerTypeDeclarationSQL($fieldDeclaration);
    }

    /**
     * {@inheritdoc}
     *
     * @param string|null      $value
     * @param AbstractPlatform $platform
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if (empty($value)) {
            return null;
        }

        if ($value instanceof UserStatus) {
            return $value;
        }

        try {
            $status = UserStatus::get((int)$value);
        } catch (InvalidArgumentException $e) {
            throw ConversionException::conversionFailed($value, self::NAME);
        }

        return $status;
    }

    /**
     * {@inheritdoc}
     *
     * @param UserStatus|null  $value
     * @param AbstractPlatform $platform
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if (empty($value)) {
            return null;
        }

        if ($value instanceof UserStatus) {
            return $value->getValue();
        }

        throw ConversionException::conversionFailed($value, self::NAME);
    }

    /**
     * {@inheritdoc}
     *
     * @return string
     */
    public function getName()
    {
        return self::NAME;
    }

    /**
     * {@inheritdoc}
     *
     * @param AbstractPlatform $platform
     *
     * @return boolean
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}

Finally, when it comes to satisfying the needs of the user interface, you can end up using enum maps. Remember that the UI could need additional functionality such as multiple language support, so you cannot mash such concerns into the domain, hence the separation:

use MabeEnum\EnumMap;

class UserStatusMap extends EnumMap
{
    public function __construct()
    {
        parent::__construct(UserStatus::class);

        $this[UserStatus::PENDING] = ['name' => 'Pending'];
        $this[UserStatus::ACTIVE] = ['name' => 'Active'];
        $this[UserStatus::SUSPENDED] = ['name' => 'Suspended'];
    }
}

You can just add as many keys you want beside 'name'. In the UI you can make use of such a map like this:

// if you want to display the name when you know the value
echo (new UserStatusMap ())[UserStatus::PENDING]['name'];
// or
echo (new UserStatusMap ())[UserStatus::PENDING()]['name'];

// if you want to build a list for a select (value => name)
$list = (new UserStatusMap ())->toArray('name');

The toArray function is not available in MabeEnum\EnumMap but you can make your own:

use MabeEnum\EnumMap as BaseEnumMap;

class EnumMap extends BaseEnumMap
{
    /**
     * @param string|null $metadataKey
     *
     * @return array
     */
    public function toArray($metadataKey = null)
    {
        $return = [];

        $flags = $this->getFlags();
        $this->setFlags(BaseEnumMap::KEY_AS_VALUE | BaseEnumMap::CURRENT_AS_DATA);

        if ($metadataKey) {
            foreach ($this as $key => $value) {
                $return[$key] = $value[$metadataKey];
            }
        } else {
            $return = iterator_to_array($this, true);
        }

        $this->setFlags($flags);

        return $return;
    }
}

To summarize:

  1. Use an Enum to define a list of alternative values for a single field.
  2. Create a Value Object which receives this Enum in the constructor if you want to add VO specific functionality to this field.
  3. Use an Enum Map to serve the UI needs.
Related