Skip to content

Les registres

Un registre laisse un package ajouter un élément à une liste que le cœur assemble — sans que le cœur connaisse cet élément. C'est la mécanique des menus, des routes, des étapes de checkout, des options et des hooks.

Le problème

Un package « promotions » veut une entrée dans le menu du back-office et ses propres routes d'administration. Mais le cœur ne doit jamais référencer « promotions ». Comment ajouter à une liste possédée par le cœur, de l'extérieur ?

Le principe

Le cœur expose un registre (un singleton). Au démarrage, chaque package y dépose sa contribution. Le cœur lit la liste agrégée au moment voulu (rendu du menu, montage des routes…).

Tout se passe dans le boot() du provider.

Exemple : une entrée de menu

php
use Slab\Framework\Core\BackMenu\Contracts\BackMenuRegistry;

$this->app->make(BackMenuRegistry::class)->add(
    'catalog.promotions', __('Promotions'),
    route: 'back.promotions.index', icon: 'local_offer', parent: 'catalog', priority: 40,
);

Exemple : des routes

php
use Slab\Framework\Core\Routing\Contracts\RouteContributionRegistrar;
use Illuminate\Support\Facades\Route;

$this->app->make(RouteContributionRegistrar::class)->back(function () {
    Route::resource('promotions', PromotionsController::class)->except('show', 'destroy');
});

Les routes contribuées héritent du groupe back-office (préfixe admin/, noms back.*, middlewares auth + CanAccessBackoffice) et sont montées dans le bon ordre vis-à-vis des routes attrape-tout du front. Le pendant front est ->front(...).

Les registres de Slab

RegistreVous y ajoutez…Détail
BackMenuRegistryune entrée de menu BOpriorité réordonnable via config/back_menu.php
RouteContributionRegistrardes routes back() / front()ordre garanti
CheckoutStepRegistryune étape de tunnelpriorité via config/checkout.php
CheckoutOptionRegistryun fournisseur d'options (livraison, paiement…)voir ci-dessous
ViewHookRegistryun onglet / une section dans une vuevoir hooks & slots
FormLayoutRegistryun onglet / une section à la charpente d'un écran BOvoir hooks & slots
DatatableExtensionRegistryune colonne / action à une datatable existantevoir ci-dessous
PriceModifierRegistry, TotalsModifierRegistryun maillon de pipelinepriorités
ModelExtensionRegistryune colonne / un cast / un contenu à un modèle du cœurvoir modèles
FormRuleRegistrydes règles à une Form Request du cœurfusionnées à la validation
AccessControlRegistryun rôle / une permissionconsommé par le seed et les écrans
SearchIndexRegistryun attribut indexé, une facette, un filtre, un trivoir ci-dessous
CatalogFilterRegistryun filtre de catalogue (calcul et rendu)voir ci-dessous
OrderMailRegistryun e-mail transactionnel proposable sur un statutvoir ci-dessous

Cas particulier : les options de checkout

Une feature qui ajoute un choix au tunnel (un transporteur, un mode de paiement) n'expose que de la donnée via un CheckoutOptionProvider — jamais de HTML :

php
use Slab\Framework\Core\Checkout\Contracts\CheckoutOptionRegistry;

$this->app->make(CheckoutOptionRegistry::class)->register(ShippingOptionProvider::class);

Le provider décrit une exigence (shipping, payment…) et renvoie une liste d'options structurées (id, libellé, prix, méta). Le frontend les rend génériquement : ajouter un transporteur le fait apparaître au checkout, sans toucher au front. Détail : Commande & checkout.

Étendre une datatable existante

Les listings du back-office sont des datatables identifiées par un id (products, taxes, users…). Le DatatableExtensionRegistry laisse un package ou votre application ajouter une colonne, une action ou une option à une datatable existante, sans la forker :

php
use Slab\Framework\Core\View\Contracts\DatatableExtensionRegistry;
use Stafe\LaravelDatatable\Core\Datatable;

$this->app->make(DatatableExtensionRegistry::class)->extend('products', function (Datatable $datatable) {
    $datatable->columns()->column('supplier')->label(__('Fournisseur'));
});

La contribution reçoit la Datatable après sa construction et la mute via l'API publique du paquet datatable (colonnes, actions, options). Les ids sont ceux passés à ->id(...) dans les datatables du cœur.

Rendre un champ cherchable et filtrable

Greffer un champ sur un modèle ne suffit pas à le rendre cherchable : le moteur doit savoir qu'il existe, ce qu'il peut en faire, et comment traduire un filtre de l'URL. C'est le rôle du SearchIndexRegistry, où le cœur déclare ses attributs comme le ferait n'importe quel package.

php
use Slab\Framework\Core\Search\Contracts\SearchIndexRegistry;
use Slab\Framework\Models\Product;

$search = $this->app->make(SearchIndexRegistry::class);

// 1. Le champ entre dans le document indexé, et devient filtrable + facette.
$search->attribute('brand', fn (Product $p): ?string => $p->brand?->slug, filterable: true, facet: true);

// 2. Le paramètre d'URL `?brand=acme` devient une expression du moteur.
$search->filter('brand', fn (mixed $value): string => 'brand = "'.$value.'"');

// 3. Optionnel : un tri `?sort=brand:asc`.
$search->sort('brand', fn (string $way): string => 'brand:'.$way);

Les listes searchableAttributes / filterableAttributes / sortableAttributes de Meilisearch en découlent : elles sont posées au boot dans config('scout.meilisearch.index-settings'), puis poussées par scout:sync-index-settings. Un filtre dont la clé est inconnue du registre est ignoré — un paramètre d'URL inattendu ne fait pas échouer la recherche.

Ajouter un filtre au catalogue

Un filtre de catégorie a deux moitiés : calculer les valeurs disponibles, et les afficher. Le CatalogFilterRegistry les réunit sous une même clé — le package apporte le calcul, le thème apporte la vue.

php
use Slab\Framework\Core\Category\Contracts\CatalogFilterRegistry;
use Illuminate\Support\Collection;
use Slab\Framework\Models\Category;

$filters = $this->app->make(CatalogFilterRegistry::class);

// Côté package : ce que la catégorie a à proposer (null = pas de filtre à afficher).
$filters->add('brand', fn (Collection $products, Category $category): array => $products
    ->pluck('brand')->filter()->unique()->values()->all(), priority: 30);

// Côté thème : comment le rendre (la vue reçoit $filter, $selected, $category).
$filters->view('brand', 'mon-theme::filters.brand');

Le résultat est figé sur la catégorie (colonne filters) à chaque réindexation, pas recalculé à chaque affichage. Un filtre calculé sans vue déclarée n'est simplement pas affiché : c'est ainsi qu'un thème peut en ignorer un.

Proposer un e-mail transactionnel

Les statuts de commande peuvent déclencher un e-mail, choisi dans une liste au back-office. Un package y inscrit le sien :

php
use Slab\Framework\Core\Order\Contracts\OrderMailRegistry;

$this->app->make(OrderMailRegistry::class)->add(ShippingMail::class);

Sans libellé explicite, celui d'un OrderStatusMail est son sujet (getSubject()).

À retenir

  • On ajoute à une liste, on ne remplace pas le cœur.
  • Tout se fait au boot(), par add() / register().
  • L'ordre se règle par priorité (souvent réordonnable en config).