Sonata est une suite de bundle pour Symfony 4. L’un d’entre eux permet de gérer du contenu à la manière de WordPress.
Vous trouverez assez peu de documentations sur le net, et pour cause, l’installation est bugué, et les rares personnes qui se sont frotté au bugtracker sur github, se sont vu répondre un RTFM bien en règle.

Rester sur ce genre d’échec n’est pas vraiment mon style.
Voici donc comment installer et configurer Sonata-Page-Bundle, avec l’installation officiel, puis les procédure de contournement de ces F###ing bugs.

1iere étape, on récupère notre skeleton de symfony 4 qui comprends notre base fonctionnelle du framework agrémenté de Sonata + FosUser + Media.
Déjà on économise une bonne journée ^^

En gros, il faut suivre la documentation : https://sonata-project.org/bundles/page/3-x/doc/reference/installation.html

Installer tous les Bundles, et suivre scrupuleusement la documentation.

La première difficulté c’est l’installation via composer. Je n’ai pas réussi à tout installer pour php 7.1. Ni pour php 7.2 d’ailleurs. Il y a toujours un package en conflit.

L’astuce dans composer, c’est de sortir du stable via l’instruction minimum-stability dans votre composer.json et là, au bout de 3 ou 4 heures à tester les versions, on arrive à tout installer.

Voici mon composer.json

{
    "type": "project",
    "license": "proprietary",
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "doctrine/doctrine-migrations-bundle": "^3.0",
        "jms/serializer-bundle": "^3.7",
        "liip/imagine-bundle": "^2.3",
        "nelmio/api-doc-bundle": "^3.6",
        "phpoffice/phpspreadsheet": "^1.14",
        "pixassociates/sortable-behavior-bundle": "^1.5",
        "prodigious/sonata-menu-bundle": "^3.0",
        "sensio/framework-extra-bundle": "^5.5",
        "sonata-project/block-bundle": "^3.20",
        "sonata-project/cache-bundle": "^2.4",
        "sonata-project/core-bundle": "^3.20",
        "sonata-project/doctrine-orm-admin-bundle": "^3.20",
        "sonata-project/formatter-bundle": "^4.2",
        "sonata-project/intl-bundle": "^2.7",
        "sonata-project/media-bundle": "^3.25",
        "sonata-project/notification-bundle": "^3.8",
        "sonata-project/page-bundle": "^3.17",
        "sonata-project/seo-bundle": "^2.11",
        "sonata-project/user-bundle": "^4.6",
        "symfony/apache-pack": "^1.0",
        "symfony/console": "4.*",
        "symfony/debug-pack": "^1.0",
        "symfony/dotenv": "4.4.*",
        "symfony/flex": "^1.3.1",
        "symfony/framework-bundle": "4.4.*",
        "symfony/serializer-pack": "^1.0",
        "symfony/swiftmailer-bundle": "^3.4",
        "symfony/templating": "4.4.*",
        "symfony/translation": "4.4.*",
        "symfony/var-dumper": "4.4.*",
        "symfony/yaml": "4.*",
        "twig/twig": "^2.9.0"
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3",
        "symfony/maker-bundle": "^1.19"
    },
    "config": {
        "preferred-install": {
            "*": "dist"
        },
        "sort-packages": true
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "replace": {
        "paragonie/random_compat": "2.*",
        "symfony/polyfill-ctype": "*",
        "symfony/polyfill-iconv": "*",
        "symfony/polyfill-php71": "*",
        "symfony/polyfill-php70": "*",
        "symfony/polyfill-php56": "*"
    },
    "scripts": {
        "auto-scripts": {
            "cache:clear": "symfony-cmd",
            "assets:install %PUBLIC_DIR%": "symfony-cmd"
        },
        "post-install-cmd": [
            "@auto-scripts"
        ],
        "post-update-cmd": [
            "@auto-scripts"
        ]
    },
    "conflict": {
        "symfony/symfony": "*"
    },
    "extra": {
        "symfony": {
            "allow-contrib": false,
            "require": "4.4.*"
        }
    }
    ,    
    "minimum-stability": "dev",
    "prefer-stable": true
}

Une fois les packages installés, et configurés, n’oubliez pas d’installer Ckeditor, et les assets. Et accessoirement, votre user super admin.

php bin/console assets:install
php bin/console ckeditor:install
mkdir -p public/uploads/media
php bin/console cache:clear
php bin/console fos:user:create admin tbourdin@partitech.com admin --super-admin

Il reste à créer le premier site :

