开发者

Symfony2 forms: How do I persist an entity with a nullable association?

开发者 https://www.devze.com 2023-04-12 11:21 出处:网络
When saving form submission data, I\'m having trouble persisting a new entity instance where the entity has a nullable association with another entity and I attempt to set it to null.After creating a

When saving form submission data, I'm having trouble persisting a new entity instance where the entity has a nullable association with another entity and I attempt to set it to null. After creating a new entity instance for the form, binding the submitted request to the form and persisting and flushing the entity instance, depending on how I populate the property for the associated entity, I either get

  1. UnexpectedTypeException: Expected argument of type "object or array", "NULL" given (if set to null), or
  2. InvalidArgumentException: A new entity was found through the relationship 'AccessLog#document' that was not configured to cascade persist operations for entity (if set to a new, empty instance of the related entity, which I don't want to persist).

If I set up cascade persist, it tries to create a record in the related table (which is not allowed by the data model in the db), even though there's no data for it to persist. If setting up cascade persist is the way to go, how do I prevent it from trying to create a new record? What's the best way to handle this?

Note, the behavior is the same whether the association is set up as unidirectional or bidirectional.

Details:

I have an entity with a many-to-one association with another entity (abbreviated):

/** @Entity */
class AccessLog
{
    /** @Id @Column(type="integer") */
    private $access_log_id;

    /** @Column(type="integer", nullable=true) */
    private $document_id;

    /**
     * @ManyToOne(targetEntity="Document", inversedBy="access_logs", cascade={"persist"})
     * @JoinColumn(name="document_id", referencedColumnName="document_id")
     */
    private $document;

    // plus other fields
    // plus getters and setters for all of the above...
}

The related entity is nothing fancy:

/** @Entity */
class Document
{
    /** @Id @Column(type="integer") */
    private $document_id;

    /** @Column(length=255) */
    private $name;

    /** @OneToMany(targetEntity="AccessLog", mappedBy="document") */
    private $access_logs;

    // plus other fields
    // plus getters and setters for all of the above...
}

I have a Symfony form for entering data for new AccessLog records:

class AccessLogFormType extends AbstractType
{
    public function getName()
    {
        return 'access_log_form';
    }

    public function getDefaultOptions(array $options)
    {
        return array('data_class' => 'AccessLog');
    }

    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('access_log_id', 'hidden');
        $builder->add('document_id', 'hidden', array(
            'required' => false
        ));
        $builder->add('document', new DocumentType(), array(
            'label' => 'Document',
            'required' => false
        ));
        //...
    }
}

With the following supporting type definition:

class DocumentType extends AbstractType
{
    public function getName()
    {
        return 'document';
    }

    public function getDefaultOptions(array $options)
    {
        return array('data_class' => 'Document');
    }

    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('name', 'text', array(
            'required' => false
        ));
    }
}

My controller includes the following:

public function save_access_log_action()
{
    $request = $this->get('request');
    $em = $this->get('doctrine.orm')->getEntityManager();

    $access_log = null;

    if ($request->getMethod() === 'POST') {
        $data = $request->request->get('access_log_form');

        if (is_numeric($data['access_log_id'])) {
            $access_log = $em->find('AccessLog', $data['access_log_id']);
        } 开发者_高级运维else {
            $access_log = new AccessLog();
        }

        if (is_numeric($data['document_id'])) {
            $document = $em->find('Document', $data['document_id']);
            $access_log->set_document($document);

        } else {
            // Not calling set_document() since there shouldn't be a
            // related Document entity.
        }

        $form = $this->get('form.factory')
            ->createBuilder(new AccessLogFormType(), $access_log)
            ->getForm();

        $form->bindRequest($request);

        if ($form->isValid()) {
            $em->persist($access_log);
            $em->flush();
        }

    } else {
        // ... (handle get request)
    }

    return $this->render('access_log_form.tpl', array(
        'form' => $form->createView()
    ));
}

The above code works fine when updating an existing access log entry or creating a new one and a document is selected in the form, but not if no document is selected.

Assuming the data model cannot be changed, how do I go about persisting a new AccessLog entity without persisting a new Document entity?


It appears that the solution was to move the set_document(null) call to right before the persist operation, since binding the request to the form caused an empty Document object to be attached to the $document property of the AccessLog object.

This solution also works after removing the cascade persist and making the association unidirectional.

Thanks to @greg0ire for his help.

[update] It also became necessary to add @ChangeTrackingPolicy("DEFERRED_EXPLICIT") to the Document entity definition as it continued to try to update the Document record associated with the AccessLog record, for example when removing an existing association (which caused the $name property to be set to null on the Document and then in the db). It's really frustrating that the default behavior is to erase/modify data when an association is removed, even when cascade persist is not specified.


I think your problem could be the way you defined your document setter in AccessLog.

if you do it like this:

setDocument(Document $document)

you will never be able to setDocument(null) since null is not an instance of Document. To do so,

setDocument($document) or setDocument(Document $docuement = null)


This was fixed in Symfony2 master. Empty forms now don't result in empty objects anymore, but return null instead.


Have you read the page of the Doctrine2 documentation about Transitive persistence / Cascade Operations? I think that if the document exists, you first need to remove it, and if it doesn't, then you should just not call setDocument(). To avoid the second error, the § explains how you can configure the cascade (see the last code snippet).


In your AccessLogFormType you should change

$builder->add('document', new DocumentType(), array(
            'label' => 'Document',
            'required' => false
        ));

to:

$builder->add('document', new DocumentType(), array(
            'label' => 'Document',
            'required' => false,
            'empty_value' => '', 
            'empty_data' => null
        ));

This allows you to save your entity without setting document.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号