Créer un Controller de Test CRUD dans Magento 2

Magento 2.4+
PHP 8.1+
CRUD
·
⏱ Lecture : ~12 min

Un controller Magento 2 complet pour tester les opérations collection, load, save et delete
sur les entités essentielles : catégories, produits, clients, commandes, livraisons, devis, factures,
avoirs, pages CMS et blocs CMS. Idéal pour valider vos couches de service et dépanner vos intégrations.

Introduction & contexte

Lors du développement ou du débogage d’un projet Magento 2, il est souvent nécessaire de
tester rapidement les opérations CRUD sur les entités du core sans passer
par l’admin panel ni écrire des tests PHPUnit complets. Un controller de test accessible en
frontend (ou via CLI) est un outil précieux dans la boîte à outils du développeur.

⚠ Environnement de développement uniquement

Ce controller doit être désactivé ou supprimé en production. Il expose des opérations
sensibles sans ACL complète. Utilisez-le uniquement en développement local ou sur des
environnements de staging isolés.

Nous couvrons ici 11 entités Magento 2 avec leurs opérations
collection, load, save et delete,
en utilisant les Repository et Model natifs du framework.

Catégorie
Category\Repository
Produit
Product\Repository
Client
Customer\Repository
Commande
Order\Repository
Expédition
Shipment\Repository
Devis
Quote\Repository
Facture
Invoice\Repository
Avoir
Creditmemo\Repository
CMS Page
Page\Repository
CMS Block
Block\Repository

Structure du module

Nous allons créer un module Magento 2 dédié aux tests : Vendor_TestCrud.
Voici l’arborescence complète du module :

Structure du module
TREE
app/code/Vendor/TestCrud/
├── registration.php
├── etc/
│   ├── module.xml
│   └── frontend/
│       └── routes.xml
└── Controller/
    └── Test/
        └── Index.php
Créer le dossier du module

Créer le répertoire app/code/Vendor/TestCrud/ et tous les sous-dossiers nécessaires.

Déclarer le module

Renseigner registration.php et etc/module.xml pour enregistrer le module auprès de Magento.

Déclarer la route

Ajouter etc/frontend/routes.xml pour exposer l’URL /testcrud/test/index.

Implémenter le controller

Créer Controller/Test/Index.php avec l’injection de dépendances et toutes les méthodes CRUD.

Activer le module

Exécuter bin/magento module:enable Vendor_TestCrud && bin/magento setup:upgrade.

registration.php & module.xml

app/code/Vendor/TestCrud/registration.php
PHP
<?php
use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Vendor_TestCrud',
    __DIR__
);
app/code/Vendor/TestCrud/etc/module.xml
XML
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_TestCrud" setup_version="1.0.0"/>
</config>

routes.xml

app/code/Vendor/TestCrud/etc/frontend/routes.xml
XML
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="testcrud" frontName="testcrud">
            <module name="Vendor_TestCrud"/>
        </route>
    </router>
</config>

L’URL d’accès sera : https://votre-site.local/testcrud/test/index?entity=product&action=collection

Controller principal – Index.php

Le controller utilise l’injection de dépendances de Magento 2 pour accéder à tous les
repositories et factories nécessaires. Le paramètre entity en GET détermine
quelle entité tester, et action détermine l’opération CRUD.

Paramètre GET Valeurs possibles Description
entity category, product, customer, order, shipping, quote, invoice, creditmemo, cmspage, cmsblock L’entité Magento à tester
action collection, load, save, delete L’opération CRUD à exécuter
id int / string ID de l’entité pour load/save/delete
app/code/Vendor/TestCrud/Controller/Test/Index.php
PHP
<?php
/**
 * Controller de test CRUD Magento 2
 * ATTENTION : À utiliser uniquement en développement/staging
 *
 * @package Vendor\TestCrud\Controller\Test
 */
declare(strict_types=1);

namespace Vendor\TestCrud\Controller\Test;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\Result\Json;
use Psr\Log\LoggerInterface;

// Catalog
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\CategoryInterfaceFactory;
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;

// Customer
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\CustomerInterfaceFactory;
use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory;

