Skip to content

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 :

ContratDomaineRôleScopé
ProductPriceServiceProductle prix d'un produitproduit
ProductVariantServiceProductles déclinaisons : génération, valeurs d'attributs, application en masseproduit
ProductIndexServiceProductl'indexation et la recherche à facettes (Scout / Meilisearch)
CategoryServiceCategoryle recalcul des filtres (par lot de catégories) et le listing paginé des produitscatégorie
CartServiceCartles opérations sur un panier : ajout, mise à jour, suppression, fusion, passage de commande
CartPriceServiceCartle total des produits d'un panierpanier
OrderServiceOrderle cycle de vie d'une commande (changement de statut)commande
OrderPriceServiceOrderles totaux figés d'une commande (snapshot, jamais recalculés)commande
OrderStatusServiceOrderla liste des mailables de statut de commande
CurrencyServiceCurrencyla devise courante : conversion, formatage, affichage des montants
LanguageServiceLanguageles langues actives du site et la langue obligatoire
TranslationServiceTranslationles traductions de l'app (locales, export/import)
MenuServiceMenules éléments de menu (types, validation, formulaire, résolution d'URL)
ConsentServiceConsentles 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 :

ContratRôle
CurrentCartResolverle panier courant
CurrentCurrencyResolverla devise d'affichage
CurrentAreaResolverla zone fiscale (pilote la taxe)
TaxResolverle taux de taxe pour une règle + une zone
PriceModifierRegistry, TotalsModifierRegistryles pipelines
PricePipeline, TotalsBuilder, OrderPlacementPipelineles orchestrations entières (prix, totaux, placement de commande), remplaçables d'un bind
BackMenuRegistry, RouteContributionRegistrar, ViewHookRegistryles registres
FormLayoutRegistryla charpente d'un écran de formulaire : onglets, sections, et déplacement d'une section du cœur (moveSection())
DatatableExtensionRegistry, FormRuleRegistryles listings et la validation des écrans de back-office
ModelExtensionRegistryles colonnes assignables, casts, champs traduisibles et contenus contribués aux modèles du cœur
CheckoutOptionRegistry, AccessControlRegistryles options de checkout, les rôles et permissions
ProductPriceServicele 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