romain-genin / symfony-starter
Starter Symfony 8 : Tailwind CSS v4, Alpine.js, Turbo, Twig Components, EasyAdmin 5, Brevo, données chiffrées, SEO & accessibilité, Docker (local + Coolify).
Package info
github.com/Romain-Genin/symfony_starter
Type:project
pkg:composer/romain-genin/symfony-starter
Requires
- php: >=8.4
- ext-ctype: *
- ext-iconv: *
- ext-intl: *
- ext-sodium: *
- doctrine/doctrine-bundle: ^3.3.4
- doctrine/doctrine-migrations-bundle: ^4.0.1
- doctrine/orm: ^3.7.6
- dompdf/dompdf: ^3.1.6
- easycorp/easyadmin-bundle: ^5.8
- symfony/asset: 8.1.*
- symfony/asset-mapper: 8.1.*
- symfony/brevo-mailer: 8.1.*
- symfony/console: 8.1.*
- symfony/dotenv: 8.1.*
- symfony/expression-language: 8.1.*
- symfony/flex: ^2.12
- symfony/form: 8.1.*
- symfony/framework-bundle: 8.1.*
- symfony/http-client: 8.1.*
- symfony/intl: 8.1.*
- symfony/mailer: 8.1.*
- symfony/monolog-bundle: ^4.1
- symfony/rate-limiter: 8.1.*
- symfony/runtime: 8.1.*
- symfony/security-bundle: 8.1.*
- symfony/serializer: 8.1.*
- symfony/stimulus-bundle: ^3.7
- symfony/string: 8.1.*
- symfony/translation: 8.1.*
- symfony/twig-bundle: 8.1.*
- symfony/uid: 8.1.*
- symfony/ux-icons: ^3.7
- symfony/ux-turbo: ^3.7
- symfony/ux-twig-component: ^3.7
- symfony/validator: 8.1.*
- symfony/yaml: 8.1.*
- symfonycasts/reset-password-bundle: ^1.26.1
- symfonycasts/tailwind-bundle: ^1.0
- symfonycasts/verify-email-bundle: ^1.18
- twig/cssinliner-extra: ^3.26
- twig/extra-bundle: ^3.29
- twig/intl-extra: ^3.30
- twig/string-extra: ^3.24
Requires (Dev)
- dama/doctrine-test-bundle: ^8.7
- doctrine/doctrine-fixtures-bundle: ^4.3
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^13.4
- symfony/browser-kit: 8.1.*
- symfony/css-selector: 8.1.*
- symfony/debug-bundle: 8.1.*
- symfony/maker-bundle: ^1.68
- symfony/stopwatch: 8.1.*
- symfony/web-profiler-bundle: 8.1.*
- zenstruck/foundry: ^2.15
Suggests
None
Provides
None
Conflicts
README
Socle de projet prêt à développer : pages essentielles, back-office, emails transactionnels, SEO, accessibilité et données personnelles chiffrées en base.
| Brique | Choix |
|---|---|
| Back | Symfony 8.1, PHP 8.4, Doctrine ORM 3, PostgreSQL 17 |
| Front | AssetMapper (aucun build Node), Tailwind CSS v4, Alpine.js, Turbo, Twig Components, UX Icons (Lucide) |
| Admin | EasyAdmin 5 (utilisateurs, messages de contact, export PDF) |
| Emails | Symfony Mailer + Brevo (prod) / Mailpit (local), templates Twig avec CSS inliné |
| Sécurité | Inscription + confirmation d'email, mot de passe oublié, limitation des tentatives, anti-spam maison |
| Qualité | PHPUnit (+ Foundry, DAMA), PHPStan niveau 6, PHP-CS-Fixer |
| Infra | Docker local (php-fpm + nginx + postgres + mailpit), Docker prod pour Coolify |
Créer un nouveau projet
composer create-project romain-genin/symfony-starter mon-projet cd mon-projet make install # build Docker, composer, importmap, tailwind, migrations, fixtures make tailwind # dans un autre terminal : recompile le CSS à chaque modification
Pas de PHP 8.4 sur la machine ? Créez le projet via Docker :
docker run --rm -it -u "$(id -u):$(id -g)" -v "$PWD":/app -w /app composer:2 sh -c "composer create-project romain-genin/symfony-starter mon-projet --ignore-platform-reqs --no-scripts && php mon-projet/bin/init-project.php"
À la création, bin/init-project.php génère des secrets de dev propres au projet (APP_SECRET, APP_ENCRYPTION_KEY dans .env.dev) et nomme le projet Docker d'après le dossier (COMPOSE_PROJECT_NAME).
| URL | |
|---|---|
| http://localhost:8080 | Site |
| http://localhost:8080/admin | Admin — admin@example.com / password-de-test-123 (fixtures) |
| http://localhost:8025 | Mailpit (tous les emails envoyés en local) |
make liste toutes les commandes disponibles (make test, make qa, make migration, make admin email=…, etc.).
Plusieurs projets en parallèle ? Changez les ports dans
.env.local:HTTP_PORT=8081,DB_PORT=5433,MAILPIT_PORT=8026.
Structure
assets/
app.js Entrée JS : Stimulus/Turbo + Alpine (+ plugins focus, collapse)
js/cookie-consent.js Store Alpine « consent » (bannière CNIL)
js/a11y.js Annonce/focus lors des navigations Turbo
styles/app.css Tailwind v4 (thème dans @theme)
styles/email.css CSS des emails (inliné)
icons/lucide/ Icônes SVG locales → {{ ux_icon('lucide:mail') }}
config/packages/
app_site.yaml ⚠️ Identité du site / infos légales (variable Twig `site`)
docker/ Dockerfile de dev, nginx, php.ini, entrypoint prod
src/
Doctrine/Type/EncryptedType.php Type Doctrine « encrypted »
Security/Encryption/Encryptor.php Chiffrement + blind index
Spam/SpamChecker.php Anti-spam (honeypot + jeton horodaté)
Mailer/AppMailer.php Tous les emails transactionnels
Twig/Components/ Composants avec logique (NavLink, Breadcrumb)
translations/ Textes du site (messages, validators, security), en français
templates/
components/ Composants anonymes (Button, Alert, Card, Section, PageHeader…)
form/tailwind_theme.html.twig Thème de formulaire accessible
bundles/TwigBundle/Exception/ Pages d'erreur
legal/ Mentions légales, confidentialité, cookies, CGU, accessibilité
Dockerfile, docker-compose.yml Production (Coolify)
compose.yaml Développement local
Chiffrement des données
Les données personnelles (email, nom, prénom, téléphone, messages) sont chiffrées au niveau applicatif avant d'être écrites en base : un dump SQL ou un accès direct à la base ne révèle rien.
-
Algorithme : XChaCha20-Poly1305 (libsodium), nonce aléatoire par valeur, chiffrement authentifié (toute altération est détectée).
-
Les sous-clés sont dérivées de
APP_ENCRYPTION_KEY. -
Usage dans une entité :
#[ORM\Column(type: EncryptedType::NAME, nullable: true)] private ?string $phone = null;
-
Une colonne chiffrée n'est ni triable, ni filtrable en SQL. Pour une recherche exacte, ajoutez un blind index (empreinte HMAC), comme
User::$emailHash, utilisé pour la connexion et l'unicité des emails (contrainte#[UniqueEmail]). -
Les mots de passe sont hachés (bcrypt/argon), jamais chiffrés.
⚠️ La clé est critique. Générez-la avec
make encryption-key, stockez-la dans les variables Coolify et dans un gestionnaire de secrets. Si vous la perdez ou la changez, les données existantes deviennent définitivement illisibles.
Emails (Brevo)
- Local :
MAILER_DSN=smtp://mailpit:1025(valeur par défaut). - Prod :
MAILER_DSN=brevo+api://VOTRE_CLE_API@default. Pensez à authentifier le domaine d'envoi dans Brevo (SPF, DKIM, DMARC). - L'expéditeur est défini globalement par
MAILER_FROM, le destinataire du formulaire de contact parCONTACT_RECIPIENT. - Chaque email porte un tag Brevo (
contact-notification,verify-email…) pour les statistiques.
Formulaire de contact & anti-spam
Les messages sont enregistrés chiffrés (consultables dans l'admin) puis notifiés par email, avec un accusé de réception envoyé à l'expéditeur. L'anti-spam n'utilise aucun service tiers, donc aucun cookie ni transfert de données :
- un champ « pot de miel » invisible ;
- un jeton horodaté signé, qui rejette les envois en moins de 3 s et les jetons falsifiés ou expirés ;
- une limitation de débit à 3 envois / 10 min par IP.
Un robot détecté reçoit une fausse confirmation, pour ne pas lui donner d'indice.
Conservation RGPD : app.contact_retention_months (12 mois par défaut). Planifiez php bin/console app:contact:purge (tâche planifiée Coolify, une fois par jour).
Traductions
Le site est monolingue, en français (framework.default_locale), mais aucun texte n'est écrit en dur :
templates, formulaires, messages flash, validation et emails passent par des clés de traduction.
| Fichier | Contenu |
|---|---|
translations/messages.fr.yaml |
Textes du site (navigation, pages, formulaires, flashs, emails…) |
translations/validators.fr.yaml |
Messages de validation |
translations/security.fr.yaml |
Messages de connexion |
- Modifier un texte : changez sa valeur dans
messages.fr.yaml, le template ne bouge pas. - Changer la langue du site (site monolingue dans une autre langue) : traduisez ces fichiers en
*.<code>.yamlet passezdefault_localeà<code>dansconfig/packages/translation.yaml. - Ajouter une langue : copiez les fichiers
*.fr.yamlen*.<code>.yamlet traduisez les valeurs. Le testTranslationFilesTestvérifie qu'aucune clé ni aucun paramètre (%name%,{{ limit }}) ne manque. Pour un site multilingue (URL par langue, sélecteur de langue…), ajoutez le bundle de votre choix et suivez sa documentation. - Pages légales : textes longs et propres au droit de chaque pays, ils se traduisent par fichier :
copiez
templates/legal/notice.html.twigentemplates/legal/notice.en.html.twig(sinon, la version française s'affiche).
SEO
<title>, meta description, canonical, Open Graph et Twitter Card, par page via des blocs Twig (title,meta_description,og_image,robots…).- JSON-LD :
OrganizationetWebSitedans le layout,BreadcrumbListvia<twig:Breadcrumb>,ContactPage. /sitemap.xmlgénéré à partir des routes qui déclarentoptions: ['sitemap' => [...]]./robots.txtdynamique. L'indexation est bloquée tant queAPP_INDEXABLE=1n'est pas défini (robots.txt, meta robots et en-têteX-Robots-Tag), ce qui protège les préprods.
Accessibilité
Lien d'évitement, un seul h1 par page, landmarks, aria-current dans la navigation, focus visible renforcé, formulaires avec erreurs reliées aux champs (aria-describedby, aria-invalid), champs obligatoires signalés, annonce des changements de page Turbo, menu mobile et modale cookies avec focus piégé et touche Échap, respect de prefers-reduced-motion. Contrastes AA vérifiés avec axe-core.
La page /accessibilite contient un modèle de déclaration RGAA, à mettre à jour après audit (statut actuel : « non conforme »).
Cookies
La bannière respecte les recommandations de la CNIL : « Tout refuser » est aussi accessible que « Tout accepter », le choix est conservé 6 mois et reste modifiable depuis le lien « Gérer les cookies » du footer. Pour charger un script uniquement après consentement :
<script type="text/plain" data-consent="analytics" data-src="https://exemple.com/analytics.js"></script>
Incrémentez app.cookie_consent_version pour redemander le consentement à tout le monde.
Composants Twig
<twig:Button href="{{ path('app_contact') }}" icon="lucide:arrow-right">Contact</twig:Button> <twig:Button variant="secondary" size="sm" type="button">Annuler</twig:Button> <twig:Alert type="success" dismissible>Enregistré.</twig:Alert> <twig:Card title="Titre" icon="lucide:zap">Contenu</twig:Card> <twig:Section id="atouts" title="Nos atouts" intro="…">…</twig:Section> <twig:PageHeader title="Contact" :breadcrumb="[{label: 'Contact'}]" /> <twig:NavLink route="app_contact">Contact</twig:NavLink>
Ajouter une icône : php bin/console ux:icons:import lucide:nom-icone.
Production (Coolify)
Coolify construit docker-compose.yml, qui utilise le Dockerfile racine : la cible php (php-fpm) et la cible nginx (fichiers statiques embarqués). À chaque démarrage, l'entrypoint applique les migrations et préchauffe le cache.
Variables d'environnement à définir dans Coolify :
| Variable | Exemple |
|---|---|
APP_SECRET |
chaîne aléatoire de 32+ caractères |
APP_ENCRYPTION_KEY |
make encryption-key ⚠️ à sauvegarder |
DATABASE_URL |
postgresql://user:pass@host:5432/app?serverVersion=17&charset=utf8 |
DEFAULT_URI |
https://www.mon-site.fr |
MAILER_DSN |
brevo+api://CLE@default |
MAILER_FROM |
"Mon Site <no-reply@mon-site.fr>" |
CONTACT_RECIPIENT |
contact@mon-site.fr |
APP_INDEXABLE |
1 uniquement sur le domaine de production |
TRUSTED_PROXIES |
private_ranges (défaut) |
Après le premier déploiement, créez l'administrateur :
php bin/console app:user:create admin@mon-site.fr --admin
Avant la mise en ligne
- Compléter
config/packages/app_site.yaml(raison sociale, SIRET, hébergeur, adresse…) - Relire les pages légales, mettre à jour les dates
- Remplacer le logo,
assets/images/og-default.png(1200×630) et les favicons - Ajuster les couleurs dans
assets/styles/app.css(@theme) - Générer et sauvegarder
APP_ENCRYPTION_KEY - Authentifier le domaine d'envoi dans Brevo
-
APP_INDEXABLE=1en production - Planifier
app:contact:purge
Publier une nouvelle version du starter
Le paquet est publié sur Packagist, qui suit les tags Git du dépôt GitHub :
git tag -a v1.1.0 -m "v1.1.0" && git push origin v1.1.0
composer create-project installe automatiquement la dernière version taguée. Les projets déjà créés ne sont pas affectés : chacun vit sa vie.