Sf2:在实体内使用服务

问题描述 投票:13回答:4

我知道这已被一遍又一遍地问过,我读了主题,但它总是专注于特定的情况,我通常试图理解为什么在实体中使用服务不是最好的做法。

给定一个非常简单的服务:

Class Age
{
  private $date1;
  private $date2;
  private $format;

  const ym = "%y years and %m month"
  const ...


  // some DateTime()->diff() methods, checking, formating the entry formats, returning different period formats for eg.
  }

和一个简单的实体:

Class People
{
  private $firstname;
  private $lastname;
  private $birthday;

  }

从控制器,我想做:

$som1 = new People('Paul', 'Smith', '1970-01-01');
$som1->getAge();

当然,我可以重写我的实体内部的getAge()函数,它不长,但我非常懒,因为我已经写了所有可能的datetime-> diff()我需要在上面的服务,我不明白为什么我不应该使用“嗯...

注意:我的问题不是关于如何在我的实体中注入容器,我可以理解为什么这没有意义,但更多的是避免在不同实​​体中重写相同功能的最佳实践。

继承似乎是一个糟糕的“好主意”,因为我可以在类BlogArticle中使用getAge(),我怀疑这个BlogArticle类应该继承与People类相同的类...

希望我很清楚,但不确定......

symfony service entities
4个回答
70
投票

许多程序员的一个主要困惑是认为学说实体“是”模型。那是一个错误。

  • 请参阅最后编辑此帖子,其中包含与CQRS + ES相关的想法 -

向您的学说实体注入服务是“尝试做更多的事情而不是存储数据”的症状。当您看到“反模式”时,您很可能违反了SOLID编程中的“单一责任”原则。

Symfony不是MVC框架,它只是一个VC框架。缺乏M部分。 Doctrine实体(我将从现在开始称它们为实体,最后看澄清)是“数据持久层”,而不是“模型层”。 SF有许多视图,Web控制器,命令控制器......但对域建模(http://en.wikipedia.org/wiki/Domain_model)没有帮助 - 甚至持久层都是Doctrine,而不是Symfony。

克服SF2中的问题

当您在数据层中“需要”服务时,触发反模式警报。存储应该只是“放在这里 - 从那里开始”系统。没有其他的。

要解决此问题,您应该将服务注入“逻辑层”(Model)并将其与“纯存储”(数据持久层)分开。遵循单一责任原则,将逻辑放在一边,将getter和setter放在另一边的mysql中。

解决方案是创建缺少的Model层,在Symfony2中不存在,并使其给出域对象的“逻辑”,完全分离并与数据持久性层解耦,后者知道“如何存储”模型到一个带有doctrine的mysql数据库,或者一个redis,或者只是一个文本文件。

所有这些存储系统应该是可以互换的,你的Model应该仍然暴露相同的公共方法,而消费者绝对没有变化。

这是你如何做到的:

第1步:将模型与数据持久性分开

为此,在您的包中,您可以在束根级别创建另一个名为Model的目录(除了testsDependencyInjection等),就像在这个游戏示例中一样。

  • Model这个名字不是强制性的,Symfony对此没有任何说明。你可以选择你想要的任何东西。
  • 如果您的项目很简单(例如一个包),则可以在同一个包中创建该目录。
  • 如果你的项目有很多捆绑,你可以考虑 将模型拆分为不同的包,或者 或者在示例图像中使用 - 包含项目所需的所有“对象”的ModelBundle(没有接口,没有控制器,没有命令,只有游戏逻辑及其测试)。在这个例子中,你会看到一个ModelBundle,提供逻辑概念,如BoardPieceTile以及许多其他逻辑概念,目录中的结构是为了清晰起见。

特别是对你的问题

在您的示例中,您可以:

Entity/People.php
Model/People.php
  • 任何与“商店”相关的东西都应该放在Entity/People.php里面 - 例如:假设您想要将生日期存储在日期时间字段中,以及三个冗余字段中:年,月,日,因为与搜索相关的任何棘手的事情或索引,与域无关(即与某人的“逻辑”无关)。
  • 与“逻辑”相关的任何事情都应该进入Model/People.php - 例如:如果一个人刚刚过了大多数年龄,如果某个出生日期和他所居住的国家(这将决定最低年龄),如何计算。如您所见,这与持久性无关。

第2步:使用工厂

然后,您必须记住模型的使用者永远不应该使用“new”创建模型对象。他们应该使用工厂,这将正确设置模型对象(将绑定到正确的数据存储层)。唯一的例外是单元测试(我们稍后会看到)。但是除了单一测试之外,用大脑中的火来抓住它,并用视网膜中的激光对其进行纹身:永远不要在控制器或命令中做“新”。改为使用工厂;)

