Доктрина, сохраняющая сущности «многие к одному» - PullRequest
0 голосов
/ 02 сентября 2018

Я использую Zend Framework 3 с Doctrine и пытаюсь сохранить сущность "Cidade", связанную с другой ведьмой сущности "Estado", уже сохраненную в базе данных. Однако Doctrine пытается сохранить сущность "Estado", и единственный атрибут, который у меня есть от Estado, - это первичный ключ в HTML-комбо.

Формы моего представления построены на основе форм Zend и наборов полей, что означает, что данные POST автоматически преобразуются в целевые объекты с помощью гидратора ClassMethods.

Проблема в том, что если я установлю атрибут $estado с помощью cascade={"persist"} в Cidade Entity, Doctrine попытается сохранить в Estado Entity пропущенные все необходимые атрибуты, кроме идентификатора первичного ключа, который поступает из запроса POST (комбинированный код HTML). Я также подумал об использовании cascade={"detach"} ir для того, чтобы Doctrine игнорировала Estado Entity в EntityManager. Но я получаю эту ошибку:

Новый объект был обнаружен в связи «Application \ Entity \ Cidade # estado», которая не была настроена для каскадного сохранения операций для объекта: Application \ Entity \ Estado@000000007598ee720000000027904e61.

Я нашел подобное сомнение здесь , и единственный способ, который я смог найти в этом вопросе, - это сначала получить объект Estado и установить его в Cidade, а затем сохранить. Если это единственный способ, могу ли я сказать, что моя структура формы не будет работать, если я не получу все отношения перед сохранением зависимых объектов? Другими словами, как лучше всего сделать это в Учении (например):

<?php
    /*I'm simulating the creation of Estado Entity representing an
    existing Estado in database, so "3" is the ID rendered in HTML combo*/
    $estado = new Entity\Estado();
    $estado->setId(3);

    $cidade = new Entity\Cidade();
    $cidade->setNome("City Test");

    $cidade->setEstado($estado); //relationship here

    $entityManager->persist($cidade);
    $entityManager->flush();

Как это сделать, не возвращая Estado все время, необходимое для сохранения Cidade? Не повлияет ли это на производительность?

My Cidade Entity:

<?php

     namespace Application\Entity;

     use Zend\InputFilter\Factory;
     use Zend\InputFilter\InputFilterInterface;
     use Doctrine\ORM\Mapping as ORM;

     /**
      * Class Cidade
      * @package Application\Entity
      * @ORM\Entity
      */
     class Cidade extends AbstractEntity
     {
         /**
          * @var string
          * @ORM\Column(length=50)
          */
         private $nome;

         /**
          * @var Estado
          * @ORM\ManyToOne(targetEntity="Estado", cascade={"detach"})
          * @ORM\JoinColumn(name="id_estado", referencedColumnName="id")
          */
         private $estado;

         /**
          * Retrieve input filter
          *
          * @return InputFilterInterface
          */
         public function getInputFilter()
         {
             if (!$this->inputFilter) {
                 $factory = new Factory();
                 $this->inputFilter = $factory->createInputFilter([
                     "nome" => ["required" => true]
                 ]);
             }
             return $this->inputFilter;
         }

         /**
          * @return string
          */
         public function getNome()
         {
             return $this->nome;
         }

         /**
          * @param string $nome
          */
         public function setNome($nome)
         {
             $this->nome = $nome;
         }

         /**
          * @return Estado
          */
         public function getEstado()
         {
             return $this->estado;
         }

         /**
          * @param Estado $estado
          */
         public function setEstado($estado)
         {
             $this->estado = $estado;
         }
     }

My Estado Entity:

