创建一个包含另一个现有实体的新实体

Creating a new entity which contains another existing entity

我有以下两个表和相应的两个实体显示在这个 post 的底部。 time_unit只包含s/second/1m/minute/60h/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 实体。