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

时间:2019-02-25 15:20:44

标签: php orm doctrine-orm datamapper

本文的底部显示了以下两个表以及相应的两个实体。 time_unit仅包含几个预设记录,分别是s/second/1m/minute/60h/hour/360等。

我需要创建一个新的时间表。尽管未显示,但我有几种时间表,它们以不同的方式使用所提供的数据,因此希望将设置器放置在实体(构造函数或某种接口方法)内而不是在服务中。要创建新时间表,请执行$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实体管理器,但是我所阅读的所有内容都指出这样做是一种不好的做法。

一个人应该如何创建一个包含另一个现有实体的新实体?


没有附加逻辑的架构和基本实体如下所示:

enter image description here

 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;
    }
}

1 个答案:

答案 0 :(得分:1)

EntityManager传递给实体是一种不好的做法,因为Doctrine中的实体用作数据对象,因此应包含最少数量的逻辑。与实体相关的所有应用程序逻辑应移至自定义存储库或属于应用程序服务层的单独类。

在您的情况下,您需要将TimeUnit的实例直接传递给构造函数,而无需尝试在实体内部构造它,或者希望通过setter方法对其进行设置。

相反,您需要修改ScheduleService::create()以允许自定义实体创建逻辑。由于您的ScheduleService基本实现了Factory method模式,因此您需要朝着实现Abstract factory模式的目标迈进一步。

抽象工厂基本上依赖于负责构造具体类实例的具体工厂列表,而不是尝试在其内部包括所有可能的逻辑。请在您的情况下找到以下实施此模式的示例。它可能看起来过于复杂,因为我已经提取了2个接口和抽象类,并且可以通过使用2个单独的接口简化该方案,允许抽象工厂和具体工厂共享公共基础,同时保留必要的区别。具体工厂的抽象类用于允许提取基本实体配置逻辑以避免代码重复。

/**
 * 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实体。