创建一个包含另一个现有实体的新实体
Creating a new entity which contains another existing entity
我有以下两个表和相应的两个实体显示在这个 post 的底部。 time_unit
只包含s/second/1
、m/minute/60
、h/hour/360
等几条预设记录
我需要创建一个新的时间表。虽然没有显示,但我有几种类型的计划,它们以不同的方式使用提供的数据,因此希望将 setter 放在实体(构造函数或某些接口方法)而不是服务中。要创建新计划,我执行 $scheduleService->create(['name'=>'the schedule name', 'other_data'=>123, 'time_unit'=>'h']);
.
<?php
namespace Michael\App\Service;
use Michael\App\Entity;
class ScheduleService
{
public function create(array $params):int {
//validation as applicable
$schedule=new Entity\Schedule($params);
$this->em->persist($schedule);
$this->em->flush();
return $schedule->getId();
}
}
然后在Schedule实体中添加如下构造函数:
public function __construct(array $params) {
$this->setName($params['name']);
$this->setOtherData($params['other_data']);
$timeUnit=new TimeUnit();
$timeUnit->setUnit($params['time_unit']);
$this->setTimeUnit($timeUnit);
}
但这行不通,因为我正在创建一个新的 TimeUnit 实例,而 Doctrine 会报错。
作为替代方案,我可以通过 Schedule 实体管理器,但我读过的所有内容都表明这样做是不好的做法。
应该如何创建一个包含另一个现有实体的新实体?
没有附加逻辑的模式和基本实体如下所示:
CREATE TABLE schedule (id INT NOT NULL, time_unit VARCHAR(1) NOT NULL, name VARCHAR(45) NOT NULL, other_data VARCHAR(45) NOT NULL, INDEX fk_schedule_time_unit_idx (time_unit), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB;
CREATE TABLE time_unit (unit VARCHAR(1) NOT NULL, name VARCHAR(45) NOT NULL, seconds INT NOT NULL, PRIMARY KEY(unit)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB;
ALTER TABLE schedule ADD CONSTRAINT FK_5A3811FB7106057E FOREIGN KEY (time_unit) REFERENCES time_unit (unit);
schedule.php
<?php
namespace Michael\App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Schedule
*
* @ORM\Table(name="schedule", indexes={@ORM\Index(name="fk_schedule_time_unit_idx", columns={"time_unit"})})
* @ORM\Entity
*/
class Schedule
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=45)
*/
private $name;
/**
* @var string
*
* @ORM\Column(name="other_data", type="string", length=45)
*/
private $other_data;
//Not included since docs state one shouldn't map foreign keys to fields in an entity
//private $time_unit;
/**
* @var \TimeUnit
*
* @ORM\ManyToOne(targetEntity="TimeUnit")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="time_unit", referencedColumnName="unit")
* })
*/
private $timeUnit;
/**
* Set id.
*
* @param int $id
*
* @return Schedule
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name.
*
* @param string $name
*
* @return Schedule
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set otherData.
*
* @param string $otherData
*
* @return Schedule
*/
public function setOtherData($otherData)
{
$this->other_data = $otherData;
return $this;
}
/**
* Get otherData.
*
* @return string
*/
public function getOtherData()
{
return $this->other_data;
}
/**
* Set timeUnit.
*
* @param TimeUnit $timeUnit (not a string)
*
* @return Schedule
*/
public function setTimeUnit($timeUnit)
{
$this->timeUnit = $timeUnit;
return $this;
}
/**
* Get timeUnit.
*
* @return TimeUnit (not a string)
*/
public function getTimeUnit()
{
return $this->timeUnit;
}
}
time_unit.php
<?php
namespace Michael\App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* TimeUnit
*
* @ORM\Table(name="time_unit")
* @ORM\Entity
*/
class TimeUnit
{
/**
* @var string
*
* @ORM\Column(name="unit", type="string", length=1)
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
*/
private $unit;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=45)
*/
private $name;
/**
* @var int
*
* @ORM\Column(name="seconds", type="integer")
*/
private $seconds;
/**
* Set unit.
*
* @param string $unit
*
* @return TimeUnit
*/
public function setUnit($unit)
{
$this->unit = $unit;
return $this;
}
/**
* Get unit.
*
* @return string
*/
public function getUnit()
{
return $this->unit;
}
/**
* Set name.
*
* @param string $name
*
* @return TimeUnit
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set seconds.
*
* @param int $seconds
*
* @return TimeUnit
*/
public function setSeconds($seconds)
{
$this->seconds = $seconds;
return $this;
}
/**
* Get seconds.
*
* @return int
*/
public function getSeconds()
{
return $this->seconds;
}
}
将 EntityManager
传递给实体是一种不好的做法,因为 Doctrine 中的实体被用作数据对象,因此应该包含最少的逻辑。与实体相关的所有应用程序逻辑都应移至自定义存储库或单独的 class 属于应用程序服务层的实体。
在您的情况下,您需要将 TimeUnit
的实例直接传递给构造函数而不尝试在实体内部构造它,或者期望它通过 setter 方法设置。
相反,您需要修改 ScheduleService::create()
以允许自定义实体创建逻辑。由于您的 ScheduleService
基本上实现了 Factory method pattern you need to make one step further towards implementation of Abstract factory 模式。
抽象工厂基本上依赖于负责构建具体 class 实例的具体工厂列表,而不是试图在其内部包含所有可能的逻辑。请在您的案例中找到以下实施此类模式的示例。它可能看起来过于复杂,因为我提取了 2 个接口和抽象 class 并且可以通过使用 2 个单独的接口简化此方案,允许抽象和具体工厂共享公共基础,同时保留必要的差异。具体工厂的摘要class用于允许提取基本实体配置逻辑以避免代码重复。
/**
* Interface for Schedule entity factories
*/
interface AbstractScheduleFactoryInterface
{
/**
* Create schedule entity by given params
*
* @param array $params
* @return Schedule
*/
public function create(array $params = []): Schedule;
}
/**
* Interface for concrete Schedule entity factories
*/
interface ScheduleFactoryInterface extends AbstractScheduleFactoryInterface
{
/**
* Decide if this factory can create schedule entity with given params
*
* @param array $params
* @return bool
*/
public function canCreate(array $params): bool;
}
/**
* Implementation of "Abstract Factory" pattern that relies on concrete factories for constructing Schedule entities
*/
class ScheduleFactory implements AbstractScheduleFactoryInterface
{
/**
* @var ScheduleFactoryInterface[]
*/
private $factories;
/**
* @param ScheduleFactoryInterface[] $factories
*/
public function __construct(array $factories)
{
$this->factories = $factories;
}
/**
* {@inheritdoc}
*/
public function create(array $params = []): Schedule
{
// Select factory that is able to create Schedule entity by given params
/** @var ScheduleFactoryInterface $factory */
$factory = array_reduce($this->factories, function (?ScheduleFactoryInterface $selected, ScheduleFactoryInterface $current) use ($params) {
if ($selected) {
return $selected;
}
return $current->canCreate($params) ? $current : null;
});
if (!$factory) {
// We have no factory to construct Schedule entity by given params
throw new \InvalidArgumentException('Unable to construct Schedule entity by given params');
}
// Construct entity by using selected concrete factory
return $factory->create($params);
}
}
/**
* Base implementation of concrete Schedule entity factory
* to allow sharing some common code between factories
*/
abstract class AbstractScheduleFactory implements ScheduleFactoryInterface
{
/**
* Basic entity configuration to avoid code duplication in concrete factories
*
* @param Schedule $entity
* @param array $params
*/
protected function configure(Schedule $entity, array $params = []): void
{
// This code is more or less copied from your code snippet
$entity->setName($params['name'] ?? '');
$entity->setOtherData($params['other_data'] ?? '');
}
}
/**
* Example implementation of Schedule entity factory with Schedules with TimeUnit
*/
class TimeUnitScheduleFactory extends AbstractScheduleFactory
{
/**
* @var EntityManager
*/
private $em;
/**
* @param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* {@inheritdoc}
*/
public function canCreate(array $params): bool
{
return array_key_exists('time_unit', $params);
}
/**
* Create schedule entity by given params
*
* @param array $params
* @return Schedule
* @throws \RuntimeException
*/
public function create(array $params = []): Schedule
{
$schedule = new Schedule();
// Perform basic Schedule configuration using shared base code
$this->configure($schedule, $params);
try {
// Attempt to assign time unit
$timeUnit = $this->em->find(TimeUnit::class, $params['time_unit']);
if (!$timeUnit instanceof TimeUnit) {
// No TimeUnit is available in database - create one
$timeUnit = new TimeUnit();
$timeUnit->setUnit($params['time_unit']);
$this->em->persist($timeUnit);
}
$schedule->setTimeUnit($timeUnit);
} catch (ORMException $e) {
throw new \RuntimeException('Failed to get TimeUnit entity', 0, $e);
}
return $schedule;
}
}
如您所见 - 此方案允许您为 Schedule
实体拥有任意数量的具体工厂,这些实体需要作为构造函数参数传递给 ScheduleFactory
。之后 ScheduleFactory::create()
可用于创建具有不同构造逻辑的任何类型的 Schedule
实体。
我有以下两个表和相应的两个实体显示在这个 post 的底部。 time_unit
只包含s/second/1
、m/minute/60
、h/hour/360
等几条预设记录
我需要创建一个新的时间表。虽然没有显示,但我有几种类型的计划,它们以不同的方式使用提供的数据,因此希望将 setter 放在实体(构造函数或某些接口方法)而不是服务中。要创建新计划,我执行 $scheduleService->create(['name'=>'the schedule name', 'other_data'=>123, 'time_unit'=>'h']);
.
<?php
namespace Michael\App\Service;
use Michael\App\Entity;
class ScheduleService
{
public function create(array $params):int {
//validation as applicable
$schedule=new Entity\Schedule($params);
$this->em->persist($schedule);
$this->em->flush();
return $schedule->getId();
}
}
然后在Schedule实体中添加如下构造函数:
public function __construct(array $params) {
$this->setName($params['name']);
$this->setOtherData($params['other_data']);
$timeUnit=new TimeUnit();
$timeUnit->setUnit($params['time_unit']);
$this->setTimeUnit($timeUnit);
}
但这行不通,因为我正在创建一个新的 TimeUnit 实例,而 Doctrine 会报错。
作为替代方案,我可以通过 Schedule 实体管理器,但我读过的所有内容都表明这样做是不好的做法。
应该如何创建一个包含另一个现有实体的新实体?
没有附加逻辑的模式和基本实体如下所示:
CREATE TABLE schedule (id INT NOT NULL, time_unit VARCHAR(1) NOT NULL, name VARCHAR(45) NOT NULL, other_data VARCHAR(45) NOT NULL, INDEX fk_schedule_time_unit_idx (time_unit), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB;
CREATE TABLE time_unit (unit VARCHAR(1) NOT NULL, name VARCHAR(45) NOT NULL, seconds INT NOT NULL, PRIMARY KEY(unit)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB;
ALTER TABLE schedule ADD CONSTRAINT FK_5A3811FB7106057E FOREIGN KEY (time_unit) REFERENCES time_unit (unit);
schedule.php
<?php
namespace Michael\App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Schedule
*
* @ORM\Table(name="schedule", indexes={@ORM\Index(name="fk_schedule_time_unit_idx", columns={"time_unit"})})
* @ORM\Entity
*/
class Schedule
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=45)
*/
private $name;
/**
* @var string
*
* @ORM\Column(name="other_data", type="string", length=45)
*/
private $other_data;
//Not included since docs state one shouldn't map foreign keys to fields in an entity
//private $time_unit;
/**
* @var \TimeUnit
*
* @ORM\ManyToOne(targetEntity="TimeUnit")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="time_unit", referencedColumnName="unit")
* })
*/
private $timeUnit;
/**
* Set id.
*
* @param int $id
*
* @return Schedule
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name.
*
* @param string $name
*
* @return Schedule
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set otherData.
*
* @param string $otherData
*
* @return Schedule
*/
public function setOtherData($otherData)
{
$this->other_data = $otherData;
return $this;
}
/**
* Get otherData.
*
* @return string
*/
public function getOtherData()
{
return $this->other_data;
}
/**
* Set timeUnit.
*
* @param TimeUnit $timeUnit (not a string)
*
* @return Schedule
*/
public function setTimeUnit($timeUnit)
{
$this->timeUnit = $timeUnit;
return $this;
}
/**
* Get timeUnit.
*
* @return TimeUnit (not a string)
*/
public function getTimeUnit()
{
return $this->timeUnit;
}
}
time_unit.php
<?php
namespace Michael\App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* TimeUnit
*
* @ORM\Table(name="time_unit")
* @ORM\Entity
*/
class TimeUnit
{
/**
* @var string
*
* @ORM\Column(name="unit", type="string", length=1)
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
*/
private $unit;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=45)
*/
private $name;
/**
* @var int
*
* @ORM\Column(name="seconds", type="integer")
*/
private $seconds;
/**
* Set unit.
*
* @param string $unit
*
* @return TimeUnit
*/
public function setUnit($unit)
{
$this->unit = $unit;
return $this;
}
/**
* Get unit.
*
* @return string
*/
public function getUnit()
{
return $this->unit;
}
/**
* Set name.
*
* @param string $name
*
* @return TimeUnit
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set seconds.
*
* @param int $seconds
*
* @return TimeUnit
*/
public function setSeconds($seconds)
{
$this->seconds = $seconds;
return $this;
}
/**
* Get seconds.
*
* @return int
*/
public function getSeconds()
{
return $this->seconds;
}
}
将 EntityManager
传递给实体是一种不好的做法,因为 Doctrine 中的实体被用作数据对象,因此应该包含最少的逻辑。与实体相关的所有应用程序逻辑都应移至自定义存储库或单独的 class 属于应用程序服务层的实体。
在您的情况下,您需要将 TimeUnit
的实例直接传递给构造函数而不尝试在实体内部构造它,或者期望它通过 setter 方法设置。
相反,您需要修改 ScheduleService::create()
以允许自定义实体创建逻辑。由于您的 ScheduleService
基本上实现了 Factory method pattern you need to make one step further towards implementation of Abstract factory 模式。
抽象工厂基本上依赖于负责构建具体 class 实例的具体工厂列表,而不是试图在其内部包含所有可能的逻辑。请在您的案例中找到以下实施此类模式的示例。它可能看起来过于复杂,因为我提取了 2 个接口和抽象 class 并且可以通过使用 2 个单独的接口简化此方案,允许抽象和具体工厂共享公共基础,同时保留必要的差异。具体工厂的摘要class用于允许提取基本实体配置逻辑以避免代码重复。
/**
* Interface for Schedule entity factories
*/
interface AbstractScheduleFactoryInterface
{
/**
* Create schedule entity by given params
*
* @param array $params
* @return Schedule
*/
public function create(array $params = []): Schedule;
}
/**
* Interface for concrete Schedule entity factories
*/
interface ScheduleFactoryInterface extends AbstractScheduleFactoryInterface
{
/**
* Decide if this factory can create schedule entity with given params
*
* @param array $params
* @return bool
*/
public function canCreate(array $params): bool;
}
/**
* Implementation of "Abstract Factory" pattern that relies on concrete factories for constructing Schedule entities
*/
class ScheduleFactory implements AbstractScheduleFactoryInterface
{
/**
* @var ScheduleFactoryInterface[]
*/
private $factories;
/**
* @param ScheduleFactoryInterface[] $factories
*/
public function __construct(array $factories)
{
$this->factories = $factories;
}
/**
* {@inheritdoc}
*/
public function create(array $params = []): Schedule
{
// Select factory that is able to create Schedule entity by given params
/** @var ScheduleFactoryInterface $factory */
$factory = array_reduce($this->factories, function (?ScheduleFactoryInterface $selected, ScheduleFactoryInterface $current) use ($params) {
if ($selected) {
return $selected;
}
return $current->canCreate($params) ? $current : null;
});
if (!$factory) {
// We have no factory to construct Schedule entity by given params
throw new \InvalidArgumentException('Unable to construct Schedule entity by given params');
}
// Construct entity by using selected concrete factory
return $factory->create($params);
}
}
/**
* Base implementation of concrete Schedule entity factory
* to allow sharing some common code between factories
*/
abstract class AbstractScheduleFactory implements ScheduleFactoryInterface
{
/**
* Basic entity configuration to avoid code duplication in concrete factories
*
* @param Schedule $entity
* @param array $params
*/
protected function configure(Schedule $entity, array $params = []): void
{
// This code is more or less copied from your code snippet
$entity->setName($params['name'] ?? '');
$entity->setOtherData($params['other_data'] ?? '');
}
}
/**
* Example implementation of Schedule entity factory with Schedules with TimeUnit
*/
class TimeUnitScheduleFactory extends AbstractScheduleFactory
{
/**
* @var EntityManager
*/
private $em;
/**
* @param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* {@inheritdoc}
*/
public function canCreate(array $params): bool
{
return array_key_exists('time_unit', $params);
}
/**
* Create schedule entity by given params
*
* @param array $params
* @return Schedule
* @throws \RuntimeException
*/
public function create(array $params = []): Schedule
{
$schedule = new Schedule();
// Perform basic Schedule configuration using shared base code
$this->configure($schedule, $params);
try {
// Attempt to assign time unit
$timeUnit = $this->em->find(TimeUnit::class, $params['time_unit']);
if (!$timeUnit instanceof TimeUnit) {
// No TimeUnit is available in database - create one
$timeUnit = new TimeUnit();
$timeUnit->setUnit($params['time_unit']);
$this->em->persist($timeUnit);
}
$schedule->setTimeUnit($timeUnit);
} catch (ORMException $e) {
throw new \RuntimeException('Failed to get TimeUnit entity', 0, $e);
}
return $schedule;
}
}
如您所见 - 此方案允许您为 Schedule
实体拥有任意数量的具体工厂,这些实体需要作为构造函数参数传递给 ScheduleFactory
。之后 ScheduleFactory::create()
可用于创建具有不同构造逻辑的任何类型的 Schedule
实体。