为此,您需要创建一个充当模型“getter”的服务。您可以将getter创建为可通过服务访问的工厂。看图像:

Use a service as a factory to get your model

你可以在那里看到BoardManager.php。这是工厂。它是与电路板相关的主要吸气剂。在这种情况下,BoardManager具有以下方法:

public function createBoardFromScratch( $width, $height )
public function loadBoardFromJson( $document )
public function loadBoardFromTemplate( $boardTemplate )
public function cloneBoard( $referenceBoard )
  • 然后,正如您在图像中看到的那样,在services.yml中定义了该管理器,并将持久层注入其中。在这种情况下,你将ObjectStorageManager注入BoardManager。对于此示例,ObjectStorageManager能够存储和加载来自数据库或文件的对象;而BoardManager是存储不可知的。
  • 您还可以在图像中看到ObjectStorageManager,然后注入@doctrine以便能够访问mysql
  • 您的经理是唯一允许使用new的地方。从不在控制器或命令中。

特别是对你的问题

在您的示例中,您将在模型中拥有PeopleManager,能够根据需要获取人物对象。

同样在模型中,您应该使用正确的单数复数名称,因为它与数据持久层分离。似乎您当前正在使用People来表示单个Person - 这可能是因为您当前(错误地)将模型与数据库表名称匹配。

因此,涉及的模型类将是:

PeopleManager -> the factory
People -> A collection of persons.
Person -> A single person.

例如(伪代码!使用C ++表示法来指示返回类型):

PeopleManager
{
    // Examples of getting single objects:
    Person getPersonById( $personId ); -> Load it from somewhere (mysql, redis, mongo, file...)
    Person ClonePerson( $referencePerson ); -> Maybe you need or not, depending on the nature the your problem that your program solves.
    Person CreatePersonFromScratch( $name, $lastName, $birthDate ); -> returns a properly initialized person.

    // Examples of getting collections of objects:
    People getPeopleByTown( $townId ); -> returns a collection of people that lives in the given town.
}

People implements ArrayObject
{
    // You could overload assignment, so you can throw an exception if any non-person object is added, so you can always rely on that People contains only Person objects.
}

Person
{
    private $firstname;
    private $lastname;
    private $birthday;
}

所以,继续你的例子,当你做...

// **Never ever** do a new from a controller!!!
$som1 = new People('Paul', 'Smith', '1970-01-01');
$som1->getAge();

...你现在可以改变为:

// Use factory services instead:
$peopleManager = $this->get( 'myproject.people.manager' );
$som1 = $peopleManager->createPersonFromScratch( 'Paul', 'Smith', '1970-01-01' );
$som1->getAge();

PeopleManager将为你做newfor。

此时,由工厂创建的$som1类型的变量Person可以预先填充必要的机制来存储并保存到持久层。

myproject.people.manager将在您的services.yml中定义,并且可以通过'myproject.persistence.manager`层或其他方式直接访问该学说。

注意:通过管理器注入持久层会产生一些副作用,这些副作用会从“如何使模型访问服务”中产生影响。为此,请参见步骤4和5。

第3步:通过工厂注入您需要的服务。

现在,您可以将所需的任何服务注入people.manager

您,如果您的模型对象需要访问该服务,您现在有两个选择:

  • 当工厂创建一个模型对象时(即当PeopleManager创建一个Person时)通过构造函数或者一个setter来注入它。
  • 在PeopleManager中代理该函数,并通过构造函数或setter注入PeopleManager。

在此示例中,我们为PeopleManager提供了模型要使用的服务。当人员经理被请求一个新的模型对象时,它会在new语句中注入它所需的服务,因此模型对象可以直接访问外部服务。

// Example of injecting the low-level service.
class PeopleManager
{
    private $externalService = null;

    class PeopleManager( ServiceType $externalService )
    {
        $this->externalService = $externalService;
    }

    public function CreatePersonFromScratch()
    {
        $externalService = $this->externalService;
        $p = new Person( $externalService );
    }
}

class Person
{
    private $externalService = null;

    class Person( ServiceType $externalService )
    {
        $this->externalService = $externalService;
    }

    public function ConsumeTheService()
    {
        $this->externalService->nativeCall();  // Use the external API.
    }
}

