Создание новой сущности, которая содержит другую существующую сущность - PullRequest
0 голосов
/ 25 февраля 2019

У меня есть следующие две таблицы и соответствующие две сущности, показанные внизу этого поста.time_unit состоит только из нескольких предустановленных записей: s/second/1, m/minute/60, h/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 голосов
/ 25 февраля 2019

Передача EntityManager сущностям является плохой практикой, поскольку сущности в Doctrine используются в качестве объектов данных и, следовательно, должны содержать минимальное количество логики.Вся логика приложения, связанная с сущностями, должна быть перемещена либо в пользовательские репозитории, либо в отдельные классы, принадлежащие уровню обслуживания приложения.

В вашем случае вам нужно либо передать экземпляр TimeUnit непосредственно в конструктор, не пытаясь создать его внутри сущности, либо ожидать, что он будет установлен через метод установки.

Вместо этого вам нужно изменитьваш ScheduleService::create(), чтобы позволить логику создания сущностей быть настраиваемой.Поскольку ваш ScheduleService в основном реализует шаблон Фабрика , вам необходимо сделать еще один шаг к реализации шаблона Абстрактная фабрика .

Абстрактная фабрика в основном опирается на список конкретныхфабрики, которые отвечают за конструирование конкретных экземпляров классов вместо того, чтобы пытаться включить всю возможную логику в себя.Ниже приведен пример реализации такой схемы в вашем случае.Это может показаться слишком сложным, потому что я извлек 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 с различной логикой построения.

...