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,hybridouall).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
| Hook | Type | Paramètres |
|---|---|---|
init | action | — (toutes les extensions sont chargées) |
request | action | $path (avant le routage : idéal pour maintenance, redirections) |
head, footer | action | — |
template, template.vars | filtre | nom du template / variables |
page_output | filtre | HTML final de la page |
block.render | filtre | $html, $block, $props |
content.saved, content.deleted | action | contenu |
seo.head, seo.schema, seo.robots_txt, seo.sitemap_entries, seo.llms_txt | filtre | — |
shop.payment_methods | filtre | tableau de moyens de paiement (label, description, process(callable $order): ?string url) |
shop.shipping_methods, shop.price, shop.totals, shop.checkout_errors | filtre | — |
shop.order_created, shop.order_status | action | commande / id, statut, ancien statut |
auth.login, auth.registered | action | utilisateur |
admin.menu, admin.dashboard.widgets | filtre | — |
mail.before_send | filtre | ['to','subject','html'] (retourner false pour bloquer, ou prendre en charge l’envoi) |
post.after_content | action | article |
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
| Hook | Arguments |
|---|---|
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_meta | panier 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.actions | zones d’affichage du thème |
admin.product.type_panels | panneau du formulaire produit selon le type |
content.trashed, content.restored, comment.created, review.created | cycle de vie des contenus |
block.conditions (filtre) | conditions d’affichage personnalisées des blocs |
emails.register | Emails::register('cle', [...]) : modèle d’e-mail modifiable par l’utilisateur |
media.path_changed | une 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
| Hook | Type | Paramètres | |
|---|---|---|---|
invoice.pdf | filtre | (\Mozaiq\Pdf $pdf, array $facture) — compléter le PDF d’une facture ou d’un avoir (ex. Factur-X) | |
content.protected_html | filtre | `($html, $contenu, 'api' | 'feed')` — remplacer un contenu réservé dans l’API REST et le flux RSS |
accounts.enabled | filtre | false — renvoyer true si votre extension gère les comptes clients sans boutique (lien « Mon compte » conservé dans les menus) | |
builder.condition_fields | filtre | champs de condition d’affichage supplémentaires (key, label, type, options), évalués via block.conditions | |
stripe.subscription_status | action | ($abonnement, $statut) | |
admin.head, admin.footer | action | aussi 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.jsonsont créées avant l’appel du callbackactivate: il peut y insérer des données initiales. - Les tests d’une extension active (
plugins/<slug>/tests/*Test.php) sont exécutés parphp 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' => trueest 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.