原则2实体中的常量


Constants in Doctrine 2 entities

假设我有以下原则2实体:

/**
 * @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;
}

用户可以有多种状态,例如:Pending、Active、Suspended。这些状态在整个代码(服务、存储库等)和UI层(用户编辑表单将在下拉菜单中显示它们)中都是需要的。

为了避免在多个地方定义它们,我到目前为止所做的是使用一个类来保存它们(所有应用程序的常量),它看起来有点像这样:

class App_Constants extends Zrzr_Constants
{
    protected static $_constants = array( 
        'users' => array( 
            'status' => array( 
                0 => 'Pending', 
                1 => 'Active',
                2 => 'Suspended' ) ) );
}

基类(Zrzr_Constants)将提供一些方法来检索它们,它看起来像这样:

class Zrzr_Constants
{
    protected static $_constants = array();
    public static function getConstantValues( $key, $subkey )
    {
        // ...
    }
    public static function getConstantByName( $name )
    {
        // ...
    }
}

常用的用法是:

// 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');

当然,这意味着有一些限制,常数标签不能包含点,但我可以忍受它。

使用原则2并采用DDD方式,告诉我"状态"字段实际上应该是一个"值对象"(但原则2还不支持值对象),或者至少我应该在实体中定义常量(使用const)。

我的问题是我该如何做到这一点,这样我就可以避免UI层的不断重新定义?我需要通过名称(在代码中)访问常量,并在UI下拉(例如)的情况下拥有此类字段的所有可能值。

我认为,你可以这样做:

class User {
  const STATUS_PENDING = 'Pending';
  const STATUS_ACTIVE = 'Active';
  const STATUS_SUSPENDED = 'Suspended';
  public static function getStatusList() {
    return array(
                 self::STATUS_PENDING, 
                 self::STATUS_ACTIVE, 
                 self::STATUS_SUSPENDED
                );
  }
  public function getStatus() {...}
  public function setStatus($value) {...}
  public function isStatusPending() {...} //If you need it
}

在UI层,你可以使用本地化服务获得状态的文本版本(如果状态常量是数字,UI层可以通过添加前缀将它们转换为字符串,例如user_status_0)。在Symfony2视图中,您可以使用trans Twig过滤器从user本地化域获取用户状态的文本版本。

如果你的网站只有一种语言,那么我认为只用User::STATUS_XXX就可以了。我认为你不应该创建一个新的类来保存用户的状态,从而使事情变得过于复杂。

如果你最终有许多状态或其他相关的东西,我认为你必须为它们创建一个单独的实体。

你可以定义你的类,如下面的例子

class ContactResource
{
   const TYPE_PHONE = 1;
   const TYPE_EMAIL = 2;
   const TYPE_BIRTDAY = 3;
   const TYPE_ADDRESS = 4;
   const TYPE_OTHER = 5;
   const TYPE_SKYPE = 6;
   const TYPE_LINKEDIN = 7;
   const TYPE_MEETUP = 8;
   const TYPE_TELEGRAM = 9;
   const TYPE_INSTAGRAM = 10;
   const TYPE_TWITTER = 11;
   public static $resourceType = array(
       ContactResource::TYPE_PHONE => "Phone",
       ContactResource::TYPE_EMAIL => "Email",
       ContactResource::TYPE_BIRTDAY => "Birtday",
       ContactResource::TYPE_ADDRESS => "Address",
       ContactResource::TYPE_OTHER => "Other",
       ContactResource::TYPE_SKYPE => "Skype",
       ContactResource::TYPE_LINKEDIN => "LinkedIn",
       ContactResource::TYPE_MEETUP => "Meetup",
       ContactResource::TYPE_TELEGRAM => "Telegram",
       ContactResource::TYPE_INSTAGRAM => "Instagram",
       ContactResource::TYPE_TWITTER => "Twitter",
   );
   /**
   * @var integer
   *
   * @ORM'Column(type="integer", length=2)
   *
   */
   private $type;

   public function __toString()
   {
      return (string)$this->getType();
   }
   public function getType()
   {
      if (!is_null($this->type)) {
          return self::$resourceType[$this->type];
      } else {
          return null;
      }
   }
   public static function getTypeList() {
      return self::$resourceType;
   }
}

如果您需要在Twig

中获取类型
{{ entity.type }}

为选项列表

ContactResource::getTypeList()

希望对你有用!

几年后,随着经验的积累,我认为正确的答案发生了变化。最初的问题是关于UI层中使用的域常量,但给出的示例和讨论实际上涉及以下概念:枚举、枚举映射和值对象。我当时没有这些概念,我的问题的答案也没有提供它们。

当你看到或想到像STATUS_PENDING, STATUS_ACTIVE, STATUS_SUSPENDED这样的常量时,你应该想到枚举。标准的PHP枚举是不够的,所以我喜欢使用第三方库,如marc-mabe/PHP -enum。下面是它的样子:

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;
}

如果需要向其添加功能,则很容易将其转换为值对象(我建议通过组合而不是继承来完成)。回到User实体,使用上面的枚举,实体最终会像这样:

/**
 * @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;
}

注意列类型是"user_status"。要使其工作,您需要定义一个自定义Doctrine类型并将其注册到Doctrine中。这样的类型看起来像这样:

/**
 * 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;
    }
}

最后,当涉及到满足用户界面的需要时,您可以最终使用enum映射。请记住,UI可能需要额外的功能,例如多语言支持,因此您不能将这些关注点放入域中,因此要进行分离:

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'];
    }
}

您可以在'name'旁边添加任意多的键。在UI中,您可以使用这样的映射:

// 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');

toArray函数在MabeEnum'EnumMap中不可用,但您可以自己制作:

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;
    }
}

总结:

  1. 使用枚举定义单个字段的可选值列表。
  2. 如果你想给这个字段添加VO特定的功能,在构造函数中创建一个接收这个Enum的值对象。
  3. 使用Enum Map来满足UI需求。