Développer une extension
Une extension SolisForms est un plugin WordPress ordinaire qui s’accroche à
solis_forms_register_addons. Elle reçoit le conteneur de services au démarrage,
et accède ainsi aux registres du plugin sans qu’ils transitent par le hook.
Squelette
<?php
/**
* Plugin Name: SolisForms — Mon extension
*/
use SolisForms\AddOns\AddOnInterface;
use SolisForms\Core\Container;
final class Mon_Extension implements AddOnInterface {
public function get_id(): string {
return 'mon-extension';
}
public function get_name(): string {
return 'Mon extension';
}
public function boot( Container $container ): void {
// Point d'entrée : brancher ici les hooks de l'extension.
}
}
add_action( 'solis_forms_register_addons', function ( $manager ) {
$manager->register( new Mon_Extension() );
} );
L’extension apparaît alors dans SolisForms → Extensions. Si son boot() lève
une erreur, le plugin poursuit son chargement sans elle et l’écran la signale en
échec : une extension défaillante ne met jamais le site à terre.
Chargez votre extension après SolisForms — un plugin ordinaire l’est déjà,
puisque le hook n’est émis que sur init.
Ajouter un type de champ
Étendre AbstractField fournit le rendu accessible commun (libellé associé,
aria-required, aria-invalid, messages d’erreur) : il ne reste que le contrôle
lui-même.
use SolisForms\Domain\Field\AbstractField;
use SolisForms\Support\Sanitizer;
final class Champ_Couleur extends AbstractField {
public function get_type(): string {
return 'couleur';
}
public function get_label(): string {
return __( 'Sélecteur de couleur', 'mon-extension' );
}
public function get_settings_schema(): array {
return array(
'label' => array( 'type' => 'string', 'default' => '' ),
'required' => array( 'type' => 'boolean', 'default' => false ),
);
}
public function sanitize( mixed $value ): mixed {
return sanitize_hex_color( Sanitizer::text( $value ) ) ?? '';
}
public function validate( mixed $value, array $config ): array {
return $this->validate_required( $value, $config );
}
protected function render_input( array $config, mixed $value, string $field_id, array $errors ): string {
return sprintf(
'<input type="color" class="slf-input"%s value="%s">',
$this->common_attributes( $config, $field_id, $errors ),
esc_attr( (string) $value )
);
}
}
Enregistrement, depuis le boot() de l’extension ou directement :
add_action( 'solis_forms_register_field_types', function ( $registry ) {
$registry->register( new Champ_Couleur() );
} );
Le schéma déclaré par get_settings_schema() suffit : le constructeur React génère
l’interface de réglage correspondante, sans une ligne de JavaScript. Un schéma
de type array produit un éditeur d’options, une clé enum une liste déroulante.
Ajouter un comportement public (JavaScript)
Un champ dont le rendu réclame du JavaScript passe par le registre des modules publics. Le point d’entrée du cœur emprunte exactement ce chemin — logique conditionnelle, calculs, répéteur, paiement s’y déclarent comme le ferait votre extension.
window.solisForms.registerModule( 'mon-extension/signature', ( form, context ) => {
const champ = form.querySelector( '[data-signature]' );
if ( ! champ ) {
return;
}
// Retourner une fonction la fait rejouer par context.refresh(), comme les
// calculs le sont quand une ligne répétable apparaît.
return () => mettreAJour( champ, context.schema );
} );
Déclarez le script avec solis-forms-frontend en dépendance :
wp_enqueue_script(
'mon-extension-frontend',
plugins_url( 'build/frontend.js', __FILE__ ),
array( 'solis-forms-frontend' ),
'1.0.0',
true
);
Trois règles du registre valent d’être connues :
- Ordre : le troisième argument accepte
{ priority: 20 }. L’interception de l’envoi est branchée à 100, après tout le reste. - Isolation : un module qui lève une erreur est consigné dans la console et ignoré. Il ne prive jamais le formulaire de sa soumission.
- Arrivée tardive : un module déclaré après le chargement de la page est branché immédiatement sur les formulaires déjà initialisés.
Emprunter les composants du constructeur
Un panneau a souvent besoin de l’éditeur de conditions du cœur, ou du nom de son
magasin de données. Les recopier ferait vivre deux éditeurs de conditions, qui
divergeraient à la première évolution du format des règles. Le constructeur les
publie donc sur window.solisForms.builder :
const { ConditionalLogicEditor, STORE_NAME } = window.solisForms.builder;
Résolvez le composant au rendu plutôt qu’au chargement du module, pour qu’un
cœur absent ou plus ancien se traduise par un panneau sans son éditeur, et non par
une erreur qui emporterait tout le constructeur.
pro/assets/src/builder/core.js en montre le procédé complet.
Ajouter une passerelle de paiement
Ce point d’extension appartient désormais à l’extension payante. Les paiements ont quitté le plugin gratuit :
solis_forms_payment_gatewaysest diffusé parSolisFormsPro\Payments\GatewayRegistry, et une passerelle supplémentaire étend donc SolisForms Pro, non le cœur. Le contrat, lui, n’a pas changé — seuls les espaces de noms sont préfixésSolisFormsPro.
use SolisForms\Payments\PaymentGatewayInterface;
add_filter( 'solis_forms_payment_gateways', function ( array $gateways ) {
$gateways['mollie'] = new Ma_Passerelle_Mollie();
return $gateways;
} );
Ma_Passerelle_Mollie implémente les neuf méthodes de PaymentGatewayInterface :
| Méthode | Rôle |
|---|---|
get_id(): string | Identifiant court, celui employé dans les réglages et les routes |
get_label(): string | Nom présenté à l’administrateur |
is_configured(): bool | Les clés nécessaires sont-elles renseignées |
supports_subscriptions(): bool | Intention de prendre en charge le récurrent |
create_payment( PaymentContext $c ): PaymentResult | Ouvre le paiement |
confirm_payment( string $ref ): PaymentResult | Confirme après retour du visiteur |
verify_webhook_signature( WP_REST_Request $r ): bool | Authentifie l’appel entrant |
handle_webhook_event( WP_REST_Request $r ): void | Traite l’événement reçu |
refund( string $ref, ?float $amount = null ): PaymentResult | Remboursement total ou partiel |
Pour les abonnements, implémenter SubscriptionGatewayInterface, qui étend la
précédente et y ajoute create_subscription(), fetch_subscription() et
cancel_subscription(). Renvoyer true depuis supports_subscriptions() sans
implémenter cette interface ne suffit pas : le cœur exige les deux, et écarte la
passerelle plutôt que de rompre au milieu d’une souscription.
Deux règles que le cœur applique et qu’une passerelle ne doit pas contourner : le montant est recalculé côté serveur, jamais repris du client ; et la signature d’un webhook est vérifiée avant tout traitement.
Déclarer ses services dans le conteneur
Une extension qui compose plusieurs objets n’a pas à tenir sa propre fabrique :
elle déclare ses services dans le conteneur que boot() lui remet. Ils
deviennent alors résolvables comme ceux du cœur — donc réutilisables, et
remplaçables par une autre extension.
public function boot( Container $container ): void {
$container->singleton(
Mon_Depot::class,
static fn (): Mon_Depot => new Mon_Depot()
);
$container->singleton(
Mon_Rendu::class,
static fn ( Container $c ): Mon_Rendu => new Mon_Rendu(
$c->make( Mon_Depot::class ),
$c->make( EntryValueFormatter::class ) // service du cœur
)
);
add_action( 'init', static fn () => $container->make( Mon_Rendu::class )->register() );
}
pro/src/Views/ViewsAddOn.php en montre un cas complet : huit services, un type
de contenu, une route REST, un bloc, un code court et un écran d’administration.
Le piège de la priorité de init
boot() s’exécute pendant init, à la priorité 5. Un abonné ajouté à cette
même priorité pendant qu’elle s’exécute ne sera jamais appelé : WP_Hook
itère une copie du tableau des abonnés de la priorité courante. Les priorités
suivantes, elles, sont bien atteintes.
add_action( 'init', $callback, 5 ); // ✗ jamais exécuté
add_action( 'init', $callback ); // ✓ priorité 10, pas encore atteinte
$this->declare_now(); // ✓ on est déjà dans init
Ce qui doit précéder vos autres abonnés — un register_post_type() que les
suivants consultent — s’appelle donc directement depuis boot() : init a
commencé, ce que register_post_type() exige, et l’ordre devient certain.
Réagir à une soumission
add_action( 'solis_forms_entry_created', function ( $event ) {
wp_remote_post( 'https://exemple.test/crm', array(
'body' => array(
'formulaire' => $event->form->title,
'valeurs' => $event->entry->values,
),
) );
} );
L’événement porte l’objet Form complet : inutile de le recharger depuis la base.
Préférez un traitement bref, ou différez-le via wp_schedule_single_event() — ce
hook est exécuté pendant la requête de soumission, et l’utilisateur en attend la
réponse.
Écrire une intégration sortante
Slack, Brevo et le webhook sortant vivent dans l’extension payante
(pro/src/Integrations/) et n’ont aucun accès réservé : ils empruntent le chemin
décrit ici, du premier au dernier hook. pro/src/Integrations/Slack/ sert donc
d’exemple complet et à jour — c’est un plugin séparé, avec son propre autoloader
et son propre domaine de traduction.
Ranger ses réglages
Deux endroits, selon la nature du réglage :
- Identifiants du compte — clé d’API, jeton : globaux, par le filtre
solis_forms_settings_sections, relus parPluginSettings::section(). - Réglages du formulaire — destinataire, gabarit, condition : par le filtre
JavaScript
solisForms.formSettingsPanels, relus parFormSettings::section( 'ma-cle' ).
Aucun endpoint REST n’est à écrire : les réglages du formulaire sont persistés par le mécanisme générique, avec le reste du formulaire.
Différer l’envoi
N’appelez jamais un service distant depuis solis_forms_entry_created. Ce
hook s’exécute pendant la requête de soumission, dont l’utilisateur attend la
réponse : un service lent le ferait patienter, un service en panne le ferait
attendre jusqu’au délai d’expiration. La soumission est déjà enregistrée à ce
stade, rien ne justifie de lui adosser le sort d’un tiers.
public function boot( Container $container ): void {
add_action( 'solis_forms_entry_created', function ( $event ) {
wp_schedule_single_event(
time(),
'mon_extension_envoi',
array( $event->entry->id, $event->form_id, 1 )
);
} );
add_action( 'mon_extension_envoi', function ( $entry_id, $form_id, $attempt ) use ( $container ) {
// Recomposez la charge utile ici, depuis les dépôts.
}, 10, 3 );
}
Ne transmettez que des identifiants : recopier des valeurs de soumission dans la table des tâches planifiées les y laisserait en clair.
Services utiles du conteneur
| Service | Usage |
|---|---|
FormRepositoryInterface, EntryRepositoryInterface | Recharger formulaire et soumission au moment de l’envoi |
EntryValueFormatter | pairs() rend les valeurs avec leur libellé, options résolues et champs sensibles déjà écartés |
MergeTagResolver | Résoudre {field:…} dans un gabarit |
ConditionalLogicEvaluator | Appliquer une condition d’envoi, comme les notifications |
OutboundDelivery | Effectuer l’appel : adresse contrôlée, délai borné, résultat consigné |
RetryPolicy | Décider d’une nouvelle tentative et de son délai |
Passer par EntryValueFormatter n’est pas un détail de confort : c’est lui qui
écarte les valeurs sensibles. Un mot de passe lu directement dans
$entry->values serait expédié au service tiers.
Consigner ses envois
OutboundDelivery écrit dans le journal visible sur le détail d’une soumission.
Indiquez votre propre canal, faute de quoi vos envois seraient imputés au webhook
générique :
$status = $container->make( OutboundDelivery::class )->send(
new OutboundRequest(
'mon-crm', // canal, pour distinguer vos envois
$entry_id,
$form_id,
$url,
'POST',
wp_json_encode( $payload ),
array( 'Content-Type' => 'application/json' )
),
$attempt
);
if ( RetryPolicy::should_retry( $status, $attempt ) ) {
// Replanifier avec RetryPolicy::delay( $attempt ).
}
RetryPolicy ne rejoue que les échecs temporaires : une panne réseau, une
indisponibilité, un quota atteint. Une requête refusée pour son contenu ne le
sera pas moins à la troisième tentative.
Ce qui s’étend sans toucher au cœur
Au-delà des champs, des passerelles et des intégrations, les filtres suivants couvrent le reste du plugin. Chacun est détaillé dans hooks-reference.md.
| Pour | Filtre |
|---|---|
| Servir ses propres routes REST | solis_forms_rest_controllers |
| Ajouter ou réécrire des notifications | solis_forms_notifications |
| Retoucher un courriel avant envoi (copie, pièce jointe) | solis_forms_mail_args |
| Ouvrir ou fermer un formulaire selon sa propre règle | solis_forms_form_availability |
| Refuser une soumission que le cœur admettait | solis_forms_can_submit |
| Ajouter une colonne au tableau des soumissions | solis_forms_entry_columns, solis_forms_entry_column_value |
| Ajouter une action de ligne | solis_forms_entry_row_actions |
| Proposer des modèles de formulaires | solis_forms_form_templates |
| Brancher un comportement public | window.solisForms.registerModule() |
| Ajouter un panneau au constructeur | solisForms.formSettingsPanels, avec window.solisForms.builder |
| Remplacer une section du cœur | solisForms.formSettingsSections |
| Ajouter un réglage propre à un type de champ | solisForms.fieldInspectorControls |
| Refuser une soumission avant enregistrement | solis_forms_submission_errors |
| Traiter une action postée sur une soumission | solis_forms_entry_action |
| Ajouter une section au détail d’une soumission | solis_forms_entry_detail_sections |
| Purger ses données quand une soumission est effacée | solis_forms_entry_deleting |
| Ajouter un écran au menu | add_submenu_page() de WordPress, avec FormsListPage::SLUG en parent |
Deux d’entre eux imposent une limite délibérée : solis_forms_can_submit ne peut
que durcir l’admission d’une soumission, jamais l’assouplir, et une liste de
colonnes amputée de sa case à cocher est rejetée en bloc. Dans les deux cas, la
valeur du cœur reprend la main.
Repères
- Liste complète des hooks : hooks-reference.md
- Organisation du code :
architecture.md - Découpage gratuit / payant :
go-live/decoupage-gratuit-payant.md