php app/console sonata:page:create-site

Please define a value for Site.name : Skeletton CMS
Please define a value for Site.host : www.skeleton-cms.test
Please define a value for Site.relativePath : /
Please define a value for Site.enabled : true
Please define a value for Site.locale : -
Please define a value for Site.enabledFrom : now
Please define a value for Site.enabledTo : +10 years
Please define a value for Site.default : true

Creating website with the following information :
  name : www.skeleton-cms.test
  site : http(s)://www.skeleton-cms.test
  enabled :  Tue, 10 Jul 2030 16:12:08 +0100 => Mon, 10 Jan 2022 16:12:08 +0100

Confirm site creation ?y

Site created !

You can now create the related pages and snapshots by running the followings commands:
  php app/console sonata:page:update-core-routes --site=1
  php app/console sonata:page:create-snapshots --site=1

Le premier site est maintenant opérationnel. Enfin presque.

Dans l’interface de page, il faut créer notre première page

image

La configuration par défaut ne contiens rien. Il faut ensuite organiser le contenu de cette page via un éditeur, qui permet d’organiser notre page sous forme de blocks, affectés à des zones de notre templates.

Dans notre configuration du bundle, nous avons déclaré un template par défaut. Et c’est cette configuration, qui nous permet de modéliser l’interface de gestion des blocks

   default_template: default
    templates:
        default:
            path: '@SonataPage/layout.html.twig'
            name: 'default'
            containers:
                header:
                    name: Header
                content_top:
                    name: Top content
                content:
                    name: Main content
                content_bottom:
                    name: Bottom content
                footer:
                    name: Footer
            matrix:
                layout: |
                    HHHHHHHH
                    HHHHHHHH
                    TTTTTTTT
                    TTTTTTTT
                    CCCCCCCC
                    CCCCCCCC
                    BBBBBBBB
                    BBBBBBBB
                    FFFFFFFF
                    FFFFFFFF

                mapping:
                    H: header
                    T: content_top
                    C: content
                    B: content_bottom
                    F: footer
image-2

Dans notre exemple plus haut, nous avons inséré 2 blocs dans notre zone.
Le premier est un texte simple, le second est un menu (un classique menu footer).
Pour insérer un block on se positionne sur une zone a gauche, et on click sur le bouton + à droite pour sélectionner le type de block après l’avoir sélectionné dans la liste.

Toute la gestion des pages se fait de cette manière.
Ce qui veux dire que nous allons devoir créer une série de blocks selon les besoins de notre application.
Et pour créer un bloc, c’est hyper simple. Nous allons prendre l’exemple de notre block de menu.

Les menus sont géré via le package Sonata-Menu-Bundle qui est compatible avec Sonata-Page-Bundle. Mais il n’est pas fournis avec un block par défaut. C’est con, parce qu’un menu, c’est un peu le truc basique.

Donc on va créer notre block dans /src/Block/SonataMenuBlockService.php

<?php
namespace App\Block;

use Sonata\AdminBundle\Form\FormMapper;
use Sonata\BlockBundle\Block\BlockContextInterface;

use Sonata\BlockBundle\Block\Service\AbstractAdminBlockService;
use Sonata\BlockBundle\Meta\Metadata;
use Sonata\BlockBundle\Model\BlockInterface;
use Sonata\Form\Type\ImmutableArrayType;
use Sonata\PageBundle\Model\Page;
use Sonata\PageBundle\Model\PageManagerInterface;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;






use Sonata\AdminBundle\Admin\Pool;

use Sonata\BlockBundle\Block\BaseBlockService;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ORM\EntityManager;


use Sonata\CoreBundle\Validator\ErrorElement;

class SonataMenuBlockService extends AbstractAdminBlockService
{
    /**
     * @var PageManagerInterface
     */
    protected $pageManager;

    /**
     * @param string $name
     */
    public function __construct($name, EngineInterface $templating, Pool $pool, EntityManager $em , PageManagerInterface $pageManager)
    {
        parent::__construct($name, $templating);


        $this->pool = $pool;
        $this->em = $em;
        $this->pageManager = $pageManager;
        
    }

    public function getName()
    {
        return 'Partitech Menu Block';
    }
    
