假设我有以下原则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;
}
}
总结:
- 使用枚举定义单个字段的可选值列表。
- 如果你想给这个字段添加VO特定的功能,在构造函数中创建一个接收这个Enum的值对象。
- 使用Enum Map来满足UI需求。