reference/socle-symfony.md

Socle Symfony (multisite, CMS, contact, i18n)

Bundle Symfony réutilisable fournissant des briques transverses (multisite, CMS Markdown, formulaire de contact, i18n, réglages, upload, back-office extensible) qu'une application hÎte assemble et configure. Le composant n'est pas une application : il n'a ni entité utilisateur, ni firewall, ni route front publique, ni page d'accueil propre. Il fournit des contrats, des entités, des services et des points d'extension.

  • Type de paquet : symfony-bundle, namespace PSR-4 Tlr\Symfony\ → src/.
  • Cible : PHP moderne (sĂ©rie 8.x), Symfony 8.x, Doctrine ORM 3.x.
  • Distribution par VCS Git (pas de dĂ©pĂŽt public de paquets).

PérimÚtre : ce que le socle fournit / délÚgue

Fourni par le socle Délégué à l'application hÎte
Entités CMS/site/réglages (auto-mappées) Migrations Doctrine (aucune livrée)
Services d'intégration (résolution site, rendu, upload
) Entité User + firewall/sécurité (ROLE_ADMIN)
Contrats (interfaces) à implémenter Routes front (accueil, contact, docs, sitemap)
Extensions et globales Twig Templates de base (html.twig, cms/index.html.twig)
ContrĂŽleurs back-office admin/config et admin/pages Envoi de mail, traductions, assets (Bootstrap/Turbo/Stimulus)

Architecture d'exĂ©cution (chaĂźne par requĂȘte)

Sur l'Ă©vĂ©nement noyau REQUEST (requĂȘte principale uniquement), trois abonnĂ©s d'Ă©vĂ©nements interviennent dans un ordre de prioritĂ© contrĂŽlĂ© :

Priorité Abonné RÎle
64 SiteSubscriber RĂ©sout le Site courant, l'inscrit dans SiteContext et l'attribut de requĂȘte site
(routeur) RouterListener Symfony Résout la route
20 LocaleSubscriber Synchronise la locale URL ↔ session (aprùs le routeur)
16 SiteAccessSubscriber Cloisonne les sites vitrine (aprĂšs le routeur, avant le firewall)

SiteContext est un service partagĂ© portant l'Ă©tat « site courant » (setSite/getSite/hasSite) sur le postulat « une requĂȘte = un process ». Il est consommĂ© par les contrĂŽleurs, le rendu CMS et l'extension Twig qui expose la variable globale site.

Configuration sémantique du bundle

NƓud racine tlr_symfony :

Clé Type Défaut RÎle
multisite booléen true Opt-in multisite. false = mode single-site (site primaire sans matching host, sans cloisonnement)
vitrine_allowed_routes liste de scalaires [] Liste blanche des noms de routes autorisés sur un site vitrine (sans préfixe localized_)

ParamĂštres du noyau applicatif repris automatiquement par injection nommĂ©e : kernel.enabled_locales → $enabledLocales, kernel.default_locale → $defaultLocale. Une variable d'environnement optionnelle (URI par dĂ©faut) alimente le seed de site (repli sur host local si absente).

Le bundle est un micro-bundle (config sémantique + extension inline). Sa méthode build() enregistre deux tags d'autoconfiguration (ConfigSectionInterface, BoPageInterface) : tout service implémentant ces interfaces est collecté automatiquement, sans cùblage.

Multisite

Entité Site (table site)

Représente un domaine servi. Champs observables :

Champ Type RĂŽle
host string(255), unique Host canonique (sans schéma ni port)
aliases json nullable Hosts supplémentaires résolvant vers ce site
slug string(64), unique Identifiant court stable (^[a-z0-9-]+$)
label string(128) Libellé d'affichage BO
name string(80) nullable Nom applicatif (fallback : host)
localeDefault string(8), défaut fr Locale par défaut du site
layoutTemplate string(128), défaut cms/index.html.twig Template que les pages CMS étendent
theme string(64) nullable Clé de thÚme (sélection CSS)
brandDomain / brandTld nullable Décomposition de marque
contactEmail / senderEmail string(255) nullable, validés Email Destinataire / expéditeur des mails du domaine
homeSlug string(255) nullable Slug de la page CMS servie en /
ogImageDefault string(512) nullable Image Open Graph par défaut
enabled bool, défaut true Site actif
isPrimary bool, défaut false Site de repli (un seul attendu)
createdAt / updatedAt datetime_immutable Horodatage (lifecycle callbacks)

Contraintes d'unicité sur host et slug. Méthode matchesHost(string): bool : teste host canonique et alias, insensible à la casse.

