AkiraAkira.dev
3 min de lecture

Laravel SISP 2.0 n'a presque livré aucune fonctionnalité

Onze pipes dans un fichier de config ont remplacé un contrôleur que personne ne pouvait atteindre. C'est toute la release.

aussi en EN PT

Voici la release entière de Laravel SISP 2.0, en un seul tableau :

// config/sisp.php
'pipelines' => [
    'payment' => [
        Akira\Sisp\Pipelines\Payment\Pipes\EnsureIpIsNotBlacklisted::class,
        Akira\Sisp\Pipelines\Payment\Pipes\EnforceRateLimits::class,
        Akira\Sisp\Pipelines\Payment\Pipes\ApplyPaymentIntent::class,
        Akira\Sisp\Pipelines\Payment\Pipes\BuildPaymentRequest::class,
        Akira\Sisp\Pipelines\Payment\Pipes\PersistTransaction::class,
        Akira\Sisp\Pipelines\Payment\Pipes\CaptureRequestMetadata::class,
    ],
    'callback' => [
        Akira\Sisp\Pipelines\Callback\Pipes\ResolveTransaction::class,
        Akira\Sisp\Pipelines\Callback\Pipes\ValidateFingerprint::class,
        Akira\Sisp\Pipelines\Callback\Pipes\EnsureCallbackMatchesTransaction::class,
        Akira\Sisp\Pipelines\Callback\Pipes\ApplyTransactionStatus::class,
        Akira\Sisp\Pipelines\Callback\Pipes\DispatchPaymentEvents::class,
    ],
],

47 commits, 126 fichiers touchés, environ 4000 lignes ajoutées et 1400 supprimées, pour rendre ces onze lignes modifiables par quelqu’un d’autre que moi.

Ce que ce tableau remplace

En 1.x, l’ordre de ces étapes n’était écrit nulle part où vous pouviez le lire. Il vivait dans le corps du contrôleur, sous huit dépendances injectées :

// src/Http/Controllers/PaymentController.php (1.0.3)
public function __construct(
    private CreateIdempotentPaymentTransactionAction $createPayment,
    private PreparePaymentAction $preparePayment,
    private CreateAndStorePaymentTransactionAction $createTransaction,
    private RenderPaymentFormBasedOnConfigAction $renderForm,
    private CheckRateLimitAction $checkRateLimit,
    private CheckBlacklistAction $checkBlacklist,
    private StoreRequestMetadataAction $storeMetadata,
    private LoadConfig $config,
) {}

Chacune de ces actions est une classe correcte. Leur enchaînement, lui, n’était pas une classe du tout : 81 lignes de contrôleur, fermées à double tour.

Votre acquéreur exige un contrôle antifraude entre la blacklist et le rate limiter ? En 1.x, il fallait forker le package ou envelopper la route et prier pour l’idempotence. Aujourd’hui votre contrôle est une classe qui implémente PaymentPipe et une ligne dans le tableau ci-dessus. Le contrôleur, lui, est tombé à 51 lignes et deux dépendances.

Ce que cela me coûte mérite d’être dit. Ce tableau est devenu un contrat public. Je ne peux plus déplacer une étape dans une version mineure, parce que des pipes qui ne m’appartiennent pas se sont installées entre les miennes. Une couture qu’on ouvre au public cesse de nous appartenir.

Les builders protègent l’appelant

Même logique du côté de l’API. Demander un paiement ne devrait pas exiger de savoir comment le package le range :

$paymentRequest = Sisp::payment()
    ->amount(1500.0)
    ->currency('132')
    ->customerEmail('acheteur@exemple.cv')
    ->locale('pt')
    ->build();

$transaction = Sisp::refund($transaction)
    ->amount(500.0)
    ->reason('partial_return')
    ->process();

En 1.x, vous assembliez les value objects vous-même. Autrement dit, mon rangement intérieur devenait votre charge de maintenance, et le moindre champ renommé apparaissait dans votre diff. RefundBuilder expose quatre méthodes : amount(), full(), reason(), process(). Toute la surface de remboursement tient là.

Une montée de version qui casse, exprès

En 1.x, la validation du callback passait par la façade Sisp. Les suites de tests en profitaient pour échanger le service dans le container :

// 1.x, ne fonctionne plus
app()->instance(\Akira\Sisp\Sisp::class, new class {
    public function validateCallback($payload): bool { return true; }
});

En 2.0, c’est un contrat qui occupe cette place, et c’est lui que vous liez :

// 2.0
app()->instance(CallbackFingerprintValidator::class, new class implements CallbackFingerprintValidator {
    public function handle(CallbackPayload $payload): bool { return true; }
});

Toutes les suites qui simulaient un callback sont passées au rouge le jour de la montée de version. J’aurais pu maintenir l’ancien chemin en parallèle. J’ai refusé : avec deux façons de simuler une vérification d’empreinte dans un package de paiement, l’une des deux finit oubliée en production. Mieux vaut une casse visible en intégration continue qu’un écart découvert sur un relevé bancaire.

« Beaucoup de classes pour une redirection »

L’objection est légitime. SISP se résume à un formulaire soumis et à un callback qui revient. Onze classes pour cela, cela sent la cérémonie.

Sauf qu’une pipe ressemble à ceci :

// src/Pipelines/Payment/Pipes/EnforceRateLimits.php
final readonly class EnforceRateLimits implements PaymentPipe
{
    public function __construct(private CheckRateLimitAction $checkRateLimit) {}

    public function handle(PaymentContext $context, Closure $next): PaymentContext
    {
        $this->checkRateLimit->handle(identifier: $context->request->ip());

        return $next($context);
    }
}

Vingt-deux lignes, namespace et imports compris. Une dépendance, un appel. L’abstraction pèse moins lourd que le paragraphe de contrôleur qu’elle remplace. Rien n’a été ajouté : une séquence a changé d’adresse, d’un endroit fermé vers un endroit ouvert.

Le reste relève de l’entretien courant. PHP 8.5, Laravel 13, #[Bind] et #[Singleton] sur les contrats, #[Fillable] sur les modèles.

La 2.0 se résume à ceci : le jour où le package ne fait pas ce qu’il vous faut, vous éditez un tableau au lieu de cloner un dépôt.

partager