    public function buildEditForm(FormMapper $formMapper, BlockInterface $block)
    {
        $menu_list=$this->pool->getContainer()->get('doctrine')->getManager()->getRepository('Prodigious\Sonata\MenuBundle\Entity\Menu')->findAll();
        
        if(!empty($menu_list))
        {
            $option_list=array();
            foreach($menu_list as $m)
            {
                
                $option_list[$m->getAlias()]=$m->getName();
            }
            
        }
          
        $formMapper->add('settings', ImmutableArrayType::class, [
            'keys' => [
               /* ['title', TextType::class, [
                    'label' => 'form.label_title',
                    'required' => false,
                ]],*/
                ['menu', ChoiceType::class, [
                    'label' => 'Menu',
                    'choices' => array_flip($option_list),
                ]],

                ['template', TextType::class, [
                    'label' => 'template',
                    'required' => false,
                ]],
                
            ],
            'translation_domain' => 'SonataPageBundle',
        ]);
    }

    public function execute(BlockContextInterface $blockContext, ?Response $response = null)
    {
  
        
        $settings=$blockContext->getSettings();
        
        $systemElements = $this->pageManager->findBy([
            'url' => null,
            'parent' => null,
        ]);

        
       
        $menu_list=$this->pool->getContainer()->get('doctrine')->getManager()->getRepository('Prodigious\Sonata\MenuBundle\Entity\Menu')->findOneBy(['alias'=> $settings['menu']]);
        if(!empty($menu_list)){
            $menu=$this->pool->getContainer()->get('doctrine')->getManager()->getRepository('Prodigious\Sonata\MenuBundle\Entity\MenuItem');
            $mm = $this->pool->getContainer()->get('prodigious_sonata_menu.manager');
            $menu = $mm->load($menu_list->getId());
            // $status = true (Get enabled menu items)
            // $status = false (Get disabled menu items)
            // getMenuItems($menu, $root = MenuManager::ITEM_CHILD, $status = MenuManager::STATUS_ALL)
            
            $menuItems = $mm->getMenuItems($menu, true, true);
        }else{
            $menuItems=false;
        }
                ;
        
        return $this->renderResponse($blockContext->getTemplate(), [
            'context' => $blockContext,
            'block' => $blockContext->getBlock(),
            'settings' => $blockContext->getSettings(),
            
            'systemElements' => $systemElements,
            'menuItems'=>$menuItems
        ], $response);
    }

    public function configureSettings(OptionsResolver $resolver)
    {
        
        $resolver->setDefaults([

            'menu'=>'menu',
            'template' => '/Block/Menu/menu.html.twig',
        ]);
    }

    public function getBlockMetadata($code = null)
    {
        return new Metadata($this->getName(), (null !== $code ? $code : $this->getName()), false, 'SonataPageBundle', [
            'class' => 'fa fa-list-ul',
        ]);
    }
}

On ajoute nos templates dans /Block/Menu/menu.html.twig et le fichier macro des menu dans /Block/Menu/menu_tree_macro.html.twig.
C’est ni plus ni moins que les fichiers par défaut du bundle entouré d’une div qui contient des informations du block pour l’édition en front.