// Using it.
$peopleManager = $this->get( 'myproject.people.manager' );
$person = $peopleManager->createPersonFromScratch();
$person->consumeTheService()

在此示例中,我们为PeopleManager提供了模型要使用的服务。然而,当人员管理器被请求一个新的模型对象时,它会将自己注入到创建的对象中,因此模型对象可以通过管理器访问外部服务,然后管理器隐藏API,因此如果外部服务更改了API,经理可以为模型中的所有消费者进行适当的转换。

// Second example. Using the manager as a proxy.
class PeopleManager
{
    private $externalService = null;

    class PeopleManager( ServiceType $externalService )
    {
        $this->externalService = $externalService;
    }

    public function createPersonFromScratch()
    {
        $externalService = $this->externalService;
        $p = new Person( $externalService);
    }

    public function wrapperCall()
    {
         return $this->externalService->nativeCall();
    }
}

class Person
{
    private $peopleManager = null;

    class Person( PeopleManager $peopleManager )
    {
        $this->peopleManager = $peopleManager ;
    }

    public function ConsumeTheService()
    {
        $this->peopleManager->wrapperCall(); // Use the manager to call the external API.
    }
}

// Using it.
$peopleManager = $this->get( 'myproject.people.manager' );
$person = $peopleManager->createPersonFromScratch();
$person->ConsumeTheService()

第4步:为所有事情投掷事件

此时,您可以在任何模型中使用任何服务。似乎一切都已完成。

然而,当你实现它时,如果你想要一个真正的SOLID模式,你会发现将模型与实体分离的问题。这也适用于将此模型与模型的其他部分分离。

问题显然出现在诸如“何时进行刷新()”或​​“何时决定是否必须保存某些内容或稍后保存”(特别是在长期存在的PHP过程中)以及有问题的变化中。如果该学说改变了它的API和类似的东西。

但是当你想要测试一个人而不测试它的House时也是如此,但是House必须“监视”Person是否更改其名称以更改邮箱中的名称。这是特别尝试长寿命的过程。