<?php

    namespace Application\Entity;

    use Doctrine\ORM\Mapping as ORM;
    use Zend\InputFilter\Factory;
    use Zend\InputFilter\InputFilterInterface;

    /**
     * Class Estado
     * @package Application\Entity
     * @ORM\Entity
     */
    class Estado extends AbstractEntity
    {
        /**
         * @var string
         * @ORM\Column(length=50)
         */
        private $nome;

        /**
         * @var string
         * @ORM\Column(length=3)
         */
        private $sigla;

        /**
         * @return string
         */
        public function getNome()
        {
            return $this->nome;
        }

        /**
         * @param string $nome
         */
        public function setNome($nome)
        {
            $this->nome = $nome;
        }

        /**
         * @return string
         */
        public function getSigla()
        {
            return $this->sigla;
        }

        /**
         * @param string $sigla
         */
        public function setSigla($sigla)
        {
            $this->sigla = $sigla;
        }

        /**
         * Retrieve input filter
         *
         * @return InputFilterInterface
         */
        public function getInputFilter()
        {
            if (!$this->inputFilter) {
                $factory = new Factory();
                $this->inputFilter = $factory->createInputFilter([
                    "nome" => ["required" => true],
                    "sigla" => ["required" => true]
                ]);
            }
            return $this->inputFilter;
        }
    }

Обе сущности расширяют мой суперкласс AbstractEntity:

<?php

    namespace Application\Entity;

    use Doctrine\ORM\Mapping\MappedSuperclass;
    use Doctrine\ORM\Mapping as ORM;
    use Zend\InputFilter\InputFilterAwareInterface;
    use Zend\InputFilter\InputFilterInterface;

    /**
     * Class AbstractEntity
     * @package Application\Entity
     * @MappedSuperClass
     */
    abstract class AbstractEntity implements InputFilterAwareInterface
    {
        /**
         * @var int
         * @ORM\Id
         * @ORM\GeneratedValue
         * @ORM\Column(type="integer")
         */
        protected $id;

        /**
         * @var InputFilterAwareInterface
         */
        protected $inputFilter;

        /**
         * @return int
         */
        public function getId()
        {
            return $this->id;
        }

        /**
         * @param int $id
         */
        public function setId($id)
        {
            $this->id = $id;
        }

        /**
         * @param InputFilterInterface $inputFilter
         * @return InputFilterAwareInterface
         * @throws \Exception
         */
        public function setInputFilter(InputFilterInterface $inputFilter)
        {
            throw new \Exception("Método não utilizado");
        }
    }

Мои входные данные HTML отображаются следующим образом:

<input name="cidade[nome]" class="form-control" value="" type="text">
<select name="cidade[estado][id]" class="form-control">
    <option value="3">Bahia</option>
    <option value="2">Espírito Santo</option>
    <option value="1">Minas Gerais</option>
    <option value="9">Pará</option>
</select>

Каждый option выше является объектом Estado, полученным из базы данных. Мои данные POST представлены в следующем примере:

[
    "cidade" => [
        "nome" => "Test",
        "estado" => [
            "id" => 3
        ]
    ]
]

В методе Zend Form isValid() эти данные POST автоматически преобразуются в целевые объекты, что приводит к сбою в этой проблеме доктрины. Как мне двигаться дальше?

1 Ответ

0 голосов
/ 03 сентября 2018

Вы должны привязать объект к вашей форме и использовать Doctrine Hydrator. В форме имена полей должны точно соответствовать имени сущности. Так что Entity#name это Form#name.

С разделением проблем я категорически против размещения InputFilter для сущности внутри самой сущности. Таким образом, я дам вам пример со всем отделенным, если вы решите смешать все вместе, это ваше дело.

AbstractEntity для идентификатора

/**
 * @ORM\MappedSuperclass
 */
abstract class AbstractEntity
{
    /**
     * @var int
     * @ORM\Id
     * @ORM\Column(name="id", type="integer")
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;
    // getter/setter
}

Cicade Entity

/**
 * @ORM\Entity
 */
class Cidade extends AbstractEntity
{
    /**
     * @var string
     * @ORM\Column(length=50)
     */
    protected $nome; // Changed to 'protected' so can be used in child classes - if any

    /**
     * @var Estado
     * @ORM\ManyToOne(targetEntity="Estado", cascade={"persist", "detach"}) // persist added
     * @ORM\JoinColumn(name="id_estado", referencedColumnName="id")
     */
    protected $estado;

