Le plugin complexe canonique. Parcouru de bout en bout.

Chaque concept couvert dans le reste de cette documentation — le flux boot-and-load, les quatre patterns de hooks, les hooks REGISTRY de layout, les migrations isolées par plugin, le flux de traduction indirect, les quatre états du cycle de vie, le pattern testsuite-par-plugin — est mis à l'épreuve dans storage/app/plugins/acelle/ai/. Le plugin livre huit modèles Eloquent, quatorze migrations, dix-huit locales, plus de soixante templates Blade, trois composants anonymes universels, tous les hooks d'injection de layout, et plus de cent tests Pest dans sa propre testsuite. Cette page est la recette de lecture — quoi regarder en premier, quoi regarder en dernier, et comment apprendre sans se perdre dans la plomberie de niveau production.

Pourquoi ce plugin est la référence canonique

La plupart des plugins en production sont petits. Un driver d'envoi est une classe plus un blade de connexion. Une passerelle de paiement est un service plus un contrôleur de redirection. Un simple module sidebar est un seul appel Hook::add. Aucun de ceux-là ne sollicite tout le plugin SDK — et les petits plugins ne sont pas le bon exercice de lecture pour un auteur qui essaie de comprendre jusqu'où un plugin peut raisonnablement grandir.

acelle/ai existe à l'autre extrémité du spectre. C'est un sous-système d'IA auto-contenu : un chatbox agent, des composants universels de réécriture de texte, des personas coach ancrés sur une KB et un dashboard d'observabilité admin. Activer le plugin dans une installation AcelleMail ajoute toute la surface d'IA sans toucher au code du cœur ; le désactiver retire tout proprement. Le plugin utilise chaque concept couvert dans le reste de cette documentation, en production. Le lire est le moyen le plus rapide de voir comment les patterns se combinent quand la fonctionnalité n'est pas triviale.

L'arborescence en un coup d'œil

Tiré du README propre au plugin :

DossierCe qu'il possède
src/AIHandler/Moteur d'exécution IA — engines, boucle d'agent, tools, résolveur de settings, writer/reader d'observabilité, lookup KB, désinfectant d'URL.
src/Models/Huit modèles Eloquent pour le substrat d'audit (AIConversation, AIMessage, AIRequest, AIToolCall, AIFeedback, AIRawBlob, AIDailyRollup, AIToolUndoRecord).
src/Controllers/Contrôleurs admin (/rui/admin/ai-*) + contrôleurs d'API publique (/api/v1/ai/*) + le PluginDashboardController de landing du plugin.
src/Services/PluginStatusReport, AISettingsService, AutomationService, AIObservabilityPolicy, etc.
src/ServiceProvider.phpUnique point d'entrée — enregistre PSR-4, routes, vues, fichiers de langue, hooks, alias de middleware, listeners de cycle de vie.
database/migrations/Quatorze migrations de substrat d'audit. Exécutées à l'activation, rollback à la suppression.
resources/views/Plus de soixante templates Blade admin + trois composants anonymes universels (<x-mc-ai-chatbox>, <x-mc-ai-rewrite>, <x-mc-ai-subject-ab-generator>) + partials JS chatbox / sparkle.
resources/assets/CSS (~14 fichiers) + JS (~21 fichiers) publiés dans public/plugins/acelle/ai/ via vendor:publish --tag=plugin --force à l'installation du plugin.
resources/lang/Dix-huit locales × neuf fichiers de langue = toute la surface du module IA traduite.
tests/Plus de cent tests Pest (Feature + Unit) + la classe de base Acelle\Ai\Tests\PluginTestCase.
routes.phpRoutes du plugin (admin + API publique + le dashboard de landing du plugin à /plugins/acelle/ai/dashboard).
composer.jsonMétadonnées du plugin ; extra.setting-route pointe vers PluginDashboardController@index pour que le bouton "Settings" de la page admin Plugins deep-linke directement dans le dashboard propre au plugin.

Aucun de ces dossiers n'est sur mesure. Chacun correspond directement à une section dans le reste de cette documentation. Lire le plugin est un processus de reconnaissance du même pattern livré à grande échelle.

Huit modèles Eloquent — le substrat d'audit

La couche données du module IA est façonnée autour de l'auditabilité : chaque conversation, chaque invocation de modèle, chaque appel d'outil, chaque retour utilisateur, et chaque blob de sortie brute du prestataire est capturé pour le replay et l'observabilité. Huit modèles couvrent ce substrat :