对此的解决方案是使用观察者模式(http://en.wikipedia.org/wiki/Observer_pattern),因此模型对象几乎可以为任何事物抛出事件,并且观察者决定将数据缓存到RAM,填充数据或将数据存储到磁盘。

这极大地增强了固/闭原理。如果您更改的内容与域无关,则永远不应更改模型。例如,添加一种新的存储方式到新类型的数据库,应该要求在模型类上进行零版本。

您可以在下图中看到此示例。在其中,我突出了一个名为“TurnBasedBundle”的捆绑包,就像每个基于回合制的游戏的核心功能一样,尽管它是否有一块板。您可以看到捆绑包只有模型和测试。

每个游戏都有一个规则集,玩家,在游戏过程中,玩家表达他们想要做的事情的愿望。

Game对象中,实例化器将添加规则集(扑克?国际象棋?tic-tac-toe?)。警告:如果我想加载的规则集不存在怎么办?

初始化时,某人(可能是/ start控制器)将添加玩家。注意:如果游戏是双人游戏而我加三个游戏怎么办?

并且在游戏期间,接收玩家移动的控制器将增加欲望(例如,如果下棋,“玩家想要将女王移动到该区块” - 这可能是有效的,或者不是 - 。

在图片中,您可以通过事件看到这3个操作受到控制。

Example of throwing events from the model for virtually everything that can happen

  • 您可以观察到捆绑包只有模型和测试。
  • 在模型中,我们定义了2个对象:Game和GameManager,以获取Game对象的实例。
  • 我们还定义了Interfaces,例如GameObserver,所以任何愿意接收Game事件的人都应该是GameObserver民谣。
  • 然后你可以看到,对于修改模型状态的任何动作(例如添加一个玩家),我有2个事件:PREPOST。看看它怎么运作: 有人调用$ game-> addPlayer($ player)方法。 一旦我们进入addPlayer()函数,就会引发PRE事件。 然后观察者可以捕捉此事件以决定是否可以添加玩家。 所有PRE事件都应该通过引用取消。因此,如果有人认为这是一个2位玩家的游戏,并且您尝试添加第3个游戏,则$ cancel将设置为true。 然后你再次进入addPlayer函数。您可以检查是否有人想取消该操作。 如果允许则执行操作(即:改变$ this-> state)。 状态更改后,引发POST事件以指示观察者操作已完成。

在图片中你看到三个,但当然它还有更多。根据经验,每个setter有近2个事件,每个方法有2个事件可以修改模型的状态,每个“不可避免”的动作有1个事件。因此,如果您在类上运行10个方法,则可能会有大约15或20个事件。

您可以在任何操作系统的任何graphyc库的典型简单文本框中轻松看到这一点:典型事件将是:gotFocus,lostFocus,keyPress,keyDown,keyUp,mouseDown,mouseMove等...

特别是在你的例子中

Person将具有类似preChangeAge,postChangeAge,preChangeName,postChangeName,preChangeLastName,postChangeLastName的内容,以防每个人都有setter。

对于像“人,走路10秒”这样的长寿行为,你可能有3:preStartWalking,postStartWalking,postStopWalking(如果不能以编程方式阻止10秒的停止)。

如果你想简化,你可以有两个单一的preChanged( $what, & $cancel )postChanged( $what )事件。

如果您永远不会阻止您的更改发生,您甚至可以只为一个事件设置changed()以及对模型进行任何更改。然后,您的实体将在每次更改时“复制”实体属性中的模型属性。对于简单的类和项目,或者您不打算为第三方使用者发布的结构,这是可以的,并节省了一些编码。如果模型类成为项目的核心类,花一些时间添加所有事件列表将节省您将来的时间。

第5步:从数据层中捕获事件。

正是在这一点上,您的数据层捆绑包进入了行动!

使您的数据层成为模型的观察者。当模型更改其内部状态时,然后使您的实体将该状态“复制”到实体状态。

在这种情况下,MVC按预期运行:Controller,对模型进行操作。这样做的后果仍然是从控制器隐藏的(因为控制器不应该访问Doctrine)。该模型“广播”所做的操作,所以任何感兴趣的人都知道,这反过来触发数据层知道模型的变化。

特别是在你的项目中

Model/Person对象将由PeopleManager创建。在创建它时,PeopleManager是一种服务,因此可以注入其他服务,可以使用ObjectStorageManager子系统。因此,PeopleManager可以获得您在问题中引用的Entity/People,并将Entity/People作为观察者添加到Model/Person

Entity/People中,你主要用事件捕手取代所有的二传手。

您可以这样读取代码:当Model/Person更改其LastName时,将通知Entity/People并将数据复制到其内部结构中。

最有可能的是,您很想将实体注入模型中,因此您可以调用实体的setter来代替抛出事件。

但是通过这种方法,你可以打破“开放 - 封闭”原则。因此,如果您想要迁移到MongoDb的任何特定点,您需要在模型中“更改”您的“实体”中的“文档”。使用观察者模式,这种变化发生在模型之外,他们永远不会知道观察者的本质,因为它是PersonObserver。

第6步:单元测试一切

最后,您需要对软件进行单元测试。正如我所解释的这种模式克服了您发现的反模式,您可以(并且您应该)单独测试模型的逻辑,而不管存储的方式如何。

遵循这种模式,可以帮助您实现SOLID原则,因此每个“代码单元”都独立于其他单元。这将允许您创建单元测试,它将测试Model的“逻辑”而无需写入数据库,因为它会将伪数据存储层注入test-double。

让我再次使用游戏示例。我在图像中向您展示游戏测试。假设所有游戏可以持续数天,并且起始日期时间存储在数据库中。我们在示例中仅当getStartDate()返回dateTime对象时才进行测试。

enter image description here

其中有一些箭头代表流动。

在这个例子中,从我告诉你的两个注入策略中,我选择第一个:向Game模型对象注入它需要的服务(在这种情况下是BoardManagerPieceManagerObjectStorageManager)而不是注入GameManager本身。

  1. 首先,你调用phpunit调用查找Tests目录,递归地在所有目录中查找名为XxxTest的类。然后将希望调用名为textSomething()的所有方法。
  2. 但在调用之前,对于每个测试方法,它都调用setup()。
  3. 在设置中,我们将创建一些测试双精度,以避免在测试时“真正访问”数据库,同时正确测试模型中的逻辑。在这种情况下,我自己的数据层管理器ObjectStorageManager的两倍。
  4. 为清楚起见,它被分配给一个临时变量......
  5. ...存储在GameTest实例中......
  6. ......以后用于测试本身。
  7. 然后使用new命令创建$ sut(被测系统)变量,而不是通过管理器创建。你还记得我说测试是一个例外吗?如果你在这里使用经理(你仍然​​可以)它不是单元测试,它是一个集成测试,因为测试两个类:经理和游戏。在new命令中,我们伪造了模型所具有的所有依赖关系(就像一个董事会经理,就像一个片断管理器)。我在这里硬编码GameId = 1。这与数据持久性有关,见下文。
  8. 然后我们可以调用被测系统(一个简单的Game模型对象)来测试它的内部结构。

我在new中硬编码“Game id = 1”。在这种情况下,我们只测试返回的类型是DateTime对象。但是如果我们想要测试它得到的日期是正确的,我们可以“调整”ObjectStorageManager(数据持久层)模拟以返回我们想要的内部调用,所以我们可以测试一下例如当我向游戏的数据层请求日期= 1时,日期是2014年1月1日,对于游戏= 2,日期是2014年2月2日。然后在testGetStartDate中,我将创建2个新实例,其中ID为1和2,并检查结果的内容。

特别是在你的项目中

您将有一个Test/Model/PersonTest单元测试,可以使用该人的逻辑,如果需要数据库中的人,您将通过模拟假冒它。

如果您想测试将人员存储到数据库中,那么无论是谁监听,都可以对事件进行单元测试。您可以创建一个假侦听器,附加到事件,并且当postChangeAge发生时标记一个标志并且什么都不做(没有真正的数据库存储)。然后断言该标志已设置。

简而言之:

  1. 不要混淆逻辑和数据持久性。创建一个与实体无关的Model,并将所有逻辑放入其中。
  2. 永远不要使用new从任何消费者那里获取您的模型。请改用工厂服务。特别注意避免控制器和命令中的新闻。例外:单元测试是唯一可以使用new的消费者。
  3. 通过工厂在模型中注入所需的服务,工厂又从services.yml配置文件中接收它。
  4. 抛出一切事件。当我说一切时,意味着一切。想象一下你观察模型。你想知道什么?为它添加一个事件。
  5. 从控制器,视图,命令和模型的其他部分捕获事件,但是,特别是在数据存储层中捕获它们,这样您就可以将对象“复制”到磁盘而不会干扰模型。
  6. 单元测试您的逻辑,而不依赖于任何真实的数据库。在生产中附加真实数据库存储系统并为测试附加虚拟实现。

似乎做了很多工作。但事实并非如此。这是一个习惯它的问题。只需考虑您需要的“对象”,创建它们并使数据层成为对象的“监视器”。然后你的对象可以自由运行,解耦。如果从工厂创建模型,请将模型中的任何所需服务注入模型,并保留数据。

编辑apr / 2016 - 将域与持久性分开

在这个答案中所有出现的单词实体都指的是“学说实体”,这是造成大多数编码人员混淆的原因,在模型层和持久层之间应该总是不同。

  • 学说是基础设施,因此根据定义,学说不在模型之内。
  • 学说有实体。因此,根据定义,学说实体也在模型之外。
  • 相反,DDD积木越来越受欢迎,因此需要澄清更多我的答案,因为DDD在模型中也使用了Entity这个词。
  • Domain entities(不是Doctrine entities)与我在Domain objects的答案中提到的相似。
  • 事实上,有许多类型的Domain objectsDomain entities(与Doctrine entites不同)。 Domain value objects(可以被认为类似于基本类型,逻辑)。 Domain events(也不同于那些Symfony events,也不同于Doctrine events)。 Domain commands(不同于那些Symfony command line控制器般的助手)。 Domain services(与Symfony framework services不同)。 等等
  • 因此,我的所有解释都是这样的:当我说“实体不是模型对象”时,只需阅读“Doctrine实体不是域实体”。

编辑jun / 2019 - CQRS + ES类比

古人已经使用持久历史方法来记录事物(例如在石头上放置标记以记录交易)。

十年来,编程中的CQRS + ES方法(Command Query Responsability Segregation + Event Sourcing)越来越受欢迎,将“历史不可改变”的概念带入我们编写的程序中,而今天许多编码员都想到将命令分离方与查询方面。如果你不知道我在说什么,不用担心,只需跳过下一段。

CQRS + ES在过去3年或4年的日益普及让我想到这里考虑一下评论,以及它与我5年前在这里回答的内容有什么关系:

这个答案被认为是单一模型,而不是写模型和读模型。但我很高兴看到许多重叠的想法。

把我在这里提到的PRE事件想象成“命令和写模型”。将POST事件视为“事件采购部分走向读取模型”。

在CQRS中,您可以根据内部状态轻松找到“可以接受或不接受命令”。通常一个实现它们抛出异常但是还有其他替代方法,比如回答命令是否被接受。

例如,在“火车”中,我可以“将其设置为X速度”。但如果状态是火车在轨道上不能再进一步达到80公里/小时,则应该拒绝将其设置为200。

这与通过引用传递的cancel布尔值相似,其中实体可以在其状态改变之前“拒绝”某些东西。

相反,POST事件不会带有“取消”事件,并在状态发生变化后抛出。这就是你无法取消它们的原因:他们谈论“实际发生的状态变化”因此无法取消:它发生了。

所以...

在我2014年的回答中,“pre”事件与CQRS + ES系统的“命令接受”匹配(命令可以被接受或拒绝),“post”事件与CQRS +的“Domain events”匹配ES系统(它只是告知实际已经发生的变化,用这些信息做任何你想做的事情)。

希望能有所帮助。

哈维。


2
投票

你已经提到了一个很好的观点。类Person的实例不是唯一可以有年龄的东西。 BlogArticles也可以与许多其他类型一起衰老。如果你使用PHP 5.4+,你可以利用特征来添加一小部分功能,而不是从容器中获得服务对象(或者你可以将它们组合起来)。

这是一个快速的模型,你可以做些什么来使它非常灵活。这是基本的想法:

  • 有一个年龄计算特征(Aging
  • 有一个特定的特性,可以返回适当的领域($birthdate$createdDate,...)
  • 使用课堂内的特质

Generic

trait Aging {
    public function getAge() { 
        return $this->calculate($this->start()); 
    }

    public function calculate($startDate) { ... }
}

For person

trait AgingPerson {
    use Aging;
    public function start() {
        return $this->birthDate;
    }   
}

class Person {
    use AgingPerson;
    private $birthDate = '1999-01-01'; 
}

For blog article

// Use for articles, pages, news items, ...
trait AgingContent {
    use Aging;
    public function start() {
        return $this->createdDate;
    }   
}

class BlogArticle {
    use AgingContent;
    private $createDate = '2014-01-01'; 
}

现在您可以询问上述类的任何实例的年龄。

echo (new Person())->getAge();
echo (new BlogArticle())->getAge();

Finally

如果你需要类型提示特征不会对你有任何好处。在这种情况下,您将需要提供一个接口,并让每个使用该特征的类实现它(实际的实现是特征,但接口启用了类型提示)。

interface Ageable {
    public function getAge();
}

class Person implements Ageable { ... }
class BlogArticle implements Ageable { ... }

function doSomethingWithAgeable(Ageable $object) { ... }

这实际上看起来很麻烦,实际上维护和扩展这种方式要容易得多。


1
投票

很大一部分是在使用数据库时没有简单的方法来注入依赖项。

$person = $personRepository->find(1); // How to get the age service injected?

一种解决方案可能是将年龄服务作为参数传递。

$ageCalculator = $container('age_service');

$person = $personRepository->find(1);

$age = $person->calcAge($ageCalculator);

但实际上,您可能最好只将年龄添加到Person类中。更容易测试和所有这些。

听起来你可能会有一些输出格式化?那种事情应该在树枝上完成。 getAge应该只返回一个数字。

同样,您的出生日期确实应该是日期对象而不是字符串。


0
投票

你是对的,一般都气馁。但是,有几种方法可以扩展实体的功能,超出数据容器的目的。当然,所有这些都可以被认为(或多或少)糟糕的做法......但不知怎的,你必须做好这项工作,对吗?

  1. 你确实可以创建一个AbstractEntity超类,所有其他实体都可以从中继承。此AbstractEntity将包含其他实体可能需要的辅助方法。
  2. 您可以使用custom Doctrine repositories,如果您需要实体上下文来与实体管理器一起工作并返回“更特殊”的结果,而不是常见的getter给您的结果。由于您可以访问存储库中的实体管理器,因此可以执行各种特殊查询。
  3. 您可以编写负责相关实体/实体的服务。缺点:您无法控制代码的其他部分(或其他开发人员)知道此服务。优势:你可以做的事情没有限制,而且它都是很好的封装。
  4. 你可以使用Lifecycle Events/Callbacks
  5. 如果您确实需要将服务注入实体,则可以考虑在实体上设置静态属性,并仅在控制器或专用服务中设置一次。然后,您不需要注意对象的每次初始化。可以与AbstractEntity方法结合使用。

如前所述,所有这些都有其优点和缺点。选择你的毒药。

© www.soinside.com 2019 - 2024. All rights reserved.