{#/Block/Menu/menu.html.twig#}
{% if block.name|length %}
    {% set name=block.name%}
{% else %}
    {% set name=block.settings.menu %}
{% endif %}
<div id="cms-block-25" class="cms-block cms-block-element ui-sortable-handle" data-id="{{ block.id }}" data-name="{{ name }}" data-role="block" data-page-id="{{ block.page.id }}">

    {% if menuItems is not null and menuItems|length > 0 %}
    
    {% import 'Block/Menu/menu_tree_macro.html.twig' as tree %}
    
    {% set currentPath = app.request.requestUri %}
    
    {{ tree.menu(menuItems, currentPath) }}
    
    {% endif %}

</div>
{#/Block/Menu/menu_tree_macro.html.twig#}
{% macro menu(items, currentPath) %}
    
    {% import _self as self %}

        <ul>
            {% for menuItem in items %}
    
            {% set url = menuItem.url %}
            {% set attributes = "menu-item" %}
            {% if menuItem.classAttribute %}
            {% set attributes = attributes ~ ' ' ~ menuItem.classAttribute %}
            {% endif %}
            {% if menuItem.hasChild() %}
            
            {% set attributes = attributes ~ ' has-child' %}
            
            {% for childItem in menuItem.children %}

            {% set childUrl = childItem.url %}

            {% if childUrl == currentPath %}
            {% set attributes = attributes ~ ' current-parent' %}
            {% endif %}
            
            {% endfor %}
            
            {% endif %}

            <li class="{{ attributes }}" role="menu-item">
                {% if menuItem.hasChild() %}
                <a href="{{ url }}" class="parent" {% if currentPath == url %} class="current"{% endif %}" {% if menuItem.target %} target="_blank"{% endif %}>{{ menuItem.name }}</a>
                {{ self.menu(menuItem.children, currentPath) }}
                {% else %}
                <a href="{{ url }}" {% if currentPath == url %} class="current"{% endif %}" {% if menuItem.target %} target="_blank"{% endif %}>{{ menuItem.name }}</a>
                {% endif %}
            </li>
            {% endfor %}
        </ul>

{% endmacro %}

On référence notre block dans nos services

services:
    sonata.block.service.sonata_menu:
        class: App\Block\SonataMenuBlockService
        arguments: 
           - "sonata.block.service.sonata_menu"
           - "@templating"
           - "@sonata.admin.pool"
           - "@doctrine.orm.entity_manager"
           - "@sonata.page.manager.page"
        tags:
            - { name: sonata.block }   

Et nous avons ensuite notre nouveau block qui est disponible dans notre liste de blocks. On peux faire en gros tout ce que l’on veux dans un block, à la manière d’un controller.

Sélection_296

On peu donc créer notre menu footer

image-3

Et l’utiliser dans note block

image-4

Voici le rendu de notre page

image-5

En fait, non. Ça c’est le rendu avec un template corrigé. Sinon le rendu ressemble a ceci :

Sélection_302

On va devoir donc dupliquer le template de base dans notre projet et corriger ce qui ne vas pas.

On créé notre base_layout et notre template de test test-layout dans le répertoire Layouts/

{#/templates/Layouts/base_layout.html.twig#}
{#

This file is part of the Sonata package.

(c) Thomas Rabaix <thomas.rabaix@sonata-project.org>

For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.

#}
{%- block sonata_page_html_tag -%}
<!DOCTYPE html>
<html {{ sonata_seo_html_attributes() }}>
{% endblock %}
    {% block sonata_page_head %}
        <head {{ sonata_seo_head_attributes() }}>

            <!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->
            {{ sonata_seo_title() }}
            {{ sonata_seo_metadatas() }}

            {% block sonata_page_stylesheets %}
                {% block page_stylesheets %} {# Deprecated block #}
                    {% for stylesheet in sonata_page.assets.stylesheets %}
                        <link rel="stylesheet" href="{{ asset(stylesheet) }}" media="all">
                    {% endfor %}
                {% endblock %}
            {% endblock %}

            {% block sonata_page_javascripts %}
                {% block page_javascripts %} {# Deprecated block #}
                    <!-- Le HTML5 shim, for IE6-8 support of HTML elements -->
                    <!--[if lt IE 9]>
                        <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
                    <![endif]-->

                    {% for js in sonata_page.assets.javascripts %}
                        <script src="{{ asset(js) }}"></script>
                    {% endfor %}
                {% endblock %}

            {% endblock %}
        </head>
    {% endblock %}

    {% block sonata_page_body_tag %}
        <body class="sonata-bc">
    {% endblock %}

        {% block sonata_page_top_bar %}
        
            {% block page_top_bar %} {# Deprecated block #}
                {% if sonata_page.isEditor or ( app.user and is_granted('ROLE_PREVIOUS_ADMIN') ) %}

                    {% if sonata_page.isEditor and sonata_page.isInlineEditionOn %}
                        <!-- CMS specific variables -->
                        <script>
                            jQuery(document).ready(function() {
                                Sonata.Page.init({
                                    url: {
                                        block_save_position: '{{ sonata_admin.objectUrl('sonata.page.admin.page', 'edit', page) }}',
                                        block_edit:          '{{ sonata_admin.url('sonata.page.admin.page|sonata.page.admin.block', 'edit', {'id': 'BLOCK_ID'}) }}'
                                    }
                                });
                            });
                        </script>
                    {% endif %}

                    <header class="sonata-bc sonata-page-top-bar navbar navbar-inverse navbar-fixed-top" role="banner"
                    {% if sonata_page.isEditor and sonata_page.isInlineEditionOn %}
                        data-page-editor='{{ {
                            url: {
                                block_save_position: sonata_admin.objectUrl('sonata.page.admin.page', 'edit', page),
                                block_edit:          sonata_admin.url('sonata.page.admin.page|sonata.page.admin.block', 'edit', {'id': 'BLOCK_ID'})
                            }
                        }|json_encode()|raw }}'
                    {% endif %}>
                        <div class="container">
                            <ul class="nav navbar-nav">
                                {% if app.user and is_granted('ROLE_SONATA_ADMIN') %}
                                    <li><a href="{{ path('sonata_admin_dashboard') }}">{{ "header.sonata_admin_dashboard"|trans({}, 'SonataPageBundle') }}</a></li>
                                {% endif %}

                                {% if sonata_page.isEditor %}
                                    {% set sites = sonata_page.siteavailables %}

                                    {% if sites|length > 1 and site is defined %}
                                        <li class="dropdown">
                                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ site.name }} <span class="caret"></span></a>
                                            <ul class="dropdown-menu">
                                                {% for site in sites %}
                                                    <li><a href="{{ site.url }}">{{ site.name }}</a></li>
                                                {% endfor %}
                                            </ul>
                                        </li>
                                    {% endif %}

                                    <li class="dropdown">
                                        <a href="#" class="dropdown-toggle" data-toggle="dropdown">Page <span class="caret"></span></a>
                                        <ul class="dropdown-menu">
                                            {% if page is defined %}
                                                <li><a href="{{ sonata_admin.objectUrl('sonata.page.admin.page', 'edit', page) }}" target="_new">{{ "header.edit_page"|trans({}, 'SonataPageBundle') }}</a></li>
                                                <li><a href="{{ sonata_admin.objectUrl('sonata.page.admin.page|sonata.page.admin.snapshot', 'list', page) }}" target="_new">{{ "header.create_snapshot"|trans({}, 'SonataPageBundle') }}</a></li>
                                                <li class="divider"></li>
                                            {% endif %}

                                            <li><a href="{{ sonata_admin.url('sonata.page.admin.page', 'list') }}" target="_new">{{ "header.view_all_pages"|trans({}, 'SonataPageBundle') }}</a></li>

                                            {% if error_codes is defined and error_codes|length %}
                                                <li class="divider"></li>
                                                <li><a href="{{ path('sonata_page_exceptions_list') }}" target="_new">{{ "header.view_all_exceptions"|trans({}, 'SonataPageBundle') }}</a></li>
                                            {% endif %}
                                        </ul>
                                    </li>

                                    {% if page is defined %}
                                        <li>
                                            <a href="{{ sonata_admin.url('sonata.page.admin.page', 'compose', {'id': page.id}) }}">
                                                <i class="fa fa-magic"></i>
                                                {{ 'header.compose_page'|trans({}, 'SonataPageBundle')}}
                                            </a>
                                        </li>
                                    {% endif %}

                                    {% if page is defined and not page.enabled %}
                                        <li><span style="padding-left: 20px; background: red;"><strong><em>{{ 'header.page_is_disabled'|trans([], 'SonataPageBundle') }}</em></strong></span></li>
                                    {% endif %}

                                    {% if sonata_page.isInlineEditionOn and page is defined %}
                                        <li>
                                            <form class="form-inline" style="margin: 0px">
                                                <label class="checkbox inline" for="page-action-enabled-edit"><input type="checkbox" id="page-action-enabled-edit">{{ 'header.show_zone'|trans({}, 'SonataPageBundle') }}</label>
                                                <input type="submit" class="btn" value="{{ 'btn_save_position'|trans({}, 'SonataPageBundle') }}" id="page-action-save-position">
                                            </form>
                                        </li>
                                    {% endif %}
                                {% endif %}

                                {% if app.user and is_granted('ROLE_PREVIOUS_ADMIN') %}
                                    <li><a href="{{ url('homepage', {'_switch_user': '_exit'}) }}">{{ "header.switch_user_exit"|trans({}, 'SonataPageBundle')}}</a></li>
                                {% endif %}

                            </ul>
                        </div>
                    </header>

                {% endif %}
            {% endblock %}
        {% endblock %}

        {% block sonata_page_container %}
            {% block page_container %}{% endblock %} {# Deprecated block #}
        {% endblock %}

        {% block sonata_page_asset_footer %}
            {% block page_asset_footer %} {# Deprecated block #}
                {% if page is defined %}
                    {% if page.javascript is not empty %}
                        <script>
                            {{ page.javascript|raw }}
                        </script>
                    {% endif %}
                    {% if page.stylesheet is not empty %}
                        <style>
                            {{ page.stylesheet|raw }}
                        </style>
                    {% endif %}
                {% endif %}
                {#
                    These includes can be done only at this point as all blocks are loaded,
                    Limition : this does not work if a global page is loaded from an ESI tag inside a container block
                #}
                {{ sonata_block_include_stylesheets('screen', app.request.basePath) }}
                {{ sonata_block_include_javascripts('screen', app.request.basePath) }}
            {% endblock %}
        {% endblock %}

        <!-- monitoring:3e9fda56df2cdd3b039f189693ab7844fbb2d4f6 -->
    </body>
</html>

et notre template

{#/templates/Layouts/test-layout.html.twig#}
{#

This file is part of the Sonata package.

(c) Thomas Rabaix <thomas.rabaix@sonata-project.org>

For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.

#}
{% extends '/Layouts/base_layout.html.twig' %}

{% block sonata_page_container %}
    <div class="container">
        <div class="content">
            <div class="row page-header">
                {{ sonata_page_render_container('header', 'global') }}
            </div>

           <!---->   {% block sonata_page_breadcrumb %}
                <div class="row">
                    {{ sonata_block_render_event('breadcrumb', { 'context': 'page', 'current_uri': app.request.requestUri }) }}
                </div>
            {% endblock %}

            {% if page is defined %}
                <div class="row">
                    {% if page.name != 'global'%}
                        {{ sonata_page_render_container('content_top', 'global') }}
                    {% endif %}
                    {{ sonata_page_render_container('content_top', page) }}
                </div>
            {% endif %}

            <div class="row">
                {% block page_content %}
                    {% if content is defined %}
                        {{ content|raw }}
                    {% else %}
                        {% set content = block('content') is defined ? block('content') : '' %}
                        {% if content|length > 0 %}
                            {{ content|raw }}
                        {% elseif page is defined %}
                            {{ sonata_page_render_container('content', page) }}
                        {% endif %}
                    {% endif %}
                {% endblock %}
            </div>

            {% if page is defined %}
                <div class="row">
                    {{ sonata_page_render_container('content_bottom', page) }}

                    {% if page.name != 'global'%}
                        {{ sonata_page_render_container('content_bottom', 'global') }}
                    {% endif %}
                </div>
            {% endif %}
        </div>

        <footer class="row">
            {{ sonata_page_render_container('footer', 'global') }}
        </footer>
    </div>
{% endblock %}

On le référence ensuite dans le fichier de configuration du bundle

sonata_page:
       test:
            path: 'Layouts/test-layout.html.twig'
            name: 'test'
            containers:
                header:
                    name: Header
                content_top:
                    name: Top content
                content:
                    name: Main content
                content_bottom:
                    name: Bottom content
                footer:
                    name: Footer
            matrix:
                layout: |
                    HHHHHHHH
                    HHHHHHHH
                    TTTTTTTT
                    TTTTTTTT
                    CCCCCCCC
                    CCCCCCCC
                    BBBBBBBB
                    BBBBBBBB
                    FFFFFFFF
                    FFFFFFFF

                mapping:
                    H: header
                    T: content_top
                    C: content
                    B: content_bottom
                    F: footer
                    

Notre édition en front est presque fonctionnelle.
Nous avons bien une barre de navigation dans notre page. Lorsque l’on active l’édition des zones, tout s’affiche correctement. Si on double clique sur une zone de bloc, une fenêtre s’affiche avec l’édition du block dans l’admin. Et lorsque l’on bouge les blocks d’une zone a une autre, ça les déplace bien. Mais si on regarde la page, rien n’est sauvegardé.
Nous allons devoir créer une méthode dédié de note coté pour sauver les paramètres.

Sélection_303

Nous allons donc créer un contrôleur dans notre bundle étendu, pour gérer la mise à jour de la donnée.

On créé notre controller UpdatePositionController avec notre méthode sonata_page_update_position()

<?php

namespace App\Application\Sonata\PageBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;





use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Sonata\MediaBundle\Entity\MediaManager; 
use Sonata\MediaBundle\Provider\FileProvider;
use Symfony\Component\HttpKernel\KernelInterface;
use Psr\Container\ContainerInterface;

class UpdatePositionController extends AbstractController
{

    
    /**
     * @Route("/sonata-page-update-position", methods={"GET","POST"}, name="sonata_page_update_position")
     *
     *
     * @return array
     */
    public function sonata_page_update_position(Request $request)
    {
        $keyword=$request->request->all();
 
        if(!empty($keyword['disposition']))
        {
            foreach($keyword['disposition'] as $i=>$v)
            {
                $id=$keyword['disposition'][$i]['id'];
                $position=$keyword['disposition'][$i]['position'];
                $parent_id=$keyword['disposition'][$i]['parent_id'];
                $page_id=$keyword['disposition'][$i]['page_id'];
                
                $connection = $this->getDoctrine()->getConnection();
                $statement = $connection->prepare("UPDATE `page__block` SET `parent_id` = :parent_id, `position` = :position, `page_id`=:page_id WHERE `page__block`.`id` = :id ");
                
                $statement->bindValue('id', $id);
                $statement->bindValue('page_id', $page_id);
                $statement->bindValue('position', $page_id);
                $statement->bindValue('parent_id', $parent_id);
                $statement->execute();
            }
        }
        return new JsonResponse( (object) [
           'result' => 'ok'
        ]);

    }
}
?>


Nous devons ensuite le référencer dans nos services :

services:
    App\Application\Sonata\PageBundle\Controller\:
        resource: '../src/Application/Sonata/PageBundle/Controller'
        tags: ['controller.service_arguments']

On doit ensuite le référencer dans les annotations pour que la route soit automatiquement géré via Symfony :

#config/routes/annotations.yaml
controllers:
    resource: ../../src/Controller/
    type: annotation

controllers_sonata_pages:
    resource: ../../src/Application/Sonata/PageBundle/Controller/
    type: annotation  

Et pour finir, nous devons modifier le lien de mise à jour de l’ordre des blocs dans notre template qui doit être : block_save_position: ‘{{ path(‘sonata_page_update_position’, {}) }}’,

{#

This file is part of the Sonata package.

(c) Thomas Rabaix <thomas.rabaix@sonata-project.org>

For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.

#}
{%- block sonata_page_html_tag -%}
<!DOCTYPE html>
<html {{ sonata_seo_html_attributes() }}>
{% endblock %}
    {% block sonata_page_head %}
        <head {{ sonata_seo_head_attributes() }}>

            <!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->
            {{ sonata_seo_title() }}
            {{ sonata_seo_metadatas() }}

            {% block sonata_page_stylesheets %}
                {% block page_stylesheets %} {# Deprecated block #}
                    {% for stylesheet in sonata_page.assets.stylesheets %}
                        <link rel="stylesheet" href="{{ asset(stylesheet) }}" media="all">
                    {% endfor %}
                {% endblock %}
            {% endblock %}

            {% block sonata_page_javascripts %}
                {% block page_javascripts %} {# Deprecated block #}
                    <!-- Le HTML5 shim, for IE6-8 support of HTML elements -->
                    <!--[if lt IE 9]>
                        <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
                    <![endif]-->
 					
                    {% for js in sonata_page.assets.javascripts %}
                        <script src="{{ asset(js) }}"></script>
                    {% endfor %}
                    <script src="/bundles/pixsortablebehavior/js/jquery-ui.min.js"></script>
                {% endblock %}

            {% endblock %}
        </head>
    {% endblock %}

    {% block sonata_page_body_tag %}
        <body class="sonata-bc">
    {% endblock %}

        {% block sonata_page_top_bar %}
        
      
            {% block page_top_bar %} {# Deprecated block #}
                {% if sonata_page.isEditor or ( app.user and is_granted('ROLE_PREVIOUS_ADMIN') ) %}

                    {% if sonata_page.isEditor and sonata_page.isInlineEditionOn %}
                        <!-- CMS specific variables -->
                        <script>
                            jQuery(document).ready(function() {
                                Sonata.Page.init({
                                    url: {
                                    	block_save_position: '{{ path('sonata_page_update_position', {}) }}',
                                        block_edit:          '{{ sonata_admin.url('sonata.page.admin.page|sonata.page.admin.block', 'edit', {'id': page.id , 'childId': 'BLOCK_ID'}) }}'
                                    }
                                });
                            });
                        </script>
                    {% endif %}

                    <header class="sonata-bc sonata-page-top-bar navbar navbar-inverse navbar-fixed-top" role="banner"
                    {% if sonata_page.isEditor and sonata_page.isInlineEditionOn %}
                        data-page-editor='{{ {
                            url: {
                            
                                block_save_position: path('sonata_page_update_position', {}),
                                block_edit:          sonata_admin.url('sonata.page.admin.page|sonata.page.admin.block', 'edit', {'id': page.id , 'childId': 'BLOCK_ID'})
                            }
                        }|json_encode()|raw }}'
                    {% endif %}>
                        <div class="container">
                            <ul class="nav navbar-nav">
                                {% if app.user and is_granted('ROLE_SONATA_ADMIN') %}
                                    <li><a href="{{ path('sonata_admin_dashboard') }}">{{ "header.sonata_admin_dashboard"|trans({}, 'SonataPageBundle') }}</a></li>
                                {% endif %}

                                {% if sonata_page.isEditor %}
                                    {% set sites = sonata_page.siteavailables %}

                                    {% if sites|length > 1 and site is defined %}
                                        <li class="dropdown">
                                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ site.name }} <span class="caret"></span></a>
                                            <ul class="dropdown-menu">
                                                {% for site in sites %}
                                                    <li><a href="{{ site.url }}">{{ site.name }}</a></li>
                                                {% endfor %}
                                            </ul>
                                        </li>
                                    {% endif %}

                                    <li class="dropdown">
                                        <a href="#" class="dropdown-toggle" data-toggle="dropdown">Page <span class="caret"></span></a>
                                        <ul class="dropdown-menu">
                                            {% if page is defined %}
                                                <li><a href="{{ sonata_admin.objectUrl('sonata.page.admin.page', 'edit', page) }}" target="_new">{{ "header.edit_page"|trans({}, 'SonataPageBundle') }}</a></li>
                                                <li><a href="{{ sonata_admin.objectUrl('sonata.page.admin.page|sonata.page.admin.snapshot', 'list', page) }}" target="_new">{{ "header.create_snapshot"|trans({}, 'SonataPageBundle') }}</a></li>
                                                <li class="divider"></li>
                                            {% endif %}

                                            <li><a href="{{ sonata_admin.url('sonata.page.admin.page', 'list') }}" target="_new">{{ "header.view_all_pages"|trans({}, 'SonataPageBundle') }}</a></li>

                                            {% if error_codes is defined and error_codes|length %}
                                                <li class="divider"></li>
                                                <li><a href="{{ path('sonata_page_exceptions_list') }}" target="_new">{{ "header.view_all_exceptions"|trans({}, 'SonataPageBundle') }}</a></li>
                                            {% endif %}
                                        </ul>
                                    </li>

                                    {% if page is defined %}
                                        <li>
                                            <a href="{{ sonata_admin.url('sonata.page.admin.page', 'compose', {'id': page.id}) }}">
                                                <i class="fa fa-magic"></i>
                                                {{ 'header.compose_page'|trans({}, 'SonataPageBundle')}}
                                            </a>
                                        </li>
                                    {% endif %}

                                    {% if page is defined and not page.enabled %}
                                        <li><span style="padding-left: 20px; background: red;"><strong><em>{{ 'header.page_is_disabled'|trans([], 'SonataPageBundle') }}</em></strong></span></li>
                                    {% endif %}

                                    {% if sonata_page.isInlineEditionOn and page is defined %}
                                        <li>
                                            <form class="form-inline" style="margin: 0px">
                                                <label class="checkbox inline" for="page-action-enabled-edit"><input type="checkbox" id="page-action-enabled-edit">{{ 'header.show_zone'|trans({}, 'SonataPageBundle') }}</label>
                                                <input type="submit" class="btn" value="{{ 'btn_save_position'|trans({}, 'SonataPageBundle') }}" id="page-action-save-position">
                                            </form>
                                        </li>
                                    {% endif %}
                                {% endif %}

                                {% if app.user and is_granted('ROLE_PREVIOUS_ADMIN') %}
                                    <li><a href="{{ url('homepage', {'_switch_user': '_exit'}) }}">{{ "header.switch_user_exit"|trans({}, 'SonataPageBundle')}}</a></li>
                                {% endif %}

                            </ul>
                        </div>
                    </header>

                {% endif %}
            {% endblock %}
        {% endblock %}

        {% block sonata_page_container %}
            {% block page_container %}{% endblock %} {# Deprecated block #}
        {% endblock %}

        {% block sonata_page_asset_footer %}
            {% block page_asset_footer %} {# Deprecated block #}
                {% if page is defined %}
                    {% if page.javascript is not empty %}
                        <script>
                            {{ page.javascript|raw }}
                        </script>
                    {% endif %}
                    {% if page.stylesheet is not empty %}
                        <style>
                            {{ page.stylesheet|raw }}
                        </style>
                    {% endif %}
                {% endif %}
                {#
                    These includes can be done only at this point as all blocks are loaded,
                    Limition : this does not work if a global page is loaded from an ESI tag inside a container block
                #}
                {{ sonata_block_include_stylesheets('screen', app.request.basePath) }}
                {{ sonata_block_include_javascripts('screen', app.request.basePath) }}
            {% endblock %}
        {% endblock %}

        <!-- monitoring:3e9fda56df2cdd3b039f189693ab7844fbb2d4f6 -->
    </body>
</html>

Voici le résultat :