ModèleTableCe qu'il représente
AIConversationai_conversationsUne ligne par session multi-tour agent / support. Porte les FK customer + user, la clé de tâche, la route d'écran, et les totaux roll-up de tokens / coût.
AIMessageai_messagesUne ligne par tour user / agent. Rôle, contenu JSON, FK vers un tool-call, latence, modèle utilisé.
AIRequestai_requestsUne ligne par appel d'API en amont. Engine, hash de prompt, latence, coût, état d'erreur. Pont entre AIMessage et le trafic HTTP réel.
AIToolCallai_tool_callsInvocations function-call générées par un tour d'agent. Nom de l'outil, JSON input/output, flag de source.
AIFeedbackai_feedbackPouces haut/bas + retour texte libre par message et par conversation.
AIRawBlobai_raw_blobsRéponses brutes originales du prestataire, conservées pour replay / audit. Table séparée car la table de roll-up doit rester petite.
AIDailyRollupai_daily_rollupAgrégat par jour pour le dashboard d'observabilité admin — totaux de tokens, coût, taux d'erreur. Pré-agrégé pour que le dashboard lise à faible coût.
AIToolUndoRecordai_tool_undo_recordsTrace les actions d'outil réversibles pour la fonctionnalité "annuler la dernière".

Trois patterns de cette liste se traduisent directement dans d'autres plugins. Séparer "réponses brutes du prestataire" dans une table distincte du "résumé roll-up" permet à la table de roll-up de rester suffisamment petite pour être scannée. Des FK nullables vers customers et users permettent à la même ligne de fonctionner pour le trafic authentifié et anonyme. Des rollups un-par-jour donnent au dashboard admin des lectures à faible coût sans un JOIN lourd contre les tables d'activité.

Quatorze migrations en une ligne chacune

Les noms de fichiers de migration dans storage/app/plugins/acelle/ai/database/migrations/ racontent leur propre histoire — additifs dans le temps, immédiatement réversibles, jamais un mouvement de schéma destructif :

Nom de fichierCe qu'il fait
2026_04_28_000001_create_ai_conversations_table.phpSessions de chat multi-tour — uid, FK customer_id, enum de statut, rollups tokens / coût
2026_04_28_000002_create_ai_messages_table.phpTour user / agent unique — rôle, contenu JSON, FK tool-call, latence, modèle utilisé
2026_04_28_000003_create_ai_requests_table.phpUne ligne par appel d'API en amont — engine, hash de prompt, latence, coût, erreur
2026_04_28_000004_create_ai_tool_calls_table.phpInvocations function-call générées par un tour d'agent — JSON input / output
2026_04_28_000005_create_ai_feedback_table.phpPouces haut/bas + retour texte libre par message et par conversation
2026_04_28_000006_create_ai_raw_blobs_table.phpRéponses brutes originales du prestataire, conservées pour replay / audit
2026_04_28_000007_create_ai_daily_rollup_table.phpAgrégat par jour pour le dashboard admin — totaux de tokens, coût, taux d'erreur
2026_04_29_000001_add_client_message_id_to_ai_messages.phpColonne de dédup cross-tab — additive, sans valeurs par défaut
2026_04_30_000002_add_source_to_ai_tool_calls.phpTrace si un appel d'outil vient de la route agent vs support
2026_05_02_180000_widen_ai_conversations_client_session_uid.phpCorrectif de largeur ULID / UUID — migration qui modifie une colonne, entièrement réversible
2026_05_02_200000_create_ai_tool_undo_records_table.phpTrace les actions d'outil réversibles pour la fonctionnalité "annuler la dernière"
2026_05_03_000001_add_url_sanitization_to_ai_requests.phpAjoute une colonne JSON pour la télémétrie d'URL désinfectée
2026_05_04_000001_create_ai_settings_table.phpParamètres admin au niveau du plugin — gardés séparément de plugins.data pour que chaque ligne puisse être indexée

Lisez les migrations de haut en bas et vous avez l'évolution de schéma de tout le module IA. Chaque migration additive est un ship de fonctionnalité — la forme additive est ce qui fait que le rollback delete_plugin_* du plugin fonctionne toujours, même quand un admin désinstalle après un an d'accrétion de fonctionnalités.

Chaque hook utilisé par le plugin

Le ServiceProvider du plugin sollicite chaque pattern de hook sauf FILTER. Un grep de Hook::* contre storage/app/plugins/acelle/ai/src/ServiceProvider.php :

