Aller au contenu

Développer une extension Mozaiq

Une extension est un dossier plugins/<slug>/ contenant au minimum :

plugins/mon-extension/
├── plugin.json     # manifeste (obligatoire)
├── plugin.php      # code chargé à chaque requête quand l’extension est active
└── assets/         # CSS/JS/images (servis publiquement)

Générer un squelette complet : php bin/mozaiq plugin:new mon-extension

plugin.json

{
  "slug": "mon-extension",
  "name": "Mon extension",
  "version": "1.0.0",
  "description": "Ce que fait l’extension.",
  "author": "Vous",
  "icon": "plug",
  "requires": "1.0.0",
  "requires_php": "8.1",
  "scope": ["site", "shop", "hybrid"],
  "depends": [],
  "main": "plugin.php",
  "settings": [
    {"key": "api_key", "label": "Clé API", "type": "text", "help": "…"},
    {"key": "enabled", "label": "Activer", "type": "toggle", "default": true}
  ],
  "tables": {
    "mon_table": {
      "columns": {"id": "INT UNSIGNED NOT NULL AUTO_INCREMENT", "label": "VARCHAR(191) NOT NULL DEFAULT ''", "created_at": "DATETIME NULL"},
      "primary": "id",
      "keys": ["KEY label (label)"]
    }
  },
  "drop_tables_on_uninstall": false,
  "marketplace": {"category": "outils", "price": 0, "changelog": "…"}
}
  • scope : types de site compatibles (site, shop, hybrid ou all).
  • settings : un formulaire de réglages est généré automatiquement (types : text, textarea, email, url, number, password, select + options, toggle, color, image, code, richtext, datetime-local). Lecture : plugin_setting('mon-extension', 'api_key').
  • tables : créées/mises à jour automatiquement à l’activation (ajout de colonnes sans perte de données). Les noms sont préfixés : utilisez {mon_table} dans vos requêtes.

API disponible dans plugin.php

// Hooks
add_action('head', fn () => print '<meta …>');          // <head> du site
add_action('footer', fn () => print '<script>…</script>');
add_filter('seo.schema', fn (array $graph) => $graph);   // JSON-LD

// Bloc pour le page builder
register_block('mon-bloc', [
  'label' => 'Mon bloc', 'icon' => 'sparkles', 'category' => 'Extensions',
  'fields' => [['key' => 'title', 'label' => 'Titre', 'type' => 'text']],
  'defaults' => ['title' => 'Bonjour'],
  'render' => fn (array $p) => '<h2>' . e($p['title']) . '</h2>',   // ou 'template' => __DIR__.'/blocks/mon-bloc.php'
]);

// Route publique
add_route('GET', '/mon-extension/{id:\d+}', fn ($id) => \Mozaiq\Response::json(['id' => $id]));
add_route('POST', '/mon-extension/envoyer', function () { verify_csrf(); /* … */ });

// Page d’administration (menu)
add_admin_page(['slug' => 'mon-extension', 'title' => 'Mon extension', 'icon' => 'plug',
  'parent' => null /* ou 'shop', 'seo', 'plugins'… */, 'cap' => 'manage_settings',
  'callback' => fn () => '<div class="card">…</div>']);

// Base de données
db()->all('SELECT * FROM {mon_table} WHERE label = ?', ['x']);
db()->insert('mon_table', ['label' => 'x', 'created_at' => now()]);
db()->update('mon_table', ['label' => 'y'], ['id' => 3]);

// Divers
option('site_name'); update_option('ma_cle', [...]);
\Mozaiq\Mailer::send($to, $sujet, $html);
current_user(); can('manage_shop'); url('/contact'); e($texte);

// Cycle de vie (facultatif) : valeur de retour du fichier
return ['activate' => fn () => null, 'deactivate' => fn () => null, 'uninstall' => fn () => null];

Hooks principaux

HookTypeParamètres
initaction— (toutes les extensions sont chargées)
requestaction$path (avant le routage : idéal pour maintenance, redirections)
head, footeraction—
template, template.varsfiltrenom du template / variables
page_outputfiltreHTML final de la page
block.renderfiltre$html, $block, $props
content.saved, content.deletedactioncontenu
seo.head, seo.schema, seo.robots_txt, seo.sitemap_entries, seo.llms_txtfiltre—
shop.payment_methodsfiltretableau de moyens de paiement (label, description, process(callable $order): ?string url)
shop.shipping_methods, shop.price, shop.totals, shop.checkout_errorsfiltre—
shop.order_created, shop.order_statusactioncommande / id, statut, ancien statut
auth.login, auth.registeredactionutilisateur
admin.menu, admin.dashboard.widgetsfiltre—
mail.before_sendfiltre['to','subject','html'] (retourner false pour bloquer, ou prendre en charge l’envoi)
post.after_contentactionarticle

Transporteurs (API Livraison)

use Mozaiq\Modules\Shipping;
// Déclare des services dont le prix est calculé au poids et par zone depuis les réglages de l'extension :
// {key}_enabled, {key}_name, {key}_rates_FR|OM1|OM2|EU|WORLD ("poids_max:prix" par ligne), free_over, handling_fee, packaging_weight
Shipping::services('mon-transporteur', 'moncarrier', [
    ['key' => 'express', 'name' => 'Express 24 h', 'zones' => ['FR'], 'relay' => false, 'locator' => ''],
]);
add_filter('shop.carriers', fn ($c) => $c + ['moncarrier' => ['name' => 'Mon transporteur', 'tracking' => 'https://suivi.exemple/{code}']]);
add_filter('shop.order_exports', fn ($e) => $e + ['moncarrier-csv' => ['label' => 'Export CSV', 'callback' => fn (array $orders) => Shipping::csv([...], [...], 'export.csv')]]);
add_filter('admin.order.panels', fn ($panels, $order) => [...$panels, ['title' => 'Étiquette', 'html' => '…']], 10);

