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.
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érationscollection, load, save et delete,
en utilisant les Repository et Model natifs du framework.
Structure du module
Nous allons créer un module Magento 2 dédié aux tests : Vendor_TestCrud.
Voici l’arborescence complète du module :
TREE
app/code/Vendor/TestCrud/
├── registration.php
├── etc/
│ ├── module.xml
│ └── frontend/
│ └── routes.xml
└── Controller/
└── Test/
└── Index.php
Créer le répertoire app/code/Vendor/TestCrud/ et tous les sous-dossiers nécessaires.
Renseigner registration.php et etc/module.xml pour enregistrer le module auprès de Magento.
Ajouter etc/frontend/routes.xml pour exposer l’URL /testcrud/test/index.
Créer Controller/Test/Index.php avec l’injection de dépendances et toutes les méthodes CRUD.
Exécuter bin/magento module:enable Vendor_TestCrud && bin/magento setup:upgrade.
registration.php & module.xml
PHP
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Vendor_TestCrud',
__DIR__
);
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
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 |
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
Magento\Catalog\Api\CategoryRepositoryInterface — les catégories sont hiérarchiques.
La collection native utilise CategoryCollectionFactory pour de meilleures performances.
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
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.
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
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
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 utiliseOrderManagementInterface::cancel() en lieu et place d’une suppression physique.
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
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
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.
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
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
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
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
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
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() :
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