The default interfaces of Sonata are CRUDs. This is incredibly practical (otherwise, we wouldn't use it). But an administration is not only composed of CRUDs.
Here we will see how to create a simple data export page, by removing the default views of the interfaces, and creating our own to manage our export button.

Sélection_239-1

1 – Adding the export library

composer require sonata-project/exporter

We then need to add a configuration file for our exporter.

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

2 – Creating our controller

We are going to create our controller which will contain our basic configuration.

<?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();
    }
}

Then we need to create our actions:
We delete all our routes, we create an export-module route for our page.

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

We then need to register our interface in the 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

As you can notice our third argument is an additional admin controller, which will manage our actions (src/Admin/CustomAction.php).

And it is directly in the action that we will define the view we will use to add our button.
So we create an exportModuleAction() method that contains our reference to our 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);
    }
}

And our twig file which is located at this hierarchy templates/Admin/export.html.twig contains our code, very simple, with a link to retrieve the export.
The link is just the export route of a CRUD admin interface which contains as an argument the desired format (csv).
The export function is an automatic function of CRUD interfaces.

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

To make our action directly available from the dashboard, we need to register the action in the sonata_admin.yml file

And to verify the route that we will provide, we simply need to use the command:

php bin/console debug:router

Once identified, we use it as the access URL for our export button in the 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"

But that's not all. We also need to configure our button on the dashboard. We are going to add the getDashboardActions() method.
We add our action and the CSS class of the icon we want to use. Here we use a Font Awesome 5 icon that we've added to our project.

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;
    }

This gives us the following PHP file:

<?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;
    }
    
}

At this stage, we have our access from the dashboard:

Sélection_238

And our download page:

Sélection_239

3 – Configuration of the export

We only miss the configuration of our export.
We saw earlier that we make reference to a default 'export' action of a CRUD interface of our admin.
So it is directly in our entity that we are going to configure the export.

In our CRUD we add the following reference:

use App\Source\DBALStatementSourceIterator;

And the following two methods:

    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'
        ];
    }

We just have to add our source iterator which is located in 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;
    }
}

And that's it!