dongxingdu9594 2018-09-02 16:05
浏览 592
已采纳

主义坚持多对一实体

I'm using Zend Framework 3 with Doctrine and I'm trying to save an Entity "Cidade" related to another Entity "Estado" witch is already stored in the database. However, Doctrine is trying to persist Entity "Estado", and the only attribute I have from Estado is the primary key in a HTML combo.

My view forms are built under Zend forms and fieldsets, which means POST data is automatically converted to the target entities using ClassMethods hydrator.

The problem is that if I set the attribute $estado with cascade={"persist"} in Cidade Entity, Doctrine tries to persist Estado Entity missing all required attributes but the primary key ID, which comes from POST request (HTML combo). I also considered using cascade={"detach"} ir order to Doctrine ignore Estado Entity in the EntityManager. But I get this error:

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.

I found a similar doubt here and the only way I could find for this matter was first retrieving Estado Entity and setting it on Cidade Entity before saving. If this is the only way, can I tell my form structure won't work unless I retrieve all relationships before saving the dependant entities? In other words, what is the best way of doing such thing in Doctrine (for example):

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

How to do that without having to retrieve an Estado all the time I need to save a Cidade? Wouldn't affect performance?

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

Both entities extend my superclass 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");
        }
    }

My HTML inputs are rendered as it follows:

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

Each option above is an Estado Entity retrieved from database. My POST data comes as the following example:

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

On Zend Form's isValid() method, this POST data is automatically converted to the target Entities, which makes me crash on this Doctrine issue. How do I move on?

  • 写回答

1条回答 默认 最新

  • donglu2523 2018-09-03 08:34
    关注

    You should bind an object to your form and use the Doctrine Hydrator. In the form the field names should exactly match that of the Entity. So Entity#name is Form#name.

    With Separation of Concerns I'm absolutely against placing the InputFilter for an Entity within the Entity itself. As such, I'll give you an example with everything separated, if you decide to mash it back together, that's up to you.

    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
    }
    

    So, above is the Entity setup for Many to One - Uni-direction relation.

    You want to manage this, easily, with forms. So we need to create InputFilters for both.

    Having InputFilters separately from the Entity allows us to nest them. This in turn allows us to create structured and nested forms.

    For example, you could create a new Estado on-the-fly. If this were a bi-directional relation, you could create multiple Cicade Entity objects on-the-fly from/during the creation of Estado.

    First: InputFilters. In the spirit of abstraction, which you started with your Entities, let's do that here as well:


    AbstractDoctrineInputFilter

    source AbstractDoctrineInputFilter & source AbstractDoctrineFormInputFilter

    This gives a nice clean setup and a requirement to fulfill. I'm glossing over the more complex elements added in the source files, feel free to look those up though.

    Both objects (Estado & Cicade) require an ObjectManager (they're Doctrine entities after all), so I'm assuming you might have more. The below should come in handy.

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

    So. Now we have 2 InputFilters, based on an AbstractInputFilter.

    EstadoInputFilter filters just the nome property. Add additional if you want ;)

    CicadeInputFilter filters the nome property and has a required estado field.

    The names match those of the Entity definition in the respective Entity classes.

    Just to be complete, below is the CicadeForm, take what you need to create the 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

    Now that the classes are there, how to use them? Slap 'em together with module config!

    In your module.config.php file, add this config:

    '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
        ],
    ],
    

    From this config we read we need a Factory for both the Form and for the InputFilter of a set.

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

    Massive preparation done, time to use it

    Specific EditController to Edit an existing Cicade Entity

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

    So, felt like giving a really expanded answer. Covers the whole thing really.

    If you have any questions about the above, let me know ;-)

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 如何实验stm32主通道和互补通道独立输出
  • ¥30 这是哪个作者做的宝宝起名网站
  • ¥60 版本过低apk如何修改可以兼容新的安卓系统
  • ¥25 由IPR导致的DRIVER_POWER_STATE_FAILURE蓝屏
  • ¥50 有数据,怎么建立模型求影响全要素生产率的因素
  • ¥50 有数据,怎么用matlab求全要素生产率
  • ¥15 TI的insta-spin例程
  • ¥15 完成下列问题完成下列问题
  • ¥15 C#算法问题, 不知道怎么处理这个数据的转换
  • ¥15 YoloV5 第三方库的版本对照问题