Доктрина, сохраняющая сущности «многие к одному»

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

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

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

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

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

<?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? Не повлияет на производительность?

Моя сущность 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’s isValid() метод, эти данные POST автоматически преобразуются в целевые объекты, что приводит к сбою в этой проблеме доктрины. Как мне двигаться дальше?

0

Решение

Вы должны связать объект с вашей формой и использовать 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
}

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

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

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

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

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


AbstractDoctrineInputFilter

источник АннотацияDoctrineInputFilter & Исходный код AbstractDoctrineFormInputFilter

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

Оба объекта (Эстадо & 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,
],
],
],
]
);
}
}

Цикада 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;
}
}

конфиг

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

В вашем 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;
}
}

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

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

0

Другие решения

Других решений пока нет …