Search code examples
phpsymfonysonata-adminsymfony-3.4

SonataAdminBundle and OneToOne relationship


I have a OneToOne relationship between Page and SuperGridContent:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="page")
 * @ORM\HasLifecycleCallbacks
 */
class Page
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    //...

    /**
     * @ORM\OneToOne(targetEntity="AppBundle\Entity\SuperGridContent", mappedBy="page", cascade={"persist", "remove"})
     */
    protected $super_grid;

    //...
}
<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="super_grid_content")
 * @ORM\HasLifecycleCallbacks
 */
class SuperGridContent
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     * @ORM\OneToOne(targetEntity="AppBundle\Entity\Page", inversedBy="super_grid")
     */
    private $page;

    //...
}

Then in the page admin class I have:

<?php

namespace AppBundle\Admin;

use AppBundle\Entity\Page;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Form\Type\ModelType;

class PageAdmin extends AbstractAdmin
{
    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->add('super_grid', ModelType::class, [
                'label' => 'Super Grid',
                'required' => false,
            ])
            // ...
        ;
    }
    
    // ...
}

The problem is that when I create or edit a Page, the super_grid field is a select tag with all existing SuperGridContent as options. Even when they already have a Page relationship. If I select one of those, of course it fails, because the relationship is suposed to be unique.

Am I missing something or is there a way Sonata could handle it?

I'm using SonataAdminBundle 3.4


Solution

  • If I understand correctly you problem, I think you could use Symfony Entity Type instead of the Model Type as stated in Sonata documentation.

    But I also think that your life would be much easier if you inverted the ownership of the OneToOne relationship. In your example SuperGridContent is owning the relationship so when you want to update the super_grid of a Page you might run into a constraint violation. If you change inversedBy to mappedBy in the SuperGridContent class and mappedBy to inversedBy in the Page class (+ if you regenerate your tables and make sure the cascading logic fits your needs) you should be OK.

    If I understand well, you want any new Page to have only the available super_grids as choices and any existing Page to have its current super_grid + all the available super_grids as choices.

    Then something like the following would do the job (careful that I am using Symfony 4.4, there might be slight syntax differences with your implementation):

    namespace App\Admin;
    
    use Sonata\AdminBundle\Admin\AbstractAdmin;
    use Sonata\AdminBundle\Form\FormMapper;
    //use Sonata\AdminBundle\Form\Type\ModelType;
    use Symfony\Bridge\Doctrine\Form\Type\EntityType;
    use Doctrine\ORM\EntityRepository;
    use App\Entity\Page;
    use App\Entity\SuperGridContent;
    
    
    final class PageAdmin extends AbstractAdmin
    {
        protected function configureFormFields(FormMapper $form): void
        {
            $subject = $this->getSubject();
    
            if($subject->getId()){
                $form
                ->add('super_grid', EntityType::class, [
                    'class' => SuperGridContent::class,
                    'query_builder' => function (EntityRepository $er) use($subject) {
                        return $er->createQueryBuilder('s')
                            ->leftjoin('s.page', 'p')
                            ->where('p.super_grid is null')
                            ->orWhere('p = :my_page')
                            ->setParameter('my_page', $subject)
                            ;
                    },
                ]);
            }
            else{
                $form
                ->add('super_grid', EntityType::class, [
                    'class' => SuperGridContent::class,
                    'query_builder' => function (EntityRepository $er) {
                        return $er->createQueryBuilder('s')
                            ->leftjoin('s.page', 'p')
                            ->where('p.super_grid is null')
                            ;
                    },
                ]);
            } 
        }
    }