    // getters/setters
}

Estado Entity

/**
 * @ORM\Entity
 */
class Estado extends AbstractEntity
{
    /**
     * @var string
     * @ORM\Column(length=50)
     */
    protected $nome;

    //getters/setters
}

Итак, выше приведена настройка сущности для Много к одному - однонаправленное отношение.

Вы хотите управлять этим легко с помощью форм. Поэтому нам нужно создать InputFilters для обоих.

Наличие входных фильтров отдельно от сущности позволяет нам вкладывать их. Это, в свою очередь, позволяет нам создавать структурированные и вложенные формы.

Например, вы можете создать новое Estado на лету. Если бы это было двунаправленное отношение, вы могли бы создать несколько объектов Cicade Entity на лету из / во время создания Estado.

Первый: InputFilters. В духе абстракции, который вы начали с вашими сущностями, давайте сделаем это и здесь:


AbstractDoctrineInputFilter

source AbstractDoctrineInputFilter & source AbstractDoctrineFormInputFilter

Это дает хорошую чистую настройку и требование для выполнения. Я закрываю глаза на более сложные элементы, добавленные в исходные файлы, но не стесняйтесь их искать.

Для обоих объектов (Estado и Cicade) требуется ObjectManager (в конце концов, это сущности Doctrine), поэтому я предполагаю, что у вас может быть больше. Ниже должно пригодиться.

<?php
namespace Application\InputFilter;

use Doctrine\Common\Persistence\ObjectManager;
use Zend\InputFilter\InputFilter;

abstract class AbstractInputFilter extends InputFilter
{
    /**
     * @var ObjectManager
     */
    protected $objectManager;

    /**
     * AbstractFormInputFilter constructor.
     *
     * @param array $options
     */
    public function __construct(array $options)
    {
        // Check if ObjectManager|EntityManager for FormInputFilter is set
        if (isset($options['object_manager']) && $options['object_manager'] instanceof ObjectManager) {
            $this->setObjectManager($options['object_manager']);
        }
    }

    /**
     * Init function
     */
    public function init()
    {
        $this->add(
            [
                'name' => 'id',
                'required' => false, // Not required when adding - should also be in route when editing and bound in controller, so just additional
                'filters' => [
                    ['name' => ToInt::class],
                ],
                'validators' => [
                    ['name' => IsInt::class],
                ],
            ]
       );

        // If CSRF validation has not been added, add it here
        if ( ! $this->has('csrf')) {
            $this->add(
                [
                    'name'       => 'csrf',
                    'required'   => true,
                    'filters'    => [],
                    'validators' => [
                        ['name' => Csrf::class],
                    ],
                ]
            );
        }
    }

    // getters/setters for ObjectManager
}

Estado InputFilter

class EstadoInputFilter extends AbstractInputFilter
{
    public function init()
    {
        parent::init();

        $this->add(
            [
                'name'        => 'nome', // <-- important, name matches entity property
                'required'    => true,
                'allow_empty' => true,
                'filters'     => [
                    ['name' => StringTrim::class],
                    ['name' => StripTags::class],
                    [
                        'name'    => ToNull::class,
                        'options' => [
                            'type' => ToNull::TYPE_STRING,
                        ],
                    ],
                ],
                'validators'  => [
                    [
                        'name'    => StringLength::class,
                        'options' => [
                            'min' => 2,
                            'max' => 255,
                        ],
                    ],
                ],
            ]
        );
    }
}

Cicade InputFilter

class EstadoInputFilter extends AbstractInputFilter
{
    public function init()
    {
        parent::init(); // Adds the CSRF

        $this->add(
            [
                'name'        => 'nome', // <-- important, name matches entity property
                'required'    => true,
                'allow_empty' => true,
                'filters'     => [
                    ['name' => StringTrim::class],
                    ['name' => StripTags::class],
                    [
                        'name'    => ToNull::class,
                        'options' => [
                            'type' => ToNull::TYPE_STRING,
                        ],
                    ],
                ],
                'validators'  => [
                    [
                        'name'    => StringLength::class,
                        'options' => [
                            'min' => 2,
                            'max' => 255,
                        ],
                    ],
                ],
            ]
        );

        $this->add(
            [
                'name'     => 'estado',
                'required' => true,
            ]
        );
    }
}

Итак. Теперь у нас есть 2 InputFilter, основанных на AbstractInputFilter.

EstadoInputFilter фильтрует только свойство nome. Добавьте дополнительные, если хотите;)