REGISTRY (Hook::add) — six contributions

  • Neuf entrées add_translation_file dans register() lignes 175-197 — une par surface de traduction (rewrite, chatbox, prompts, wait, subject AB, settings, admin usage, audit, permissions). Chaque surface est un fichier éditable séparément dans l'UI admin Languages.
  • layout.head.assets dans boot() ligne 688 — contribue le CSS + JS du chatbox à chaque page qui étend les layouts master app / admin.
  • layout.body.before_close dans boot() ligne 699 — contribue le HTML de la bulle chatbox et le popover sparkle avant le </body> de chaque page.
  • admin.sidebar.groups dans boot() ligne 718 — ajoute le groupe AI avec ses trois ou quatre liens enfants à la sidebar admin.

Les six se gardent eux-mêmes avec aiPluginAvailable() — un helper qui se résout finalement à Plugin::getByName('acelle/ai')->isActive(). Renvoyer null quand le plugin est désactivé est la manière conventionnelle de disparaître proprement sans décharger le service provider.

EVENT (Hook::on) — deux listeners de cycle de vie

  • activate_plugin_acelle/ai ligne 472 — exécute artisan migrate contre le dossier de migrations du plugin.
  • delete_plugin_acelle/ai ligne 546 — accepte le flag $keepData, fait un rollback des migrations quand il n'est pas défini, préserve les tables d'audit quand il l'est.

BEHAVIOR (Hook::set) — un override d'URL d'icône

  • icon_url_acelle/ai ligne 522 — override le hook BEHAVIOR par-plugin pour que la page admin Plugins rende l'icône propre au module IA au lieu de l'plugin.svg par défaut de l'hôte.

Ensemble, ceux-ci constituent l'essentiel de la surface de hooks du plugin. Lire ServiceProvider.php de haut en bas est le moyen le plus rapide de voir comment les patterns se combinent en production.

La surface UI du chatbox

Le plugin contribue trois composants Blade universels dans resources/views/components/ — aucun d'entre eux n'exige que l'application hôte sache qu'ils existent :

  • <x-mc-ai-chatbox> — la bulle chatbox flottante qui s'ouvre en une conversation agent multi-tour. Montée via le hook REGISTRY layout.body.before_close pour apparaître sur chaque page app + admin.
  • <x-mc-ai-rewrite> — affordance universelle "réécrire ce texte" qui peut être placée à côté de n'importe quel textarea de l'hôte. Namespacée par le plugin, aucun enregistrement central.
  • <x-mc-ai-subject-ab-generator> — génère des variantes de ligne de sujet A/B à partir d'un prompt. Utilisée dans l'éditeur de campagne.

Ces trois composants montrent le pattern pour "un plugin contribue à l'UI de plusieurs pages hôte sans forker chaque page" : livrez le composant comme un composant Blade anonyme sous le namespace de vue de votre plugin, enregistrez-le via les hooks REGISTRY de layout pour un montage global, ou laissez les pages hôte opter pour son inclusion directe. Les deux patterns fonctionnent ; le plugin IA utilise les deux.

Neuf fichiers × dix-huit locales

Le register() du plugin enregistre neuf fichiers de traduction séparés. La machinerie Language::dump() matérialise ensuite chacun en dix-sept dossiers runtime non-anglais sous storage/app/data/plugins/acelle/ai/lang/. Le résultat sur disque : 153 fichiers de traduction runtime (9 fichiers × 17 locales non-anglaises + 9 originaux anglais = 162 moins les 9 masters anglais = 153 dump-clones), chacun éditable séparément via l'UI admin Languages de l'hôte.

Les neuf fichiers de surface (enregistrés via la boucle $aiLangFiles dans ServiceProvider::register()) :

  • ai_rewrite — le composant universel de réécriture de texte
  • ai_chatbox — l'UI du chatbox
  • ai_chatbox_prompts — prompts prédéfinis affichés dans le chatbox
  • ai_chatbox_wait — l'UI smart-wait ("recherche en cours… exécution d'un outil… composition de la réponse")
  • ai_subject_ab — le générateur de sujet A/B
  • ai_settings — libellés de la page admin settings
  • admin_ai_usage — dashboard admin d'usage / coût
  • admin_ai_audit — UI admin d'audit / replay
  • admin_ai_permissions — toggles admin de permission par fonctionnalité

Ce découpage est la réponse pratique à "comment garder les fichiers de traduction suffisamment petits pour qu'un traducteur puisse en éditer un en une seule séance" : un master par surface logique, enregistré séparément, matérialisé par locale, édité indépendamment via l'UI admin.

Fichiers de config possédés par le plugin

