Skip to content

Codifyo VueCrudBundle ​

Le VueCrudBundle est un bundle Symfony puissant et hautement modulaire fournissant une architecture backend clé en main pour générer rapidement des API CRUD (Create, Read, Update, Delete) destinées à être consommées par le composant frontend vue-crud.

Ce bundle permet de configurer dynamiquement des grilles de données (DataGrids), des vues détaillées, des formulaires, des actions globales et par élément (item), tout en offrant un système d'Action Handlers extensible basé sur l'injection de dépendances Symfony.


Sommaire ​

  1. Installation
  2. Utilisation de base
  3. Configuration Générale (CrudConfig)
  4. Colonnes ($config->columns())
  5. Actions ($config->actions())
  6. Architecture des Actions & Action Handlers
  7. Vues Détaillées (ViewConfig & ViewProperty)
  8. Personnalisation de la Requête Doctrine (QueryBuilder)
  9. Pagination & bornes (limit / page)
  10. Récupérer des listes depuis PHP (CrudListFetcher)
  1. Cache
  2. Gestion de la Sécurité
  3. Format des Réponses HTTP & Validation
  4. Endpoints REST Générés

Installation ​

Assurez-vous que le bundle est enregistré dans config/bundles.php :

php
return [
    // ...
    Codifyo\VueCrudBundle\CodifyoVueCrudBundle::class => ['all' => true],
];

Utilisation de base ​

Pour créer un contrôleur CRUD, faites hériter votre classe de AbstractCrudController et implémentez la méthode configureCrud() :

php
namespace App\Controller;

use Codifyo\VueCrudBundle\Controller\AbstractCrudController;
use Codifyo\VueCrudBundle\Config\CrudConfig;
use App\Entity\Product;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/api/product')]
class ProductCrudController extends AbstractCrudController
{
    protected function configureCrud(): CrudConfig
    {
        $config = new CrudConfig();
        
        $config->setEntityClass(Product::class)
               ->setIdKey('id')
               ->setListEndpoint('/api/product')
               ->setFilterForm(\App\Form\ProductFilterType::class);

        // Définition des colonnes de la grille
        $config->columns()
            ->addText('id', 'ID')
            ->addText('name', 'Nom')
            ->addText('price', 'Prix');

        // Actions globales (en haut de la table)
        $config->actions()->global()
            ->addForm('create', 'Créer', \App\Form\ProductType::class, 'POST');

        // Actions par élément (sur chaque ligne)
        $config->actions()->item()
            ->addForm('edit', 'Modifier', \App\Form\ProductType::class)
            ->addDeleteAction('Supprimer');

        return $config;
    }
}

Configuration Générale (CrudConfig) ​

La classe CrudConfig centralise la configuration du contrôleur :

  • setEntityClass(string $class) : Entité Doctrine cible (obligatoire).
  • setIdKey(string $key) : Propriété servant de clé primaire (par défaut 'id').
  • setListEndpoint(string $endpoint) : URL de base de la ressource pour la génération automatique des routes d'actions et de données.
  • setPerPage(int $perPage) : Nombre d'éléments par page pour la pagination (par défaut 15).
  • setPerPageChoices(?array $choices) : Tailles de page proposées dans le sélecteur du frontend (ex. [10, 25, 50]). La plus grande sert aussi de maximum côté serveur.
  • setMaxPerPage(?int $max) : Taille de page maximale acceptée par l'API (paramètre limit), prioritaire sur perPageChoices. Voir Pagination & bornes.
  • setFilterForm(string $formType) : Classe FQCN du type de formulaire Symfony à utiliser pour le panneau de filtres.
  • setEnableColumnChooser(bool $enable) : Active ou désactive l'outil de sélection des colonnes affichées côté frontend.
  • setSerializationGroups(array $groups) : Groupes de sérialisation par défaut pour les réponses de l'API.
  • setQueryBuilder(\Closure $closure) : Permet de personnaliser la requête Doctrine ORM (filtres custom, jointures, etc.).
  • setGranted(string|\Closure|null $granted) : Contrôle l'accès global au CRUD.

Colonnes ($config->columns()) ​

Gérez les colonnes retournées par l'API et affichées dans la liste frontend :

Types de colonnes disponibles : ​

  • addText(string $key, string $label) : Colonne texte standard.
  • addBoolean(string $key, string $label) : Affichage booléen (Oui/Non, Checkmark).
  • addBadge(string $key, string $label) : Affichage sous forme de badge/tag coloré.
  • addImage(string $key, string $label) : Colonne avec aperçu d'image.
  • addLink(string $key, string $label, ?string $linkText = null, ?string $target = null, bool|\Closure|string|array $sortable = false, ?string $linkUrl = null, ?bool $showExternalIcon = null, bool|string|null $backUrl = null) : Lien cliquable interne ou externe.
  • addHtml(string $key, string $label) : Contenu HTML rendu brut.
  • addArray(string $key, string $label) : Liste ou tableau de valeurs.