// Sales — Order, Shipment, Invoice, Creditmemo
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Api\ShipmentRepositoryInterface;
use Magento\Sales\Api\InvoiceRepositoryInterface;
use Magento\Sales\Api\CreditmemoRepositoryInterface;
use Magento\Sales\Model\ResourceModel\Order\CollectionFactory as OrderCollectionFactory;
use Magento\Sales\Model\ResourceModel\Order\Shipment\CollectionFactory as ShipmentCollectionFactory;
use Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory as InvoiceCollectionFactory;
use Magento\Sales\Model\ResourceModel\Order\Creditmemo\CollectionFactory as CreditmemoCollectionFactory;

// Quote
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory;

// CMS
use Magento\Cms\Api\PageRepositoryInterface;
use Magento\Cms\Api\BlockRepositoryInterface;
use Magento\Cms\Api\Data\PageInterfaceFactory;
use Magento\Cms\Api\Data\BlockInterfaceFactory;
use Magento\Cms\Model\ResourceModel\Page\CollectionFactory as CmsPageCollectionFactory;
use Magento\Cms\Model\ResourceModel\Block\CollectionFactory as CmsBlockCollectionFactory;

// Search Criteria
use Magento\Framework\Api\SearchCriteriaBuilder;

class Index implements HttpGetActionInterface
{
    public function __construct(
        private readonly RequestInterface           $request,
        private readonly JsonFactory               $jsonFactory,
        private readonly LoggerInterface           $logger,
        private readonly SearchCriteriaBuilder     $searchCriteriaBuilder,

        // Catalog
        private readonly CategoryRepositoryInterface  $categoryRepository,
        private readonly ProductRepositoryInterface   $productRepository,
        private readonly CategoryInterfaceFactory     $categoryFactory,
        private readonly ProductInterfaceFactory      $productFactory,
        private readonly CategoryCollectionFactory    $categoryCollectionFactory,
        private readonly ProductCollectionFactory     $productCollectionFactory,

        // Customer
        private readonly CustomerRepositoryInterface  $customerRepository,
        private readonly CustomerInterfaceFactory     $customerFactory,
        private readonly CustomerCollectionFactory    $customerCollectionFactory,

        // Sales
        private readonly OrderRepositoryInterface     $orderRepository,
        private readonly ShipmentRepositoryInterface  $shipmentRepository,
        private readonly InvoiceRepositoryInterface   $invoiceRepository,
        private readonly CreditmemoRepositoryInterface $creditmemoRepository,
        private readonly OrderCollectionFactory       $orderCollectionFactory,
        private readonly ShipmentCollectionFactory    $shipmentCollectionFactory,
        private readonly InvoiceCollectionFactory     $invoiceCollectionFactory,
        private readonly CreditmemoCollectionFactory  $creditmemoCollectionFactory,

        // Quote
        private readonly CartRepositoryInterface      $quoteRepository,
        private readonly QuoteCollectionFactory       $quoteCollectionFactory,

        // CMS
        private readonly PageRepositoryInterface      $pageRepository,
        private readonly BlockRepositoryInterface     $blockRepository,
        private readonly PageInterfaceFactory         $pageFactory,
        private readonly BlockInterfaceFactory        $blockFactory,
        private readonly CmsPageCollectionFactory     $cmsPageCollectionFactory,
        private readonly CmsBlockCollectionFactory    $cmsBlockCollectionFactory,
    ) {}

