Les interfaces par défaut de Sonata sont des CRUD. Ce qui est archi pratique (sinon on ne l’utiliserais pas). Mais une administration n’est pas uniquement composée de CRUD.
Ici nous allons voir comment on peux faire une page d’export de donnée de manière simple, en supprimant les vues par défaut des interfaces, et en créant la notre pour gérer notre bouton d’export.

Sélection_239-1

1 – Ajout de la librairie d’export

composer require sonata-project/exporter

Nous devons ensuite ajouter un fichier de configuration pour notre exporter.

#config/packages/sonata_exporter.yml
sonata_exporter:
  writers:
    csv:
      delimiter: ";"

2 – Création de notre controller

Nous allons créer notre controller qui contiendra notre configuration de base.

<?php
namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface as RoutingUrlGeneratorInterface;
use Sonata\FormatterBundle\Form\Type\SimpleFormatterType;
use Knp\Menu\ItemInterface as MenuItemInterface;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Route\RouteCollection;



class ExportModuleAdmin extends AbstractAdmin
{
    
    public function __construct( $code, $class, $baseControllerName ) {
        parent::__construct( $code, $class, $baseControllerName );
        

    }
    
    public function configure()
    {
        parent::configure();
    }
}

Nous devons ensuite créer nos actions :
On supprime toutes nos routes, nous nous créons une route export-module pour notre page.

    protected function configureRoutes(RouteCollection $collection)
    {
        $collection->clearExcept(['export-module']);
        $collection->remove('create');
        $collection->add('export-module');
    }

Nous devons ensuite enregistrer notre interface dans les services

    admin.export:
        class: App\Admin\ExportModuleAdmin
        arguments: [~, App\Entity\Export, App\Admin\CustomAction]
        tags:
            - { name: sonata.admin, manager_type: orm, label: "Export CSV" , label_translator_strategy: sonata.admin.label.strategy.underscore, label_catalogue: default}
        public: true

Comme vous pouvez le remarquer notre troisième argument est un controller admin additionnel, qui va gérer nos actions (src/Admin/CustomAction.php).

Et c’est directement dans l’action que nous allons définir la vue que nous allons utiliser pour ajouter notre bouton.
Nous créons donc une méthode exportModuleAction() qui contient notre référence à notre template.

<?php

namespace App\Admin;

use Sonata\AdminBundle\Controller\CRUDController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\Query\ResultSetMapping;

class CustomAction extends CRUDController
{
    public function exportDelefAction(){
        return $this->renderWithExtraParams('Admin/export.html.twig', array(
            'action' => 'export',
            'elements' => $this->admin->getShow(),
        ), null);
    }
}

Et notre fichier twig qui se trouve sur cette arborescence templates/Admin/export.html.twig contient notre code, tout simple, avec un lien pour récupérer l’export.
Le lien est uniquement la route d’export d’une interface d’admin CRUD qui contient en argument le format souhaité (csv).
La fonction d’export est une fonction automatique des interfaces CRUD.

{% extends '@SonataAdmin/standard_layout.html.twig' %}
{% block sonata_admin_content %}
{% include 'SonataCoreBundle:FlashMessage:render.html.twig' %}

<div>
    <h2 class="title-border">Export CSV</h2>

    <div class="box box-primary">
        <div class="box-body">
            <ul class="menu list-unstyled mb-0">
                <li><a class="btn-link" href="{{ path('admin_app_wdeclar_export', {'format' : 'csv'}) }}">Cliquez ici pour télécharger le fichier csv</a></li>
            </ul>
        </div>
    </div>
</div>
{% endblock %}

Pour que notre action soit disponible directement depuis le dashboard, nous devons inscrire l’action dans le fichier sonata_admin.yml

Et pour vérifier la route que nous allons renseigner, il nous suffit d’utiliser la commande :

php bin/console debug:router

Une fois repérée nous l’utilisons comme URL d’accès à notre bouton d’export dans le menu.

#config/packages/sonata_admin.yaml
sonata_admin:
    title: 'Sonata Admin'
    dashboard:
        blocks:
            - { type: sonata.admin.block.admin_list, position: left }
        groups:
            delef.admin.group.contrats:
                label: Gestion des contrats
                icon: '<i class="fa fa-cogs "></i>'
                items:
                    - route: admin_app_export_export-module
                      label:  "Export CSV"

Mais ce n’est pas tout. Il nous faut ensuite configurer notre bouton sur le dashboard. Nous allons ajouter la méthode getDashboardActions().
On y ajoute notre action et la classe css de l’icône que nous voulons utiliser. Ici on utilise une icône fontawsome 5 que nous avons ajouté à notre projet.

