主义坚持多对一实体

问题描述 投票:0回答:1

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

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

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

通过“Application \ Entity \ Cidade#estado”关系找到了一个新实体,该关系未配置为级联实体的持久化操作:Application \ Entity \ Estado @ 000000007598ee720000000027904e61。

我发现了一个类似的疑问here,我能找到的唯一方法就是首先检索Estado Entity并在保存前将其设置在Cidade Entity上。如果这是唯一的方法,我可以告诉我的表单结构不起作用,除非我在保存依赖实体之前检索所有关系吗?换句话说,在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();

如何在不需要检索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;
         }
     }

我的国家实体:

<?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问题上崩溃。我该如何继续前进?

php doctrine-orm cascade many-to-one zend-framework3
1个回答
0
投票

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

关注分离我绝对不反对在实体本身内放置实体的InputFilter。因此,如果你决定将它们混合在一起,我会给你一个分开的例子,这取决于你。

AbstractEntity for 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 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
}

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

您希望使用表单轻松管理此操作。所以我们需要为两者创建InputFilters。

将InputFilters与Entity分开允许我们嵌套它们。这反过来允许我们创建结构化和嵌套的表单。

例如,您可以即时创建新的Estado。如果这是双向关系,您可以在创建Estado时动态创建多个Cicade Entity对象。

第一:InputFilters。本着抽象的精神,你从你的实体开始,我们也在这里做:


AbstractDoctrineInputFilter

source AbstractDoctrineInputFiltersource 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个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;
    }
}

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。

Below the 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,
            ]
        );
    }
}

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

所以,感觉就像给出一个真正扩展的答案。真的涵盖了整个事情。

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

© www.soinside.com 2019 - 2024. All rights reserved.