条令-模型与实体脱钩


Doctrine - decoupled model from entity

这是交易。使用Doctrine ORM for PHP和我需要将模型从实体持久层"解耦"。假设我们有UserEntity,它为数据库映射保存了所有漂亮的东西,如:注释、属性、setters/getters等等。另一方面,我希望有一个单独的User类,它只包含与业务相关的逻辑,例如:User::getFullName()。此外,我希望User扩展UserEntity,以便User继承所有的访问方法。

我检查过的可能的解决方案对我不起作用:

  • 仅仅从实体扩展模型,然后在DQL中指定模型是行不通的
  • makeUserEntity/**@MappedSuperclass*/不起作用,因为在这种情况下,UserEntity"本身不是一个实体"
  • InheritanceType/DiscriminatorColumn/DiscrimentorMap不能正常工作,原因是模型不是实体

有什么想法吗?

明白了!(>=PHP 5.4解决方案)

简而言之:使实体成为一种特质,在模型类中使用实体特质。签出此文档页:http://doctrine-orm.readthedocs.org/en/latest/tutorials/override-field-association-mappings-in-subclasses.html.

示例:假设我们有用户模型。首次创建用户实体:

/**
 * @ORM'Table(name="user")
 */
trait UserEntity {
     /**
      * @var integer
      *
      * @ORM'Column(name="id", type="integer", nullable=false)
      * @ORM'Id
      * @ORM'GeneratedValue(strategy="IDENTITY")
      */
      protected $id;
     /**
      * @var string
      *
      * @ORM'Column(name="first_name", type="string", length=100, nullable=true)
      */
      protected $firstName;
     /**
      * @var string
      *
      * @ORM'Column(name="last_name", type="string", length=100, nullable=true)
      */
      protected $lastName;
      // other mapping here...
}

然后创建一个模型并在其中使用一个实体特征:

/**
 * @ORM'Entity
 */
class User {
    use UserEntity;
    public function getFullName() {
        return $this->firstName . ' ' . $this->lastName;
    }
}

用户模型中的Mind@Entity注释。这是直接使用模型所必需的在实体管理器中。

现在假设我们需要一个Admin模型来扩展User one。这有点棘手。我们必须在User中将@Entity更改为@MappedSuperclass,以便对其进行扩展。然后创建Admin模型,将其声明为@Entity,并在其上重新声明表名使用@Table注释(否则,由于某种原因,Doctrine将混淆从哪个表中获取)。看起来像这样:

/**
 * @ORM'MappedSuperclass
 */
class User {
    use UserEntity;
    public function getFullName() {
        return $this->firstName . ' ' . $this->lastName;
    }
}
/**
 * @ORM'Entity
 * @ORM'Table(name="user")
 */
class Admin extends User {
    public function getFullName() {
        return parent::getFullName() . ' (admin)';
    }
}

这样,用户和管理模型(=实体)都可以在条令中使用。

不,我们可以对实体做我们通常会做的事情:通过实体管理器查找(),直接在查询中使用模型,等等。

嘿,孩子们想买一些魔法吗

您可以在PHP中使用魔术方法来转发所有的访问器方法调用,例如:

class UserModel
{
    protected $userEntity;
    public function __construct($entity)
    {
        $this->userEntity = $entity;
    }
    public function __call($name, $arguments)
    {
        if (!method_exists($this, $name) AND method_exists($this->userEntity, $name)) {
            return call_user_func_array(array($this, $name), $arguments);
        }
    }
}

您可以定义自己的本地EntityRepository类,该类扩展了Doctrine'ORM'EntityRepository。通过这种方式,您可以确保findfindByfindOneBy方法将创建Model的新实例,在Model中设置$entity对象,并返回新的Model实例而不是entity。

想要与条令结果输出进行更干净的集成吗

您可以编写一个扩展实体类的模型类(无需将其注册到Doctrine2),并编写自己的自定义Hydrator类(并将其注册在Doctrine2中),以确保您的结果将作为model类的数组而不是entity类的数组返回。

想用Symfony的方式吗

您可以在实体存储库类中定义所有这些"与数据库相关的业务逻辑",也可以在服务类中定义这些"与非数据库相关的商业逻辑"(如果愿意,您可以创建专用于一个实体的服务)。然后使用数据库逻辑参数(实体)调用业务逻辑层(服务、存储库)。

另一方面,Symfony方式非常灵活,因为您可以在预先建立的Symfony结构之外构建任何功能,只要它们是PSR即可。

关于自定义水合器。简言之,这个解决方案对我来说也不是一个选择(不过我可能会错过任何东西)。这里有两个陷阱:

  1. 从原始数据库数据重新映射到自定义hydrator中的模型对象并不方便。问题是db表中的字段是"单个单词"(如:firstname/lastname),而在对象中,我手动将它们修改为驼色大小写(为了可读性)。这当然可以通过从实体的注释中读取元信息来解决,但这将涉及魔术和额外的时间资源。所以这个解决方案会很"脏"
  2. 如何在例如"查找"功能中使用自定义水合器?比如:$em->find('My''Model''User',1)?因为我希望我的模型在每个实体使用的地方都能百分之百地使用

因此,米哈伊·斯坦库提出的任何解决方案都不适用于我

  • 模型类内部的魔术:不是一个干净的解决方案,因为它涉及将实体传递给构造函数,并且不会给您类型提示(魔术__call的原因)
  • 自定义hydrator:看起来更干净,但不能在所有情况下都使用,从数据库原始结果到模型对象的重新映射很复杂(=耗时)
  • Symfony的服务方式:也不是干净的解决方案,因为我需要100%分离模型逻辑和数据库相关逻辑(所以我的模型中没有一个映射信息/注释,只有一个纯业务逻辑)

好吧,我最近一直在问自己这个问题,我得到的简单答案是将实体中的所有节点移到yaml或xml中,以清理我的"模型"/实体。

然后,我的用户类中有我所有的属性,但所有的持久性配置都被移走了,然后你也可以对getter和setter做任何你想做的事情(在我看来,尽可能多地删除)。