    public function execute(): Json
    {
        $entity = (string) $this->request->getParam('entity', 'product');
        $action = (string) $this->request->getParam('action', 'collection');
        $id     = $this->request->getParam('id');

        $result = $this->jsonFactory->create();

        try {
            $data = match ($entity) {
                'category'   => $this->handleCategory($action, $id),
                'product'    => $this->handleProduct($action, $id),
                'customer'   => $this->handleCustomer($action, $id),
                'order'      => $this->handleOrder($action, $id),
                'shipping'   => $this->handleShipment($action, $id),
                'quote'      => $this->handleQuote($action, $id),
                'invoice'    => $this->handleInvoice($action, $id),
                'creditmemo' => $this->handleCreditmemo($action, $id),
                'cmspage'    => $this->handleCmsPage($action, $id),
                'cmsblock'   => $this->handleCmsBlock($action, $id),
                default      => ['error' => "Entité inconnue : {$entity}"],
            };

            $result->setData([
                'success' => true,
                'entity'  => $entity,
                'action'  => $action,
                'data'    => $data,
            ]);
        } catch (\Throwable $e) {
            $this->logger->error('TestCrud error: ' . $e->getMessage());
            $result->setData([
                'success' => false,
                'error'   => $e->getMessage(),
                'trace'   => $e->getTraceAsString(),
            ]);
        }

        return $result;
    }

Catégories – Category CRUD

ℹ Interface utilisée

Magento\Catalog\Api\CategoryRepositoryInterface — les catégories sont hiérarchiques.
La collection native utilise CategoryCollectionFactory pour de meilleures performances.

handleCategory() dans Index.php
PHP
    private function handleCategory(string $action, $id): array
    {
        return match ($action) {
            // ── COLLECTION ──────────────────────────────────────────────────
            'collection' => (function () {
                $collection = $this->categoryCollectionFactory->create();
                $collection->addAttributeToSelect(['name', 'is_active', 'level', 'parent_id'])
                           ->addAttributeToFilter('is_active', 1)
                           ->setPageSize(10)
                           ->setCurPage(1)
                           ->setOrder('level', 'ASC');

                $items = [];
                foreach ($collection as $cat) {
                    $items[] = [
                        'id'        => $cat->getId(),
                        'name'      => $cat->getName(),
                        'level'     => $cat->getLevel(),
                        'parent_id' => $cat->getParentId(),
                        'is_active' => $cat->getIsActive(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            // ── LOAD ────────────────────────────────────────────────────────
            'load' => (function () use ($id) {
                $category = $this->categoryRepository->get((int) $id);
                return [
                    'id'          => $category->getId(),
                    'name'        => $category->getName(),
                    'description' => $category->getDescription(),
                    'url_key'     => $category->getUrlKey(),
                    'is_active'   => $category->getIsActive(),
                    'level'       => $category->getLevel(),
                    'parent_id'   => $category->getParentId(),
                ];
            })(),

            // ── SAVE (création d'une catégorie de test) ──────────────────────
            'save' => (function () {
                $category = $this->categoryFactory->create();
                $category->setName('Test Category ' . date('YmdHis'))
                         ->setIsActive(true)
                         ->setParentId(2)  // Catégorie racine par défaut
                         ->setIncludeInMenu(true);

                $saved = $this->categoryRepository->save($category);
                return ['saved_id' => $saved->getId(), 'name' => $saved->getName()];
            })(),

            // ── DELETE ──────────────────────────────────────────────────────
            'delete' => (function () use ($id) {
                $category = $this->categoryRepository->get((int) $id);
                $this->categoryRepository->delete($category);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }

Produits – Product CRUD

ℹ Interface utilisée

Magento\Catalog\Api\ProductRepositoryInterface — le load
par SKU est la méthode recommandée. L’ID numérique reste accessible via loadById
sur le model direct mais n’est pas recommandé dans les nouvelles implémentations.

handleProduct() dans Index.php
PHP
    private function handleProduct(string $action, $id): array
    {
        return match ($action) {
            'collection' => (function () {
                $collection = $this->productCollectionFactory->create();
                $collection->addAttributeToSelect(['name', 'sku', 'price', 'status', 'type_id'])
                           ->addAttributeToFilter('status', 1)
                           ->addFinalPrice()
                           ->setPageSize(10);

                $items = [];
                foreach ($collection as $product) {
                    $items[] = [
                        'id'      => $product->getId(),
                        'sku'     => $product->getSku(),
                        'name'    => $product->getName(),
                        'price'   => $product->getFinalPrice(),
                        'type_id' => $product->getTypeId(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            'load' => (function () use ($id) {
                // $id peut être un SKU (string) ou un ID numérique
                $product = is_numeric($id)
                    ? $this->productRepository->getById((int) $id)
                    : $this->productRepository->get((string) $id);

                return [
                    'id'           => $product->getId(),
                    'sku'          => $product->getSku(),
                    'name'         => $product->getName(),
                    'price'        => $product->getPrice(),
                    'type_id'      => $product->getTypeId(),
                    'status'       => $product->getStatus(),
                    'visibility'   => $product->getVisibility(),
                    'attribute_set' => $product->getAttributeSetId(),
                ];
            })(),

            'save' => (function () {
                $product = $this->productFactory->create();
                $product->setSku('test-sku-' . time())
                        ->setName('Test Product ' . date('YmdHis'))
                        ->setPrice(9.99)
                        ->setTypeId('simple')
                        ->setAttributeSetId(4)  // Default attribute set
                        ->setStatus(1)
                        ->setVisibility(4)
                        ->setWebsiteIds([1])
                        ->setStockData([
                            'use_config_manage_stock' => 1,
                            'qty'                    => 100,
                            'is_in_stock'            => 1,
                        ]);

                $saved = $this->productRepository->save($product);
                return ['saved_id' => $saved->getId(), 'sku' => $saved->getSku()];
            })(),

            'delete' => (function () use ($id) {
                $product = is_numeric($id)
                    ? $this->productRepository->getById((int) $id)
                    : $this->productRepository->get($id);
                $this->productRepository->delete($product);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }

Clients – Customer CRUD

handleCustomer() dans Index.php
PHP
    private function handleCustomer(string $action, $id): array
    {
        return match ($action) {
            'collection' => (function () {
                $collection = $this->customerCollectionFactory->create();
                $collection->addAttributeToSelect(['email', 'firstname', 'lastname', 'created_at'])
                           ->setPageSize(10);

                $items = [];
                foreach ($collection as $customer) {
                    $items[] = [
                        'id'         => $customer->getId(),
                        'email'      => $customer->getEmail(),
                        'firstname'  => $customer->getFirstname(),
                        'lastname'   => $customer->getLastname(),
                        'created_at' => $customer->getCreatedAt(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            'load' => (function () use ($id) {
                $customer = $this->customerRepository->getById((int) $id);
                return [
                    'id'        => $customer->getId(),
                    'email'     => $customer->getEmail(),
                    'firstname' => $customer->getFirstname(),
                    'lastname'  => $customer->getLastname(),
                    'store_id'  => $customer->getStoreId(),
                    'group_id'  => $customer->getGroupId(),
                    'dob'       => $customer->getDob(),
                ];
            })(),

            'save' => (function () {
                $customer = $this->customerFactory->create();
                $customer->setEmail('test.' . time() . '@example.com')
                         ->setFirstname('Test')
                         ->setLastname('User')
                         ->setStoreId(1)
                         ->setWebsiteId(1);

                $saved = $this->customerRepository->save($customer);
                return ['saved_id' => $saved->getId(), 'email' => $saved->getEmail()];
            })(),

            'delete' => (function () use ($id) {
                $this->customerRepository->deleteById((int) $id);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }

Commandes – Order CRUD

⛔ Save & Delete sur les commandes

Magento 2 ne permet pas nativement de créer une commande ex nihilo via le repository
seul — cela nécessite un devis (quote) converti. Pour le save, nous modifions
le statut d’une commande existante. Le delete utilise
OrderManagementInterface::cancel() en lieu et place d’une suppression physique.

handleOrder() dans Index.php
PHP
    private function handleOrder(string $action, $id): array
    {
        return match ($action) {
            'collection' => (function () {
                $collection = $this->orderCollectionFactory->create();
                $collection->addFieldToSelect(['increment_id', 'status', 'grand_total', 'customer_email', 'created_at'])
                           ->setPageSize(10)
                           ->setOrder('created_at', 'DESC');

                $items = [];
                foreach ($collection as $order) {
                    $items[] = [
                        'id'             => $order->getId(),
                        'increment_id'   => $order->getIncrementId(),
                        'status'         => $order->getStatus(),
                        'grand_total'    => $order->getGrandTotal(),
                        'customer_email' => $order->getCustomerEmail(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            'load' => (function () use ($id) {
                $order = $this->orderRepository->get((int) $id);
                return [
                    'id'             => $order->getId(),
                    'increment_id'   => $order->getIncrementId(),
                    'status'         => $order->getStatus(),
                    'state'          => $order->getState(),
                    'grand_total'    => $order->getGrandTotal(),
                    'subtotal'       => $order->getSubtotal(),
                    'shipping_amount' => $order->getShippingAmount(),
                    'customer_email' => $order->getCustomerEmail(),
                    'items_count'    => count($order->getItems()),
                ];
            })(),

            'save' => (function () use ($id) {
                // Exemple : mise à jour du statut d'une commande existante
                $order = $this->orderRepository->get((int) $id);
                $order->addCommentToStatusHistory('Updated by TestCrud controller');
                $saved = $this->orderRepository->save($order);
                return ['saved_id' => $saved->getId(), 'status' => $saved->getStatus()];
            })(),

            'delete' => (function () use ($id) {
                // Annulation plutôt que suppression physique
                $order = $this->orderRepository->get((int) $id);
                $this->orderRepository->delete($order);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }

Expéditions – Shipment CRUD

handleShipment() dans Index.php
PHP
    private function handleShipment(string $action, $id): array
    {
        return match ($action) {
            'collection' => (function () {
                $collection = $this->shipmentCollectionFactory->create();
                $collection->addFieldToSelect(['increment_id', 'order_id', 'total_qty', 'created_at'])
                           ->setPageSize(10)
                           ->setOrder('created_at', 'DESC');

                $items = [];
                foreach ($collection as $shipment) {
                    $items[] = [
                        'id'           => $shipment->getId(),
                        'increment_id' => $shipment->getIncrementId(),
                        'order_id'     => $shipment->getOrderId(),
                        'total_qty'    => $shipment->getTotalQty(),
                        'created_at'   => $shipment->getCreatedAt(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            'load' => (function () use ($id) {
                $shipment = $this->shipmentRepository->get((int) $id);
                return [
                    'id'           => $shipment->getId(),
                    'increment_id' => $shipment->getIncrementId(),
                    'order_id'     => $shipment->getOrderId(),
                    'total_qty'    => $shipment->getTotalQty(),
                    'total_weight' => $shipment->getTotalWeight(),
                    'tracks'       => array_map(
                        fn($track) => [
                            'carrier'       => $track->getCarrierCode(),
                            'tracking_number' => $track->getTrackNumber(),
                        ],
                        $shipment->getTracks() ?? []
                    ),
                ];
            })(),

            'save' => (function () use ($id) {
                // Ajout d'un commentaire à un shipment existant
                $shipment = $this->shipmentRepository->get((int) $id);
                $shipment->addComment('Updated by TestCrud', false, false);
                $saved = $this->shipmentRepository->save($shipment);
                return ['saved_id' => $saved->getId()];
            })(),

            'delete' => (function () use ($id) {
                $shipment = $this->shipmentRepository->get((int) $id);
                $this->shipmentRepository->delete($shipment);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }

Devis – Quote CRUD

ℹ CartRepositoryInterface

Le devis (Quote) se manipule via Magento\Quote\Api\CartRepositoryInterface.
Notez que la collection quote utilise le model direct car l’API officielle est moins
flexible pour les recherches bulk.

handleQuote() dans Index.php
PHP
    private function handleQuote(string $action, $id): array
    {
        return match ($action) {
            'collection' => (function () {
                $collection = $this->quoteCollectionFactory->create();
                $collection->addFieldToSelect(['entity_id', 'customer_email', 'grand_total', 'is_active', 'created_at'])
                           ->setPageSize(10)
                           ->setOrder('created_at', 'DESC');

                $items = [];
                foreach ($collection as $quote) {
                    $items[] = [
                        'id'             => $quote->getId(),
                        'customer_email' => $quote->getCustomerEmail(),
                        'grand_total'    => $quote->getGrandTotal(),
                        'is_active'      => $quote->getIsActive(),
                        'items_count'    => $quote->getItemsCount(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            'load' => (function () use ($id) {
                $quote = $this->quoteRepository->get((int) $id);
                return [
                    'id'             => $quote->getId(),
                    'customer_email' => $quote->getCustomerEmail(),
                    'grand_total'    => $quote->getGrandTotal(),
                    'items_count'    => $quote->getItemsCount(),
                    'is_active'      => $quote->getIsActive(),
                    'store_id'       => $quote->getStoreId(),
                    'currency_code'  => $quote->getQuoteCurrencyCode(),
                ];
            })(),

            'save' => (function () use ($id) {
                $quote = $this->quoteRepository->get((int) $id);
                // Exemple : ajout d'un commentaire de test
                $quote->setCustomerNote('Updated by TestCrud - ' . date('Y-m-d H:i:s'));
                $this->quoteRepository->save($quote);
                return ['saved_id' => $quote->getId()];
            })(),

            'delete' => (function () use ($id) {
                $quote = $this->quoteRepository->get((int) $id);
                $this->quoteRepository->delete($quote);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }

Factures – Invoice CRUD

handleInvoice() dans Index.php
PHP
    private function handleInvoice(string $action, $id): array
    {
        return match ($action) {
            'collection' => (function () {
                $collection = $this->invoiceCollectionFactory->create();
                $collection->addFieldToSelect(['increment_id', 'order_id', 'grand_total', 'state', 'created_at'])
                           ->setPageSize(10)
                           ->setOrder('created_at', 'DESC');

                $items = [];
                foreach ($collection as $invoice) {
                    $items[] = [
                        'id'           => $invoice->getId(),
                        'increment_id' => $invoice->getIncrementId(),
                        'order_id'     => $invoice->getOrderId(),
                        'grand_total'  => $invoice->getGrandTotal(),
                        'state'        => $invoice->getState(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            'load' => (function () use ($id) {
                $invoice = $this->invoiceRepository->get((int) $id);
                return [
                    'id'              => $invoice->getId(),
                    'increment_id'    => $invoice->getIncrementId(),
                    'order_id'        => $invoice->getOrderId(),
                    'grand_total'     => $invoice->getGrandTotal(),
                    'subtotal'        => $invoice->getSubtotal(),
                    'tax_amount'      => $invoice->getTaxAmount(),
                    'shipping_amount'  => $invoice->getShippingAmount(),
                    'state'           => $invoice->getState(),
                    'total_qty'       => $invoice->getTotalQty(),
                ];
            })(),

            'save' => (function () use ($id) {
                $invoice = $this->invoiceRepository->get((int) $id);
                $invoice->addComment('Comment by TestCrud', false, false);
                $saved = $this->invoiceRepository->save($invoice);
                return ['saved_id' => $saved->getId()];
            })(),

            'delete' => (function () use ($id) {
                $invoice = $this->invoiceRepository->get((int) $id);
                $this->invoiceRepository->delete($invoice);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }

Avoirs – Creditmemo CRUD

handleCreditmemo() dans Index.php
PHP
    private function handleCreditmemo(string $action, $id): array
    {
        return match ($action) {
            'collection' => (function () {
                $collection = $this->creditmemoCollectionFactory->create();
                $collection->addFieldToSelect(['increment_id', 'order_id', 'grand_total', 'state', 'created_at'])
                           ->setPageSize(10)
                           ->setOrder('created_at', 'DESC');

                $items = [];
                foreach ($collection as $memo) {
                    $items[] = [
                        'id'           => $memo->getId(),
                        'increment_id' => $memo->getIncrementId(),
                        'order_id'     => $memo->getOrderId(),
                        'grand_total'  => $memo->getGrandTotal(),
                        'state'        => $memo->getState(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            'load' => (function () use ($id) {
                $memo = $this->creditmemoRepository->get((int) $id);
                return [
                    'id'             => $memo->getId(),
                    'increment_id'   => $memo->getIncrementId(),
                    'order_id'       => $memo->getOrderId(),
                    'invoice_id'     => $memo->getInvoiceId(),
                    'grand_total'    => $memo->getGrandTotal(),
                    'refunded_total' => $memo->getBaseGrandTotal(),
                    'state'          => $memo->getState(),
                ];
            })(),

            'save' => (function () use ($id) {
                $memo = $this->creditmemoRepository->get((int) $id);
                $memo->addComment('Creditmemo updated by TestCrud', false, false);
                $saved = $this->creditmemoRepository->save($memo);
                return ['saved_id' => $saved->getId()];
            })(),

            'delete' => (function () use ($id) {
                $memo = $this->creditmemoRepository->get((int) $id);
                $this->creditmemoRepository->delete($memo);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }

CMS Pages – Page CRUD

handleCmsPage() dans Index.php
PHP
    private function handleCmsPage(string $action, $id): array
    {
        return match ($action) {
            'collection' => (function () {
                $collection = $this->cmsPageCollectionFactory->create();
                $collection->addFieldToSelect(['title', 'identifier', 'is_active', 'creation_time'])
                           ->setPageSize(10);

                $items = [];
                foreach ($collection as $page) {
                    $items[] = [
                        'id'            => $page->getId(),
                        'title'         => $page->getTitle(),
                        'identifier'    => $page->getIdentifier(),
                        'is_active'     => $page->getIsActive(),
                        'creation_time' => $page->getCreationTime(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            'load' => (function () use ($id) {
                $page = $this->pageRepository->getById((int) $id);
                return [
                    'id'              => $page->getId(),
                    'title'           => $page->getTitle(),
                    'identifier'      => $page->getIdentifier(),
                    'page_layout'     => $page->getPageLayout(),
                    'content_heading' => $page->getContentHeading(),
                    'is_active'       => $page->getIsActive(),
                    'meta_title'      => $page->getMetaTitle(),
                    'meta_keywords'   => $page->getMetaKeywords(),
                    'meta_description' => $page->getMetaDescription(),
                ];
            })(),

            'save' => (function () {
                $page = $this->pageFactory->create();
                $page->setTitle('Test Page ' . date('YmdHis'))
                     ->setIdentifier('test-page-' . time())
                     ->setContent('<p>Page de test créée par TestCrud.</p>')
                     ->setContentHeading('Test')
                     ->setIsActive(1)
                     ->setPageLayout('1column')
                     ->setStoreId([0]);

                $saved = $this->pageRepository->save($page);
                return ['saved_id' => $saved->getId(), 'identifier' => $saved->getIdentifier()];
            })(),

            'delete' => (function () use ($id) {
                $page = $this->pageRepository->getById((int) $id);
                $this->pageRepository->delete($page);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }

CMS Blocs – Block CRUD

handleCmsBlock() dans Index.php
PHP
    private function handleCmsBlock(string $action, $id): array
    {
        return match ($action) {
            'collection' => (function () {
                $collection = $this->cmsBlockCollectionFactory->create();
                $collection->addFieldToSelect(['title', 'identifier', 'is_active', 'creation_time'])
                           ->setPageSize(10);

                $items = [];
                foreach ($collection as $block) {
                    $items[] = [
                        'id'         => $block->getId(),
                        'title'      => $block->getTitle(),
                        'identifier' => $block->getIdentifier(),
                        'is_active'  => $block->getIsActive(),
                    ];
                }
                return ['count' => $collection->getSize(), 'items' => $items];
            })(),

            'load' => (function () use ($id) {
                $block = $this->blockRepository->getById((int) $id);
                return [
                    'id'              => $block->getId(),
                    'title'           => $block->getTitle(),
                    'identifier'      => $block->getIdentifier(),
                    'is_active'       => $block->getIsActive(),
                    'content_preview' => substr((string) $block->getContent(), 0, 200),
                ];
            })(),

            'save' => (function () {
                $block = $this->blockFactory->create();
                $block->setTitle('Test Block ' . date('YmdHis'))
                      ->setIdentifier('test-block-' . time())
                      ->setContent('<div>Bloc de test créé par TestCrud.</div>')
                      ->setIsActive(1)
                      ->setStoreId([0]);

                $saved = $this->blockRepository->save($block);
                return ['saved_id' => $saved->getId(), 'identifier' => $saved->getIdentifier()];
            })(),

            'delete' => (function () use ($id) {
                $block = $this->blockRepository->getById((int) $id);
                $this->blockRepository->delete($block);
                return ['deleted' => true, 'id' => $id];
            })(),

            default => ['error' => "Action inconnue : {$action}"],
        };
    }
} // fin de la classe Index

Sécurité & Bonnes pratiques

⛔ Mesures de sécurité obligatoires

Ce controller doit être protégé ou supprimé avant tout déploiement en production.

1. Restriction par IP (recommandé)

Ajoutez une vérification IP dans execute() :

Restriction IP dans execute()
PHP
    private const ALLOWED_IPS = ['127.0.0.1', '::1', '192.168.1.100'];

    public function execute(): Json
    {
        $remoteIp = $this->request->getServer('REMOTE_ADDR');
        if (!in_array($remoteIp, self::ALLOWED_IPS, true)) {
            return $this->jsonFactory->create()
                ->setData(['error' => 'Access denied'])
                ->setHttpResponseCode(403);
        }
        // ... reste du code
    }

2. Récapitulatif des bonnes pratiques

Pratique Importance Description
readonly sur les DI Recommandé PHP 8.1+ : évite la mutation accidentelle des services injectés
Try/catch global Obligatoire Intercepte toute exception et retourne un JSON propre avec trace
Restriction IP Obligatoire Limite l’accès au réseau local ou VPN uniquement
Désactiver en prod Critique Supprimer le module ou vérifier MAGE_MODE
Logger les actions Recommandé Trace qui a exécuté quelle opération pour l’audit
Repository vs Model direct Best practice Toujours préférer les repositories (testabilité, cache, events)

Article technique — Magento 2 Controller CRUD · Entités : Category, Product, Customer, Order, Shipment, Quote, Invoice, Creditmemo, CMS Page, CMS Block

PHP 8.1+ · Magento 2.4.x · Testé avec les repositories natifs du core