CicadeInputFilter фильтрует свойство nome и имеет обязательное поле estado.

Имена соответствуют именам определения сущности в соответствующих классах сущности.

Просто чтобы завершить, ниже CicadeForm, возьмите то, что вам нужно для создания EstadoForm.

class CicadeForm extends Form
{

    /**
     * @var ObjectManager
     */
    protected $objectManager;

    /**
     * AbstractFieldset constructor.
     *
     * @param ObjectManager $objectManager
     * @param string        $name Lower case short class name
     * @param array         $options
     */
    public function __construct(ObjectManager $objectManager, string $name, array $options = [])
    {
        parent::__construct($name, $options);

        $this->setObjectManager($objectManager);
    }

    public function init()
    {
        $this->add(
            [
                'name'     => 'nome',
                'required' => true,
                'type'     => Text::class,
                'options'  => [
                    'label' => _('Nome',
                ],
            ]
        );

        // @link: https://github.com/doctrine/DoctrineModule/blob/master/docs/form-element.md
        $this->add(
            [
                'type'       => ObjectSelect::class,
                'required'   => true,
                'name'       => 'estado',
                'options'    => [
                    'object_manager'     => $this->getObjectManager(),
                    'target_class'       => Estado::class,
                    'property'           => 'id',
                    'display_empty_item' => true,
                    'empty_item_label'   => '---',
                    'label'              => _('Estado'),
                    'label_attributes'   => [
                        'title' => _('Estado'),
                    ],
                    'label_generator'    => function ($targetEntity) {
                        /** @var Estado $targetEntity */
                        return $targetEntity->getNome();
                    },
                ],
            ]
        );

        //Call parent initializer. Check in parent what it does.
        parent::init();
    }

    /**
     * @return ObjectManager
     */
    public function getObjectManager() : ObjectManager
    {
        return $this->objectManager;
    }

    /**
     * @param ObjectManager $objectManager
     *
     * @return AbstractDoctrineFieldset
     */
    public function setObjectManager(ObjectManager $objectManager) : AbstractDoctrineFieldset
    {
        $this->objectManager = $objectManager;
        return $this;
    }
}

Config

Теперь, когда есть классы, как их использовать? Бросьте их вместе с конфигурацией модуля!

В ваш файл module.config.php добавьте эту конфигурацию:

'form_elements'   => [
    'factories' => [
        CicadeForm::class => CicadeFormFactory::class,
        EstadoForm::class => EstadoFormFactory::class,

        // If you create separate Fieldset classes, this is where you register those
    ],
],
'input_filters'   => [
    'factories' => [
        CicadeInputFilter::class => CicadeInputFilterFactory::class,
        EstadoInputFilter::class => EstadoInputFilterFactory::class,

        // If you register Fieldsets in form_elements, their InputFilter counterparts go here
    ],
],

Из этой конфигурации мы читаем, что нам нужна фабрика как для формы, так и для InputFilter набора.

Ниже CicadeInputFilterFactory

class CicadeInputFilterFactory implements FactoryInterface
{
    /**
     * @param ContainerInterface $container
     * @param string             $requestedName
     * @param array|null         $options
     *
     * @return CicadeInputFilter
     * @throws \Psr\Container\ContainerExceptionInterface
     * @throws \Psr\Container\NotFoundExceptionInterface
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        /** @var ObjectManager|EntityManager $objectManager */
        $objectManager = $this->setObjectManager($container->get(EntityManager::class));

