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);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(), 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 | produit |
ProductVariantService | Product | les déclinaisons : génération, valeurs d'attributs, application en masse | produit |
ProductIndexService | Product | l'indexation et la recherche à facettes (Scout / Meilisearch) | — |
CategoryService | Category | le recalcul des filtres (par lot de catégories) et le listing paginé des produits | catégorie |
CartService | Cart | les opérations sur un panier : ajout, mise à jour, suppression, fusion, passage de commande | — |
CartPriceService | Cart | le total des produits d'un panier | panier |
OrderService | Order | le cycle de vie d'une commande (changement de statut) | commande |
OrderPriceService | Order | les totaux figés d'une commande (snapshot, jamais recalculés) | commande |
OrderStatusService | Order | la liste des mailables de statut de commande | — |
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 éléments de menu (types, validation, formulaire, résolution d'URL) | — |
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 : CartService, lui, reçoit le panier en argument de méthode (addProduct($cart, $product)) — « le panier courant » relève du CurrentCartResolver, jamais de ce service.
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 |
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 |
PricePipeline, TotalsBuilder, OrderPlacementPipeline | les orchestrations entières (prix, totaux, placement de commande), remplaçables d'un bind |
BackMenuRegistry, RouteContributionRegistrar, ViewHookRegistry | les registres |
FormLayoutRegistry | la charpente d'un écran de formulaire : onglets, sections, et déplacement d'une section du cœur (moveSection()) |
DatatableExtensionRegistry, FormRuleRegistry | les listings et la validation des écrans de back-office |
ModelExtensionRegistry | les colonnes assignables, casts, champs traduisibles et contenus contribués aux modèles du cœur |
CheckoutOptionRegistry, AccessControlRegistry | les options de checkout, les rôles et permissions |
ProductPriceService | le prix d'un produit (service de domaine, scopé) |
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.
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.