Vamos a ver cómo explotar los tipos de campo Jsonb de PostgreSQL en una interfaz generada de Sonata.

Comenzamos con el supuesto de que ya dominas los conceptos básicos de Symfony, Sonata y PostgreSQL.

Primero, creemos una tabla simple en PostgreSQL que contendrá un campo Jsonb.

Sélection_161
CREATE SEQUENCE public.table1_id_seq;
CREATE TABLE public.table1 (
    id integer DEFAULT nextval('public.table1_id_seq'::regclass) NOT NULL,
    var1 character varying(250),
    var2 jsonb
);
ALTER TABLE ONLY public.table1
    ADD CONSTRAINT table1_pkey PRIMARY KEY (id);


Luego, genera la entidad y su interfaz de administración

php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity
php bin/console make:entity --regenerate App
php bin/console make:sonata:admin App/Entity/Table1

Notamos el tipo de campo en nuestra entidad

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Table1
 *
 * @ORM\Table(name="table1")
 * @ORM\Entity
 */
class Table1
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="SEQUENCE")
     * @ORM\SequenceGenerator(sequenceName="table1_id_seq", allocationSize=1, initialValue=1)
     */
    private $id;

    /**
     * @var string|null
     *
     * @ORM\Column(name="var1", type="string", length=250, nullable=true)
     */
    private $var1;

    /**
     * @var json|null
     *
     * @ORM\Column(name="var2", type="json", nullable=true)
     */
    private $var2;

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

    public function getVar1(): ?string
    {
        return $this->var1;
    }

    public function setVar1(?string $var1): self
    {
        $this->var1 = $var1;

        return $this;
    }

    public function getVar2(): ?array
    {
        return $this->var2;
    }

    public function setVar2(?array $var2): self
    {
        $this->var2 = $var2;

        return $this;
    }


}

Y nuestra interfaz de administración generada en su formato más simple

<?php

declare(strict_types=1);

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;

final class Table1Admin extends AbstractAdmin
{

    protected function configureDatagridFilters(DatagridMapper $datagridMapper): void
    {
        $datagridMapper
            ->add('id')
            ->add('var1')
            ->add('var2')
            ;
    }

    protected function configureListFields(ListMapper $listMapper): void
    {
        $listMapper
            ->add('id')
            ->add('var1')
            ->add('var2')
            ->add('_action', null, [
                'actions' => [
                    'show' => [],
                    'edit' => [],
                    'delete' => [],
                ],
            ]);
    }

    protected function configureFormFields(FormMapper $formMapper): void
    {
        $formMapper
            ->add('id')
            ->add('var1')
            ->add('var2')
            ;
    }

    protected function configureShowFields(ShowMapper $showMapper): void
    {
        $showMapper
            ->add('id')
            ->add('var1')
            ->add('var2')
            ;
    }
}

Nuestro objetivo ahora es crear una estructura de formulario anidado para gestionar nuestros datos en json.
Para esto, utilizaremos las colecciones de Symfony.
Los pasos son los siguientes: vamos a crear un formulario que se integrará en el campo. Tendrá un botón de añadir/eliminar para agregar elementos a nuestra colección.
Luego vincularemos nuestro formulario a nuestro campo "var2".

Creamos nuestro formulario en el directorio Form de src.

<?php #src/Form/fieldvar2.php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type as FormType;

class fieldvar2 extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('var2_titre', FormType\TextType::class, [
                'label' => 'Var2 titre',
            ])
            ->add('var2_valeur', FormType\TextType::class, [
                'label' => 'var2 valeur',
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
    }
}

?>

Luego, necesitamos vincular este formulario a nuestro campo.

use Symfony\Component\Form\Extension\Core\Type as FormType;
use App\Form\fieldvar2;

final class Table1Admin extends AbstractAdmin
{

    protected function configureFormFields(FormMapper $formMapper): void
    {
        $formMapper
            //->add('id') #on supprime le champs id puisqu'il est auto incrémenté
            ->add('var1')
            ->add('var2', FormType\CollectionType::class, [
                'allow_add' => true,
                'allow_delete' => true,
                'entry_type' => 'App\\Form\\fieldvar2',
                'label' => 'Var jsonb',
            ])
            ;
        
           
            
    }
jsonb-insert

Ahora vamos a ver cómo podríamos tener una anidación de formularios dentro de nuestro formulario. En otras palabras, "colecciones anidadas".
Y para esto, tendremos que usar un componente de Colección de Sonata porque es el único que funcionará con múltiples anidaciones (teóricamente, infinitamente).

Añadiremos la referencia a Sonata\AdminBundle\Form\Type\CollectionType, crearemos un segundo formulario y lo implementaremos en nuestro primer formulario.

<?php #src/Form/fieldvar2valeur.php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type as FormType;

class fieldvar2valeur extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('valeurs_multiples', FormType\TextType::class, [
                'label' => 'Netsted 2 val',
            ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'allow_extra_fields' => true,
            'allow_add' => true,
        ]);
    }
    
    
}

?>

Nuestra implementación de la colección de nivel 2

<?php #src/Form/fieldvar2.php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type as FormType;
use App\Form\fieldvar2valeur;
use Sonata\AdminBundle\Form\Type\CollectionType;

class fieldvar2 extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        
        
       $builder->add('var2_titre', FormType\TextType::class, [
            'label' => 'Var2 titre',
        ])
        ->add('var2_valeur', CollectionType::class, [
                'allow_add' => true,
                'allow_delete' => true,
                'entry_type' => 'App\\Form\\fieldvar2valeur',
                'label' => 'Data Nested 2',
            ])
       ;
          


    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'allow_extra_fields' => true,
            'allow_add' => true,
        ]);
    }
}

?>

Y todo lo que queda es modificar la configuración de nuestra colección para usar el componente SonataAdmin

<?php

declare(strict_types=1);

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;

use App\Form\fieldvar2;


use Sonata\AdminBundle\Form\Type\CollectionType;

final class Table1Admin extends AbstractAdmin
{

    protected function configureDatagridFilters(DatagridMapper $datagridMapper): void
    {
        $datagridMapper
            ->add('id')
            ->add('var1')
            ->add('var2')
            ;
    }

    protected function configureListFields(ListMapper $listMapper): void
    {
        $listMapper
            ->add('id')
            ->add('var1')
            ->add('var2')
            ->add('_action', null, [
                'actions' => [
                    'show' => [],
                    'edit' => [],
                    'delete' => [],
                ],
            ]);
    }

    protected function configureFormFields(FormMapper $formMapper): void
    {
        $formMapper
            //->add('id')
            ->add('var1')
            ->add('var2', CollectionType::class, [
                'allow_add' => true,
                'allow_delete' => true,
                'entry_type' => 'App\\Form\\fieldvar2',
                'label' => 'Var jsonb',
            ])
            ;
        
            
            
    }

    protected function configureShowFields(ShowMapper $showMapper): void
    {
        $showMapper
            ->add('id')
            ->add('var1')
            ->add('var2')
            ;
    }
}

Así es como se ve

jsonb-insert-nested2