Apparence
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
| Registre | Vous y ajoutez… | Détail |
|---|---|---|
BackMenuRegistry | une entrée de menu BO | priorité réordonnable via config/back_menu.php |
RouteContributionRegistrar | des routes back() / front() | ordre garanti |
CheckoutStepRegistry | une étape de tunnel | priorité via config/checkout.php |
CheckoutOptionRegistry | un fournisseur d'options (livraison, paiement…) | voir ci-dessous |
ViewHookRegistry | un onglet / une section dans une vue | voir hooks & slots |
FormLayoutRegistry | un onglet / une section à la charpente d'un écran BO | voir hooks & slots |
DatatableExtensionRegistry | une colonne / action à une datatable existante | voir ci-dessous |
PriceModifierRegistry, TotalsModifierRegistry | un maillon de pipeline | priorités |
ModelExtensionRegistry | une colonne / un cast / un contenu à un modèle du cœur | voir modèles |
FormRuleRegistry | des règles à une Form Request du cœur | fusionnées à la validation |
AccessControlRegistry | un rôle / une permission | consommé par le seed et les écrans |
SearchIndexRegistry | un attribut indexé, une facette, un filtre, un tri | voir ci-dessous |
CatalogFilterRegistry | un filtre de catalogue (calcul et rendu) | voir ci-dessous |
OrderMailRegistry | un e-mail transactionnel proposable sur un statut | voir 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(), paradd()/register(). - L'ordre se règle par priorité (souvent réordonnable en config).