Apparence
Le conteneur & les contrats
C'est la mécanique de base : presque tout comportement du cœur passe par un contrat (une interface) lié à une implémentation par défaut dans le conteneur Laravel. Vous pouvez la remplacer ou la décorer — sans jamais éditer le cœur.
Le problème
Vous voulez changer comment Slab fait quelque chose : résoudre la devise autrement, calculer la taxe selon vos règles, remplacer un service. Éditer vendor/slab/framework est exclu — perdu à la prochaine mise à jour. Il faut un point de bascule depuis l'extérieur.
Le principe
Le cœur ne dépend jamais d'une classe concrète : il dépend d'une interface, et lie un défaut dans le conteneur.
Vous redirigez l'interface vers votre implémentation. Tout le cœur l'utilise aussitôt, sans le savoir.
Remplacer une implémentation
Dans le boot() (ou register()) d'un provider — celui de votre app ou d'un package :
php
use Slab\Framework\Core\Tax\Contracts\TaxResolver;
$this->app->bind(TaxResolver::class, MonTaxResolver::class);Votre classe doit implémenter l'interface :
php
use Slab\Framework\Core\Tax\Contracts\TaxResolver;
use Slab\Framework\Core\Tax\TaxRate;
use Slab\Framework\Models\{Area, TaxRule};
final class MonTaxResolver implements TaxResolver
{
public function resolve(?TaxRule $taxRule, Area $area): TaxRate
{
// votre logique…
}
}Décorer plutôt que remplacer
Pour ajouter un comportement (cache, log, métrique) en gardant le défaut, décorez-le avec extend — l'implémentation d'origine vous est passée :
php
$this->app->extend(TaxResolver::class, function (TaxResolver $inner, $app) {
return new TaxResolverAvecCache($inner); // délègue à $inner, met en cache
});Les services de domaine (scopés à une entité)
Les services de domaine du cœur — le calcul du prix d'un produit, le panier, la commande — suivent exactement le même modèle : un contrat dans Contracts/, lié à un Default…. La seule particularité : certains sont scopés à une entité, donc résolus en passant l'entité en paramètre nommé.
php
use Slab\Framework\Core\Product\Contracts\ProductPriceService;
// Le cœur résout le service avec le produit concerné :
$price = app(ProductPriceService::class, ['product' => $product])->getMoney(ti: true);Un produit n'a pas le même prix pour tout le monde : le service sert la personne connectée, à moins que l'appelant ne nomme le client. forCustomer() rend alors une instance liée à lui, la première continuant de servir la session — ce dont a besoin tout canal qui n'est pas une requête du navigateur : une IA, une tâche planifiée, un export.
php
$price = app(ProductPriceService::class, ['product' => $product])
->forCustomer($customer) // `null` = le visiteur, sans compte
->getMoney(ti: true);Vous le remplacez comme n'importe quel contrat — votre implémentation reçoit l'entité au constructeur :
php
use Slab\Framework\Core\Product\Contracts\ProductPriceService;
use Slab\Framework\Models\Product;
final class PrixDegressifB2B implements ProductPriceService
{
public function __construct(private Product $product) {}
// getMoney(), getPrice(), forCustomer(), addPrice()…
}
$this->app->bind(ProductPriceService::class, PrixDegressifB2B::class);Liaison par chaîne de classe, pas par closure
Liez le contrat à sa classe (bind(Contrat::class, MaClasse::class)), pas à une closure : Laravel injecte alors automatiquement les paramètres nommés (['product' => $p]) au constructeur, et une nouvelle instance est rendue à chaque résolution (jamais partagée entre deux entités).
Le prix passe déjà par un pipeline
Pour ajuster un prix (remise, palier) sans réécrire le service, préférez un modificateur de prix : DefaultProductPriceService délègue au PricePipeline (getMoney(ti: true, withModifiers: false) renvoie le prix catalogue brut, avant modificateurs). Remplacer le service entier ne se justifie que pour changer la façade de calcul elle-même.
Les autres services de domaine suivent ce modèle — un contrat dans Contracts/, un Default… lié, remplaçable depuis l'extérieur :
| Contrat | Domaine | Rôle | Scopé |
|---|---|---|---|
ProductPriceService | Product | le prix d'un produit, tel que le voit celui qu'on sert : la personne connectée, ou le client que l'appelant nomme (forCustomer(), qui rend une instance liée à lui) | produit |
ProductVariantService | Product | les déclinaisons : génération, valeurs d'attributs, application en masse, et les axes que la famille propose à une fiche (siblingAxes() — un VariantAxis par attribut qui décline, chaque valeur portant la déclinaison qu'elle mène) — l'arbre complet des attributs, lui, est un utilitaire global | produit |
ProductIndexService | Product | l'indexation et la recherche à facettes (Scout / Meilisearch) — les attributs indexés sont déclarés au SearchIndexRegistry, et c'est ce service qui dit ce que le moteur sait d'un périmètre (facetsFor()), la matière de ses filtres ; sa recherche rend au plus le plafond de la boutique (shop.search.max_results, taillé pour un panneau de recherche), qu'un appelant qui pagine lui-même — un outil, un export — demande à sa mesure (getFacetedSearch(max_results: …)) | — |
ProductAvailabilityService | Product | la disponibilité d'un produit, calculée depuis son stock (état, quantité vendable, commandable) — le contrat à substituer pour lire un WMS / ERP | — |
CategoryService | Category | le périmètre que la catégorie découpe dans le catalogue (scope()), les filtres qu'elle a de quoi proposer (calculés par le CatalogFilterRegistry, lus dans l'index) et le listing paginé de ses produits | catégorie |
CatalogFilterPresenter | Catalog | ce qu'un périmètre de catalogue (CatalogScope : une catégorie, une marque, une recherche — forCategory(), forSearch()) propose à ses visiteurs : quels filtres, sous quel nom, dans quel ordre — c'est lui que le front consomme, jamais la liste brute des valeurs ; un périmètre peut lui livrer des valeurs déjà comptées (proposing()), et l'index n'est alors pas interrogé — c'est le cas d'un panneau de recherche, qui filtre ce qu'il affiche | — |
CartService | Cart | les opérations sur un panier : ajout, mise à jour, suppression, fusion, passage de commande — chaque geste date le panier, dont la date de mise à jour dit alors la dernière fois qu'on y a touché (c'est ainsi qu'on reconnaît un panier abandonné) ; un panier encore en mémoire ne s'enregistre qu'à l'ajout de son premier produit (addProduct()), et modifier ou retirer une ligne d'un panier qui n'existe pas encore ne fait rien | — |
CartPriceService | Cart | le total des produits d'un panier, compté au tarif de son titulaire (Cart::pricingCustomer() : son compte s'il en a un, les prix publics sinon) — jamais la personne connectée : un panier lu du back-office ou par une IA ne prend pas les montants de celui qui le consulte | panier |
OrderService | Order | le cycle de vie d'une commande (changement de statut) et le journal de l'équipe (la note interne : addNote() rend l'OrderNote écrite, signée de qui est connecté — elle s'ajoute, et une commande verrouillée se note encore, le verrou tenant les lignes et les montants, pas ce que l'équipe s'écrit) | commande |
OrderPriceService | Order | les totaux figés d'une commande (snapshot, jamais recalculés) | commande |
OrderModificationService | Order | retoucher une commande tant qu'elle n'est pas verrouillée : ses lignes, ses composants de total, ses adresses — chaque geste consulte l'OrderLockPolicy, propose un OrderModifying annulable, puis refait les totaux depuis l'instantané, jamais depuis le catalogue courant | commande |
OrderStatusService | Order | la liste des mailables de statut de commande (alimentée par l'OrderMailRegistry) | — |
OrderSearch | Order | ce que « chercher une commande » veut dire, traduit une seule fois pour toutes ses surfaces : le filtre d'une colonne appliqué à la requête en cours (apply()), la recherche d'un bloc de critères (query()), et celle d'un client bornée à ses seules commandes (ownedBy(), portée imposée par l'appelant, jamais lue dans les critères) | — |
DeletionService | Deletion | supprimer un objet du domaine, par le chemin des écrans d'administration : refuser une suppression se fait en écoutant ModelDeleting (une marque encore portée par des produits) ; substituer le contrat change la manière de supprimer (archivage, corbeille généralisée, audit tenu ailleurs) | — |
SavingService | Saving | son pendant : créer ou modifier un objet du domaine sur des valeurs validées — celles de la Form Request, jamais la requête entière —, par les écrans comme par une IA : écrire au-delà des colonnes (les catégories d'un produit, un parent par défaut) se déclare par type d'objet au SaveHookRegistry ; refuser un enregistrement se fait en écoutant ModelSaving ; substituer le contrat change la manière d'enregistrer | — |
CurrencyService | Currency | la devise courante : conversion, formatage, affichage des montants | — |
LanguageService | Language | les langues actives du site et la langue obligatoire | — |
TranslationService | Translation | les traductions de l'app (locales, export/import) | — |
MenuService | Menu | les entrées de menu : types, données de formulaire, et leur validation — celle du formulaire (validateItemForm()) comme celle d'une demande qui n'en vient pas (validateItem(), une IA, un import : les règles dépendent du type de l'entrée) —, leur enregistrement sous leur parent (saveItem(), qui pose le rattachement de force, le paquet contentable gardant model_id, model_type et type contre l'assignation de masse) et leur rangement (reorder(), tel que le glisser-déposer les pose) ; enfin la résolution d'URL (getItemUrl() rend null quand la cible n'existe plus ou n'a pas d'adresse : un produit supprimé, une entrée de texte, une famille dont aucune déclinaison ne se montre) | — |
SettingsService | Settings | les réglages qu'un administrateur a changés, versés par-dessus les fichiers de config au démarrage — jamais pendant config:cache ni optimize, qui les figeraient comme s'ils venaient des fichiers — et rappelés au seuil de chaque job, un worker ne redémarrant pas entre deux (réglages) | — |
RegistrationService | Account | qui peut créer un compte et à quelles conditions il sert : le mode en vigueur (mode()), l'inscription elle-même (register(), enregistrée dans tous les cas — une inscription refusée à l'entrée ne laisserait rien à valider) et sa validation (approve(), cliquet : revalider ne réécrit pas la date ni ne renvoie d'invitation) (comptes) | — |
ConsentService | Consent | les consentements d'un modèle consentable (RGPD, newsletter…) | consentable |
Un service scopé se résout en passant son entité en paramètre nommé — le nom est celui du paramètre de constructeur : product, cart, order, category, consentable (app(OrderService::class, ['order' => $order]), comme ProductPriceService ci-dessus). Les autres sont des services applicatifs simples, résolus sans paramètre et recevant leur entité en argument de méthode : CartService (addProduct($cart, $product)), ProductAvailabilityService (for($product)), CatalogFilterPresenter (for($scope) — un périmètre, pas un modèle), OrderSearch (ownedBy($customer, $criteria)), DeletionService (delete($model)), SavingService (save($model, $values)), MenuService (saveItem($parent, $values)), RegistrationService (approve($user)) — « le panier courant » relève, lui, du CurrentCartResolver, jamais de ce service.
Empêcher une action ne demande pas de remplacer le service
Les mutations du CartService, de l'OrderService (sa transition de statut), de l'OrderModificationService, du DeletionService et du SavingService annoncent leur intention par un événement annulable (CartItemAdding, CartItemUpdating, CartItemRemoving, OrderStatusChanging, OrderModifying, OrderPlacing, ModelDeleting, ModelSaving…) : pour refuser un ajout, une transition, une retouche de commande ou une suppression, il suffit de s'y opposer — la méthode rend alors false sans rien avoir modifié, ou lève ActionCancelled quand son type de retour ne permet pas d'exprimer l'échec (changeStatus(), OrderPlacementPipeline::place(), DeletionService::delete(), SavingService::save(), et toutes les retouches de l'OrderModificationService). Le motif passé à cancel() devient le message de cette exception ; sur les méthodes qui rendent false, il ne sort pas de l'événement. Le cœur s'appuie sur le même retour pour ses propres gardes : addProduct() rend false sur un produit non commandable — statut qui ne l'autorise pas, disponibilité qui s'y oppose — avant même d'annoncer l'ajout, tout comme updateProduct() sur une hausse de quantité, et updateProduct() comme deleteProduct() sur un panier encore en mémoire. Voir les événements.
Où Slab l'utilise
Tous les contrats marqués « surchargeable » dans la référence suivent ce modèle. Les plus courants :
| Contrat | Rôle |
|---|---|
CurrentCartResolver | le panier courant — vide et en mémoire tant que rien n'y a été mis (un visiteur de passage, un robot d'indexation), retenu pour les pages suivantes une fois enregistré (remember() : le cookie du visiteur, ou le compte) — et sa transition à la connexion (setOnLogin()) |
CurrentCurrencyResolver | la devise d'affichage |
CurrentAreaResolver | la zone fiscale (pilote la taxe) |
TaxResolver | le taux de taxe pour une règle + une zone |
PriceModifierRegistry, TotalsModifierRegistry | les pipelines — un maillon de totaux voit les lignes déjà contribuées et pose sa contrepartie, jamais la réécriture de la ligne d'un autre |
PricePipeline, TotalsBuilder, OrderPlacementPipeline | les orchestrations entières (prix, totaux, placement de commande), remplaçables d'un bind |
OrderLockPolicy | ce qui rend une commande immuable — par défaut le cliquet posé par un statut, enregistré et jamais recalculé depuis l'historique — et le motif qu'on oppose à celui qui tente de la modifier |
SaleDefinition | ce qui compte comme une vente : une seule règle pour le chiffre d'affaires, la popularité du catalogue et les indicateurs — une boutique qui ne compte une vente qu'une fois réglée lie la sienne, et tout ce qui compte des ventes la suit |
IndicatorRegistry | les indicateurs nommés de la boutique (chiffre d'affaires, meilleures ventes…), que le tableau de bord et l'IA disent à l'identique : chacun est un Measure (un chiffre sur une période, et sa série) ou un Ranking (un classement) ; un package en ajoute (add()), en retire (forget()) ou en substitue un en déclarant la même clé — une fois le cœur démarré (booted()), puisque celui-ci déclare les siens à son démarrage (registres) |
DashboardTileRegistry | les tuiles du tableau de bord du back-office — un chiffre, un graphique, une liste —, par priorité croissante : chacune est un DashboardTile qui rend sa propre vue pour la période choisie (render(), null quand elle n'a rien à montrer) et lit ses chiffres dans les indicateurs ; un package en ajoute (add(), une instance ou sa classe), en retire (forget()), en substitue une en déclarant la même clé, ou la déplace sans la redéfinir (prioritize()) — lui aussi une fois le cœur démarré (booted()) |
BackMenuRegistry, RouteContributionRegistrar, ViewHookRegistry | les registres — dont le menu du back-office, qu'un projet réordonne sans redéclarer une entrée qu'il ne possède pas (prioritize()) |
FormLayoutRegistry | la charpente d'un écran de formulaire : onglets — rangés sur le côté dès qu'il y a de quoi naviguer —, sections, et déplacement d'une section du cœur (moveSection()) ; les sections d'un onglet s'empilent sur toute la largeur (le paramètre columns de tab() est désormais sans effet, déprécié), et une section déclarée en marge (section(…, aside: true)) partage son onglet en deux colonnes — le principal à gauche, sur deux tiers, la marge à côté —, chacune s'empilant pour son compte |
DatatableExtensionRegistry, BulkActionRegistry, FormRuleRegistry | les listings de back-office — colonnes contribuées et actions groupées sur les lignes cochées, déclarées par id de listing (products, categories…) — et la validation de leurs écrans |
DataSchemaRegistry, DataFormat, DataExchangeService | l'échange de fichiers d'un listing : les colonnes déclarées (les deux sens dans un seul schéma), le format qui les porte (CSV par défaut) et le service qui applique l'un à l'autre — voir le back-office |
ModelExtensionRegistry | les colonnes assignables, casts, champs traduisibles et contenus contribués aux modèles du cœur |
SaveHookRegistry | ce que le SavingService écrit au-delà des colonnes d'un objet — ses relations, son prix, un défaut que le formulaire laisse vide —, déclaré une fois par classe de modèle (sous-classes comprises) : avant l'enregistrement (saving(), le hook rend les valeurs complétées) ou après (saved(), l'objet a sa clé), par priorité croissante ; l'écran et une IA écrivent donc la même chose, et une valeur absente ne touche à rien — un enregistrement partiel n'efface rien (enregistrer depuis un écran) |
SearchIndexRegistry, CatalogFilterRegistry, CatalogFilterPresenter | ce qu'un champ devient dans le catalogue : indexé, filtrable, triable, exposé en facette ou en filtre d'un périmètre — catégorie, marque, recherche (calcul contribué, rendu posé par le thème) —, le champ sous lequel ce filtre part dans l'URL (fieldFor()), les facettes dont une source se nourrit (facetsOf() — dans quel champ du document lire la valeur qu'on coche) puis l'ordre dans lequel ce périmètre le propose ; les clés de filtre déclarées (filterKeys()) disent aussi au robots.txt quelles adresses écarter des robots — celles d'un package comprises |
ProductStatusRegistry, ProductAvailabilityService | ce qu'un produit autorise : les statuts déclarés (visible ? commandable ?) et la disponibilité lue du stock |
OrderDocumentRegistry | les documents qu'une commande peut produire (devis, bon de livraison, facture…) : le cœur n'en déclare aucun, un package pose la condition d'existence et le rendu de chacun |
MenuIconRegistry | les icônes qu'une entrée de menu peut porter : le cœur n'en déclare aucune — elles appartiennent au thème, qui déclare les siennes (add()) ; tant que rien ne l'est, le back-office laisse saisir la clé à la main |
ThirdPartyServiceRegistry | les services tiers qu'une page charge chez le visiteur — une carte, une vidéo — et qu'il doit accepter avant. Un service se définit une fois, par sa clé (define(), rangé dans une ConsentCategory, liste fermée), et ici la première définition gagne : le catalogue du cœur (KnownServices : Google Maps, YouTube…) est défini dès la création du registre, donc toujours en premier. Un usage dit ce qu'une brique en fait (uses(), actif selon sa condition $when) : deux briques qui affichent Google Maps partagent le même service, et le visiteur ne l'accepte qu'une fois. Seuls les services qui ont un usage actif sont montrés (active()) ; le registre ne connaît aucune plateforme de consentement |
CheckoutOptionRegistry, OrderMailRegistry, AccessControlRegistry | les options de checkout — et si un paquet installé sert une exigence (offers()) : c'est ce qui fait exister une étape du tunnel, même quand aucune de ses options ne dessert le panier —, les e-mails proposables sur un statut de commande, les rôles et permissions |
ProductPriceService | le prix d'un produit (service de domaine, scopé), pour la personne connectée ou pour le client qu'on nomme (forCustomer()) |
RegistrationService | la politique d'inscription : le mode, la création du compte et sa validation |
OrderSearch, OrderSearchTermRegistry | ce que « chercher une commande » veut dire, pour les trois surfaces qui la cherchent (listing du back-office, agent, espace client) — et où chercher en plus : un package inscrit sa clause (contribute(), une contremarque, un numéro de suivi) au lieu de substituer le service entier ; elle est appliquée dans le groupe du texte libre, jamais à la racine de la requête, sinon un mot cherché contournerait la portée d'un client |
StructuredDataRegistry | ce qu'une page dit d'elle-même aux machines — moteurs, assistants qui naviguent : un graphe JSON-LD (schema.org) par entité servie. Une source se déclare pour un modèle (register(), sous-classes comprises : une boutique qui étend Product garde ce que le cœur en dit) et reçoit le sac tel que les précédentes l'ont rempli, par priorité croissante — le cœur décrit à 10 ce qu'il connaît du produit, du rayon et de la page ; un package pose son propre graphe (add()) ou complète celui d'un type déjà posé (merge(), l'image d'un produit, sa marque) sans avoir à le réécrire ni à connaître son rang, et un type que personne n'a posé ne laisse aucun graphe orphelin |
SitemapSourceRegistry, SitemapGenerator | d'où viennent les adresses du sitemap.xml, et qui l'écrit : chaque famille d'adresses est un SitemapSource — une clé qui nomme son fichier (products → products.xml, et déclarer une clé déjà prise remplace la source précédente), ses URL rendues à la demande (un catalogue entier ne tient pas en mémoire) et les modèles qui la périment (watchedModels()) ; le générateur écrit un fichier par source plus l'index que sert /sitemap.xml, et une édition en masse ne déclenche qu'une régénération différée (queueRegeneration()) |
Le sitemap.xml : qui l'écrit
Une boutique annonce ses adresses aux moteurs. Deux contraintes s'opposent : le fichier doit être à jour — un produit publié ce matin y figure — sans qu'un robot déclenche un parcours du catalogue à chaque visite, et sans qu'une édition en masse le régénère vingt fois.
Le principe
Slab sépare d'où viennent les adresses et qui écrit le fichier. Le SitemapSourceRegistry porte les familles d'adresses — les pages, les rayons, les produits, celles qu'un paquet ajoute. Slab\Framework\Core\Sitemap\Contracts\SitemapGenerator porte l'écriture : un fichier par source, plus l'index qui les rassemble. Une source ne sait rien du fichier, et le générateur ne nomme aucun modèle.
Le contrat tient en trois méthodes :
php
namespace Slab\Framework\Core\Sitemap\Contracts;
interface SitemapGenerator
{
public function generate(): void; // (re)génère les fichiers, source par source
public function indexPath(): string; // le fichier d'index — celui que sert `/sitemap.xml`
public function queueRegeneration(): void; // une régénération différée, et une seule
}Chacune a son appelant dans le cœur : generate() est invoquée par la commande php artisan sitemap:generate (immédiate, sans file) et par le job Slab\Framework\Jobs\GenerateSitemap (ShouldBeUnique, uniqueId() = generate-sitemap) ; indexPath() par SitemapController::index(), qui sert le fichier tel quel et rend 404 tant qu'aucune génération n'a eu lieu ; queueRegeneration() par les observateurs que le CoreServiceProvider branche une fois tous les paquets démarrés (booted()), sur les saved et deleted de chaque modèle déclaré par SitemapSourceRegistry::watchedModels().
Ce que fait l'implémentation par défaut
DefaultSitemapGenerator écrit sous public_path(config('shop.sitemap.directory')) (sitemaps), donc servi par le serveur web sans traverser PHP. Une source qui n'annonce rien voit son fichier supprimé et disparaît de l'index. Une adresse qu'on n'arrive pas à écrire est journalisée en warning et ignorée — elle n'emporte pas le sitemap. Les versions par langue (hreflang) sont posées par le générateur, jamais déclarées par les sources, et n'apparaissent qu'à partir de deux langues actives : une adresse qui nomme son modèle (l'argument model: de SitemapUrl) prend dans chaque langue celle que le modèle y donne, slug compris ; les autres ne changent que de préfixe et de segments de route. queueRegeneration() ne pousse rien si shop.sitemap.enabled est à false, sinon diffère le job de shop.sitemap.debounce_minutes (3 par défaut).
Le remplacer
Le contrat est lié en bind : une boutique qui pousse son sitemap ailleurs — un stockage objet, un CDN — change ce service sans toucher aux sources.
php
use Slab\Framework\Core\Sitemap\Contracts\SitemapGenerator;
$this->app->bind(SitemapGenerator::class, ObjectStorageSitemapGenerator::class);Le plus souvent, on garde le défaut et on lui ajoute une étape — l'écriture locale reste, la copie part ensuite :
php
use Slab\Framework\Core\Sitemap\Contracts\SitemapGenerator;
final class ObjectStorageSitemapGenerator implements SitemapGenerator
{
public function __construct(private readonly SitemapGenerator $inner) {}
public function generate(): void
{
$this->inner->generate();
// … puis copier les fichiers écrits vers le stockage objet
}
public function indexPath(): string
{
return $this->inner->indexPath();
}
public function queueRegeneration(): void
{
$this->inner->queueRegeneration();
}
}
$this->app->extend(
SitemapGenerator::class,
fn (SitemapGenerator $inner) => new ObjectStorageSitemapGenerator($inner),
);indexPath() est lu comme un fichier local
SitemapController::index() teste l'existence du chemin rendu, puis en sert le contenu. Une implémentation qui n'écrit que sur un stockage distant doit donc rendre un chemin lisible localement — ou remonter sa propre route sur sitemap.xml. Attention aussi à ce que rien ne porte ce nom dans public/ : le serveur web servirait le fichier avant PHP, sans que rien ne le signale.
Le déclencher soi-même
Un traitement qui écrit hors des modèles surveillés — un import, une reprise de données — ne passe par aucun observateur. Demandez alors une régénération différée plutôt qu'une génération immédiate : on ne fait pas attendre un marchand pendant qu'on parcourt le catalogue.
php
app(SitemapGenerator::class)->queueRegeneration();Ajouter des adresses ne demande pas de remplacer le générateur
Pour annoncer ce que le cœur ne connaît pas — des marques, des réalisations —, déclarez un SitemapSource au SitemapSourceRegistry : sa clé nomme son fichier, ses URL sont rendues à la demande, et les modèles qu'elle cite périment le sitemap tout seuls. Quand une adresse désigne un modèle RedirectableBySlug, nommez-le (model:) : sans lui, sa version dans une autre langue garderait le slug d'origine et ne mènerait à la bonne page qu'en redirigeant. Reprendre une clé du cœur (categories) remplace la source — c'est ainsi qu'un paquet affine ce qui est annoncé.
Comment savoir ce qui est remplaçable
La commande php artisan slab:extension-points (et la référence générée) liste chaque contrat, son domaine, son type (surchargeable = un défaut existe ; implémentable = à fournir par un package) et son implémentation par défaut. CheckoutOptionProvider illustre le second type : le cœur n'en fournit aucune implémentation, c'est une feature (livraison, paiement…) qui en écrit une par exigence qu'elle introduit.
Une règle d'or
N'éditez jamais un fichier sous vendor/slab/…. Tout se fait depuis l'extérieur : bind, decorate, contribuer, surcharger une vue. C'est ce qui garde vos mises à jour indolores.
Voir aussi
- Les pipelines et les registres : des contrats particuliers, faits pour accumuler des contributions.
- Personnaliser le cœur pour les vues.