Search code examples
symfonysymfony-formssonata-adminsonataform-fields

Use Sonata Field Type on normal Form Class


I'm trying to insert custom sonata form field type on the front page, not in SonataAdmin, something like this:

 $form = $this->createFormBuilder($content)
            ->add('titleEs', 'text', array('required' => true, 'label' => 'label.title.spanish', 'attr' => array('class' => 'col-xs-12 form-control input-lg')))
            ->add('contentEs', 'ckeditor', array('required' => true,'label' => 'label.content.spanish', 'attr' => array('class' => 'col-xs-12')))
            ->add('titleEn', 'text', array('required' => true,'label' => 'label.title.english', 'attr' => array('class' => 'col-xs-12 form-control input-lg')))
            ->add('contentEn', 'ckeditor', array('required' => true, 'label' => 'label.content.english', 'attr' => array('class' => 'col-xs-12')))
            ->add('header', 'sonata_type_model', array('required' => true,'label' => 'label.content.headerImage'), array('link_parameters' => array('context' => 'content/front', 'size' => 'big')))
            //->add('coverImage', 'sonata_type_model_list', array('required' => true,'label' => 'label.content.coverImage'), array('link_parameters' => array('context' => 'content/front', 'size' => 'small')))
            //->add('sliderImage', 'sonata_type_model_list', array('required' => false,'label' => 'label.content.sliderImage'), array('link_parameters' => array('context' => 'content/slider', 'size' => 'normal')))
            ->getForm(); 

But when I execute that, it throws an error:

Catchable Fatal Error: Argument 1 passed to Sonata\AdminBundle\Form\ChoiceList\ModelChoiceList::__construct() must implement interface Sonata\AdminBundle\Model\ModelManagerInterface, null given

I can't understand why Symfony throws that error, if the Sonata Form Field Types are services.


Solution

  • Thanks to @gabtzi's answer I poked around in the source code of Sonata Admin and came up with a very similar solution. Assuming that we have two entities Movie and Genre with a many-to-many relation between them (Movie is the owning side), the solution in Symfony 4 and Sonata Admin 3.x would look like this:

    <?php
    
    namespace App\Form\Type;
    
    use App\Entity\Movie;
    use App\Entity\Genre;
    use Symfony\Component\Form\AbstractType;
    use Sonata\AdminBundle\Form\Type\ModelType;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    
    class MovieType extends AbstractType
    {
        private $container;
    
        public function __construct(ContainerInterface $container)
        {
            $this->container = $container;
        }
    
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                // add fields
                ->add('genres', ModelType::class, [
                    'multiple' => true,
                    'class' => Genre::class,
                    'property' => 'name',  // assuming Genre has property name
                    'model_manager' => $this->container->get('sonata.admin.manager.orm'),
                    'by_reference' => false
                ])
                // add more fields
            ;
        }
    
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => Movie::class,
            ));
        }
    }
    

    This is a very basic example, but should give an idea how to proceed further. Important things are:

    • you don't have to register the form type as a service if you use autowiring. Check that autowire is set to true in your config/services.yaml. Read the official documentation for more detailed information;

    • pass ContainerInterface to the constructor to get the container;

    • you don't use sonata_type_model anymore. You have to use ModelType::class. Pay attention to the use statements;

    • you can set mutiple to true for a M2M relation, otherwise it defaults to false;

    • you have to pass the entity class to class - in this case Movie::class;

    • you can specify property to use certain property of Genre. You don't have to declare this if you have defined __toString method in the entity class. Then the return value of this method will be used;

    • the most important thing: now that you have the container, get the service sonata.admin.manager.orm and pass it to model_manager. Without this everything falls in water.

    I haven't however managed to display the button + Add new. It's worth mentioning that admin class for the related property must exist and be accessible (proper permissions set) - in this case GenreAdmin would be required, otherwise the button couldn't even theoretically work.