Configuration avancée des liens (LinkColumn & addBackUrl) ​

Pour les liens internes (ex: navigation vers un autre CRUD) ou externes, vous pouvez configurer l'URL template, le texte interpolé, la cible de navigation, l'icône FontAwesome et l'ajout automatique de retour (backUrl) :

php
// Exemple fluide complet avec LinkColumn :
$config->columns()->add(
    LinkColumn::create('equipment_id', 'Équipement')
        ->setLinkUrl('/equipments/{equipment_id}')
        ->setLinkText('Fiche {equipment_name}')
        ->setTarget('_self')
        ->setShowExternalIcon(false)
        ->addBackUrl('Équipements') // génère automatiquement &backUrl={currentUrl}&backLabel=Équipements
);

// Ou directement via addLink avec arguments nommés :
$config->columns()->addLink(
    key: 'website',
    label: 'Site Web',
    linkUrl: 'https://example.com/org/{org_id}',
    linkText: 'Visiter',
    target: '_blank',
    showExternalIcon: true
);

Aide à la navigation retour (addBackUrl) :

  • ->addBackUrl() ou ->addBackUrl(true) : ajoute automatiquement ?backUrl={currentUrl} (ou &backUrl={currentUrl}).
  • ->addBackUrl('Mon Label') : ajoute automatiquement &backUrl={currentUrl}&backLabel=Mon Label.
  • Disponible à l'identique sur les fiches détaillées avec LinkViewProperty ou ViewConfig::addLink().

Extraction de valeur personnalisée (setValueExtractor) ​

Si la donnée est une relation Doctrine ou une propriété calculée :

php
$config->columns()
    ->addText('category_name', 'Catégorie')
    ->setValueExtractor(function(Product $product) {
        return $product->getCategory() ? $product->getCategory()->getName() : 'Non catégorisé';
    });

Suppression de colonnes (remove) ​