        return new CicadeInputFilter(
            [
                'object_manager' => objectManager,
            ]
        );
    }
}

Соответствует CicadeFormFactory

class CicadeFormFactory implements FactoryInterface
{
    /**
     * @param ContainerInterface $container
     * @param string             $requestedName
     * @param array|null         $options
     *
     * @return CicadeForm
     * @throws \Psr\Container\ContainerExceptionInterface
     * @throws \Psr\Container\NotFoundExceptionInterface
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null) : CicadeForm
    {
        $inputFilter = $container->get('InputFilterManager')->get(CicadeInputFilter::class);

        // Here we creazte a new Form object. We set the InputFilter we created earlier and we set the DoctrineHydrator. This hydrator can work with Doctrine Entities and relations, so long as data is properly formatted when it comes in from front-end.
        $form = $container->get(CicadeForm::class);
        $form->setInputFilter($inputFilter);
        $form->setHydrator(
            new DoctrineObject($container->get(EntityManager::class))
        );
        $form->setObject(new Cicade());

        return $form;
    }
}

Массовая подготовка сделана, время ее использовать

Определенный EditController для редактирования существующего Cicade Объекта

class EditController extends AbstractActionController // (Zend's AAC)
{
    /**
     * @var CicadeForm
     */
    protected $cicadeForm;

    /**
     * @var ObjectManager|EntityManager
     */
    protected $objectManager;

    public function __construct(
        ObjectManager $objectManager, 
        CicadeForm $cicadeForm
    ) {
        $this->setObjectManager($objectManager);
        $this->setCicadeForm($cicadeForm);
    }

    /**
     * @return array|Response
     * @throws ORMException|Exception
     */
    public function editAction()
    {
        $id = $this->params()->fromRoute('id', null);

        if (is_null($id)) {

            $this->redirect()->toRoute('home'); // Do something more useful instead of this, like notify of id received from route
        }

        /** @var Cicade $entity */
        $entity = $this->getObjectManager()->getRepository(Cicade::class)->find($id);

        if (is_null($entity)) {

            $this->redirect()->toRoute('home'); // Do something more useful instead of this, like notify of not found entity
        }

        /** @var CicadeForm $form */
        $form = $this->getCicadeForm();
        $form->bind($entity); // <-- This here is magic. Because we overwrite the object from the Factory with an existing one. This pre-populates the form with value and allows us to modify existing one. Assumes we got an entity above.

        /** @var Request $request */
        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setData($request->getPost());

            if ($form->isValid()) {
                /** @var Cicade $cicade */
                $cicade = $form->getObject();

                $this->getObjectManager()->persist($cicade);

                try {
                    $this->getObjectManager()->flush();
                } catch (Exception $e) {

                    throw new Exception('Could not save. Error was thrown, details: ', $e->getMessage());
                }

                $this->redirect()->toRoute('cicade/view', ['id' => $entity->getId()]);
            }
        }

        return [
            'form'               => $form,
            'validationMessages' => $form->getMessages() ?: '',
        ];
    }

    /**
     * @return CicadeForm
     */
    public function getCicadeForm() : CicadeForm
    {
        return $this->cicadeForm;
    }

    /**
     * @param CicadeForm $cicadeForm
     *
     * @return EditController
     */
    public function setCicadeForm(CicadeForm $cicadeForm) : EditController
    {
        $this->cicadeForm = $cicadeForm;

        return $this;
    }

    /**
     * @return ObjectManager|EntityManager
     */
    public function getObjectManager() : ObjectManager
    {
        return $this->objectManager;
    }

    /**
     * @param ObjectManager|EntityManager $objectManager
     *
     * @return EditController
     */
    public function setObjectManager(ObjectManager $objectManager) : EditController
    {
        $this->objectManager = $objectManager;
        return $this;
    }
}

Итак, хотелось дать действительно расширенный ответ. Охватывает все это на самом деле.

Если у вас есть какие-либо вопросы по поводу вышесказанного, дайте мне знать ;-)

...