Doctrine 坚持多对一实体

Doctrine persisting Many-To-One entities

我将 Zend Framework 3 与 Doctrine 一起使用,我正在尝试保存一个与另一个实体 "Estado" 相关的实体 "Cidade" 已存储在数据库中。但是,Doctrine 试图保留实体 "Estado",而我从 Estado 获得的唯一属性是 HTML 组合中的主键。

我的视图表单是在 Zend 表单和字段集下构建的,这意味着 POST 数据会使用 ClassMethods hydrator 自动转换为目标实体。

问题是,如果我在 Cidade 实体中将属性 $estado 设置为 cascade={"persist"},Doctrine 会尝试保留 Estado 实体缺少所有必需的属性,但主键 ID 来自 POST 请求(HTML 组合)。我还考虑过使用 cascade={"detach"} ir 命令 Doctrine 忽略 EntityManager 中的 Estado 实体。但是我得到这个错误:

A new entity was found through the relationship 'Application\Entity\Cidade#estado' that was not configured to cascade persist operations for entity: Application\Entity\Estado@000000007598ee720000000027904e61.

我发现了类似的疑问here,我能找到的唯一方法是先检索 Estado 实体并将其设置在 Cidade 实体上,然后再保存。如果这是唯一的方法,我可以告诉我的表单结构不起作用,除非我在保存依赖实体之前检索所有关系吗? 换句话说,在 Doctrine 中做这种事情的最佳方式是什么(例如):

<?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();

如何在我需要保存 Cidade 时不必一直检索 Estado?不会影响性能吗?

我的 Cidade 实体:

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

我的 Estado 实体:

<?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 数据会自动转换为目标实体,这让我在这个 Doctrine 问题上崩溃了。我该如何继续?

您应该将一个对象绑定到您的表单并使用 Doctrine Hydrator。在表单中,字段名称应与实体的名称完全匹配。所以 Entity#nameForm#name

考虑到关注点分离,我绝对反对将实体的 InputFilter 放在实体本身中。因此,我会给你一个所有东西都分开的例子,如果你决定把它重新组合在一起,那取决于你。

ID 的抽象实体

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

Cicade 实体

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

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

    //getters/setters
}

所以,以上是 Many to One - Uni-direction 关系的实体设置。

您想使用表单轻松地管理它。所以我们需要为两者创建 InputFilters。

从实体中单独输入过滤器允许我们嵌套它们。这反过来又允许我们创建结构化和嵌套的表单。

例如,您可以即时创建一个新的 Estado。如果这是双向关系,您可以即时创建多个 Cicade 实体对象 from/during Estado 的创建。

首先:输入过滤器。本着您从实体开始的抽象精神,让我们在这里也这样做:


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 输入过滤器

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 输入过滤器

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 个 InputFilters,基于 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;
    }
}

配置

现在类有了,怎么用呢? 将它们与模块配置一起拍打!

在您的 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
    ],
],

从这个配置中我们读到,我们需要一个用于 Form 和集合的 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;
    }
}

所以,我想给出一个真正扩展的答案。真的涵盖了整个事情。

如果您对以上内容有任何疑问,请告诉我 ;-)