Vous pouvez supprimer une ou plusieurs colonnes (idéal lors du partage ou de la surcharge d'une configuration de base) :

php
$config->columns()->remove('tenant_id');
$config->columns()->remove('col1', 'col2'); // arguments multiples
$config->columns()->remove(['col1', 'col2']); // ou via un tableau

Actions ($config->actions()) ​

Les actions s'enregistrent dans $config->actions()->global() ou $config->actions()->item().

Types d'actions intégrés : ​

  1. FormAction : Formulaire Symfony modal ou page.

    php
    $actions->addForm('edit', 'Modifier', ProductType::class, 'PUT', 'primary');

    Gestion des conflits : Si vous souhaitez gérer et afficher des conflits de versionnement ou de concurrence sur un formulaire, vous pouvez utiliser les options suivantes :

    php
    $formAction = FormAction::create('edit', 'Modifier')
        ->setFormType(ProductType::class)
        ->setShowConflicts(true)
        // Optionnel : inverse 'data' et 'referenceData' dans le JSON pour pré-remplir
        // le formulaire avec les données de référence et afficher les données du formulaire en tant que conflits
        ->setSwapDataAndReference(true);
  2. ConfirmAction : Demande de confirmation avant d'exécuter une callback.

    php
    $actions->addConfirm('publish', 'Publier', function(Product $product, EntityManagerInterface $em) {
        $product->setPublished(true);
        $em->flush();
    }, 'success', 'check', 'Confirmation', null, 'ROLE_ADMIN');
  3. DeleteAction : Raccourci d'action de suppression d'entité.

    php
    $actions->addDeleteAction('Supprimer');
  4. InlineEditAction : Édition rapide directe dans la ligne du tableau.

    php
    $actions->addInlineEdit('inline_price', 'Modifier prix', [
        'price' => ['type' => 'number', 'label' => 'Prix']
    ]);
  5. ViewAction : Fiche détaillée de l'entité.

    php
    $actions->addView('view', 'Voir les détails', $viewConfig);
  6. CustomAction : Action sur-mesure (retourne soit une Response Symfony, soit un mixed sérialisé en JSON). Type retourné au frontend : 'link' avec locations: [] par défaut (invisible par défaut, idéal pour des appels spéciaux, téléchargements de fichiers ou données pour slots customisés).

    php
    // Exemple 1 : Téléchargement d'un fichier (retourne une BinaryFileResponse)
    $actions->addCustom('download_pdf', 'Télécharger', function(Invoice $invoice) {
        return new BinaryFileResponse($invoice->getPdfPath());
    });
    
    // Exemple 2 : Données sur-mesure pour un slot customisé (retourne du mixed sérialisé)
    $actions->addCustom('custom_data', function(Product $product) {
        return ['analytics' => $product->getStats(), 'rating' => 4.8];
    });
  7. DropdownAction : Bouton conteneur de menu déroulant pour regrouper plusieurs actions sous un seul bouton dropdown. Les sous-actions restent des actions standard (confirm, form, view, custom, etc.) et ciblent le menu déroulant via l'emplacement dropdown-{name} (ou via la méthode ->inDropdown('name')).

    php
    // Déclaration du bouton dropdown
    $actions->addDropdown('more', 'Options', icon: 'fas fa-ellipsis-v', dropdownAlign: 'right', autoHideIfEmpty: true);
    
    // Ciblage fluide via ->inDropdown('more')
    $actions->addConfirm('duplicate', 'Dupliquer', $duplicateCallback, icon: 'fas fa-copy')
        ->inDropdown('more');
    
    // Ou directement via le paramètre locations
    $actions->addDeleteAction('Supprimer', locations: ['dropdown-more']);
    
    // Multi-emplacements : dans le menu déroulant sur la liste, et sous forme de bouton classique dans la fiche view
    $actions->addConfirm('archive', 'Archiver', $archiveCallback, locations: ['dropdown-more', 'view']);

    Options du DropdownAction :

    • dropdownAlign / dropdown_align : "right" (défaut sur tableau) ou "left".
    • autoHideIfEmpty / auto_hide_if_empty : true (défaut). Masque automatiquement le bouton dropdown si aucune action enfant n'est visible/autorisée par _allowed_actions.
    • actions : Liste ordonnée optionnelle des identifiants d'actions (ex: ['duplicate', 'delete']).

Contrôle de l'emplacement d'affichage (locations) ​

Chaque action peut être restreinte à des pages/emplacements spécifiques (ex: 'list', 'view', 'edit', '*' ou [] pour masquer) :

php
// Afficher uniquement sur la page de liste et la fiche détail
$action->setLocations(['list', 'view']);

// Ou via l'API Builder :
$actions->addConfirm('archive', 'Archiver', $callback, locations: ['view']);

Suppression d'actions (remove) ​

Vous pouvez retirer des actions configurées sur les groupes item() ou global() :

php
$config->actions()->item()->remove('delete');
$config->actions()->item()->remove('edit', 'delete'); // arguments multiples ou tableau
$config->actions()->global()->remove('export');

Icônes par défaut & Personnalisation ​

Le bundle intègre un résolveur automatique d'icônes (ActionIconResolver). Si aucune icône n'est spécifiée sur une action, une icône par défaut est automatiquement attribuée selon son nom ou son type :

  • Par nom : create / new (fas fa-plus), edit (fas fa-pencil-alt), delete (fas fa-trash), duplicate (fas fa-copy), export (fas fa-download), import (fas fa-upload), filter (fas fa-filter), refresh (fas fa-sync-alt), save (fas fa-save), archive (fas fa-archive), etc.
  • Par type : view (fas fa-eye), confirm (fas fa-check), inline-edit (fas fa-pencil-alt) (les actions dropdown n'ont aucune icône par défaut).

1. Désactiver une icône sur une action (icon: false) ​

Pour afficher un bouton avec du texte pur sans icône :

php
$actions->addDeleteAction('Supprimer', icon: false);
// ou de manière fluide :
$action->setIcon(false);

2. Surcharger globalement les icônes via la configuration (YAML) ​

Dans config/packages/codifyo_vue_crud.yaml, vous pouvez surcharger uniquement les clés de votre choix. Toutes les autres valeurs par défaut restent conservées :

yaml
codifyo_vue_crud:
    icons:
        by_name:
            create: 'fas fa-folder-plus' # Remplace uniquement l'icône de create
            delete: false                # Désactive globalement l'icône par défaut de delete
        by_type:
            dropdown: 'fas fa-bars'      # Remplace l'icône de tous les boutons dropdown

Mode d'affichage des boutons (button_display) ​

Le paramètre button_display permet de piloter finement le mode de rendu visuel des boutons d'actions côté front-end (icône seule, libellé seul, ou les deux combinés) :

  • "both" (défaut) : Affiche à la fois l'icône ET le texte (si configurés).
  • "icon" : Affiche uniquement l'icône (le label sert automatiquement d'infobulle HTML title et de aria-label).
  • "text" : Affiche uniquement le texte (ignore l'icône).
  • "auto" : Équivalent à "both".

1. Configuration scalaire ​

php
// Via paramètre nommé :
$actions->addDeleteAction('Supprimer', buttonDisplay: 'icon');

// Ou via méthodes fluides :
$actions->addDeleteAction('Supprimer')->displayIconOnly();
$actions->addConfirm('duplicate', 'Dupliquer')->displayTextOnly();
$actions->addForm('edit', 'Modifier', ProductType::class)->displayBoth();

2. Configuration avancée par emplacement (locations) ​

Vous pouvez adapter le rendu selon l'emplacement où se trouve le bouton (list, view, dropdown-{name}, dropdown-*, default) :

php
$actions->addConfirm('export', 'Exporter', buttonDisplay: [
    'list' => 'icon',            // Icône seule dans le tableau
    'view' => 'both',            // Icône + texte dans la fiche détail
    'dropdown-*' => 'both',      // Icône + texte dans les menus déroulants
    'default' => 'both',
]);

// Ou via la méthode helper :
$action->setButtonDisplayForLocation('list', 'icon');

Architecture des Actions & Action Handlers ​

Cascade de résolution des actions à 3 niveaux ​

Lorsqu'une action est appelée via /action/{name} (action globale) ou /item/{id}/action/{name} (action item), le bundle applique la cascade suivante :

                                  [ Requête d'action ]
                                           │
                                           ▼
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 1. Méthode explicite dans le contrôleur (ex: `handleGlobalActionExport`)         │
└──────────────────────────────────────────────────────────────────────────────────┘
                                           │ NON trouvée
                                           ▼
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 2. Action Handler applicatif (Service Symfony taggué `codifyo_crud.action_handler`)│
└──────────────────────────────────────────────────────────────────────────────────┘
                                           │ NON trouvé
                                           ▼
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 3. Action Handler par défaut du bundle (Form, Confirm, View, InlineEdit)          │
└──────────────────────────────────────────────────────────────────────────────────┘

Niveau 1 : Surcharge dans le Contrôleur ​

Pour un besoin spécifique à un seul contrôleur, vous n'avez pas besoin de créer de service. Déclarez une méthode suivant la convention de nommage handleGlobalAction{ActionNameInCamelCase} ou handleItemAction{ActionNameInCamelCase} :

php
// Pour l'action item nommée "send_invoice"
public function handleItemActionSendInvoice(Order $order, Action $action, Request $request): JsonResponse
{
    $this->mailer->sendInvoice($order);

    return $this->json(['success' => true, 'message' => 'Facture envoyée']);
}

Niveau 2 : Handlers Applicatifs (Services Symfony) ​

Pour créer un type d'action réutilisable dans l'ensemble de votre projet (ex: ExportAction, LogAction), créez un Action Handler implémentant ActionHandlerInterface ou étendant AbstractActionHandler.

Grâce à l'autoconfiguration Symfony, tout handler implémentant ActionHandlerInterface est automatiquement enregistré avec le tag codifyo_crud.action_handler.

Exemple : Action et Handler d'Exportation PDF ​

  1. Définition du DTO d'action :
php
namespace App\Crud\Action;

use Codifyo\VueCrudBundle\Config\Action\Action;
use Codifyo\VueCrudBundle\Config\Action\UrlConfigurableInterface;

class ExportPdfAction extends Action implements UrlConfigurableInterface
{
    protected string $type = 'export_pdf';

    public function resolveDefaultUrls(string $endpoint, string $scope): void
    {
        $base = $scope === 'item' ? $endpoint . '/item/{id}/action/' : $endpoint . '/action/';
        if (!$this->url) {
            $this->url = $base . $this->name;
        }
    }
}
  1. Création du Handler (Service autowired) :
php
namespace App\Crud\ActionHandler;

use Codifyo\VueCrudBundle\ActionHandler\AbstractActionHandler;
use Codifyo\VueCrudBundle\ActionHandler\ActionContext;
use Codifyo\VueCrudBundle\Config\Action\Action;
use App\Crud\Action\ExportPdfAction;
use App\Service\PdfGenerator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ExportPdfActionHandler extends AbstractActionHandler
{
    public function __construct(
        private PdfGenerator $pdfGenerator
    ) {
    }

    public function supports(Action $action, ?object $item = null): bool
    {
        return $action instanceof ExportPdfAction;
    }

    public function handle(Action $action, ?object $item, Request $request, ActionContext $context): Response
    {
        $pdfContent = $this->pdfGenerator->generateFor($item);

        return new Response($pdfContent, 200, [
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => 'attachment; filename="export.pdf"',
        ]);
    }
}
  1. Utilisation dans n'importe quel contrôleur CRUD :
php
$config->actions()->item()->add(
    ExportPdfAction::create('export_pdf', 'Exporter en PDF')
);

Utilisation du ActionContext ​

L'objet ActionContext transmis aux handlers permet d'accéder au contexte sans créer de couplage fort avec le contrôleur :

  • $context->getCrudConfig() : Accède à la configuration CrudConfig.
  • $context->getController() : Instance du contrôleur AbstractCrudController.
  • $context->getSerializationContext(?Action $action = null) : Retourne le tableau de contexte de sérialisation (['groups' => ...]) configuré sur l'action ou sur le CRUD.
  • $context->createEntityInstance() : Instancie automatiquement un nouvel objet de la classe d'entité gérée.

Vues Détaillées (ViewConfig & ViewProperty) ​

Les vues de détails (ViewAction) se configurent avec ViewConfig et des propriétés typées :

php
use Codifyo\VueCrudBundle\Config\View\ViewConfig;
use Codifyo\VueCrudBundle\Config\View\Property\TextViewProperty;
use Codifyo\VueCrudBundle\Config\View\Property\BadgeViewProperty;

$viewConfig = new ViewConfig();
$viewConfig->addProperty(TextViewProperty::create('name', 'Nom du Produit'))
          ->addProperty(BadgeViewProperty::create('status', 'Statut'));

$config->actions()->item()->addView('detail', 'Consulter', $viewConfig);

Organisation en Sections et Onglets (tabs) ​

Vous pouvez structurer votre vue en sections et en onglets (addTab) :

php
$viewConfig = ViewConfig::create()
    ->addTab('Détails', ViewConfig::create()
        ->addText('description', 'Description'),
        icon: 'fas fa-info',
        key: 'tab-details'
    )
    ->addTab('Sous-sections', ViewConfig::create()
        ->addSection('Stocks', ViewConfig::create()->addText('stock', 'Stock'), size: 12),
        key: 'tab-sub'
    );

Suppression d'éléments dans ViewConfig ​

Vous pouvez retirer des propriétés, sections ou onglets d'une ViewConfig (y compris si imbriqués dans des onglets ou des sous-sections) :

php
// Suppression de propriétés (à la racine ou imbriquées)
$viewConfig->removeProperty('secret_token');
$viewConfig->removeProperty('field1', 'field2'); // arguments multiples ou tableau

// Suppression d'onglets (par clé ou par label)
$viewConfig->removeTab('tab-sub');
$viewConfig->removeTab('Sous-sections');

// Suppression de sections (par clé ou par titre, y compris dans un onglet)
$viewConfig->removeSection('Stocks');
$viewConfig->removeSection('section-stocks');

Partage de Configuration entre CRUDs (clone) ​

Tous les builders (CrudConfig, ColumnsBuilder, ActionsBuilder, ViewConfig, ViewAction) supportent le clonage en profondeur (__clone). Vous pouvez ainsi définir un modèle de configuration global ou partagé et l'adapter par CRUD sans effets de bord :

php
// Exemple : Dériver d'un CRUD partagé
$config = clone $baseCrudConfig;

// Personnalisations spécifiques à ce CRUD
$config->columns()->remove('tenant_id');
$config->actions()->item()->remove('delete');

if ($viewAction = $config->actions()->getItemAction('detail')) {
    if ($viewAction->getViewConfig() instanceof ViewConfig) {
        $viewAction->getViewConfig()->removeProperty('internal_notes');
    }
}

Personnalisation de la Requête Doctrine (QueryBuilder) ​

Pour appliquer des filtres complexes, des jointures ou des restrictions spécifiques :

php
$config->setQueryBuilder(function(QueryBuilder $qb, array &$filters, Request $request) {
    $qb->andWhere('e.active = :active')
       ->setParameter('active', true);

    // Si un filtre personnalisé est géré à la main, on peut l'unsetter :
    if (isset($filters['search'])) {
        $qb->andWhere('e.name LIKE :search OR e.description LIKE :search')
           ->setParameter('search', '%' . $filters['search'] . '%');
        unset($filters['search']);
    }
});

Pagination & bornes (limit / page) ​

La route de liste (GET /) accepte page et limit. Les valeurs reçues sont normalisées côté serveur, sans erreur :

Paramètre reçuValeur appliquée
limit absent ou négatifperPage du CRUD
limit supérieur au maximumle maximum
limit=0total seul : {"data": [], "total": N}, seule la requête de comptage est exécutée
page < 11

Le maximum est déterminé dans cet ordre, et n'est jamais inférieur à perPage :

  1. CrudConfig::setMaxPerPage() ;
  2. sinon la plus grande valeur de setPerPageChoices() ;
  3. sinon la configuration globale du bundle (100 par défaut) :
yaml
# config/packages/codifyo_vue_crud.yaml
codifyo_vue_crud:
    list:
        max_per_page: 100

Pour personnaliser cette règle, surchargez resolvePagination() ou resolveMaxPerPage() dans votre contrôleur.

Calcul du total : le total est obtenu par une requête directe COUNT(DISTINCT e.<id>), sans tri, sur les mêmes filtres et jointures que la liste. Il reste juste avec une jointure qui multiplie les lignes. Les lignes elles-mêmes sont toujours récupérées par le Paginator Doctrine. Le comptage du Paginator est conservé si votre setQueryBuilder() ajoute un GROUP BY ou un HAVING, si l'entité a une clé primaire composée, ou si la requête a plusieurs entités dans son FROM. Pour forcer ce repli sur un CRUD, surchargez canCountDirectly().


Récupérer des listes depuis PHP (CrudListFetcher) ​

Le service CrudListFetcher renvoie la liste et le total d'un CRUD depuis votre propre code, par exemple pour un dashboard qui agrège plusieurs CRUD, ou le même CRUD avec des filtres différents. Il applique exactement les mêmes règles que la route de liste : contrôle d'accès, formulaire de filtres, setQueryBuilder(), bornes de pagination, cache, et vos surcharges de generateListData().

php
use Codifyo\VueCrudBundle\Service\CrudListFetcher;

#[Route('/api/dashboard', methods: ['GET'])]
public function dashboard(CrudListFetcher $fetcher): JsonResponse
{
    return $this->json([
        'pending_orders' => $fetcher->fetch(OrderCrudController::class, ['status' => 'pending'], limit: 5, sort: 'createdAt', dir: 'desc')->toArray(),
        'late_orders'    => $fetcher->count(OrderCrudController::class, ['late' => true]),
        'low_stock'      => $fetcher->fetch(ProductCrudController::class, ['stock_lt' => 5], limit: 10)->toArray(),
    ]);
}
  • fetch($crud, array $filters = [], ?int $limit = null, int $page = 1, ?string $sort = null, ?string $dir = null, array $options = []): CrudListResult : $result->data (lignes au format de l'API), $result->total, $result->toArray(). limit: null = perPage ; limit: 0 = total seul.
  • count($crud, array $filters = [], array $options = []): int : total filtré, sans charger de lignes.
  • $options : options du CRUD, qui remplacent ses valeurs par défaut. Une valeur invalide lève CrudOptionsException, une option non déclarée UndefinedOptionsException.
  • $crud : FQCN du contrôleur CRUD (tout contrôleur AbstractCrudController autoconfiguré est enregistré automatiquement), ou directement une instance.
  • $filters : mêmes clés et mêmes valeurs que le paramètre filters de l'API, donc scalaires, null ou tableaux de ceux-ci. Passez un identifiant plutôt qu'une entité, une date au format texte plutôt qu'un DateTime. Un objet lève InvalidArgumentException.
  • Erreurs : CrudAccessDeniedException (accès refusé), CrudFilterValidationException (filtres invalides, getErrors()), toutes deux filles de CrudListException ; InvalidArgumentException si le CRUD est introuvable.

Sécurité : le fetcher applique le contrôle d'accès du CRUD (CrudConfig::setGranted()), mais pas les règles liées à la route, comme #[IsGranted] posé sur la classe du contrôleur CRUD ou access_control par chemin. Protégez la route de votre dashboard en conséquence.

Surcharges : le fetcher passe par getCrudListData() puis generateListData(). Une surcharge de listAction() n'est donc pas prise en compte. Une surcharge de generateListData() l'est, mais elle échappe aux bornes de pagination si elle n'appelle pas resolvePagination().

Options du CRUD (configureOptions) ​

Un CRUD peut adapter sa config à des options déclarées avec un OptionsResolver. Les valeurs par défaut sont calculées par le contrôleur lui-même, depuis la source de son choix : URL, route, session, utilisateur… Le bundle ne lit rien automatiquement dans la requête. CrudListFetcher peut ensuite passer ses propres valeurs.

php
protected function configureOptions(OptionsResolver $resolver, Request $request): void
{
    $resolver->setDefaults([
        'type' => $request->query->get('type'),   // source choisie par le contrôleur
        'readonly' => false,
    ]);
    $resolver->setAllowedValues('type', [null, 'promo', 'archive']);
    $resolver->setAllowedTypes('readonly', 'bool');
}

protected function configureCrud(): CrudConfig
{
    $type = $this->getOption('type');
    $config = CrudConfig::create(Product::class)
        ->setQueryBuilder(static function (QueryBuilder $qb) use ($type): void {
            if ($type === 'promo') {
                $qb->andWhere('e.promo = true');
            }
        });
    // ...
    return $config;
}
php
$fetcher->fetch(ProductCrudController::class, options: ['type' => 'promo']);
  • Priorité : les valeurs par défaut de configureOptions(), remplacées par les $options du fetcher (ou de getCrudListData($request, $options) / getConfig($request, $options)). Les routes HTTP n'utilisent que les valeurs par défaut.
  • $request : la requête HTTP reçue, ou celle fabriquée par le fetcher. Celle du fetcher est une copie de la requête courante (session, en-têtes, attributs de route…) dont seule la query string est remplacée par les paramètres de liste : $request->query n'y contient donc pas les paramètres d'URL de votre dashboard.
  • Lecture : getOption('nom') et getOptions(), depuis configureCrud() et pendant tout l'appel. Lire les valeurs dans configureCrud() puis les passer aux closures par use évite toute dépendance à l'état du contrôleur.
  • Validation : une valeur invalide, y compris une valeur par défaut tirée de l'URL, renvoie une 400 sur les routes ({"error": "Invalid CRUD options", "message": …}) et lève CrudOptionsException avec le fetcher.
  • Valeurs coûteuses : une valeur par défaut sous forme de closure (fn (Options $o) => …) n'est calculée que si l'option n'est pas fournie.
  • Cache de liste : la clé inclut les options. Les enums et les dates sont pris en charge. Avec un autre objet (entité…), le cache est contourné pour cet appel, sauf si vous surchargez normalizeOptionsForCacheKey() (voir Cache).

Cache ​

Tout se configure via $config->cache() :

php
$config->cache()
    ->enable()                                       // raccourci : $config->enableCache()
    ->scope(fn (?UserInterface $u) => ...)           // portée de la clé (voir plus bas)
    ->tags('catalog', Category::class)               // groupes d'invalidation
    ->config('1 day')                                // cache navigateur de /config
    ->list('1 hour')                                 // cache serveur de la liste
    ->invalidateOn(
        methods: ['POST', 'PUT', 'PATCH', 'DELETE'], // défaut
        exceptActions: ['export'],
        actions: ['archive' => ['POST', 'GET']],
    );
  • Liste : la réponse (lignes et total) est stockée dans le pool cache.app.taggable de l'application. Le contrôle d'accès du CRUD (setGranted()) est réévalué à chaque appel, même quand la réponse vient du cache. Les réponses d'erreur (filtres invalides…) ne sont pas mises en cache.
  • /config : la réponse reçoit Cache-Control: private, max-age=<durée> (cache du navigateur uniquement).
  • Les actions et l'item (GET /{id}) ne sont pas mis en cache (#5).

Durées ​

config() et list() acceptent un nombre de secondes ou une durée relative au format PHP ('1 day', '6 hours', '90 minutes', '1 day 2 hours'). null reprend la valeur globale, false désactive le cache pour cette cible seulement. Une durée illisible, égale à 0 ou négative ('-1 day') lève une InvalidArgumentException. Dans la configuration globale, la désactivation s'écrit null, et non false.

Valeurs globales, avec leurs valeurs par défaut :

yaml
codifyo_vue_crud:
    cache:
        ttl:
            config: '1 day'   # null = pas de cache navigateur
            list: '1 hour'    # null = sans expiration (jusqu'à invalidation)

Clé : portée (scope) ​

La clé contient le contrôleur, l'entité, l'URI complète (filtres, page, tri compris), les options du CRUD et une portée. Par défaut, la portée est l'identifiant de l'utilisateur : chaque utilisateur a ses propres entrées. scope() permet de partager une entrée entre plusieurs utilisateurs :

php
$config->cache()->enable()
    ->scope(fn (?UserInterface $user) => in_array('ROLE_ADMIN', $user?->getRoles() ?? [], true)
        ? ['group' => 'admins']                    // une entrée commune à tous les admins
        : ['user' => $user?->getUserIdentifier()]); // les autres : une entrée chacun

Gardez des valeurs de groupe et d'utilisateur distinctes, ici avec les clés group et user. Avec 'admins' d'un côté et l'identifiant brut de l'autre, un utilisateur nommé « admins » recevrait l'entrée des admins.

La closure renvoie un scalaire, null ou un tableau de ceux-ci, par exemple ['role' => 'admin', 'tenant' => 12].

⚠️ Portée partagée : le premier membre du groupe calcule la liste, et tous les autres reçoivent ensuite exactement le même résultat. Ne partagez une portée que si le queryBuilder, le listDataFormatter et les actions autorisées par ligne donnent le même résultat pour tout le groupe. Sinon, les données d'un utilisateur seront servies aux autres.

Tout ce dont la liste dépend en plus de l'URI et de l'utilisateur (rôles, locale, session…) doit figurer dans la portée ou dans les options. Sinon, ces variations ne sont pas distinguées dans le cache.

Pour un contrôle total, surchargez getCrudCacheKeyParts(CrudConfig $config, Request $request): array. Si un élément reste un objet après normalisation, le cache est contourné pour l'appel. Les options sont normalisées par normalizeOptionsForCacheKey(array $options): array : enums et dates sont convertis par défaut, et vous pouvez la surcharger pour transformer par exemple une entité en 'Classe#id'.

Tags : relier plusieurs CRUD (tags) ​

Chaque liste porte le tag de son entité. tags() ajoute des groupes : les listes portent tous leurs tags, et une écriture invalide tous les tags du CRUD qui l'effectue, même s'il n'a pas de cache lui-même.

php
// ProductCrudController : les listes de produits affichent category.name
$config->cache()->enable()->tags('catalog');

// CategoryCrudController
$config->cache()->tags('catalog');
// → modifier une catégorie invalide les listes de produits, et inversement

Un nom de classe d'entité donne le tag de cette entité : tags(Category::class) relie le CRUD à tous les CRUD des catégories.

Quand le cache est invalidé (invalidateOn) ​

Après une action réussie (réponse 2xx) du CRUD qui écrit, ses tags sont invalidés selon ces règles, par ordre de priorité :

  1. actions : les méthodes propres à une action, par exemple ['archive' => ['POST', 'GET']]. Elles priment sur les deux règles suivantes.
  2. exceptActions : les actions qui n'invalident jamais, par exemple un export.
  3. methods : les méthodes qui invalident pour toutes les autres actions. Par défaut : POST, PUT, PATCH et DELETE.

Chaque appel à invalidateOn() remplace la configuration précédente. Pour un réglage plus fin, surchargez shouldInvalidateCrudCache() dans le contrôleur.

Invalider depuis votre code (CrudCacheInvalidator) ​

Les écritures faites hors des actions du bundle (autre contrôleur, commande, worker…) n'invalident rien automatiquement. Utilisez le service CrudCacheInvalidator :

php
use Codifyo\VueCrudBundle\Cache\CrudCacheInvalidator;

public function __construct(private CrudCacheInvalidator $crudCache) {}

// après l'écriture :
$this->crudCache->invalidate(Product::class, 'catalog'); // classes d'entité et/ou tags de cache()->tags()

Gestion de la Sécurité ​

Le contrôle d'accès peut être défini :

  1. Par rôle/attribut de Voter (string) : $action->setGranted('ROLE_ADMIN')
  2. Par Closure dynamique :
    php
    $action->setGranted(function(?object $item, ?UserInterface $user) {
        return $user && $item && $item->getOwner() === $user;
    });

Format des Réponses HTTP & Validation ​

Le bundle standardise les réponses HTTP renvoyées au frontend :

  • 200 OK : Succès de l'action ou retour de données JSON.
  • 400 Bad Request : Action non supportée ou callback de confirmation ayant retourné false / un résultat d'échec.
  • 403 Forbidden : Accès refusé par la sécurité (isActionGranted ou checkCrudAccess).
  • 404 Not Found : Entité ou action introuvable.
  • 422 Unprocessable Entity : Échec de validation d'un formulaire. Retourne la liste structurée des erreurs par champ :
    json
    {
      "success": false,
      "errors": {
        "name": "Ce champ ne peut pas être vide.",
        "price": "Le prix doit être positif."
      }
    }

Endpoints REST Générés ​

Chaque contrôleur héritant d'AbstractCrudController génère automatiquement les routes suivantes :

MéthodeRouteDescription
GET/configExporte la configuration JSON de la grille (colonnes, actions, filtres).
GET/Récupère la liste paginée et filtrée des éléments (page, limit, sort, dir, filters ; limit=0 = total seul).
GET/{id}Récupère la représentation JSON d'un élément spécifique.
GET/POST/action/{actionName}Exécute une action globale (ex: création, export global).
GET/POST/PUT/PATCH/DELETE/item/{id}/action/{actionName}Exécute une action sur un élément spécifique.