坚持多对一实体的学说

时间:2018-09-02 16:05:12

标签: php doctrine-orm cascade many-to-one zend-framework3

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

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

问题是,如果我在Cidade实体中将属性$estadocascade={"persist"}设置为cascade={"detach"},则Doctrine会尝试保留Estado实体,使其缺少所有必需属性,但缺少主键ID,该属性来自POST请求(HTML组合)。我也考虑过使用<?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(); 来使Doctrine在EntityManager中忽略Estado实体。但是我得到这个错误:

  

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

我发现了类似的疑问here,对此我唯一能找到的方法是首先保存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;
         }
     }

该如何执行而不必一直保存Cidade时都无需检索Estado?不会影响性能吗?

我的Cidade实体:

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

我的Estado实体:

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

两个实体都扩展了我的超类AbstractEntity:

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

我的HTML输入呈现如下:

option

上面的每个[ "cidade" => [ "nome" => "Test", "estado" => [ "id" => 3 ] ] ] 是从数据库中检索的Estado实体。我的POST数据如下例所示:

isValid()

在Zend Form的Select yearSelector = new Select(driver.findElement(By.id("years"))); yearSelector.selectByIndex(2000); 方法上,此POST数据会自动转换为目标实体,这使我在此教义问题上崩溃。我该如何前进?

1 个答案:

答案 0 :(得分:0)

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

考虑到分离问题,我绝对反对在实体本身内放置实体的InputFilter。因此,我将为您提供一个示例,其中将所有内容分开,如果您决定将其混搭在一起,则由您决定。

ID的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实体

/**
 * @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。

在实体中具有单独的InputFilters允许我们嵌套。反过来,这使我们可以创建结构化和嵌套的表单。

例如,您可以即时创建一个新的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个基于AbstractInputFilter的InputFilters。

EstadoInputFilter仅过滤nome属性。如果需要,添加其他;)

CicadeInputFilter过滤nome属性,并具有必填字段estado

名称与各个Entity类中Entity定义的名称匹配。

为完整起见,下面是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
    ],
],

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

因此,感觉就像是给出了一个扩展的答案。确实覆盖了整个事情。

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