public function getDashboardActions()
    {
        $actions = parent::getDashboardActions();
    
        $actions['import'] = [
            'label'              => 'Export',
            'url'                => $this->generateUrl('export-delef'),
            'icon'               => ' fas fa-file-export',
            'translation_domain' => 'SonataAdminBundle', // optional
            'template'           => '@SonataAdmin/CRUD/dashboard__action.html.twig', // optional
        ];
    
        return $actions;
    }

Ceci nous donne le fichier php suivant :

<?php
namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface as RoutingUrlGeneratorInterface;
use Sonata\FormatterBundle\Form\Type\SimpleFormatterType;
use Knp\Menu\ItemInterface as MenuItemInterface;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Route\RouteCollection;


class ExportModuleAdmin extends AbstractAdmin
{
    
    public function __construct( $code, $class, $baseControllerName ) {
        parent::__construct( $code, $class, $baseControllerName );
        

    }
    
    public function configure()
    {
        parent::configure();
    }
    
    
    protected function configureRoutes(RouteCollection $collection)
    {
        //$collection->clearExcept(['list']);
        $collection->clearExcept(['export-delef']);
        $collection->remove('create');
        $collection->add('export-delef');
    }
    

    public function setContainer(ContainerInterface $container)
    {
        $this->container = $container;
    }
    
   
    public function getDashboardActions()
    {
        $actions = parent::getDashboardActions();
    
        $actions['import'] = [
            'label'              => 'Export',
            'url'                => $this->generateUrl('export-delef'),
            'icon'               => ' fas fa-file-export',
            'translation_domain' => 'SonataAdminBundle', // optional
            'template'           => '@SonataAdmin/CRUD/dashboard__action.html.twig', // optional
        ];
    
        return $actions;
    }
    
}

A ce stade nous avons notre accès depuis le dashboard :

Sélection_238

Et notre page de téléchargement :

Sélection_239

3 – Configuration de l’export

Il nous manque plus que la configuration de notre export.
Nous avons vu plus haut que nous faisons référence à une action par défaut ‘export’ d’une interface CRUD de notre admin.
C’est donc directement dans notre entité que nous allons configurer l’export.

Dans notre CRUD nous ajoutons la référence suivante :

use App\Source\DBALStatementSourceIterator;

Et les deux méthodes suivantes :

    public function getDataSourceIterator()
    {
        $container = $this->getConfigurationPool()->getContainer();
        $em = $container->get('doctrine.orm.entity_manager');
        $conn = $em->getConnection();
        $fields = $this->getExportFields();
        $field_str = implode(',', $fields);
        $sql = "SELECT {$field_str} FROM myTable d where champs1 ='valeur' order by id asc";
        $stmt = $conn->prepare($sql);
        $stmt->execute();

        return new DBALStatementSourceIterator($stmt);
    }

    public function getExportFields() {
        return [
            'd.champs1','d.champs2','d.champs3'
        ];
    }

Il nous reste à ajouter notre source iterator qui se trouve dans src/Source/DBALStatementSourceIterator.php

<?php

namespace App\Source;

use Sonata\Exporter\Exception\InvalidMethodCallException;
use Sonata\Exporter\Source\SourceIteratorInterface;

class DBALStatementSourceIterator implements SourceIteratorInterface
{
    /**
     * @var \Doctrine\DBAL\Statement
     */
    protected $statement;

    /**
     * @var mixed
     */
    protected $current;

    /**
     * @var int
     */
    protected $position;

    /**
     * @var bool
     */
    protected $rewinded;

    /**
     * @param \Doctrine\DBAL\Statement $statement
     */
    public function __construct(\Doctrine\DBAL\Statement $statement)
    {
        $this->statement = $statement;
        $this->position = 0;
        $this->rewinded = false;
    }

    /**
     * {@inheritdoc}
     */
    public function current()
    {
        return $this->current;
    }

    /**
     * {@inheritdoc}
     */
    public function next()
    {
        $this->current = $this->statement->fetch(\Doctrine\DBAL\FetchMode::ASSOCIATIVE);
        ++$this->position;
    }

    /**
     * {@inheritdoc}
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * {@inheritdoc}
     */
    public function valid()
    {
        return \is_array($this->current);
    }

    /**
     * {@inheritdoc}
     */
    public function rewind()
    {
        if ($this->rewinded) {
            throw new InvalidMethodCallException('Cannot rewind a PDOStatement');
        }

        $this->current = $this->statement->fetch(\Doctrine\DBAL\FetchMode::ASSOCIATIVE);
        $this->rewinded = true;
    }
}

Et le tour est joué !