Sonata es un conjunto de paquetes para Symfony 4. Uno de ellos permite gestionar contenido de manera similar a WordPress.
Hay poca documentación disponible en internet y por una buena razón, la instalación tiene errores y a las pocas personas que han lidiado con el rastreador de errores en GitHub, se les ha dicho sin rodeos que lean el manual (RTFM).

Quedarse estancado con tal fracaso no es realmente mi estilo.
Aquí está cómo instalar y configurar Sonata-Page-Bundle, con la instalación oficial y las soluciones para estos malditos errores.

Paso 1, recuperamos nuestro esqueleto de Symfony 4 que incluye nuestra base funcional del marco complementada por Sonata + FosUser + Media.
Ya nos ahorramos un buen día ^^

En resumen, debes seguir la documentación: https://sonata-project.org/bundles/page/3-x/doc/reference/installation.html

Instalar todos los paquetes y seguir meticulosamente la documentación.

La primera dificultad es la instalación a través de composer. No pude instalar todo para php 7.1. Ni para php 7.2, para el caso. Siempre hay un paquete en conflicto.

El truco en composer es salir del estado estable con la instrucción minimum-stability en tu archivo composer.json y allí, después de 3 o 4 horas de probar versiones, puedes instalar todo.

Aquí está mi 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
}

Una vez que los paquetes están instalados y configurados, no olvides instalar CKEditor y los activos. Y de paso, tu super usuario administrador.

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

Queda por crear el primer sitio:

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

El primer sitio ya está operativo. Bueno, casi.

En la interfaz de páginas, necesitas crear nuestra primera página

image

La configuración predeterminada no contiene nada. Entonces es necesario organizar el contenido de esta página a través de un editor, que permite organizar nuestra página en forma de bloques, asignados a áreas de nuestras plantillas.

En nuestra configuración del paquete, hemos declarado una plantilla predeterminada. Y es esta configuración la que nos permite modelar la interfaz de gestión de bloques

   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

En nuestro ejemplo anterior, hemos insertado 2 bloques en nuestra zona.
El primero es un texto simple, el segundo es un menú (un menú clásico de pie de página).
Para insertar un bloque nos posicionamos en una zona a la izquierda y hacemos clic en el botón + a la derecha para seleccionar el tipo de bloque después de haberlo seleccionado de la lista.

Toda la gestión de páginas se hace de esta manera.
Lo que significa que tendremos que crear una serie de bloques según las necesidades de nuestra aplicación.
Y para crear un bloque, es súper simple. Tomaremos el ejemplo de nuestro bloque de menú.

Los menús son gestionados a través del paquete Sonata-Menu-Bundle que es compatible con Sonata-Page-Bundle. Pero no viene con un bloque predeterminado. Es estúpido porque un menú es una cosa bastante básica.

Entonces crearemos nuestro bloque en /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',
        ]);
    }
}

Agregamos nuestras plantillas en /Block/Menu/menu.html.twig y el archivo de macro de menú en /Block/Menu/menu_tree_macro.html.twig.
Simplemente son los archivos predeterminados del paquete rodeados por un div que contiene información del bloque para la edición en el front-end.

{#/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 %}

Hacemos referencia a nuestro bloque en nuestros servicios

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 }   

Y luego tenemos nuestro nuevo bloque que está disponible en nuestra lista de bloques. Básicamente puedes hacer lo que quieras en un bloque, a la manera de un controlador.

Sélection_296

Así podemos crear nuestro menú de pie de página

image-3

Y usarlo en nuestro bloque

image-4

Aquí está la representación de nuestra página

image-5

De hecho, no. Esa es la representación con una plantilla corregida. De lo contrario, la representación se vería así:

Sélection_302

Así que vamos a tener que duplicar la plantilla base en nuestro proyecto y corregir lo que está mal.

Creamos nuestro base_layout y nuestra plantilla de prueba test-layout en el directorio 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>

y nuestra plantilla

{#/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 %}

Luego lo referenciamos en el archivo de configuración del paquete

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
                    

Nuestra edición en el front-end está casi funcional.
Tenemos una barra de navegación en nuestra página. Cuando la edición de zonas está activada, todo se muestra correctamente. Si hacemos doble clic en un área de bloques, aparece una ventana con la edición del bloque en el admin. Y cuando movemos bloques de un área a otra, los mueve bien. Pero si miramos la página, nada se guarda.
Tendremos que crear un método dedicado de nuestro lado para guardar los ajustes.

Sélection_303

Entonces crearemos un controlador en nuestro paquete extendido para gestionar la actualización de datos.

Creamos nuestro UpdatePositionController con nuestro método 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'
        ]);

    }
}
?>


Luego debemos referenciarlo en nuestros servicios:

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

Luego debemos referenciarlo en las anotaciones para que la ruta se gestione automáticamente a través de Symfony:

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

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

Y finalmente, debemos modificar el enlace para actualizar el orden de los bloques en nuestra plantilla que debería ser: 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>

Este es el resultado: