Cartes et QR codes¶
Ce document décrit le versant technique de la génération d'une carte de visite numérique : quels services interviennent, dans quel ordre, et quels fichiers ils produisent sur le disque. Pour les parcours métier (création par un particulier, commande d'entreprise, scan d'une carte), voir architecture.md.
Chaîne de génération¶
Deux points d'entrée créent une BusinessCardAize et déclenchent le même enchaînement technique,
avec des variantes :
Création individuelle (CardController::createCard(), route /business-card/create) :
- Le formulaire
BusinessCardTypeest validé ; un éventuel CV est déposé sur le disque. - L'entité
BusinessCardAizeest persistée et flushée une première fois, pour obtenir un identifiant. - Si
publicHashest encore vide, il est calculé parhash_hmac('sha256', (string) $id, APP_SECRET)puis flushé. QRCodeService::generateQRCodeByHash()encode l'URL publique de la carte et écrit l'image sur le disque ; le nom de fichier retourné est affecté àqrCodePathet flushé.Doctrine::postPersistdéclencheBusinessCardCreationSubscriber, qui appelleBCardContactSyncService::syncContactWithBCard()pour reporter les coordonnées de la carte sur un contact existant partageant la même adresse e-mail.
Un échec de génération du QR code n'annule pas la création de la carte : seul un message
d'avertissement est affiché, et la carte reste sans qrCodePath.
Génération en masse pour une entreprise (BusinessCardGenerationService::generateBusinessCardsForOrder(),
appelée après le paiement d'une commande d'entreprise) : pour chaque ligne de commande portant un
employé, une transaction crée ou met à jour la BusinessCardAize de l'employé (méthode privée
createBusinessCardForEmployee()), qui suit le même schéma persist → hash → QR code que
ci-dessus, puis publie la carte via publishBusinessCard(), ce qui déclenche l'envoi de l'e-mail
de mise à disposition par BusinessCardEmailService.
Dans les deux cas, la personnalisation (logo, couleurs) est appliquée séparément via
LogoManagementService, et la suppression d'une carte purge ses fichiers associés via
UploaderService::deleteAssociatedFiles().
Génération de la carte¶
BusinessCardGenerationService— pilote la génération en masse des cartes d'une commande d'entreprise payée (generateBusinessCardsForOrder()), la publication d'une carte (publishBusinessCard(), qui refuse une carte déjà publiée ou un employé sans commande validée), le retour en brouillon (setAsDraft()) et les mises à jour partielles de carte (updateBusinessCard(), qui ne recopie que les clés présentes dans le tableau reçu). Une erreur sur un employé de la commande est journalisée et accumulée sans interrompre le traitement des autres lignes.DefaultProCardService— applique la règle métier de la carte physique offerte à la première commande d'un abonné Pro : il lit le modèle configuré dansSettings::defaultProCardet restreint le configurateur à ce seul modèle tant que l'utilisateur n'a commandé aucune carte physique. Il ne modifie rien en base — c'est un service de lecture pure.BusinessCardCreationSubscriber— abonné Doctrine surEvents::postPersist. Pour toute nouvelleBusinessCardAize, il délègue àBCardContactSyncService::syncContactWithBCard()la recherche d'un contact existant partageant l'adresse e-mail de la carte, afin de le mettre à jour automatiquement. Toute exception est journalisée sans être propagée.
QR code¶
QRCodeService s'appuie sur la bibliothèque endroid/qr-code (writers PNG, JPEG et SVG,
encodage UTF-8, libellé optionnel rendu en police NotoSans). Les images sont écrites dans le
répertoire désigné par le paramètre de conteneur qrcode_directory
(public/uploads/qrcodes, créé à la volée si absent), sous un nom rendu unique par uniqid() —
ce qui impose de supprimer explicitement l'ancien fichier lors d'une régénération.
Deux méthodes de génération existent :
generateQRCode()encode l'identifiant numérique de la carte ({baseUrl}/{id}/{prénom}/{nom}en repli, ou{baseUrl}/card/{entreprise}/{id}/{poste}si l'optionsimple_formatest activée et que les deux slugs sont disponibles).generateQRCodeByHash()— utilisée par les deux points d'entrée décrits plus haut — encode lepublicHashde la carte plutôt que sa clé primaire, afin de ne pas exposer l'identifiant numérique. Trois formes d'URL sont possibles selon les données disponibles, la plus fréquente étant{racine}/card/{entreprise}/{hash}/{poste}.
Taille et format par défaut : image de 300 px avec une marge de 10 px, format PNG,
libellé « SCAN ME » en 20 pt — ces valeurs sont surchageables via le tableau d'options.
downloadQRCode() sert le fichier existant en pièce jointe (aucune conversion de format n'est
réalisée : le paramètre format ne change que le nom proposé au téléchargement) et
deleteQRCode() supprime le fichier du disque, sans jamais lever d'erreur si le chemin est
null ou le fichier déjà absent.
Toute exception levée pendant une génération est interceptée, journalisée via error_log() et
convertie en retour null — ni QRCodeService::generateQRCode() ni generateQRCodeByHash() ne
propagent d'exception à l'appelant.
Rendu PDF¶
DomPdfService encapsule le moteur dompdf/dompdf pour convertir du HTML en PDF. Dans le
code actuel, il ne sert pas à produire un export PDF de la carte de visite elle-même, mais
exclusivement à générer les factures de paiement : admin/payment/invoice.html.twig (route
download_invoice, contrôleur admin/PaymentController) et
admin/pro_payment/invoice.html.twig (route admin_pro_payment_download_invoice, contrôleur
admin/ProPaymentController). Les deux contrôleurs assemblent les données de paiement, encodent
en base64 le logo BCard (et, pour la seconde facture, la première image de la carte commandée)
puis rendent le gabarit Twig en HTML avant de le transmettre à DomPdfService::generatePdfFile().
L'instance Dompdf est créée une fois dans le constructeur du service et réutilisée à chaque
appel : deux générations successives partagent donc le même moteur. À noter, en lisant
generatePdfFile() : un objet Options y est bien construit (activation des ressources
distantes, parseur HTML5, police par défaut, marges de 10 mm) mais n'est jamais transmis à
l'instance Dompdf — ces réglages restent donc sans effet sur le rendu produit. Une seconde
méthode, generateBinaryPdf(), existe pour un envoi en flux direct (Dompdf::stream()) mais
n'est appelée par aucun contrôleur au moment de la rédaction de ce document.
Téléversements et stockage¶
UploaderService centralise le dépôt sur disque des fichiers déposés par les utilisateurs (CV,
image de profil, photo de contact, brochures) et normalise leur nom : le nom d'origine est
translittéré par le slugger de Symfony, suffixé par un identifiant unique (uniqid()) et complété
par l'extension devinée d'après le type MIME réel du fichier — pas d'après l'extension fournie
par le client. Il expose aussi deleteAssociatedFiles(), qui purge du disque le CV, l'image de
profil, le QR code et les brochures d'activité d'une carte donnée, chaque suppression étant
précédée d'un test d'existence (un fichier déjà absent est ignoré silencieusement).
Les répertoires de destination sont résolus depuis des paramètres du conteneur
(config/services.yaml), tous situés sous public/ :
Répertoire (sur public/) |
Paramètre(s) de conteneur | Contenu |
|---|---|---|
cv |
cv_directory |
CV joints à une carte de visite |
profil_img |
profil_directory |
Image de profil de la carte de visite |
qrcodes |
(historique — voir note) | Ancien emplacement des QR codes |
design |
design_directory |
Fichiers de design déposés pour une commande de carte physique |
card |
image_directory |
Images des cartes du catalogue (back-office) |
profil_user |
(historique — voir note) | Ancien emplacement des photos de profil utilisateur |
brochure |
(historique — voir note) | Ancien emplacement des brochures |
uploads |
qrcode_directory, custom_logos_directory, profile_user_directory, brochure_directory, company_brochures_directory, company_logos_directory, contact_photo_directory |
Sous-répertoire fourre-tout : QR codes actuels, logos personnalisés, photos de profil utilisateur actuelles, brochures actuelles, etc. |
Ces huit répertoires sont tous ignorés par git (voir .gitignore, section dédiée au projet en
plus du bloc générique Symfony) : leur contenu n'est jamais versionné et disparaît à chaque
déploiement qui ne le préserve pas. En exploitation, cela signifie qu'ils doivent être sauvegardés
séparément et montés en volume persistant (au sens conteneur) plutôt que reconstruits depuis
le dépôt — sans quoi CV, logos, QR codes et images de cartes déjà générés sont perdus à chaque
redéploiement.
Logos d'entreprise¶
LogoManagementService gère le logo et les couleurs personnalisés d'une carte de visite
individuelle (et non le logo de l'entreprise elle-même, téléversé séparément par les contrôleurs
d'administration via company_logos_directory). L'accès à cette personnalisation est réservé aux
comptes disposant des fonctionnalités premium (User::hasAccessToPremiumFeatures()) :
canUseCustomLogo() et canUseCustomColors() reposent tous deux sur ce même critère.
uploadCustomLogo() enchaîne le contrôle des droits, la validation du fichier
(validateLogoFile() : taille maximale 2 Mo, type MIME dans
image/jpeg, image/jpg, image/png, image/gif, image/svg+xml, extension devinée cohérente),
la suppression physique de l'ancien logo s'il existe, la création du répertoire
custom_logos_directory si nécessaire, puis délègue le déplacement du fichier à
UploaderService::uploadFile(). Aucune écriture en base n'est faite par le service : le nom de
fichier retourné doit être affecté à l'entité et persisté par l'appelant.
Les couleurs personnalisées (setCustomColors()) sont validées au format strict #RRGGBB et
persistées uniquement en mémoire (pas de flush()). À défaut de couleurs personnalisées,
getDisplayColors() retombe sur des couleurs par défaut codées en dur
(#6A2C70 / #F39C12), et setCompanyColors() permet de reporter sur la carte les couleurs
définies au niveau de l'entreprise du propriétaire — sans contrôle de droits premium ni validation
de format, à la différence de setCustomColors().
Sérialisation¶
Deux services construisent à la main les tableaux exposés par l'API JSON :
BusinessCardSerializerService::serializeBusinessCard()aplatit les champs d'uneBusinessCardAize(identité, contact, réseaux sociaux, statut, hash public…), y joint la liste de ses activités (id,name,brochure) et, si la carte a un employé rattaché, un blocemployeeréduit àid/firstname/lastname, enrichi deemail/phone/position/departmentseulement si$includeEmployeeDetailsvauttrue. La clécompanyest volontairement dupliquée sous l'orthographe historiquecompagny, pour compatibilité ascendante.CardSerializerService::serializeCard()sérialise les cartes du catalogue produit (NFC) et n'expose que les options de couleur et d'impression actives — les options inactives sont filtrées avant sérialisation.
Ces deux services sont indépendants du composant Serializer de Symfony : ils ne prennent pas en
paramètre de « groupe » de sérialisation et retournent des tableaux associatifs prêts pour
json_encode(). Par ailleurs, l'entité BusinessCardAize déclare bien des attributs
#[Groups(['businessCard:read', 'businessCard:write'])] du composant Serializer sur la plupart de
ses propriétés, mais une recherche dans src/Controller ne montre aucun appel normalisant
l'entité avec ces groupes : au moment de la rédaction de ce document, ces annotations ne semblent
consommées par aucun endpoint, l'API s'appuyant à la place sur BusinessCardSerializerService.
Régénérer une carte existante¶
QRCodeService::regenerateQRCode() et regenerateQRCodeByHash() suivent le même schéma : le
fichier pointé par $oldQrCodePath est supprimé du disque s'il existe, puis la génération est
relancée (respectivement generateQRCode() ou generateQRCodeByHash()). La suppression a lieu
avant la nouvelle génération : si celle-ci échoue et retourne null, la carte se retrouve
temporairement sans image de QR code, jusqu'à une régénération réussie.
Côté contrôleur, CardController::regenerateQRCode() (route associée à la carte, voir
src/Controller/CardController.php) appelle regenerateQRCodeByHash() pour reconstruire le QR
code d'une carte déjà publiée, typiquement après une modification de l'entreprise ou du poste
affiché dans l'URL encodée.