mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 16:07:10 +00:00
418 lines
23 KiB
PHP
418 lines
23 KiB
PHP
<?php
|
|
/**
|
|
* ASPIC — consignes_fonctions.php
|
|
*
|
|
* Bibliothèque des fonctions du catalogue ASP_Fonctions (chantier des
|
|
* consignes, 6 septembre 2026 — voir Réflexions stratégiques). Chaque
|
|
* fonction ci-dessous porte le nom exact enregistré dans
|
|
* ASP_Fonctions.FON_NomPHP, et est appelée dynamiquement par le moteur
|
|
* générique (voir moteur_consignes.php) — plus aucun appel en dur dans
|
|
* contacts.php depuis la version du 6 septembre 2026 (soir).
|
|
*
|
|
* Convention pour écrire une nouvelle fonction ici :
|
|
* - Traitement à UN champ : signature (string $valeur): string — reçoit la
|
|
* valeur actuelle du champ, retourne la valeur transformée.
|
|
* - Test à UN champ : signature (string $valeur): bool — retourne vrai si la
|
|
* valeur respecte la consigne, faux sinon (le message affiché à l'échec
|
|
* vient de CNS_MessageErreur en base, jamais de cette fonction).
|
|
* - Traitement/Test à PLUSIEURS champs (extension du 7 septembre 2026, voir
|
|
* moteur_consignes.php) : quand CNS_Champ contient plusieurs noms de champs
|
|
* séparés par une virgule, la fonction reçoit un tableau associatif
|
|
* [nom_du_champ => valeur] au lieu d'une simple chaîne — un Traitement
|
|
* retourne alors ce même type de tableau (valeurs mises à jour, mêmes clés),
|
|
* un Test retourne toujours un simple bool. Voir
|
|
* consigne_completer_code_postal() ci-dessous pour un exemple.
|
|
* - Test à UN champ avec accès base (8 septembre 2026) : une fonction de Test
|
|
* qui a besoin d'interroger la base (ex. vérifier qu'une valeur n'est pas
|
|
* déjà utilisée ailleurs) peut déclarer deux paramètres supplémentaires,
|
|
* dans cet ordre : (string $valeur, PDO $pdo, ?int $idExclusion): bool — le
|
|
* moteur les fournit systématiquement à tout Test à un champ (voir
|
|
* moteur_consignes.php), $idExclusion étant l'identifiant de la fiche en
|
|
* cours de modification (null en Création). Voir consigne_mail_unique() et
|
|
* consigne_portable_unique() ci-dessous, premier cas réel.
|
|
* - Jumeau "_seraitDeclenchee" (7 septembre 2026, soir) : toute fonction de
|
|
* Traitement qui interroge un service externe (IA) et porte
|
|
* FON_VisuelAttenteIA = 'O' DOIT avoir, en plus, une fonction nommée
|
|
* "<nom_de_la_fonction>_seraitDeclenchee", signature (array $valeurs): bool
|
|
* — répond "est-ce que j'agirais sur ces valeurs", sans jamais rien faire
|
|
* d'autre (aucun appel réseau). La vraie fonction appelle ce jumeau EN
|
|
* PREMIER (voir consigne_completer_code_postal() ci-dessous) : la condition
|
|
* de déclenchement n'existe ainsi qu'à un seul endroit, jamais recopiée ni
|
|
* devinée ailleurs (pas même dans ce fichier) — c'est ce jumeau
|
|
* qu'interroge consignes_apercu.php pour piloter le badge IA générique côté
|
|
* écran, voir ce fichier et moteur_consignes.php
|
|
* (auMoinsUneConsigneIaSeraitDeclenchee()).
|
|
* Fichier volontairement sans dépendance à la session : chaque fonction ne
|
|
* fait que transformer ou tester une ou plusieurs valeurs qu'on lui passe (et,
|
|
* depuis le 8 septembre 2026, éventuellement interroger $pdo — voir ci-dessus
|
|
* — mais jamais $_SESSION). Deux exceptions à la dépendance base/réseau :
|
|
* - Depuis le 7 septembre 2026 (soir) : une fonction de Traitement peut
|
|
* interroger un service externe (API d'IA, voir
|
|
* consigne_completer_code_postal()) quand la consigne elle-même l'exige
|
|
* explicitement, toujours avec un repli silencieux (valeurs inchangées) en
|
|
* cas d'échec : un Traitement ne doit jamais faire échouer un
|
|
* enregistrement à cause d'un service tiers indisponible.
|
|
* - Depuis le 8 septembre 2026 : une fonction de Test à un champ peut
|
|
* interroger $pdo, reçu directement du moteur (voir "Test à UN champ avec
|
|
* accès base" ci-dessus) — jamais de connexion ouverte ici, toujours celle
|
|
* déjà établie par l'appelant (db.php).
|
|
*/
|
|
|
|
require __DIR__ . '/moteur_prompts.php';
|
|
|
|
// ASP_Fonctions.FON_Nom = 'MAJUSCULE' (Nature=Traitement)
|
|
// Met en majuscules la valeur d'un champ, en respectant les caractères
|
|
// accentués français (mb_strtoupper avec encodage explicite : un simple
|
|
// strtoupper() laisserait "é"/"è"/... inchangés selon la locale du serveur).
|
|
function consigne_majuscule(string $valeur): string
|
|
{
|
|
return mb_strtoupper($valeur, 'UTF-8');
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'NON_VIDE' (Nature=Test)
|
|
// Vrai si la valeur n'est pas vide une fois débarrassée des espaces
|
|
// superflus en début/fin (une chaîne de simples espaces est considérée vide).
|
|
function consigne_non_vide(string $valeur): bool
|
|
{
|
|
return trim($valeur) !== '';
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'PREMIERE_LETTRE_MAJUSCULE' (Nature=Traitement)
|
|
// "georges" -> "Georges", "JOSEPH" -> "Joseph", "jean-marc" -> "Jean-Marc".
|
|
// Tout le reste en minuscules, une majuscule après le début de la chaîne et
|
|
// après chaque tiret ou espace (pour les prénoms composés). Volontairement
|
|
// pas de ucwords() : cette fonction native de PHP n'est pas fiable en UTF-8
|
|
// (elle raisonne en octets, pas en caractères — un prénom commençant par une
|
|
// lettre accentuée type "Éric" serait corrompu).
|
|
function consigne_premiere_lettre_majuscule(string $valeur): string
|
|
{
|
|
$valeur = mb_strtolower($valeur, 'UTF-8');
|
|
return (string)preg_replace_callback(
|
|
'/(^|[\s\-])(\p{L})/u',
|
|
function (array $m): string {
|
|
return $m[1] . mb_strtoupper($m[2], 'UTF-8');
|
|
},
|
|
$valeur
|
|
);
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'MAIL_VALIDE' (Nature=Test), 7 septembre 2026.
|
|
// Vrai si la valeur ressemble à une adresse mail syntaxiquement correcte,
|
|
// faux sinon — une simple cohérence de saisie ("@" présent, domaine
|
|
// plausible), jamais une vérification que la boîte existe réellement.
|
|
// Champ facultatif : une valeur vide est considérée conforme ici (le
|
|
// caractère obligatoire ou non du champ reste la responsabilité d'une
|
|
// consigne NON_VIDE séparée, combinable avec celle-ci sur le même champ).
|
|
//
|
|
// Volontairement PAS de regex écrite à la main du style "@ suivi de deux
|
|
// groupes de lettres séparés par un point" : une adresse valide peut avoir
|
|
// un domaine à plus de deux niveaux (ex. toto@tata.ndd.com), un chiffre, un
|
|
// tiret... filter_var(FILTER_VALIDATE_EMAIL) est le validateur natif de PHP,
|
|
// déjà utilisé ailleurs dans ASPIC pour la même vérification (voir
|
|
// prospects_core.php, contrôle sur PSP_Mail) — le reprendre ici garde une
|
|
// seule et même définition de "adresse mail valide" dans toute l'application
|
|
// plutôt que deux logiques différentes qui pourraient diverger.
|
|
function consigne_mail_valide(string $valeur): bool
|
|
{
|
|
$valeur = trim($valeur);
|
|
if ($valeur === '') return true;
|
|
return filter_var($valeur, FILTER_VALIDATE_EMAIL) !== false;
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'RETIRER_POINTS' (Nature=Traitement), 8 septembre
|
|
// 2026. Retire tous les points (".") d'une valeur, sans autre transformation
|
|
// — demandée pour un numéro de téléphone saisi avec des points comme
|
|
// séparateurs (ex. "02.98.12.34.56" -> "0298123456"). Générique et
|
|
// réutilisable tel quel sur n'importe quel champ texte, même principe que
|
|
// SANS_CHIFFRE ci-dessus : aucune fonction par champ, juste une ligne dans
|
|
// ASP_Consignes par champ concerné (CNT_Tel et/ou CNT_Portable). Volontairement
|
|
// limitée aux points, sur la demande telle que formulée par Jean-Marc — à
|
|
// étendre (espaces, tirets...) le jour où un besoin réel et concret se
|
|
// présentera, pas par anticipation.
|
|
function consigne_retirer_points(string $valeur): string
|
|
{
|
|
return str_replace('.', '', $valeur);
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'SANS_CHIFFRE' (Nature=Test), 7 septembre 2026.
|
|
// Vrai si la valeur ne contient aucun chiffre, faux sinon — champ facultatif
|
|
// (une valeur vide est conforme, même principe que consigne_mail_valide() :
|
|
// le caractère obligatoire est la responsabilité d'une consigne NON_VIDE
|
|
// séparée, combinable sur le même champ). Générique et réutilisable tel
|
|
// quel sur n'importe quel champ texte : Jean-Marc l'a demandée pour Nom,
|
|
// Prénom et Ville, ce qui ne demande ici AUCUNE fonction par champ — juste
|
|
// trois lignes dans ASP_Consignes (CNT_Nom, CNT_Prenom, CNT_AdrVille)
|
|
// pointant toutes vers cette même fonction, chacune avec son propre
|
|
// CNS_MessageErreur si on veut un message différent par champ.
|
|
function consigne_sans_chiffre(string $valeur): bool
|
|
{
|
|
return trim($valeur) === '' || !preg_match('/\d/', $valeur);
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'CP_DEPUIS_VILLE' (Nature=Traitement, CNS_Champ =
|
|
// "CNT_AdrVille,CNT_AdrCP"), 7 septembre 2026 (soir).
|
|
// Première consigne "qui va chercher sa réponse avec l'IA" (demande de
|
|
// Jean-Marc) : complète le code postal à partir de la ville quand celui-ci
|
|
// n'a pas été saisi. Un champ seul ne suffisait pas ici (il faut la ville en
|
|
// entrée pour produire le code postal en sortie) — voir l'extension du
|
|
// moteur à plusieurs champs dans moteur_consignes.php.
|
|
//
|
|
// Ne fait RIEN (retourne les valeurs inchangées) si le code postal est déjà
|
|
// renseigné — jamais d'écrasement d'une saisie existante, même douteuse —
|
|
// ou si la ville est vide (rien à déduire). Sinon interroge le même modèle
|
|
// que contact_ia_interprete.php (OPENAI_API_KEY, voir ce fichier pour le
|
|
// détail de l'intégration) avec une sortie structurée stricte : le modèle
|
|
// renvoie soit un code postal à 5 chiffres qu'il connaît avec une confiance
|
|
// raisonnable, soit null s'il n'est pas sûr — jamais un code inventé, même
|
|
// principe de prudence que consigne_mail_valide() ci-dessus (mieux vaut
|
|
// laisser le champ vide qu'afficher une fausse information). Une ville qui
|
|
// couvre plusieurs codes postaux sans qu'aucun ne soit clairement principal
|
|
// (ex. arrondissements de Paris/Lyon/Marseille) doit renvoyer null plutôt que
|
|
// de deviner — c'est écrit explicitement dans la consigne envoyée au modèle.
|
|
//
|
|
// Toute défaillance (clé absente, réseau, quota, réponse inattendue) laisse
|
|
// toujours CNT_AdrCP inchangé — un Traitement ne doit jamais bloquer
|
|
// l'enregistrement d'un contact à cause d'un service externe (voir
|
|
// commentaire d'en-tête de moteur_consignes.php) — mais n'est PLUS silencieuse
|
|
// pour autant depuis le 8 septembre 2026 (correctif demandé par Jean-Marc,
|
|
// constat d'un échec resté invisible sur une ville inconnue) : chaque cas
|
|
// d'échec pose désormais un "_avertissement" pour l'écran, et un
|
|
// "_realisation" quand un appel a réellement été tenté (voir le corps de la
|
|
// fonction ci-dessous) — contrairement
|
|
// à un Test, dont l'échec est lui affiché à l'utilisateur.
|
|
// Jumeau "_seraitDeclenchee" (voir en-tête de fichier) : réplique à elle
|
|
// seule la condition de déclenchement de consigne_completer_code_postal()
|
|
// ci-dessous, sans jamais faire le moindre appel réseau. C'est la SEULE
|
|
// fonction que consignes_apercu.php interroge pour savoir si le badge IA doit
|
|
// apparaître — la vraie fonction l'appelle elle-même en premier, pour que
|
|
// cette condition n'existe jamais qu'à cet unique endroit.
|
|
function consigne_completer_code_postal_seraitDeclenchee(array $valeurs): bool
|
|
{
|
|
$ville = trim((string)($valeurs['CNT_AdrVille'] ?? ''));
|
|
$codePostal = trim((string)($valeurs['CNT_AdrCP'] ?? ''));
|
|
|
|
return $codePostal === '' && $ville !== '';
|
|
}
|
|
|
|
function consigne_completer_code_postal(array $valeurs): array
|
|
{
|
|
if (!consigne_completer_code_postal_seraitDeclenchee($valeurs)) {
|
|
return $valeurs;
|
|
}
|
|
|
|
$ville = trim((string)($valeurs['CNT_AdrVille'] ?? ''));
|
|
|
|
$apiKey = trim((string) getenv('OPENAI_API_KEY'));
|
|
if (!$apiKey) {
|
|
// Panne de configuration (8 septembre 2026) : jusqu'ici un retour
|
|
// totalement silencieux — repéré par Jean-Marc comme le même défaut
|
|
// que celui déjà corrigé le 7 septembre pour une ville ambiguë
|
|
// ("un constat d'échec doit toujours être visible, jamais un simple
|
|
// silence"). Pas de "_realisation" ici : aucun appel réseau n'a
|
|
// réellement eu lieu, donc rien à journaliser comme un coût engagé
|
|
// (voir en-tête de moteur_consignes.php) — seulement l'avertissement
|
|
// écran, pour que l'utilisateur ne reste pas devant un champ vide
|
|
// sans explication.
|
|
$valeurs['_avertissement'] = "Code postal non déterminé pour « $ville » "
|
|
. "(service indisponible). Saisissez le code postal vous-même.";
|
|
return $valeurs;
|
|
}
|
|
|
|
// Prompt sorti du code le 11 septembre 2026 — chargé ici (pas reçu de
|
|
// l'appelant : cette fonction, comme les autres Traitements à un/plusieurs
|
|
// champs, ne reçoit pas $pdo par contrat, voir en-tête de fichier) via sa
|
|
// propre connexion, comme le ferait n'importe quel autre endpoint. Un
|
|
// échec ici est traité comme la clé API absente ci-dessus : aucun appel
|
|
// réseau OpenAI n'a encore été tenté, donc pas de "_realisation" à poser,
|
|
// seulement l'avertissement écran.
|
|
try {
|
|
$promptSysteme = chargerPromptSysteme(getDbConnection(), 'CONTACT_IA_CODE_POSTAL');
|
|
} catch (Throwable $e) {
|
|
$valeurs['_avertissement'] = "Code postal non déterminé pour « $ville » "
|
|
. "(prompt système indisponible). Saisissez le code postal vous-même.";
|
|
return $valeurs;
|
|
}
|
|
|
|
$schema = [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'code_postal' => [
|
|
'type' => ['string', 'null'],
|
|
'description' => "Code postal français à 5 chiffres correspondant à la ville donnée, uniquement si connu avec une confiance raisonnable. null si la ville est ambiguë, inconnue, introuvable en France, ou couvre plusieurs codes postaux sans qu'un seul soit clairement le plus probable.",
|
|
],
|
|
],
|
|
'required' => ['code_postal'],
|
|
'additionalProperties' => false,
|
|
];
|
|
|
|
$corpsRequete = [
|
|
'model' => 'gpt-5.6-luna', // même modèle que contact_ia_interprete.php, voir ce fichier.
|
|
'messages' => [
|
|
['role' => 'system', 'content' => $promptSysteme],
|
|
['role' => 'user', 'content' => $ville],
|
|
],
|
|
'response_format' => [
|
|
'type' => 'json_schema',
|
|
'json_schema' => ['name' => 'code_postal_ville', 'strict' => true, 'schema' => $schema],
|
|
],
|
|
];
|
|
|
|
$ch = curl_init('https://api.openai.com/v1/chat/completions');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode($corpsRequete),
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $apiKey,
|
|
],
|
|
// Appelée en synchrone pendant l'enregistrement d'un contact (choix
|
|
// de Jean-Marc, 7 septembre 2026 : "oui, tel quel" malgré le coût/la
|
|
// latence) : délai volontairement court, on préfère abandonner
|
|
// (valeurs inchangées) plutôt que de faire attendre longtemps sur un
|
|
// enrichissement facultatif.
|
|
CURLOPT_TIMEOUT => 8,
|
|
CURLOPT_CONNECTTIMEOUT => 5,
|
|
]);
|
|
$reponseBrute = curl_exec($ch);
|
|
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($reponseBrute === false || $codeHttp !== 200) {
|
|
// Échec réseau/HTTP (8 septembre 2026) : contrairement au cas
|
|
// "clé API absente" ci-dessus, un appel a bien été tenté (et peut
|
|
// avoir un coût, selon où l'échec survient côté fournisseur) — donc
|
|
// journalisé comme le cas "ville ambiguë" plus bas, sur le même
|
|
// principe posé le 7 septembre par Jean-Marc ("un appel fait à l'IA
|
|
// [doit laisser] une trace dans les réalisations").
|
|
$valeurs['_realisation'] = [
|
|
'action' => 'IA_CP',
|
|
'commentaire' => "Échec technique de l'appel IA pour la ville « $ville » (HTTP $codeHttp).",
|
|
];
|
|
$valeurs['_avertissement'] = "Code postal non déterminé pour « $ville » "
|
|
. "(service temporairement indisponible). Saisissez le code postal vous-même.";
|
|
return $valeurs;
|
|
}
|
|
|
|
$reponseDecodee = json_decode($reponseBrute, true);
|
|
$contenuJson = $reponseDecodee['choices'][0]['message']['content'] ?? null;
|
|
$extraction = $contenuJson ? json_decode($contenuJson, true) : null;
|
|
$codePostalDeduit = $extraction['code_postal'] ?? null;
|
|
|
|
// Garde-fou défensif (même principe qu'ailleurs dans ASPIC : jamais
|
|
// confiance aveugle en un service externe) : un code postal français est
|
|
// exactement 5 chiffres, sinon la réponse est ignorée.
|
|
if (is_string($codePostalDeduit) && preg_match('/^\d{5}$/', $codePostalDeduit)) {
|
|
$valeurs['CNT_AdrCP'] = $codePostalDeduit;
|
|
// Journalisation (7 septembre 2026, soir, demande de Jean-Marc après
|
|
// son premier essai réel — "indispensable" pour pouvoir relier une
|
|
// ligne de coût IA à sa cause) : clé réservée "_realisation", voir
|
|
// moteur_consignes.php pour le mécanisme générique. Pas de champ
|
|
// 'agent' ici (contrairement au tout premier jet, qui inventait "Mat"
|
|
// en dur) : Jean-Marc a fait remarquer, à raison, qu'ASP_Consignes
|
|
// porte déjà CNS_Agent — c'est le moteur (executerTraitements(), voir
|
|
// moteur_consignes.php) qui l'y lit et l'attribue à la réalisation,
|
|
// jamais cette fonction. ACT_Code 'IA_CP' à créer à la main dans
|
|
// ASP_Actions (table gérée à la main, voir realisations.php) avant le
|
|
// premier passage réel en production.
|
|
$valeurs['_realisation'] = [
|
|
'action' => 'IA_CP',
|
|
'commentaire' => "Code postal $codePostalDeduit déduit de la ville « $ville » par l'IA.",
|
|
];
|
|
} else {
|
|
// Appel IA fait, coût réellement engagé, mais sans réponse
|
|
// exploitable — ex. Paris/Lyon/Marseille, découpées en
|
|
// arrondissements sans code postal unique, pour lesquelles le modèle
|
|
// renvoie sciemment null plutôt que de deviner (voir le prompt
|
|
// système ci-dessus). Repéré par Jean-Marc (7 septembre 2026, nuit,
|
|
// test sur "Paris") : "il y a un appel fait à l'IA et pas de trace
|
|
// dans les réalisations, ce qui n'est pas normal" — un appel qui ne
|
|
// trouve rien coûte quand même des tokens, il doit donc apparaître
|
|
// au même titre qu'un appel réussi. Même ACT_Code 'IA_CP', un
|
|
// commentaire différent suffit à distinguer les deux cas dans
|
|
// l'historique, sans demander à Jean-Marc de créer un deuxième code
|
|
// dans ASP_Actions.
|
|
$valeurs['_realisation'] = [
|
|
'action' => 'IA_CP',
|
|
'commentaire' => "Aucun code postal déduit pour la ville « $ville » (ambiguë ou inconnue pour l'IA).",
|
|
];
|
|
// '_avertissement' (nouvelle clé réservée, voir moteur_consignes.php) :
|
|
// contrairement à '_realisation' (historique consulté a posteriori),
|
|
// ce message est affiché IMMÉDIATEMENT à l'écran — sans lui,
|
|
// l'utilisateur ne voit rien du tout et ne sait pas que l'IA a bien
|
|
// réfléchi puis sciemment renoncé, plutôt qu'un simple silence.
|
|
// Message volontairement impératif (7 septembre 2026, demande de
|
|
// Jean-Marc) : le toast qui l'affiche reste à l'écran tant que
|
|
// l'utilisateur n'a pas cliqué pour le refermer (voir
|
|
// liste_contacts.html/afficherToast()), l'instruction doit donc être
|
|
// sans ambiguïté sur ce qu'il reste à faire.
|
|
$valeurs['_avertissement'] =
|
|
"Code postal non déterminé pour « $ville » (ville ambiguë ou "
|
|
. "avec plusieurs codes postaux possibles). Saisissez le code "
|
|
. "postal vous-même.";
|
|
}
|
|
|
|
return $valeurs;
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'MAIL_UNIQUE' (Nature=Test), 8 septembre 2026.
|
|
// Vrai si l'adresse mail n'est pas déjà utilisée par un AUTRE contact actif,
|
|
// faux sinon. Champ facultatif, même principe que consigne_mail_valide()
|
|
// ci-dessus : une valeur vide est toujours conforme (deux fiches sans mail ne
|
|
// sont pas en conflit) — le caractère obligatoire reste la responsabilité
|
|
// d'une consigne NON_VIDE séparée, combinable sur le même champ.
|
|
//
|
|
// Fonction dédiée à CNT_Mail plutôt qu'un test générique "valeur unique"
|
|
// paramétrable par nom de champ (demande explicite de Jean-Marc, 8 septembre
|
|
// 2026) : contrairement à SANS_CHIFFRE ci-dessus, volontairement générique
|
|
// parce que ça a du sens sur Nom/Prénom/Ville indifféremment, une contrainte
|
|
// d'unicité n'a de sens QUE sur certains champs précis — l'ouvrir à un
|
|
// paramétrage libre depuis ASP_Consignes exposerait un futur opérateur du
|
|
// catalogue à l'appliquer par erreur à un champ comme Prénom, où deux
|
|
// contacts homonymes sont parfaitement normaux. Voir consigne_portable_unique()
|
|
// ci-dessous, jumelle sur CNT_Portable — toutes deux appuyées sur le même
|
|
// utilitaire privé valeurDejaUtilisee() pour ne pas dupliquer la requête SQL.
|
|
function consigne_mail_unique(string $valeur, PDO $pdo, ?int $idExclusion = null): bool
|
|
{
|
|
return !valeurDejaUtilisee($pdo, 'CNT_Mail', $valeur, $idExclusion);
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'PORTABLE_UNIQUE' (Nature=Test), 8 septembre 2026.
|
|
// Même principe que consigne_mail_unique() ci-dessus, appliqué à
|
|
// CNT_Portable — fonction séparée plutôt qu'un simple paramétrage de la même
|
|
// fonction sur un autre champ, pour la même raison (voir commentaire de
|
|
// consigne_mail_unique()).
|
|
function consigne_portable_unique(string $valeur, PDO $pdo, ?int $idExclusion = null): bool
|
|
{
|
|
return !valeurDejaUtilisee($pdo, 'CNT_Portable', $valeur, $idExclusion);
|
|
}
|
|
|
|
// Utilitaire privé partagé par consigne_mail_unique() et
|
|
// consigne_portable_unique() ci-dessus — pas une fonction du catalogue
|
|
// (aucun ASP_Fonctions.FON_NomPHP ne pointe dessus, jamais appelée depuis
|
|
// ASP_Consignes) : factorise uniquement la requête SQL commune aux deux.
|
|
// $champ vient toujours d'une constante écrite en dur dans l'une des deux
|
|
// fonctions ci-dessus, jamais d'une donnée utilisateur — pas de risque
|
|
// d'injection SQL malgré l'interpolation dans le nom de colonne.
|
|
// $idExclusion (identifiant du contact en cours de modification, voir
|
|
// moteur_consignes.php) permet à une fiche de conserver sa propre valeur sans
|
|
// se bloquer elle-même ; toujours null en Création, où rien n'existe encore à
|
|
// exclure.
|
|
function valeurDejaUtilisee(PDO $pdo, string $champ, string $valeur, ?int $idExclusion): bool
|
|
{
|
|
$valeur = trim($valeur);
|
|
if ($valeur === '') return false;
|
|
|
|
$sql = "SELECT 1 FROM ASP_Contacts WHERE $champ = ? AND CNT_Actif = 1";
|
|
$params = [$valeur];
|
|
if ($idExclusion !== null) {
|
|
$sql .= ' AND CNT_Incr != ?';
|
|
$params[] = $idExclusion;
|
|
}
|
|
$sql .= ' LIMIT 1';
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
return (bool)$stmt->fetchColumn();
|
|
}
|