2
votes

I have an entity 'administration' which has a field 'firstPeriod'. This field is not nullable (neither in the class definition nor in the database) and nor should it be, as this field should never ever be empty, the application would fail.

However, this field does not have a default value because if an oblivious user would simply submit the form without changing the default value, chaos would ensue. The user must make a conscious choice here. There is validation in place to ensure the field is not empty and within accepted range.

When I try to render the form, the 'propertyAccessor' of the formbuilder component throws this exception:

Type error: Return value of AppBundle\Entity\Administration::getFirstPeriod() must be of the type integer, null returned

It looks like the formbuilder tries to get the value of the field before it is set, which ofcourse leads to said exception.

How can I handle this situation so that the form will render without providing the user with a default value?

To further clarify: Null is not okay, but neither is any default I can provide, the user must make a conscious decision. The same goes for any dev that instantiates this entity directly. It must be provided before the entity is persisted, but I cannot give a default because if the default is left as it is, the application will not function 12 out of 13 times.

  • If I allow null on the entity field "?int" I'm effectively making a field nullable that should never be null
  • If I provide a default, the default might be accepted blindly, which would lead to wrong results further on in the application that are very hard to spot for most users.
  • I've already tried to set the 'empty_data => 0' in the formType, to no avail

sorry for the mess below, the 'Code Sample' does not handle this code well

My (truncated) entity:

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/** * Administration * * @ORM\Table(name="administration") * @ORM\Entity(repositoryClass="AppBundle\Repository\AdministrationRepository") */ class Administration { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id;

/**
 * @var int
 *
 * @ORM\Column(name="first_period", type="smallint", nullable=false)
 */
private $firstPeriod;

/**
 * Get id.
 *
 * @return int
 */
public function getId()
{
    return $this->id;
}


/**
 * @return int
 */
public function getFirstPeriod(): int
{
    return $this->firstPeriod;
}

/**
 * @param int $firstPeriod
 */
public function setFirstPeriod(int $firstPeriod): void
{
    $this->firstPeriod = $firstPeriod;
}

}

My (truncated) formType (as best as i could get it formatted here):

    public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('firstPeriod', null, [
            'label' => 'First period'
        ])
    ;
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'data_class' => Administration::class
    ]);
}

public function getBlockPrefix()
{
    return 'app_bundle_administration_type';
}

}

My controller:


namespace AppBundle\Controller\Admin;

class AdministrationController extends Controller
{
    public function editAction(
        EntityManager $em,
        Router $router,
        Security $security,
        Session $session,
        LoggerInterface $logger,
        Request $request,
        Administration $administration = null
    ): Response {

        if ($administration === null) {
            $new = true;
            $administration = new Administration();
            $pageTitle = 'New';
        } else {
            $new = false;
            $pageTitle = 'Edit';
        }
        $breadcrumbs->add($crumbs);

        $form = $this->createForm(AdministrationType::class, $administration);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {

            /** @var Administration $administration */
            $administration = $form->getData();

            try {
                $em->persist($administration);
                $em->flush();
            } catch (ORMException $e) {
                $logger->critical($e->getMessage());

                $session->getFlashBag()->add('error', 'Database error.');

                if ($new) {
                    return $this->redirectToRoute('administration_new');
                } else {
                    return $this->redirectToRoute(
                        'administration_edit',
                        ['administration' => $administration->getId()]
                    );
                }
            }

            $session->getFlashBag()->add('success', 'Success!');

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

        return $this->render(':Admin/Administration:edit.html.twig', [
            'administrationForm' => $form->createView(),
            'pageTitle' => $pageTitle
        ]);
    }
}

My validation:

AppBundle\Entity\Administration:
properties:
    firstPeriod:
        - NotBlank:
              message: 'adm.firstperiod.empty'
        - Range:
              min: 1
              max: 13
              minMessage: 'adm.firstperiod.too_low'
              maxMessage: 'adm.firstperiod.too_high'
1
Just use default values in your entity. private $firstPeriod = 1; It's something you would want to do anyways. No point in having an entity floating around with invalid data if you can help it.Cerad
@Cerad Did you read the question?Douwe
Of course I did not read it. Much more fun to just randomly post comments. Intentionally allowing entities to have invalid data strikes me as a really bad idea. But if null is okay then just change the return type on getFirstPeriod to ?int.Cerad
@Cerad Null is not okay, but neither is any default I can provide, the user must make a conscious decision. The same goes for any dev that instantiates this entity directly. It must be provided before the entity is persisted, but I cannot give a default because if the default is left as it is, the application will not function 12 out of 13 times. I'll edit the question to hopefully make this more clear.Douwe
@Cerad, I was hoping to avoid that, I wasn't kidding when I said I truncated the entity I posted here, there's a lot of other properties and complexities. I thought this was something trivial where I was simply not aware of the fix, like "use the ignore_getters constraint" or something :( Maybe this problem is not as common as I thought, it is common in our field though.Douwe

1 Answers

2
votes

Since symfony form uses property accessor, as @Cerad says, you can add a unmapped field to the form, and get/set the field in form events, adding a specific method for get uninitialized $first_period ...

An example code may be ...

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\AdministrationRepository")
 */ 
class Administration
{ 
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()   
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="integer")
     */
    private $first_period;

    public function getId(): ?int
    {
        return $this->id;
    }
  
    public function getFirstPeriod(): int
    {
        return $this->first_period;
    }

    public function setFirstPeriod(int $first_period): self
    {
        $this->first_period = $first_period;

        return $this;
    }

    public function getFirstPeriodOrNull(): ?int
    {
        return $this->first_period;
    }
}

Form

<?php

namespace App\Form;

use App\Entity\Administration;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
  
class AdministrationType extends AbstractType
{ 
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('first_period', null, [
                'mapped' => false,
                'required' => false,
            ])
            ->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) {
                /** @var Administration */
                $a = $event->getData();

                $event->getForm()->get('first_period')->setData($a->getFirstPeriodOrNull());
  
            })
            ->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
                $f = $event->getForm()->get('first_period')->getData();

                if (is_int($f)) {
                    /** @var Administration */
                    $a = $event->getData();
                    $a->setFirstPeriod($f);
                }
            });
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Administration::class,
        ]);
    }
}

This works fine in Symfony 4.2