Le plugin possède deux fichiers de config sous config/ — enregistrés dans ServiceProvider::boot() via $this->mergeConfigFrom() et accessibles via les helpers standards config('ai.*') et config('ai-navigation-hints.*'). Une config possédée par le plugin est le bon endroit pour les métadonnées statiques qui ne changent pas par installation (catalogue d'engines, templates de prompts, défauts de navigation) ; les paramètres éditables par admin vivent dans la table ai_settings seedée par migration.

Le découpage — config pour les défauts immuables livrés par le plugin, lignes DB pour les paramètres mutables éditables par admin — est un pattern qui se transpose proprement à d'autres plugins. Mettre les deux dans la colonne JSON plugins.data est tentant mais punit les performances de l'UI admin ; une table indexée dédiée a été le bon choix.

L'infrastructure de tests

Le répertoire de tests du plugin suit la forme du tests/ de l'hôte — dossiers Unit + Feature plus une classe de base PluginTestCase à la racine :

storage/app/plugins/acelle/ai/tests/
├── PluginTestCase.php          ← seeds the plugin row as active before every test
├── Feature/
│   ├── AIHandler/              ← engines, agent loop, tools, observability writer
│   └── PluginLifecycle/        ← lifecycle integration tests
├── Unit/                       ← isolated unit tests (no Laravel boot for some)
├── Fixtures/                   ← test fixtures + factories
├── Snapshots/                  ← Pest snapshot artefacts
└── Support/                    ← test-only helpers

Le phpunit.xml de l'hôte enregistre la testsuite du plugin comme <testsuite name="Plugin: acelle/ai">. ./vendor/bin/pest --testsuite="Plugin: acelle/ai" exécute la suite complète en parallèle avec les suites Unit + Feature propres à l'hôte.

Le PluginTestCase à la racine montre le piège du reset de cache de gate par requête que chaque plugin livrant un middleware devrait adopter — voir la Tests § piège du cache de gate pour le pattern complet. Sans lui, le deuxième test de la suite et les suivants observent un état de cache obsolète provenant du boot du premier test.

Comment apprendre à partir de cela

Lire chaque ligne d'un plugin de cent mille tokens n'est pas le bon exercice. Une recette en quatre étapes est plus utile :

  1. Clonez ou symlinkez le plugin dans une installation hôte. Activez-le. Ouvrez la sidebar admin — le groupe AI doit apparaître. Cliquez sur "Settings" sur l'entrée de la page admin Plugins — le dashboard de landing du plugin doit se charger. Cela prouve que la surface UI du plugin est câblée dans votre hôte local.
  2. Lisez src/ServiceProvider.php de haut en bas. Quarante minutes. Chaque concept couvert dans Architecture des plugins + Système de hooks + Injection d'UI + Traductions apparaît dans ce seul fichier à l'échelle de la production. Faites des références croisées avec les pages deep-dive au fur et à mesure.
  3. Tracez une fonctionnalité de bout en bout. Choisissez la bulle chatbox. Trouvez le point d'entrée (hook layout.body.before_close dans ServiceProvider), suivez le partial qu'il renvoie (ai::partials.body_assets), trouvez le composant anonyme qui rend la bulle, trouvez le JS qui le monte, trouvez la route API que le JS appelle, trouvez le contrôleur, suivez-le jusqu'au moteur d'exécution AIHandler, regardez les lignes AIConversation + AIMessage + AIRequest être insérées. Deux à trois heures, de bout en bout. Après cela, vous saurez quels patterns le plugin IA utilise et lesquels il n'utilise pas.
  4. Adaptez un pattern à votre propre plugin. Choisissez le plus petit qui s'applique — habituellement la boucle d'enregistrement par surface de traduction, ou le groupe sidebar admin, ou l'approche table de rollup par jour. Retirez le code spécifique à l'IA ; gardez le pattern structurel. C'est le gain de productivité du premier jour pour l'auteur de plugin.

Où aller ensuite

C'est le dernier des onze deep-dives développeurs. À partir d'ici, le hub d'index est la récap naturelle : Index de la documentation montre chaque page du cluster organisée en Fondation / Construction / Qualité / Référence. La landing développeur est le point d'entrée à forme marketing pour les nouveaux visiteurs arrivant depuis la recherche.

Deux étapes pratiques suivantes quand vous êtes prêt à livrer un vrai plugin : le cycle activer → tester → supprimer du deep-dive Tests prouve que le listener de hook delete_plugin_* du plugin nettoie correctement. Architecture des plugins § Récupération depuis un état cassé est la page à mettre en favori pour les soucis d'exécution en production — trois modes d'échec plus le chemin de correctif exact pour chacun.

Au-delà des patterns spécifiques à AcelleMail, l'écosystème Laravel plus large s'applique. Le code du plugin est du Laravel vanille ; le loader d'exécution de l'hôte est la seule pièce non standard. Tout ce que vous savez sur Eloquent, Blade, Pest, les queues, le scheduling ou le middleware fonctionne dans le dossier du plugin sans changement.