Aller au contenu

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) :

  1. Le formulaire BusinessCardType est validé ; un éventuel CV est déposé sur le disque.
  2. L'entité BusinessCardAize est persistée et flushée une première fois, pour obtenir un identifiant.
  3. Si publicHash est encore vide, il est calculé par hash_hmac('sha256', (string) $id, APP_SECRET) puis flushé.
  4. QRCodeService::generateQRCodeByHash() encode l'URL publique de la carte et écrit l'image sur le disque ; le nom de fichier retourné est affecté à qrCodePath et flushé.
  5. Doctrine::postPersist déclenche BusinessCardCreationSubscriber, qui appelle BCardContactSyncService::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é dans Settings::defaultProCard et 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 sur Events::postPersist. Pour toute nouvelle BusinessCardAize, 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'option simple_format est activée et que les deux slugs sont disponibles).
  • generateQRCodeByHash() — utilisée par les deux points d'entrée décrits plus haut — encode le publicHash de 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'une BusinessCardAize (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 bloc employee réduit à id/firstname/lastname, enrichi de email/phone/position/department seulement si $includeEmployeeDetails vaut true. La clé company est volontairement dupliquée sous l'orthographe historique compagny, 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.