Un mode de livraison peut aussi être ajouté « à la main » via le filtre shop.shipping_methods avec une clé calc => fn(array $ctx): ?float ($ctx : subtotal, discount, weight, country, count ; retourner null = indisponible).

Autres hooks utiles

forms.submitted (formulaire, données, id de réponse) · backup.created, backup.restored · shop.order_tracking · blocks.style_fields (ajouter des réglages de style à tous les blocs).

Nouveautés 1.3

Types de contenu et champs

register_content_type('recette', [
    'label' => 'Recettes', 'singular' => 'Recette', 'icon' => 'book', 'rewrite' => 'recettes', 'archive' => true,
    'taxonomy' => ['slug' => 'recette_cat', 'label' => 'Catégories de recettes'],
    'fields' => [['key' => 'duree', 'label' => 'Durée (min)', 'type' => 'number'], ['key' => 'niveau', 'label' => 'Niveau', 'type' => 'select', 'options' => "facile : Facile\nexpert : Expert"]],
]);
// Dans un gabarit : field('duree') (valeur brute) ou the_field('duree') (HTML formaté)

Types de champs : text, textarea, richtext, number, select, toggle, image, url, email, date, color.

Événements supplémentaires

HookArguments
shop.product_types (filtre)types de produit (['label', 'virtual']) — ex. cartes cadeaux, abonnements
shop.totals (filtre)totaux du panier ; ajoutez des deductions (moyens de paiement comme une carte cadeau)
shop.cart_saved, shop.cart_item_metapanier enregistré ; données personnalisées d’une ligne
shop.refund (filtre)($result, $order, $amount, $credit) — remboursement chez le prestataire de paiement
shop.checkout.after_totals, shop.cart.after_totals, shop.account.panels, shop.product.before_add_to_cart, shop.product_card.actionszones d’affichage du thème
admin.product.type_panelspanneau du formulaire produit selon le type
content.trashed, content.restored, comment.created, review.createdcycle de vie des contenus
block.conditions (filtre)conditions d’affichage personnalisées des blocs
emails.registerEmails::register('cle', [...]) : modèle d’e-mail modifiable par l’utilisateur
media.path_changedune image a été retouchée (nouveau chemin)

Tâches planifiées, e-mails, HTTP

schedule_task('mon_extension_sync', 3600, fn () => 'OK', 'Synchronisation horaire');
\Mozaiq\Emails::send('mon_modele', $email, ['prenom' => 'Léa']);
$r = http_request('POST', 'https://api.exemple.com', ['json' => ['a' => 1], 'headers' => ['Authorization: Bearer x']]);
\Mozaiq\Cache::remember('cle', 600, fn () => calcul_couteux());

Traductions et tests

Enveloppez les textes affichés dans __('Mon texte') : ils apparaissent dans Réglages › Langues › Textes de l’interface. Ajoutez vos tests dans tests/*Test.php (tableau « nom » ⇒ function (Mozaiq\Testing $t)), exécutés par php bin/mozaiq test.

Nouveautés 1.4

Hooks

HookTypeParamètres
invoice.pdffiltre(\Mozaiq\Pdf $pdf, array $facture) — compléter le PDF d’une facture ou d’un avoir (ex. Factur-X)
content.protected_htmlfiltre`($html, $contenu, 'api''feed')` — remplacer un contenu réservé dans l’API REST et le flux RSS
accounts.enabledfiltrefalse — renvoyer true si votre extension gère les comptes clients sans boutique (lien « Mon compte » conservé dans les menus)
builder.condition_fieldsfiltrechamps de condition d’affichage supplémentaires (key, label, type, options), évalués via block.conditions
stripe.subscription_statusaction($abonnement, $statut)
admin.head, admin.footeractionaussi exécutés dans le page builder

PDF : polices intégrées, pièces jointes, PDF/A-3

$pdf->embedFonts($ttfRegulier, $ttfGras)            // polices TrueType (sous-ensemble automatique)
    ->attach('donnees.xml', $xml, 'text/xml', 'Data', 'Description')
    ->pdfa(['title' => 'Facture FA-2026-00001', 'author' => 'Ma société', 'xmp' => '…']);

Divers

  • Les tables déclarées dans plugin.json sont créées avant l’appel du callback activate : il peut y insérer des données initiales.
  • Les tests d’une extension active (plugins/<slug>/tests/*Test.php) sont exécutés par php bin/mozaiq test.
  • Un gabarit d’extension passé à View::page() peut être surchargé par un thème portant le même nom de fichier : préfixez vos gabarits (ex. views/rdv-page.php).
  • Un champ personnalisé avec 'hidden' => true est modifiable dans l’éditeur mais n’apparaît pas dans la liste publique des champs.

Publier sur la marketplace

php bin/mozaiq plugin:pack mon-extension               # dist/mon-extension-1.0.0.zip
php bin/mozaiq plugin:publish mon-extension --token=… # envoi via l’API développeur

Incrémentez version à chaque publication : les sites reçoivent la mise à jour dans Extensions.