Résolution et cloisonnement

  • SiteResolver::resolve(string $host): ?Site : normalise, parcourt les sites actifs, retourne le premier matchesHost(), sinon primary(). Cache mĂ©moire (un chargement par requĂȘte via SiteRepository::findAllEnabled(), triĂ©s isPrimary DESC, label ASC).
  • SiteResolver::primary(): ?Site : premier site isPrimary, sinon premier site actif.
  • Cloisonnement vitrine (SiteAccessSubscriber) : sur un site vitrine (non primaire) dont le host matche rĂ©ellement, toute route applicative absente de vitrine_allowed_routes (prĂ©fixe localized_ retirĂ©) renvoie 404. IgnorĂ© en mode single-site, pour le site primaire, pour les routes internes (prĂ©fixe _) et si aucune route n'est matchĂ©e. Le site primaire garde l'application entiĂšre.

Garantie d'au moins un site

Commande idempotente tlr:site:ensure-default (options --host, --label, --slug) : crĂ©e un Site primaire par dĂ©faut seulement si aucun n'existe ; ne touche jamais les sites gĂ©rĂ©s par l'app/BO. À jouer au dĂ©ploiement aprĂšs les migrations (le resolver exige ≄ 1 site, faute de quoi l'app renvoie 404 partout).

Moteur CMS

Entité CmsContent (table cms_content)

Page éditoriale en Markdown, rattachée obligatoirement à un Site. Slug unique par site (contrainte sur site_id, slug), pas globalement.

Champ Type RĂŽle
site ManyToOne Site, non null Domaine propriétaire
title string(255) Titre
slug string(255), ^[a-z0-9-]+$ Segment d'URL ; auto-dérivé du titre si absent
excerpt text nullable Résumé (méta description / OG)
markdown text, non vide Corps en Markdown
status string(20) draft | published | archived (défaut draft)
publishedAt datetime nullable Posée au premier publish()
updatedAt datetime MAJ auto (lifecycle)
visibility string(20) public | protected | private (défaut public)
type string(50), défaut page Type libre ; valeur spéciale block
position int, défaut 0 Ordre de nav (tri ASC)
noindex bool, défaut false Exclut de l'indexation (robots + sitemap)
ogImage string(512) nullable Image OG (défaut = celle du site)
layout string(50) nullable Clé de mise en page (default | three-col)
author ManyToOne CmsUserInterface, non null Auteur (résolu par l'app)
tags ManyToMany CmsTag Étiquettes
images OneToMany CmsImage Images rattachées

Constantes exposées : STATUSES, VISIBILITIES, LAYOUTS, TYPE_BLOCK. Helpers : isPublished(), isPublic(), publish().

Statut (cycle de vie) et visibilité (portée d'accÚs) sont deux axes indépendants. Le socle ne fait pas respecter la visibilité protected/private : CmsContentRepository::findPublished() filtre le statut publié et exclut les blocs, mais l'arbitrage d'accÚs reste au contrÎleur applicatif.

Blocs de chrome (type = block)

Fragment Ă©ditable de chrome (nav, footer, bandeau) : jamais servi comme page, jamais indexĂ©, exclu des requĂȘtes pages/sitemap/nav. Rendu dans le layout via la fonction Twig site_block(slug).

CmsTag (table cms_tag)

Vocabulaire de classement partagé entre sites (non scopé). name + slug unique (auto-dérivé). Le tag doc a un rÎle : findDocNavBySite() liste les pages taguées doc d'un site, publiées, triées par position ASC, pour bùtir une nav de documentation.

CmsImage (table cms_image)

Métadonnées d'un fichier uploadé : filename, mime, size, width, height, checksum (dédup via findOneByChecksum), createdAt. Rattachée à un owner (CmsUserInterface, non null), un site nullable (onDelete SET NULL, bibliothÚque média par domaine), optionnellement un CmsContent. Expose getSlug(): ?string retournant null (contrat attendu par l'uploader).

Édition et rendu

  • CmsContentType (formulaire BO) : expose site, titre, slug, excerpt, markdown, statut, visibilitĂ©, type, position, noindex, ogImage, layout, tags. L'auteur est posĂ© cĂŽtĂ© contrĂŽleur (utilisateur courant). Le socle ne fournit pas de contrĂŽleur CMS : l'app cĂąble le CRUD front et BO.
  • Layouts fournis (templates/cms/layouts/) : default.html.twig (Ă©tend site.layoutTemplate, remplit mĂ©tas SEO/OG, rend body_html) et three-col.html.twig (mise en page docs Ă  3 colonnes ; rĂ©fĂ©rence un contrĂŽleur et une route de doc cĂŽtĂ© application).

Contact & anti-spam

ContactDTO

Objet de transport (data_class du formulaire). Validation :

Champ Contraintes
name NotBlank, Length 3–200
email NotBlank, Email
message NotBlank, Length 10–2000

ContactType (formulaire)

Champs name, email, message (textarea avec compteur via contrÎleur Stimulus character-counter), send. Libellés = clés de traduction (catalogue fourni par l'app). Option max_length (défaut 2000) pilotant la contrainte, l'attribut HTML maxlength et le compteur.

Honeypot anti-bot : champ website non mappĂ©, masquĂ© (tabindex -1, aria-hidden, autocomplete off). Le socle dĂ©finit le champ mais ne vĂ©rifie pas qu'il est vide — la dĂ©tection est laissĂ©e Ă  l'app.

Contrat anti-spam

interface ContactSpamAnalyzerInterface {
    public function analyze(string $name, string $email, string $message): SpamAnalysisResult;
    public function isEnabled(): bool;
}

SpamAnalysisResult (readonly) : bool $isSpam, string $reason, bool $apiError = false. Le socle définit le contrat mais ne fournit aucune implémentation (l'app branche son analyseur, potentiellement distant).

Internationalisation (i18n)

Mécanique de locale basée sur préfixe d'URL /{_locale}/
, la locale par défaut restant sans préfixe.

  • Convention de routage : locale par dĂ©faut = route « nue » ; autres locales = route prĂ©fixĂ©e localized_ avec paramĂštre _locale. Convention structurante : le retrait du prĂ©fixe localized_ conditionne aussi le cloisonnement vitrine.
  • LocaleSubscriber (aprĂšs le routeur) : si l'URL porte une locale activĂ©e, la mĂ©morise en session ; sinon applique la locale de session ou la locale par dĂ©faut. Ne fait rien sans session prĂ©alable.
  • LocalizedUrlGenerator : generate($route, $params, $refType), getLocalizedRoute($route), getRouteParams($params).

Réglages applicatifs (Settings)

Magasin clé→valeur persistant. EntitĂ© AppSetting (table app_setting) : name string(128) clĂ© primaire, value text nullable.

Service AppSettings (readonly) :

Méthode Comportement
get(name, default = null): ?string Valeur ou défaut
set(name, value): void Upsert + flush immédiat
getBool(name, default = false): bool Vrai si valeur ∈ {1, true, yes, on}
setBool(name, value): void Stocke 1/0
getDateTime(name): ?DateTimeImmutable Parse, null si vide/invalide

Distinct du hub de configuration BO : AppSettings persiste les valeurs, le hub organise l'UI.

Markdown & upload de fichiers

MarkdownRenderer

toHtml(string $markdown, string $currentPath = '', bool $rewriteDocLinks = true): string. Pipeline CommonMark durci :

  • html_input => escape (HTML brut Ă©chappĂ©), allow_unsafe_links => false (liens dangereux neutralisĂ©s).
  • Extensions : cƓur CommonMark, GitHub-Flavored (tables, task lists, barrĂ©, autoliens), coloration syntaxique serveur, FrontMatter (retire le cartouche YAML du corps rendu).
  • Réécriture optionnelle des liens .md vers une route de viewer de doc (dĂ©faut docs.show, injectable), avec navigation AJAX ; ne traite que les chemins .md, rĂ©sout les chemins relatifs en mĂ©moire (sans accĂšs disque). À dĂ©sactiver hors viewer (le rendu CMS l'appelle avec false).

FileUploader

upload(object $entity, UploadedFile $file): ?string — chemin relatif stockĂ© ou null si refus. Garde-fous :

  • Limite de taille (plafond en Mo ; au-delĂ , refus + log).
  • Validation MIME rĂ©elle via finfo (pas le MIME dĂ©clarĂ©) ; liste blanche image (jpeg, png, webp, gif).
  • Nommage : slug (entity->getSlug() ou nom d'origine) + identifiant unique + extension devinĂ©e.
  • Rangement sous le rĂ©pertoire public, dans un sous-dossier dĂ©rivĂ© du nom court de la classe. RĂ©pertoire d'upload surchargeable par config service.

delete(string $fileName): bool — protection anti-traversal : retire les ../, rĂ©sout le chemin rĂ©el, vĂ©rifie qu'il reste sous le rĂ©pertoire public, sinon refuse + log ; idempotent (vrai si le fichier n'existe pas).

Contrat entitĂ© : toute entitĂ© passĂ©e Ă  upload() doit exposer getSlug(): ?string (null autorisĂ© → nom d'origine).

Back-office extensible

Deux registres à points d'extension. N'importe quel bundle/app ajoute une entrée en implémentant une interface auto-taggée, sans toucher la navigation ni les contrÎleurs.

Hub de configuration (/admin/config)

ConfigSectionInterface : key() (segment d'URL kebab-case), category() (groupe de nav), label(), icon(), priority() (tri croissant), handle(Request): Response (fragment HTML ou RedirectResponse pour PRG). ConfigSectionRegistry collecte les sections taggées, trie par priorité, groupe par catégorie (get(key), first(), groupedByCategory()). ConfigController (ROLE_ADMIN) rend le hub avec la section active dans un turbo-frame config.

Pages BO (/admin/pages)

BoPageInterface : key(), group(), label(), icon(), priority(), handle(Request): Response. BoPageRegistry collecte, trie, groupe par group(). BoPageController (ROLE_ADMIN) rend la page plein cadre.

Les deux contrÎleurs Admin portent ROLE_ADMIN et supposent un firewall applicatif : la sécurité HTTP n'est pas fournie par le socle.

Contrats publics (récapitulatif intégrateur)

Interfaces à implémenter

Interface Qui l'implémente Méthodes
CmsUserInterface l'entité User de l'app (via resolve_target_entities) getId(): ?int
ContactSpamAnalyzerInterface service app (optionnel) analyze(...), isEnabled()
ConfigSectionInterface services app/bundle (auto-taggés) key, category, label, icon, priority, handle
BoPageInterface services app/bundle (auto-taggés) key, group, label, icon, priority, handle

Auto-mapping Doctrine et résolution d'entité

Toutes les entités sont annotées par attributs et découvertes par Doctrine (auto-mapping ou mapping explicite du namespace Tlr\Symfony\Entity). Aucune migration livrée : l'app génÚre les siennes (tables site, cms_content, cms_image, cms_tag, app_setting). CmsContent.author et CmsImage.owner ciblent l'interface CmsUserInterface : l'app doit la résoudre vers son entité concrÚte, sinon le schéma Doctrine est invalide.

doctrine:
    orm:
        auto_mapping: true
        resolve_target_entities:
            Tlr\Symfony\Contract\CmsUserInterface: App\Entity\User

Services publics autowirables

SiteContext, SiteResolver, LocalizedUrlGenerator, MarkdownRenderer, FileUploader, AppSettings, ConfigSectionRegistry, BoPageRegistry, et les repositories (SiteRepository, CmsContentRepository, CmsImageRepository, CmsTagRepository, AppSettingRepository).

Extensions et globales Twig

Nom Type RĂŽle
site variable globale SiteContext::getSite() (peut ĂȘtre null)
site_block(slug) fonction Rend un bloc CMS publiĂ© (Markdown → HTML), repli chaĂźne vide
enabled_locales() / default_locale() fonctions Locales du noyau
localized_route(name) fonction Nom de route adapté à la locale courante
route_exists(name) fonction Vrai si la route existe (sélecteur de langue)

Templates Ă  fournir par l'app

html.twig (base des templates admin/config/front/erreurs), cms/index.html.twig (base du layout par défaut et du three-col), traductions du formulaire de contact, route de viewer de doc si la réécriture de liens .md est active, contrÎleur de panneau doc si le layout three-col est utilisé.

Posture d'intégration minimale

  1. Enregistrer le bundle.
  2. Activer l'auto-mapping Doctrine (ou mapper le namespace des entités).
  3. Déclarer resolve_target_entities pour CmsUserInterface.
  4. Fournir les templates de base (html.twig, cms/index.html.twig).
  5. Configurer les locales activées et par défaut.
  6. Optionnellement configurer tlr_symfony.multisite et vitrine_allowed_routes.
  7. Jouer tlr:site:ensure-default au dĂ©ploiement pour garantir ≄ 1 site.

Dépendances

Runtime : doctrine/doctrine-bundle, doctrine/orm, league/commonmark, psr/log, composants Symfony (config, dependency-injection, doctrine-bridge, event-dispatcher, form, http-foundation, http-kernel, options-resolver, routing, string, validator), un moteur de coloration syntaxique, twig/twig.

Dépendances de framework supposées mais non déclarées (fournies par l'app) : symfony/framework-bundle (contrÎleurs AbstractController), un composant de sécurité (ROLE_ADMIN), symfony/twig-bundle, un systÚme de session (i18n), symfony/console (commande de seed).

Absences notables : aucune suite de tests déclarée dans le bundle, aucune migration Doctrine, aucun asset (Bootstrap, Bootstrap Icons, Turbo, Stimulus référencés dans les templates mais fournis par l'app).

Assistant documentaire

Posez une question sur la documentation. Les rĂ©ponses citent leurs sources — un clic ouvre le document Ă  gauche.