2

Following this guide https://symfony.com/doc/current/form/embedded.html I created a form with three entities. Patient, PatientSample and PatientOrder. When I submit the form I get an error that patient_id can not be null.

I've been trying various ways to create a new Patient object in the PatientOrderController and getting the data there then calling $entityManager->persist($patient) but was having a problem populating the new Patient object with form data.

PatientOrderType

public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('accession_number')
            ->add('patient', PatientType::class)
            ->add('patientSample', PatientSampleType::class);
    }

PatientOrderController

    {
        $patientOrder = new PatientOrder();

        $form = $this->createForm(PatientOrderType::class, $patientOrder);

        $form->handleRequest($request);

        if($form->isSubmitted() && $form->isValid()){

            $patientOrder = $form->getData();

            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($patientOrder);
            $entityManager->flush();

            return $this->redirectToRoute('patient_order_index');
   }

Patient class

class Patient
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    public $id;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\PatientOrder", 
     *   mappedBy="patient")
     */
    public $patientOrders;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\PatientSample", 
     * mappedBy="patient")
     */
    public $patientSamples;

PatientOrder Class

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Patient", 
     *  inversedBy="patientOrders", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     * @Assert\Valid()
     */
    protected  $patient;


/**
     * @ORM\ManyToOne(targetEntity="App\Entity\PatientSample", 
     *inversedBy="patientOrders", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     * @Assert\Valid()
     */
    private $patientSample;

PatientSample class

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Patient", 
     * inversedBy="patientSamples")
     * @ORM\JoinColumn(nullable=false)
     */
    private $patient;

   /**
     * @ORM\OneToMany(targetEntity="App\Entity\PatientOrder", 
     * mappedBy="patientSample")
     */
    private $patientOrders;

My expectations are that when the user clicks the save button it will insert 3 new rows into 3 separate tables in the database. Instead I'm getting SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'patient_id' cannot be null

6
  • 1
    please provide us with the shortened definitions of Patient, PatientSample and PatientOrder (only associations, their mappings, fields and setters are relevant here). Commented Aug 13, 2019 at 15:15
  • @Jakumi added the fields I thought might be relevant to this issue. If that's not enough information please let me know and I'll add anything you need. Commented Aug 13, 2019 at 15:25
  • 1
    that appears to be alright, the persist cascade should take care of everything when the PatientOrder is persisted. however, you seem to assume that the error is triggered by the PatientOrder's patient_id field. your code might suggest, that the PatientSample - if it's actually an entity - might have a patient_id field as well which might not be connected to a patient yet (hence missing it's value -> null). So I guess, I want to ask, if there's a PatientSample entity and if so, what are the associations for it Commented Aug 13, 2019 at 15:30
  • Ah, I see. Patient has a OneToMany relationship with PatientSample so the PatientSample class has a Patient field and vice versa - I'll update the code with that information. Commented Aug 13, 2019 at 15:37
  • 1
    one last piece missing is probably the association between PatientOrder and PatientSample? but I already see that my assumption in my last comment is probably the reason... Commented Aug 13, 2019 at 15:42

1 Answer 1

4

okay. So the problem is the following:

Patient is an independent entity, which is good. It has two associations, one with each PatientSample and PatientOrder. PatientSample and PatientOrder also have an association

Your form creates a new PatientOrder, that is automatically associated with the Patient, since PatientOrder is the owning side of that association, and also automatically persisted, due to the cascade="PERSIST" in the mapping.

PatientOrder also is automatically associated with the PatientSample, which you probably marked as cascade persisted as well. However, the patient property/association of the PatientSample is not automatically set, as far as I can see. That is at least a problem, if not the problem. - This luckily is a small problem, since it only creates a triangle in your dependency graph, however - since Patient is independent, it doesn't create a cycle (that would be a big problem). However, your form isn't easily capable of setting those kinds of dependencies, especially since they're not obvious. Technically, the sample could belong to another patient... (see validation comment at the end of answer)

the easy solution: set it explicitly

There are probably several solutions, the easiest, but not the best:

    if($form->isSubmitted() && $form->isValid()){

        $patientOrder = $form->getData();
        // NEW! However, I don't know if this violates the Law of Demeter:
        $patientOrder->getSample()->setPatient($patientOrder->getPatient());

        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->persist($patientOrder);
        $entityManager->flush();

        return $this->redirectToRoute('patient_order_index');
    }

the cleaner way would be to do this somewhere in a form event though...

adding a form event handler (instead)

you should adapt the PatientOrderType code to something like this:

// be aware you need to set the following use clauses:
// use Symfony\Component\Form\FormEvent;
// use Symfony\Component\Form\FormEvents;

$builder
   // ...
   ->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
         $patientOrder = $event->getData(); // view data, i.e. your objects
         $patientSample = $patientOrder->getPatientSample();
         if(!$patientSample->getPatient()) { 
             $patientSample->setPatient($patientOrder->getPatient());
         }
   })
;

This approach is somewhat tame. You could potentially also throw an exception if $patientSample already has a different Patient assigned (a form can be pre-filled/pre-set with entities). However, this approach will set a missing patient, which avoids the form leaving a PatientSample without a Patient.

why do this in a form event instead of directly setting it in the controller? because this way the form type is reusable without remembering "oh, i have to explicitly set the person on the PersonSample with this form type".

bonus: adding validation

for validating that a patient sample belongs to a patient order for the same patient, you could add the following Expression constraint on the PatientOrder (the entity, not the form type):

use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity(...)
 * @Assert\Expression("this.getPatientSample().getPatient() == this.getPatient()",
      message="PatientOrder's patient must be PatientSample's patient")
 */
class PatientOrder {...}

or a callback constraint and essentially do the same ... actually it feels weird to write code in an annotation (IDEs on average won't be of much help)

update: mixed up patient order and patient sample in validation part

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you @jakumi this got me to the next step which I believe is the same issue but with the user id which is in the PatienSample entity. I think with your information here though I can figure it out.
@BrianBarrick yeah, once you know what to look for, it's easier ;o) good luck!
If you have a source such as a blog or anything that I could learn more from on this, such as handling it in a form event, I would greatly appreciate it.
@BrianBarrick I have added some more links and comments at the bottom
Thank you so much, I appreciate your help. I spent more time than I want to admit trying to figure this out and I wasn't even close. :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.