# welcomattic, contenu intégral > Blog de Mathieu Santostefano (welcomattic), Tech Expert chez SensioLabs, > membre de la Symfony Core Team. Dump markdown des articles et talks. --- # Symfony sur Clever Cloud : préparer son app à vivre sur un PaaS URL: https://blog.welcomattic.com/blog/2026-09-01-symfony-clever-cloud-1-preparer-son-app-paas/ Date: 2026-09-01 _This blog post is also available in 🇬🇧 English: [Symfony on Clever Cloud: preparing your app to live on a PaaS](/blog/2026-09-01-symfony-clever-cloud-1-prepare-your-app-for-a-paas/)._ > **Transparence.** Je suis ambassadeur Clever Cloud. J'écris cette série en toute indépendance, personne chez eux ne relit cette série, et je m'y autorise les mêmes critiques que sur n'importe quelle autre plateforme. Après [un SDK PHP pour piloter l'API de Clever Cloud](/blog/2026-08-26-sdk-php-clever-cloud/), je m'attaque à ce qui tourne sur ce cloud provider : une série sur le déploiement d'applications Symfony sur leur PaaS. De l'audit de l'application jusqu'à l'autoscaling et la configuration distribuée. Mais je ne vais pas commencer par `clever create`. Parce qu'en accompagnant des migrations vers un PaaS, j'ai fini par constater une chose : la quasi-totalité des problèmes ne vient pas de la plateforme choisie. Elle vient de la configuration d'applications Symfony qui n'ont jamais été écrites pour tourner ailleurs que sur le serveur d'origine où elles ont été initialisées (souvent un couple Nginx + PHP-FPM sur un VPS unique). Sessions sur le disque, uploads dans `public/`, logs dans `var/log/prod.log`, host de base de données en dur dans le fichier `.env.prod`. Rien de tout ça n'est un problème sur un VPS ou dans une VM. Tout ça peut casser sur un PaaS. Alors avant de choisir la taille de l'instance Clever sur laquelle nous allons déployer, faisons un audit rapide d'une application classique. À la fin de cet article, vous saurez si votre application est prête, et ce qu'il faut changer si elle ne l'est pas. Le contenu vaut pour Clever Cloud, mais aussi pour Scalingo, Upsun, Scaleway, Render, Fly.io, Heroku ou quasiment n'importe quel autre PaaS : c'est le même contrat. ## Le contrat, c'est 12-factor En 2011, Adam Wiggins, cofondateur de Heroku, publie [The Twelve-Factor App](https://12factor.net/). Le document part d'un constat simple : Heroku hébergeait des milliers d'applications, et celles qui se déployaient sans douleur partageaient les mêmes propriétés. Wiggins les a écrites. Quinze ans plus tard, c'est toujours la meilleure grille de lecture disponible, et surtout : **un PaaS est, entre autres, la mise en œuvre opérationnelle de ces douze principes**. Quand vous déployez sur un PaaS, vous ne choisissez pas d'adopter 12-factor, vous l'adoptez de fait. On présente souvent les douze facteurs comme une liste de règles à cocher. C'est la meilleure façon de ne rien en retenir, parce que ce sont en réalité douze déclinaisons d'une seule idée : **votre application doit être un processus interchangeable**. La plateforme doit pouvoir l'arrêter, la dupliquer, la déplacer sur une autre machine ou la remplacer par une version plus récente, sans que vos utilisateurs s'en aperçoivent et sans que rien ne se perde au passage. Tout le reste en découle. Les 12 principes sont : - I. Codebase - II. Dependencies - III. Config - IV. Backing services - V. Build, release, run - VI. Processes - VII. Port binding - VIII. Concurrency - IX. Disposability - X. Dev/prod parity - XI. Logs - XII. Admin processes Pour les rendre digestes, regroupons-les en quatre familles. ### Ce qui entre dans l'application (I, II, III) Un dépôt versionné unique produit tous les déploiements : la production, la préproduction et le poste de chaque développeur exécutent le même code, à des versions différentes. Les dépendances sont déclarées explicitement, dans `composer.json` et figées dans `composer.lock`, ce qui veut dire que personne ne va installer une extension à la main sur le serveur pour dépanner un vendredi soir. Et tout ce qui change d'un environnement à l'autre, l'adresse de la base de données, la clé d'API du prestataire de paiement, le niveau de log, sort du code pour devenir une variable d'environnement. Sur une application Symfony, les deux premiers sont acquis d'office : Git et Composer font le travail sans qu'on ait rien à décider. Le troisième est celui qui coince, parce qu'il ne se voit pas tant qu'on n'a qu'un serveur : c'est le `.env.prod` recopié à la main, le vieux `parameters.yml` hérité d'une version antérieure du projet, ou le host de base de données écrit en dur dans un fichier de configuration. ### Ce qu'elle a autour de l'application (IV, VII, XI) La base de données, le serveur Redis, le stockage de fichiers, le serveur d'envoi de mails sont des **ressources attachées** : l'application les joint via une adresse qu'on lui fournit au démarrage, et on doit pouvoir en débrancher une pour la remplacer par une autre sans toucher au code. Basculer d'une base PostgreSQL locale à une base managée doit être un changement de variable d'environnement, rien de plus. L'application expose ensuite son service sur un port, en se suffisant à elle-même plutôt qu'en étant installée à l'intérieur d'un serveur web. C'est le facteur que le monde PHP applique le moins naturellement : avec le couple classique Nginx plus PHP-FPM, ce n'est pas votre application qui écoute sur le réseau, c'est le serveur web qui l'invoque. Ce n'est pas un défaut à corriger, le runtime de la plateforme joue ce rôle pour vous. Notez simplement que FrankenPHP rend ce facteur littéral en embarquant le serveur dans l'application elle-même, et nous y reviendrons dans un prochain article. Enfin, l'application n'écrit pas de fichiers de log. Elle écrit ses logs sur sa sortie standard, au fil de l'eau, et c'est l'environnement qui décide où ils atterrissent : la console d'un développeur en local, un système de collecte en production. ### Comment l'application est fabriquée et lancée (V, XII) Trois étapes, strictement distinctes. Le **build** transforme le code source en un artefact exécutable : dépendances installées, assets compilés, cache préchauffé. La **release** associe cet artefact à la configuration de l'environnement visé. Le **run** exécute le résultat. La règle qui compte est qu'une release, une fois constituée, ne se modifie plus : on ne corrige pas un bug en éditant un fichier sur le serveur, on refait un build. C'est ce qui garantit que ce qui tourne en production correspond exactement à un commit identifié, et que revenir en arrière consiste à réexécuter une release précédente. Les tâches ponctuelles suivent la même logique : une migration Doctrine ou une commande de maintenance est un processus lancé à part, sur le même artefact et avec la même configuration que l'application qui sert le trafic. Pas un script à part, pas un accès manuel à la base. ### Comment l'application se comporte en vie (VI, VIII, IX, X) L'application ne garde rien entre deux requêtes : c'est le facteur sans état (stateless), celui qui ouvre la checklist plus bas et qui coûte le plus cher à ignorer. Elle monte en charge en multipliant les processus plutôt qu'en grossissant la machine, ce qui n'est possible que si le facteur précédent est respecté. Elle démarre vite et s'arrête proprement, parce que la plateforme peut la tuer à tout moment, pour un redéploiement, une panne matérielle ou une réduction du nombre d'instances. Et les environnements se ressemblent : le même moteur de base de données en local et en production, et un délai court entre un commit et sa mise en ligne. ### Ce que le PaaS impose de fait Sur un PaaS, plusieurs de ces facteurs cessent d'être des recommandations pour devenir des contraintes physiques. Vous n'avez pas de serveur où vous connecter, donc la configuration passe forcément par l'environnement (III) et les tâches ponctuelles par des commandes déclarées (XII). Le disque est éphémère et plusieurs instances peuvent tourner en parallèle, donc l'application doit être sans état (VI) et écrire ses logs sur la sortie standard (XI). La plateforme construit d'abord, exécute ensuite, donc le build et le run sont séparés que vous l'ayez voulu ou non (V). Les autres restent de votre côté : ils rendront votre vie plus simple, mais rien ne vous forcera la main. Sur une application Symfony existante, tous ces principes ne demandent pas le même travail. Certains sont déjà respectés sans qu'on y ait jamais pensé, d'autres demandent de vrais changements. La checklist qui suit passe en revue ceux qui comptent, une question à la fois. Chaque question indique entre parenthèses le facteur auquel elle se rattache. ## Les 8 questions à se poser ### 1. Mon application est-elle vraiment sans état ? (facteur VI) Commençons par le vocabulaire, parce que tout le reste en découle. L'état, ici, c'est l'ensemble des informations que votre application écrit quelque part et relit ensuite pour continuer à travailler : la session d'un utilisateur qui vient de se connecter, le fichier qu'il a envoyé il y a dix secondes, le résultat d'un calcul coûteux mis en cache pour ne pas le refaire. Une application est dite **avec état** (*stateful*) quand elle range ces informations chez elle, dans la mémoire ou sur le disque de la machine qui exécute son code. Elle est dite **sans état** (*stateless*) quand elle les range à l'extérieur, dans un service que toutes les copies de l'application peuvent joindre : une base de données, un serveur Redis, un stockage objet. L'application, elle, ne garde rien entre deux requêtes. Sur un serveur unique, la distinction est invisible : il n'y a qu'un disque et qu'une mémoire, donc "chez elle" et "à l'extérieur" désignent la même machine. Elle apparaît le jour où plusieurs exemplaires de l'application tournent en parallèle, ce qui est le mode de fonctionnement normal d'un PaaS. Chaque exemplaire a sa mémoire et son disque à lui, et le répartiteur de charge envoie les requêtes d'un même utilisateur tantôt sur l'un, tantôt sur l'autre. Ce que l'exemplaire A a écrit chez lui, l'exemplaire B ne le voit pas. Pire : ces exemplaires sont détruits et recréés à chaque déploiement, donc ce qui était écrit chez eux disparaît sans prévenir. C'est la question qui coûte le plus cher à ignorer, parce que le symptôme est délicieusement intermittent : ça marche, jusqu'au jour où vous passez à deux instances et où vos utilisateurs se déconnectent une fois sur deux. Trois endroits à inspecter : - **Les sessions.** Symfony ne choisit pas où elles atterrissent. Sa configuration par défaut (`session: true`) s'en remet au gestionnaire natif de PHP, qui écrit là où pointe le `session.save_path` de son `php.ini`. Sur une machine unique, c'est un dossier local (`/tmp`, ou `/var/lib/php/sessions` sur Debian) et personne ne s'en aperçoit jamais. Avec deux instances, chacune a le sien : deux jeux de sessions, et l'utilisateur se déconnecte une requête sur deux. C'est donc la plateforme, et non l'application, qui décide si vos sessions survivent au passage à l'échelle. Renseignez-vous sur ce qu'elle en fait, et à défaut de réponse claire, basculez sur un stockage partagé entre les instances (comme Redis). - **Le cache.** `var/cache/` est local à l'instance. Pour le cache applicatif partagé (`cache.app`), il faut un adaptateur distribué (Redis aussi peut convenir ici). - **Les uploads.** Si vous écrivez dans `public/uploads/`, le fichier n'existe que sur l'instance qui l'a reçu, et il disparaît au prochain déploiement. Il faut un stockage partagé de fichiers (un Object Storage comme S3 ou un système de fichier réseau comme NFS). Clever Cloud traite ce point sur son runtime PHP : un FS Bucket est créé automatiquement pour chaque application, et comme Symfony s'en remet au `php.ini`, une application par défaut y écrit ses sessions, partagées entre les instances, sans rien changer. Ce bucket n'existe pas en région HDS, ni sur les runtimes Docker et FrankenPHP. Clever recommande d'ailleurs un stockage de sessions partagé comme Redis ou Materia KV, plus performant que le bucket. ### 2. Mes secrets sont-ils dans le code ? (facteur III) Le `.env` est-il commité avec des vraies valeurs dedans ? Y a-t-il un `.env.local` que vous copiez à la main sur le serveur ? Utilisez-vous le [vault de Symfony](https://symfony.com/doc/current/configuration/secrets.html) ? Sur un PaaS, la réponse est uniforme : les secrets sont déclarés dans l'environnement de la plateforme, jamais dans le dépôt. Le vault Symfony reste utilisable, mais il vous faut alors gérer la clé de déchiffrement comme variable d'environnement, ce qui revient au même problème avec une étape en plus. Pour la plupart des projets, les variables de la plateforme suffisent. ### 3. Mon accès base de données est-il configurable au runtime ? (facteurs III et IV) Derrière cette question se cache le principe le plus structurant de tout l'article : **une application ne doit rien savoir de l'environnement dans lequel elle tourne**. Le même code, issu du même build, doit pouvoir démarrer sur votre poste, sur une préproduction et en production sans qu'on y change une seule ligne. Ce qui distingue ces trois exécutions, ce sont uniquement les valeurs que l'environnement fournit à l'application au démarrage. Autrement dit, votre application ne dit pas "ma base de données est sur `10.0.0.12`". Elle dit "ma base de données est là où on me dira qu'elle est, au moment où je démarrerai". C'est l'environnement qui répond, et c'est lui qui a raison. Dans `config/packages/doctrine.yaml`, la ligne doit ressembler à ça : ```yaml doctrine: dbal: url: '%env(resolve:DATABASE_URL)%' ``` La syntaxe `%env()%` n'est pas une simple commodité d'écriture : c'est elle qui rend cette décorrélation possible. Symfony ne remplace pas la valeur au moment où il compile son conteneur d'injection de dépendances, mais au moment où l'application démarre. La même application déjà construite peut donc pointer sur des bases différentes selon l'endroit où elle s'exécute, sans être reconstruite. Si vous avez un `host: 10.0.0.12` en dur, ou un `dbname` figé, c'est perdu d'avance : sur un PaaS, l'URL de la base est fournie par la plateforme au démarrage, et elle peut changer, et c'est normal. Et la base de données n'est qu'un exemple. Le même raisonnement vaut pour tout ce que votre application ne détient pas elle-même : le serveur Redis, le stockage objet, le serveur d'envoi de mails, l'API tierce que vous appelez. Chacun est une ressource attachée, décrite par une variable d'environnement, remplaçable sans toucher au code. C'est exactement ce que dit le facteur IV. ### 4. Mes assets sont-ils construits au déploiement ? (facteur V) En local, vous lancez `npm run build` ou `bin/console asset-map:compile` à la main. Sur le serveur, qui le fait ? Si la réponse est "personne, je commite `public/build/`", ça fonctionne encore, mais vous versionnez des artefacts de build et vous allez au conflit de merge. Le build d'assets doit devenir une étape automatisée du déploiement. AssetMapper, Webpack Encore ou Symfony Reprise, peu importe : ce qui compte est que ce soit scripté. ### 5. Mes migrations Doctrine tournent-elles toutes seules ? (facteur XII) Question honnête : aujourd'hui, comment déployez-vous ? Si la séquence est `git pull` puis un SSH pour lancer `bin/console doctrine:migrations:migrate --no-interaction` à la main, ce geste doit disparaître. Sur un PaaS, vous n'avez pas de serveur où vous connecter, et surtout vous ne voulez pas être dans la boucle. C'est le point qui demande le plus de soin, parce que le timing compte : les migrations doivent tourner après le build et avant que l'application ne serve du trafic. Et avec plusieurs instances, il faut éviter qu'elles se lancent en parallèle. On traitera ça en détail dans l'article suivant. ### 6. Mes logs partent-ils sur la sortie standard ? (facteur XI) Si Monolog écrit dans `var/log/prod.log`, ce fichier est sur un disque éphémère, sur une instance parmi N, et personne ne le lira jamais. Le PaaS collecte ce qui sort sur `stdout` et `stderr`, point. Bonne nouvelle : si vous n'avez jamais touché à `config/packages/monolog.yaml`, la recipe Symfony fait déjà ce qu'il faut. L'essentiel de ce qu'elle pose en prod : ```yaml when@prod: monolog: handlers: main: type: fingers_crossed action_level: error handler: nested excluded_http_codes: [404, 405] buffer_size: 50 nested: type: stream path: php://stderr level: debug formatter: monolog.formatter.json console: type: console process_psr_3_messages: false channels: ["!event", "!doctrine"] ``` Trois choses à en retenir. Le handler `nested` écrit sur `php://stderr`, la sortie que la plateforme collecte. Le `formatter: monolog.formatter.json` n'est pas cosmétique : sans lui, une stack trace part en autant de lignes de log qu'elle a de niveaux, et votre collecte vous rend un puzzle au lieu d'un événement. Et le handler `console` couvre les commandes CLI, ce qui compte plus qu'on ne croit sur un PaaS, où les migrations et les tâches d'administration tournent dans des hooks de déploiement. Le `fingers_crossed` en amont évite de noyer la collecte : il ne relâche le buffer que quand une erreur survient, ce qui vous donne le contexte des requêtes qui échouent, et le silence pour les autres. Attention à la lecture de `buffer_size: 50` : ce sont les 50 derniers enregistrements, pas la requête entière. Au-delà, le début du contexte est perdu. C'est le seul point de cette checklist où les plateformes divergent vraiment. La doc PHP de Clever Cloud, elle, documente un handler `error_log`, qui envoie vers le mécanisme de log de PHP plutôt que directement sur la sortie standard : ```yaml monolog: handlers: clever_logs: type: error_log level: warning ``` L'idée reste la même dans les deux cas : ne jamais écrire dans un fichier, mais plutôt là où la plateforme attend et peut lire les logs de façon standardisée. ### 7. Ai-je un healthcheck ? (hors 12-factor) Une route qui répond 200 sans toucher à la base de données, pour que la plateforme sache si l'instance est vivante : ```php #[Route('/health', name: 'health', methods: ['GET'])] public function health(): JsonResponse { return new JsonResponse(['status' => 'ok'], 200); } ``` Sans base de données, volontairement. Un endpoint healthcheck qui interroge la base transforme un incident de base de données en indisponibilité totale de l'application : la plateforme croit que toutes vos instances sont mortes et les recycle en boucle. Si vous voulez vérifier vos dépendances, faites-en une seconde route, distincte, que vous surveillez sans la brancher au healthcheck de la plateforme. ### 8. Mon application démarre-t-elle vite ? (facteur IX) Le facteur IX, "Disposability", est celui qu'on découvre au premier pic de charge. Quand la plateforme démarre une instance supplémentaire, un `cache:warmup` à froid peut prendre une dizaine de secondes. Pendant ce temps, l'instance est en train de se lever alors que le trafic, lui, est déjà là. La règle est simple : le warmup se fait à la construction, pas au démarrage. L'artefact de build doit contenir un cache déjà chaud. ## Comment ça se traduit chez Clever Cloud Voilà pour la partie universelle. Maintenant, la correspondance concrète, sur la plateforme dont parle cette série : | Principe 12-factor | Chez Clever Cloud | |---|---| | III. Config | Variables d'environnement via la console, `clever env` via la CLI, ou un addon config-provider partagé entre plusieurs applications pour partager des variables dont les valeurs sont communes à plusieurs apps | | IV. Backing services | Addons : PostgreSQL, MySQL, MongoDB, Redis, Cellar (S3), FS Buckets, Materia KV | | VI. Processus sans état | Sessions partagées d'office par le FS Bucket du runtime PHP, ou sur Redis et Materia KV, uploads sur Cellar, plus rien sur le disque local | | VIII. Concurrence | Scalers horizontaux, `--min-instances` et `--max-instances` | | XI. Logs | Handler Monolog `error_log`, et drains optionnels vers Datadog, Elastic ou OVH | Clever renvoie officiellement à 12-factor dans sa doc des bonnes pratiques : [les 12 facteurs côté Clever](https://www.clever.cloud/developers/doc/best-practices/12-factors). La page tient en trois liens, mais elle dit l'essentiel : c'est bien le contrat attendu. Trois variables à retenir dès maintenant, parce qu'elles reviendront à chaque article : - **`CC_WEBROOT`** doit valoir `/public`. Sans elle, le runtime sert la racine du dépôt, et vous exposez votre `composer.json` et votre `.env` au monde entier. - **`APP_ENV`** doit valoir `prod`. Si vous l'oubliez, vous déployez avec le profiler et les assertions de debug actives. - **`APP_SECRET`** doit être définie, identique sur toutes les instances, et stable d'un déploiement à l'autre au risque d'invalider tout ce que ce secret sert à signer (cookie remember-me, URL signées, etc). ## Et ensuite Votre application est prête à être auditée selon ces critères. Dans le prochain article, on passe à l'action : `clever create`, l'addon PostgreSQL, les hooks de déploiement et les migrations au bon moment, jusqu'à un premier déploiement en production. > **Code source.** Chaque article de la série a sa branche dans le dépôt d'accompagnement : [welcoMattic/symfony-clever-cloud-series](https://github.com/welcoMattic/symfony-clever-cloud-series). Pour ce premier article, il n'y a rien à montrer d'autre que le point de départ : la branche [`01-fresh-symfony-app`](https://github.com/welcoMattic/symfony-clever-cloud-series/tree/01-fresh-symfony-app) contient une application Symfony 8.1 créée avec `symfony new --version=8.1 --webapp --docker`, sans une ligne ajoutée à la main. --- # Symfony on Clever Cloud: preparing your app to live on a PaaS URL: https://blog.welcomattic.com/blog/2026-09-01-symfony-clever-cloud-1-prepare-your-app-for-a-paas/ Date: 2026-09-01 _Cet article est aussi disponible en 🇫🇷 Français : [Symfony sur Clever Cloud : préparer son app à vivre sur un PaaS](/blog/2026-09-01-symfony-clever-cloud-1-preparer-son-app-paas/)._ > **Transparency.** I am a Clever Cloud ambassador. I write this series independently, nobody on their side proofreads this series, and I allow myself the same criticism here as on any other platform. After [a PHP SDK to drive Clever Cloud's API](/blog/2026-08-26-sdk-php-clever-cloud/) (in French), I am tackling what actually runs on this cloud provider: a series about deploying Symfony applications on their PaaS. From auditing the application all the way to autoscaling and distributed configuration. But I am not going to start with `clever create`. Because while helping teams migrate to a PaaS, I ended up noticing one thing: almost none of the problems come from the platform you picked. They come from the configuration of Symfony applications that were never written to run anywhere other than the original server they were bootstrapped on (often an Nginx + PHP-FPM pair on a single VPS). Sessions on disk, uploads in `public/`, logs in `var/log/prod.log`, database host hardcoded in the `.env.prod` file. None of that is a problem on a VPS or in a VM. All of it can break on a PaaS. So before picking the size of the Clever instance we are going to deploy on, let's run a quick audit of a typical application. By the end of this article, you will know whether your application is ready, and what to change if it is not. The content holds for Clever Cloud, but also for Scalingo, Upsun, Scaleway, Render, Fly.io, Heroku or just about any other PaaS: it is the same contract. ## The contract is 12-factor In 2011, Adam Wiggins, co-founder of Heroku, published [The Twelve-Factor App](https://12factor.net/). The document starts from a simple observation: Heroku was hosting thousands of applications, and the ones that deployed painlessly all shared the same properties. Wiggins wrote them down. Fifteen years later, it is still the best available lens on the subject, and above all: **a PaaS is, among other things, the operational implementation of those twelve principles**. When you deploy on a PaaS, you do not choose to adopt 12-factor, you adopt it whether you meant to or not. The twelve factors are often presented as a list of rules to tick off. That is the best way to retain none of them, because they are really twelve variations on a single idea: **your application must be an interchangeable process**. The platform must be able to stop it, duplicate it, move it to another machine or replace it with a newer version, without your users noticing and without anything being lost along the way. Everything else follows from that. The 12 principles are: - I. Codebase - II. Dependencies - III. Config - IV. Backing services - V. Build, release, run - VI. Processes - VII. Port binding - VIII. Concurrency - IX. Disposability - X. Dev/prod parity - XI. Logs - XII. Admin processes To make them digestible, let's group them into four families. ### What goes into the application (I, II, III) A single versioned repository produces every deployment: production, staging and each developer's machine run the same code, at different revisions. Dependencies are declared explicitly, in `composer.json`, and pinned in `composer.lock`, which means nobody is going to install an extension by hand on the server to save the day on a Friday night. And everything that changes from one environment to the next, the database address, the payment provider's API key, the log level, moves out of the code and becomes an environment variable. On a Symfony application, the first two come for free: Git and Composer do the work without anyone having to decide anything. The third one is where things get stuck, because it does not show as long as you only have one server: it is the `.env.prod` copied over by hand, the old `parameters.yml` inherited from an earlier version of the project, or the database host hardcoded in a configuration file. ### What sits around the application (IV, VII, XI) The database, the Redis server, the file storage, the mail server are **backing services**: the application reaches them through an address handed to it at startup, and you must be able to unplug one and replace it with another without touching the code. Switching from a local PostgreSQL database to a managed one must be an environment variable change, nothing more. The application then exposes its service on a port, being self-contained rather than living inside a web server. That is the factor the PHP world applies the least naturally: with the classic Nginx plus PHP-FPM pair, your application is not the thing listening on the network, the web server is the one invoking it. That is not a flaw to fix, the platform's runtime plays that role for you. Just note that FrankenPHP makes this factor literal by embedding the server inside the application itself, and we will come back to it in a future article. Finally, the application does not write log files. It writes its logs to standard output, as they happen, and the environment decides where they land: a developer's console locally, a collection system in production. ### How the application is built and started (V, XII) Three strictly separate stages. The **build** turns the source code into an executable artifact: dependencies installed, assets compiled, cache warmed up. The **release** combines that artifact with the configuration of the target environment. The **run** executes the result. The rule that matters is that a release, once assembled, is never modified: you do not fix a bug by editing a file on the server, you make a new build. That is what guarantees that whatever runs in production matches exactly one identified commit, and that rolling back means re-running a previous release. One-off tasks follow the same logic: a Doctrine migration or a maintenance command is a process launched separately, on the same artifact and with the same configuration as the application serving traffic. Not a script on the side, not manual access to the database. ### How the application behaves once alive (VI, VIII, IX, X) The application keeps nothing between two requests: that is the stateless factor, the one that opens the checklist below and the most expensive one to ignore. It scales by multiplying processes rather than by growing the machine, which is only possible if the previous factor is respected. It starts fast and shuts down cleanly, because the platform can kill it at any moment, for a redeployment, a hardware failure or a reduction in the number of instances. And environments look alike: the same database engine locally and in production, and a short delay between a commit and its going live. ### What the PaaS enforces by design On a PaaS, several of these factors stop being recommendations and become physical constraints. You have no server to connect to, so configuration necessarily goes through the environment (III) and one-off tasks through declared commands (XII). The disk is ephemeral and several instances can run in parallel, so the application has to be stateless (VI) and write its logs to standard output (XI). The platform builds first and runs second, so build and run are separated whether you wanted it or not (V). The others stay on your side: they will make your life easier, but nothing will force your hand. On an existing Symfony application, these principles do not all require the same amount of work. Some are already respected without anyone ever thinking about it, others call for real changes. The checklist below goes through the ones that matter, one question at a time. Each question mentions in parentheses the factor it relates to. ## The 8 questions to ask yourself ### 1. Is my application really stateless? (factor VI) Let's start with the vocabulary, because everything else follows from it. State, here, is the set of information your application writes somewhere and reads back later to keep working: the session of a user who just logged in, the file they uploaded ten seconds ago, the result of an expensive computation cached so it does not have to be redone. An application is said to be **stateful** when it stores that information at home, in the memory or on the disk of the machine running its code. It is said to be **stateless** when it stores it outside, in a service that every copy of the application can reach: a database, a Redis server, an object storage. The application itself keeps nothing between two requests. On a single server, the distinction is invisible: there is only one disk and one memory, so "at home" and "outside" designate the same machine. It shows up the day several copies of the application run in parallel, which is the normal mode of operation of a PaaS. Each copy has its own memory and its own disk, and the load balancer sends the requests of a single user sometimes to one, sometimes to another. What copy A wrote at home, copy B does not see. Worse: those copies are destroyed and recreated on every deployment, so whatever was written at home disappears without warning. This is the question that costs the most to ignore, because the symptom is delightfully intermittent: it works, until the day you scale to two instances and your users get logged out half the time. Three places to inspect: - **Sessions.** Symfony does not pick where they land. Its default configuration (`session: true`) defers to PHP's native handler, which writes wherever its `php.ini` `session.save_path` points. On a single machine that is a local folder (`/tmp`, or `/var/lib/php/sessions` on Debian) and nobody ever notices. With two instances, each gets its own: two sets of sessions, and your user is logged out every other request. So it is the platform, not the application, that decides whether your sessions survive scaling out. Find out what yours does with them, and absent a clear answer, switch to storage shared between the instances (Redis, for instance). - **The cache.** `var/cache/` is local to the instance. For the shared application cache (`cache.app`), you need a distributed adapter (Redis fits here too). - **Uploads.** If you write to `public/uploads/`, the file only exists on the instance that received it, and it disappears on the next deployment. You need shared file storage (an object storage such as S3, or a network filesystem such as NFS). Clever Cloud handles this on its PHP runtime: an FS Bucket is created automatically for each application, and since Symfony defers to `php.ini`, a default application writes its sessions there, shared between instances, with nothing to change. That bucket does not exist in HDS regions, nor on the Docker and FrankenPHP runtimes. Clever recommends a shared session store such as Redis or Materia KV anyway, which performs better than the bucket. ### 2. Are my secrets in the code? (factor III) Is `.env` committed with real values in it? Is there a `.env.local` that you copy by hand onto the server? Do you use [Symfony's vault](https://symfony.com/doc/current/configuration/secrets.html)? On a PaaS, the answer is uniform: secrets are declared in the platform's environment, never in the repository. The Symfony vault remains usable, but you then have to manage the decryption key as an environment variable, which brings you back to the same problem with one extra step. For most projects, the platform's variables are enough. ### 3. Is my database access configurable at runtime? (factors III and IV) Behind this question hides the most structuring principle of the whole article: **an application must know nothing about the environment it runs in**. The same code, out of the same build, must be able to start on your machine, on a staging environment and in production without a single line being changed. What distinguishes those three runs is only the values the environment hands to the application at startup. In other words, your application does not say "my database is at `10.0.0.12`". It says "my database is wherever I am told it is, at the moment I start". The environment answers, and the environment is right. In `config/packages/doctrine.yaml`, the line must look like this: ```yaml doctrine: dbal: url: '%env(resolve:DATABASE_URL)%' ``` The `%env()%` syntax is not a mere writing convenience: it is what makes that decoupling possible. Symfony does not substitute the value when it compiles its dependency injection container, but when the application starts. The very same, already built application can therefore point at different databases depending on where it runs, without being rebuilt. If you have a hardcoded `host: 10.0.0.12`, or a frozen `dbname`, you have already lost: on a PaaS, the database URL is provided by the platform at startup, and it can change, and that is normal. And the database is only one example. The same reasoning holds for everything your application does not own itself: the Redis server, the object storage, the mail server, the third-party API you call. Each one is a backing service, described by an environment variable, replaceable without touching the code. That is exactly what factor IV says. ### 4. Are my assets built at deploy time? (factor V) Locally, you run `npm run build` or `bin/console asset-map:compile` by hand. On the server, who does it? If the answer is "nobody, I commit `public/build/`", it still works, but you are versioning build artifacts and you are heading straight for a merge conflict. The asset build has to become an automated step of the deployment. AssetMapper, Webpack Encore or Symfony Reprise, it does not matter: what counts is that it is scripted. ### 5. Do my Doctrine migrations run on their own? (factor XII) Honest question: today, how do you deploy? If the sequence is `git pull` then an SSH session to run `bin/console doctrine:migrations:migrate --no-interaction` by hand, that gesture has to disappear. On a PaaS, you have no server to connect to, and above all you do not want to be in the loop. This is the point that requires the most care, because timing matters: migrations have to run after the build and before the application serves any traffic. And with several instances, you have to prevent them from starting in parallel. We will handle that in detail in the next article. ### 6. Do my logs go to standard output? (factor XI) If Monolog writes to `var/log/prod.log`, that file sits on an ephemeral disk, on one instance among N, and nobody will ever read it. The PaaS collects what comes out on `stdout` and `stderr`, full stop. Good news: if you have never touched `config/packages/monolog.yaml`, the Symfony recipe already does the right thing. The gist of what it sets up in prod: ```yaml when@prod: monolog: handlers: main: type: fingers_crossed action_level: error handler: nested excluded_http_codes: [404, 405] buffer_size: 50 nested: type: stream path: php://stderr level: debug formatter: monolog.formatter.json console: type: console process_psr_3_messages: false channels: ["!event", "!doctrine"] ``` Three things to take away from it. The `nested` handler writes to `php://stderr`, the output the platform collects. The `formatter: monolog.formatter.json` is not cosmetic: without it, a stack trace is split into as many log lines as it has levels, and your collection hands you back a puzzle instead of an event. And the `console` handler covers CLI commands, which matters more than you would think on a PaaS, where migrations and administration tasks run inside deployment hooks. The `fingers_crossed` sitting upstream avoids drowning the collection: it only releases the buffer when an error occurs, which gives you the context of the requests that fail, and silence for the others. Careful how you read `buffer_size: 50`: those are the last 50 records, not the whole request. Beyond that, the beginning of the context is lost. This is the only point of this checklist where platforms really diverge. Clever Cloud's PHP documentation, for its part, documents an `error_log` handler, which sends to PHP's own logging mechanism rather than directly to standard output: ```yaml monolog: handlers: clever_logs: type: error_log level: warning ``` The idea stays the same in both cases: never write to a file, but rather write where the platform expects it and can read the logs in a standardized way. ### 7. Do I have a healthcheck? (outside 12-factor) A route that answers 200 without touching the database, so the platform knows whether the instance is alive: ```php #[Route('/health', name: 'health', methods: ['GET'])] public function health(): JsonResponse { return new JsonResponse(['status' => 'ok'], 200); } ``` Without the database, deliberately. A healthcheck endpoint that queries the database turns a database incident into a total outage of the application: the platform believes all your instances are dead and recycles them in a loop. If you want to check your dependencies, make it a second, separate route, one that you monitor without wiring it to the platform's healthcheck. ### 8. Does my application start fast? (factor IX) Factor IX, "Disposability", is the one you discover at the first traffic peak. When the platform starts an extra instance, a cold `cache:warmup` can take about ten seconds. During that time, the instance is still coming up while the traffic is already there. The rule is simple: warmup happens at build time, not at startup. The build artifact must contain an already warm cache. ## How this translates at Clever Cloud So much for the universal part. Now, the concrete mapping, on the platform this series is about: | 12-factor principle | At Clever Cloud | |---|---| | III. Config | Environment variables through the console, `clever env` through the CLI, or a config-provider addon shared between several applications, to share variables whose values are common to several apps | | IV. Backing services | Addons: PostgreSQL, MySQL, MongoDB, Redis, Cellar (S3), FS Buckets, Materia KV | | VI. Stateless processes | Sessions shared out of the box by the PHP runtime's FS Bucket, or on Redis and Materia KV, uploads on Cellar, nothing left on the local disk | | VIII. Concurrency | Horizontal scalers, `--min-instances` and `--max-instances` | | XI. Logs | Monolog `error_log` handler, plus optional drains to Datadog, Elastic or OVH | Clever officially points back to 12-factor in its best practices documentation: [the 12 factors at Clever](https://www.clever.cloud/developers/doc/best-practices/12-factors). The page fits in three links, but it says what matters: this really is the expected contract. Three variables to keep in mind right now, because they will come back in every article of the series: - **`CC_WEBROOT`** must be set to `/public`. Without it, the runtime serves the root of the repository, and you expose your `composer.json` and your `.env` to the whole world. - **`APP_ENV`** must be set to `prod`. If you forget it, you deploy with the profiler and the debug assertions active. - **`APP_SECRET`** must be defined, identical on every instance, and stable from one deployment to the next, otherwise you invalidate everything this secret is used to sign (remember-me cookie, signed URLs, and so on). ## What comes next Your application is ready to be audited against these criteria. In the next article, we move to action: `clever create`, the PostgreSQL addon, the deployment hooks and migrations at the right moment, all the way to a first production deployment. > **Source code.** Every article in the series has its own branch in the companion repository: [welcoMattic/symfony-clever-cloud-series](https://github.com/welcoMattic/symfony-clever-cloud-series). For this first article, there is nothing to show beyond the starting point: the [`01-fresh-symfony-app`](https://github.com/welcoMattic/symfony-clever-cloud-series/tree/01-fresh-symfony-app) branch contains a Symfony 8.1 application created with `symfony new --version=8.1 --webapp --docker`, without a single line added by hand. --- # A modern PHP SDK for Clever Cloud URL: https://blog.welcomattic.com/blog/2026-08-26-sdk-php-clever-cloud/ Date: 2026-08-26 ## Genesis As Clever Cloud Ambassador, I deploy most of my side projects on [their PaaS](https://www.clever-cloud.com). The day I wanted to script a few things around my apps (list them, tail their logs, flip an environment variable from a small internal tool), I went looking for a PHP SDK. Clever Cloud maintains an official Go client ([`clevercloud-client-go`](https://github.com/CleverCloud/clevercloud-client-go)), a JavaScript client ([`clever-client.js`](https://github.com/CleverCloud/clever-client.js)), and the excellent [`clever-tools`](https://github.com/CleverCloud/clever-tools) CLI. Nothing idiomatic for PHP, though. So I crafted one and open-sourced it: [`welcomattic/clevercloud-php-sdk`](https://github.com/welcoMattic/clevercloud-php-sdk). It is a community project, not an official Clever Cloud release, but it already covers most of the v2 and v4 REST surface plus the api-bridge gateway used for API tokens. The [coverage matrix](https://github.com/welcoMattic/clevercloud-php-sdk#coverage-matrix) in the README lists exactly what is in and what is not. Here is what makes it worth a look. ## Three lines to a first call Mint a token from the [Console](https://console.clever-cloud.com/) (section "Personal API tokens"), and you are talking to the API: ```php use CleverCloud\Sdk\Auth\Credentials; use CleverCloud\Sdk\ClientBuilder; $client = (new ClientBuilder()) ->withCredentials(Credentials::apiToken($_SERVER['CC_API_TOKEN'])) ->build(); $me = $client->self->get(); echo $me->email, "\n"; ``` API tokens are the recommended path. OAuth 1.0a is also supported, three-legged flow included, for consumers that still need it. ## One HTTP client, on purpose If you read my previous article on the SensioLabs blog about [bringing your own HTTP client](https://sensiolabs.com/blog/2025/bring-your-own-http-client), you might expect this SDK to be transport-agnostic. It is not, and that was a deliberate call. To be precise: the SDK still speaks PSR-18 and PSR-7 internally. Regular calls go through Symfony's `Psr18Client`, and the request and response hooks are typed against PSR-7 messages. What you cannot swap is the transport underneath, because live log streaming (more on that below) needs Symfony's `EventSourceHttpClient`. The injection point is `HttpClientInterface`, not an arbitrary PSR-18 client. SSE is not covered by any PSR, and that is the whole problem. Guzzle can read an SSE body with `stream => true`, so raw plumbing is not the blocker: what you do not get is frame parsing, reconnection and `Last-Event-ID` resume, which `EventSourceHttpClient` handles for free. I could have kept PSR-18 for the 95% of calls that are plain request and response, and required Symfony only for `logs->stream()`. That buys you two configuration paths, two retry stories and two testing stories in the same SDK. I took the other trade-off: one coherent API, at the cost of a second HTTP client in your `vendor/` if your application is on Guzzle. You keep the part that actually matters for testing, though: `MockHttpClient` is a Symfony HttpClient, so you swap it in and drive every response without touching the network: ```php use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; $mock = new MockHttpClient([ new MockResponse( json_encode(['id' => 'app_42', 'name' => 'hello'], JSON_THROW_ON_ERROR), ['response_headers' => ['content-type' => 'application/json']], ), ]); $client = (new ClientBuilder()) ->withCredentials(Credentials::apiToken('test')) ->withHttpClient($mock) ->build(); $app = $client->applications->get('app_42'); self::assertSame('hello', $app->name); // inside your PHPUnit test case ``` No mocking library, no HTTP recorder, no network in your test suite. ## The fun part: live log streaming This is the feature I enjoyed building the most. Clever Cloud exposes application logs as Server-Sent Events, and the SDK streams them for you using Symfony's `EventSourceHttpClient` under the hood. You get a plain iterable of typed log entries: ```php foreach ($client->logs->stream($appId, $orgId) as $entry) { echo $entry->message, "\n"; } ``` That is a live `tail -f` on your production logs, in PHP, in a `foreach`. Pass `null` as the organisation id and the SDK resolves your personal one for you, at the cost of one extra `/v2/self` call. Need to filter? Pass a filter string: ```php foreach ($client->logs->stream($appId, $orgId, ['filter' => 'level:error']) as $entry) { echo $entry->message, "\n"; } ``` Need history instead of a live feed? `$client->logs->query(...)` consumes the same stream and hands you a list. Give it a `since` filter, otherwise you only get what happens from now on, and it takes a maximum duration so a quiet application cannot leave you hanging. ## Going further I kept this one short on purpose. The rest lives in the [documentation](https://welcomattic.github.io/clevercloud-php-sdk/): - **Retries are on by default**: three attempts, exponential backoff with jitter, capped at five seconds, on 429 and 5xx responses. `RetryPolicy::none()` if you want none of it. - **Observability**: `onRequest` / `onResponse` hooks on the builder for tracing and metrics, plus a PSR-3 logger with structured context (`attempt`, `method`, `uri`, `status`, `requestId`). - **Configuration**: user agent, timeouts and the three base URLs, all on a single `Configuration` object. The SDK also fits nicely in PHP task runners. With [Castor](https://castor.jolicode.com), add it to your Castor project's Composer dependencies (see [remote imports](https://castor.jolicode.com/docs/going-further/extending-castor/remote-imports/), which is how Castor pulls Composer packages into `.castor/vendor/`): ```bash castor composer require welcomattic/clevercloud-php-sdk ``` Then write the task you actually wanted: ```php use Castor\Attribute\AsTask; use CleverCloud\Sdk\Auth\Credentials; use CleverCloud\Sdk\ClientBuilder; use function Castor\io; #[AsTask(description: 'Restart an application')] function restart(string $appId): void { $client = (new ClientBuilder()) ->withCredentials(Credentials::apiToken($_SERVER['CC_API_TOKEN'])) ->build(); $client->applications->restart($appId); io()->success("Restarted {$appId}"); } ``` ```console $ castor restart app_42 ``` ## Give it a spin ```bash composer require welcomattic/clevercloud-php-sdk ``` That gets you [2.0](https://github.com/welcoMattic/clevercloud-php-sdk/releases/tag/v2.0.0), the current release. The major bump comes from a pass where I exercised every call against the live API: a handful of my paths, payload shapes and field names did not line up with what the platform returns, and correcting them meant removing public symbols. What you install today matches what Clever Cloud actually serves, and the breaking changes are listed in the release notes. If you run PHP on Clever Cloud, or just want to automate it from a PHP script, I would love your feedback. Issues, pull requests, and "it would be great if it also did X" are all welcome on [GitHub](https://github.com/welcoMattic/clevercloud-php-sdk). --- # Sleepr URL: https://blog.welcomattic.com/blog/2023-10-27-sleepr/ Date: 2023-10-27 ## Sleepr [Sleepr](https://sleepr.app/) brings back to life the missing sleep timer feature of macOS, removed from Ventura. It provides a quick access to set a timer at the end of which your Mac goes into sleep mode. When the timer is near the end, a native notification will suggest you to cancel the timer or extend it by 10 minutes. If no action is done by the end of the timer, the Mac will go to sleep. ## Tech Built with 100% Swift and SwiftUI code, it leverage all brand new features released by Apple during WWDC23. Not one useless line of code, no tracking, only the essential to work. ## Future Sleepr will evolve in future months to gain new features and ease users to retrieve an essential feature of macOS. --- # Talk - Des images au cordeau pour vos applications Symfony URL: https://blog.welcomattic.com/blog/2019-06-04-talk-des-images-au-cordeau-pour-vos-applications-symfony/ Date: 2019-04-14 J'ai eu la chance en 2019 d'être retenu en tant que conférencier au SymfonyLive Paris. Le sujet que j'avais proposé lors du CFP traitait des images responsives, chose que j'ai du mettre en place sur plusieurs projets clients différents. À mi chemin entre retour d'expérience et recommandations, voici les slides de la présentation [💬 Slides](https://slides.com/welcomattic/des-images-au-cordeau-pour-vos-applications-symfony/#/) La conférence a été filmée, [voici la vidéo](https://symfonycasts.com/screencast/paris2019/des-images-au-cordeau-pour-vos-applications-symfony) ! --- # How to deploy Ghost on Clever Cloud? URL: https://blog.welcomattic.com/blog/2019-04-09-how-to-deploy-ghost-on-clevercloud/ Date: 2019-04-09 ## 👻 Install Ghost locally First, we need to init a fresh new Ghost application. According to [the documentation](https://github.com/TryGhost/Ghost#quickstart-install), we need to install the `ghost-cli` package globally: `$ npm install ghost-cli -g` Then, we can install a local Ghost with this command: `$ ghost install local` 🎉 We have a brand new Ghost application accessible via > 🤔 "Where is my data?" Ghost documentation says: > The SQLite3 database is auto-setup and located in`/content/data/` Okay, now let's explore what's just been installed: ![Ghost directory tree](/img/ghost-tree.png) > 🤔 bis "Where is package.json?" It's located in `current` which is a symlink to the current version of Ghost for our instance. Yes, it's a bit strange but ok, let's deal with it. Clever Cloud documentation says: - you have pushed in master branch - you listen on 0.0.0.0:8080 - your package.json either has a scripts.start or main field With our Ghost app, we have 2 problems: - We don't listen on 8080 (default port of Ghost is 2368) - We don't have a `package.json` at the root of our app Let's fix this! ## 🔧 Make Ghost ready to be deployed The directory `versions/2.19.3` directory contains this: ![Ghost version directory tree](/img/ghost-version-tree.png) We can see a `content` directory, which contains the same structure as the root `content` directory. So, I suppose that we can merge them. Let's move this structure to the root of our app (make sure to replace version number by our own): - backup our `content` directory `$ mv content _content` - move Ghost instance to the root directory `$ mv versions/2.19.3/* .` > ⚠ You have to merge manually `_content` which contains **your data, files and themes** and `content` which contains default things. - see the result ![root directory result](/img/ghost-mv.png) Now, we have to configure Ghost for production environment. To do this, we copy `config.development.json` to `config.production.json` and edit it like this: ```json { "url": "https://YOUR_CC_APP_ID.cleverapps.io/", "server": { "port": 8080, "host": "0.0.0.0" }, "database": { "client": "sqlite3", "connection": { "filename": "content/data/ghost-prod.db" } }, "mail": { "transport": "Direct" }, "logging": { "transports": [ "file", "stdout" ] }, "process": "local", "paths": { "contentPath": "content/" } } ``` As mentionned earlier, Clever Cloud needs a `start` entry under `scripts` in package.json. By default Ghost gets one, but we have to update it to force Ghost to to start in production mode: ```yaml "start": "NODE_ENV=production node index", ``` A last step before next chapter, we must add `content` directory to the `.gitignore` file, which should be like this: ``` node_modules/ content/ .ghostpid ``` Now, our app is ready. Let's open Clever Cloud dashboard to prepare the deployment. ## 💡☁ Create a NodeJS application on Clever Cloud I invite you to follow [official documentation](https://www.clever-cloud.com/doc/clever-cloud-overview/add-application/#create-an-application) to create a NodeJS application. Once it's done, we need to link an FS addon to our application. To do this, go to the [addon creation page](https://console.clever-cloud.com/users/me/addons/new) and select FS Bucket: ![Addon creation page](/img/console-clever-cloud.png) Then, go back in your application settings, go to "Service dependencies" item, and link your addon to your app. The last step takes place in "Environment variables". You should see a `BUCKET_HOST` variable on the second aprt of the page. Now, create a new variable `CC_FS_BUCKET` with this value `content:`. ## 🚀 Let's push! If your create your Clever Cloud application from a GitHub repository, you only have to `git push` your code to GitHub, and Clever Cloud will automatically deploy your code! If not, you have to follow this documentation to add Clever Cloud remote to your git configuration. Then you will be able to push to Clever Cloud!
Thanks for reading! enjoy(); --- # Comment je suis devenu développeur ? URL: https://blog.welcomattic.com/blog/2014-11-26-comment-je-suis-devenu-developpeur/ Date: 2014-11-26 On va commencer à répondre à cette question par élimination : non, je n'ai pas fait d'école d'ingénieur, non je n'ai pas fait d'école privée d'informatique (SUPINFO, EPITECH, 42 etc ...), non, je n'ai fait aucun stage en Californie. En fait j'ai juste été infecté par un virus quand j'étais petit. Ce virus c'est mon grand-père qui me l'a transmis. À l'époque (en 1996-97), mon grand-père avait un ordinateur, qui tournait sous Windows 95 (ultra récent !). Il s'en servait principalement pour maquetter (sur Designer 3.5) des plans 2D d'une maison qu'il retapait. Moi, qui devait avoir 4-5 ans, je le regardais faire les yeux écarquillés en voyant que l'on pouvait dessiner à l'infini sans avoir du feutre partout sur les doigts. Un jour alors que mon grand-père était au téléphone dans son bureau, je lui demande si je peux dessiner sur l'ordinateur. Il acquiesce , et me dit que l'on enregistrera mon œuvre sur une disquette ensuite, mais surtout que je l'attende pour faire ça. D'après ce qu'il m'a raconté, il est revenu 15 minutes plus tard et j'avais enregistré mon fichier sur une disquette tout seul "comme un grand". À force de le regarder j'avais mémorisé les actions sans même savoir lire. Ok, vous me direz, c'est bien mais quel rapport avec le métier de développeur ? Je dirais que c'est la curiosité. Après cette anecdote sur Windows 95, on a eu Windows 98, et Internet ! C'est à cette époque que j'ai commencé à jouer avec Photoshop 2.0. En fait plus le temps passait, plus j'allais explorer ce que l'on pouvait faire avec un ordinateur. Et depuis ce temps là je n'ai jamais arrêté ! Aujourd'hui mon grand-père m'appelle de temps en temps pour que je le dépanne car il ne retrouve pas cette pièce jointe qu'il a enregistré la veille ^^' Pour en revenir à ce que j'ai fait comme études pour devenir développeur, j'ai un Bac STG GSI (Gestion des Systèmes d'Information), un BTS Info Gestion option Développeur d'Applications, et enfin un niveau Licence de Chef de Projet Multimédia. Mais ces diplômes ne se suffisent pas à eux même, et ce serait identique si j'avais 4 masters et 2 doctorats de +. L'important c'est d'aimer ce que l'on fait, d'être curieux et d'être en veille permanente. Pratiquer au quotidien et pas seulement écrire des lignes de code. Il est important aussi d'expérimenter des systèmes, des architectures, des manières de faire les choses. Pour trouver de l'inspiration, je passe du temps à dériver sur GitHub de repo en repo au lieu de procrastiner sur Facebook, et je cherche à voir ce que l'on peut faire avec différents langages, ça améliore ma créativité ;) Car oui, un développeur peut et DOIT être créatif ! Si ce n'était pas le cas nous aurions encore des logiciels à la MicroApplication, avec des interfaces immondes, aucune UI/UX pensée, et des fonctionnalités qui ne correspondent pas aux besoins des utilisateurs. Voilà, selon moi, comment on peut devenir développeur. Ce n'est évidement pas le seul moyen, mais c'en est un, j'en suis la preuve. Ne vous faites pas enfermer par tel dictat de telle école, ni par un système universitaire majoritairement en retard sur son temps au niveau numérique. Restez libre de découvrir tout ce qu'offre le métier de développeur au sens large. --- # [Talk] [ConFoo 2026] - APIs secured, the modern way URL: https://blog.welcomattic.com/talks/2026-02-27-api-secured-the-modern-way/ Date: 2026-02-27 [💬 Slides](https://speakerdeck.com/welcomattic/api-secured-the-modern-way) Securing APIs has become more standardized with modern solutions like OAuth2, OIDC, JWT and rate limiting. Now, tools like Keycloak and Symfony's new features simplify implementing these protocols. As an API developer, you’ll learn to enhance your API's security. As an API consumer, you’ll discover how to interact securely with external APIs. Join us to master secure API practices, without the complexity! --- # [Talk] [ConFoo 2026] - Unveil Symfony AI URL: https://blog.welcomattic.com/talks/2026-02-25-unveil-symfony-ai/ Date: 2026-02-25 [💬 Slides](https://speakerdeck.com/welcomattic/unveil-symfony-ai) July 2025, the Symfony AI was launched as an initiative, just like Symfony UX, to help developers integrate AI into their apps with ready-to-use components and bundles. In this talk, let's discover Symfony AI's origins, explore its components, and learn how to use them with any AI platform. We'll also cover how to get started using it in your PHP apps and contribute to the project! --- # [Talk] [Message à caractère informatique] - #144 Grâce au Pulsar multi nucléaire, Franken prouve que PHP est bien vivant ! URL: https://blog.welcomattic.com/talks/2025-10-10-podcast-grace-au-pulsar-multi-nucleaire-franken-prouve-que-php-est-bien-vivant/ Date: 2025-10-10 J'ai été invité à participer à deux épisodes du podcast [Message à caractère informatique](https://www.clever.cloud/podcast/) de Clever Cloud. Écoute-les directement ci-dessous, ou regarde la version vidéo. ## Épisode 144 : Grâce au Pulsar multi nucléaire, Franken prouve que PHP est bien vivant ! {{< youtube zs4huFbJQYI >}} [🎙️ Écouter l'épisode 144 sur clever.cloud](https://www.clever.cloud/podcast/grace-au-pulsar-multi-nucleaire-franken-prouve-que-php-est-bien-vivant/) ## Épisode 145 : Java de base ouverte sur Symfony de logs en couleurs {{< youtube OtcIMve6lcY >}} [🎙️ Écouter l'épisode 145 sur clever.cloud](https://www.clever.cloud/podcast/java-de-base-ouverte-sur-symfony-de-logs-en-couleurs/) --- # [Talk] [API Platform Con 2025] - Mercure, SSE, API Platform et un LLM élèvent un chat(bot) URL: https://blog.welcomattic.com/talks/2025-09-18-mercure-sse-api-platform-et-un-llm-elevent-un-chat-bot/ Date: 2025-09-18 {{< youtube bvM82QfgVBo >}} [💬 Slides](https://welcomattic.github.io/slides-real-time-ai-chatbot-with-mercure) Construire un chat temps réel avec Mercure, c'est le cas d'exemple basique, mais parlant. Bien. Mais à l'ère de l'IA, voyons ensemble comment intégrer un LLM dans un chatbot mettant en scène un client JS, une API PHP et Mercure ! Notre challenge sera de faire communiquer les différentes briques à travers Mercure uniquement, pour un maximum de performance et de fluidité! Symfony et API Platform sont-ils prêts à affronter l'ère de l'IA ? Est-il possible de recevoir des SSE via PHP de manière optimale et performante ? Comment interagir efficacement avec un LLM en PHP ? Venez le découvrir ! PS : nous prendrons le temps de démystifier tous les buzzwords utilisés dans ce texte 😁 --- # [Talk] [SyliusDays 2025] - Translate the UIs of your Sylius applications efficiently URL: https://blog.welcomattic.com/talks/2025-05-22-translate-the-uis-of-your-sylius-applications-efficiently/ Date: 2025-05-22 [💬 Slides](https://welcomattic.github.io/sylius-ui-translation-slides/#/1) Leverage Symfony's Translation component to efficiently translate the UIs of your Sylius applications. This talk will cover best practices, tips, and tricks for managing translations in Sylius, ensuring your application is accessible to a global audience. From translation keys extractions to SaaS tools used to translate your static texts, this talk will cover everything you need to know to efficiently translate the UIs of your Sylius applications. --- # [Talk] [SymfonyLive 2025] - L'année des agents GenAI URL: https://blog.welcomattic.com/talks/2025-03-27-the-year-of-genai-agents/ Date: 2025-03-27 [💬 Slides (Speaker Deck)](https://speakerdeck.com/welcomattic/2025-lannee-des-agents-genia) · [Gamma](https://gamma.app/docs/2025-Lannee-des-Agents-GenAI-ulbl1uomqqurvgz) Lightning talk de 7 minutes présenté lors du SymfonyLive Paris 2025. --- # [Talk] [SymfonyLive Paris 2023] - Au moins 4 façons de définir un DateTime avec un Time réinitialisé (Lightning Talk) URL: https://blog.welcomattic.com/talks/2023-03-23-au-moins-4-facons-de-definir-un-date-time-avec-un-time-reinitialise/ Date: 2023-03-23 [💬 Slides](https://welcomattic.github.io/lt-datetime-sflive-2023) Lorsque nous créons des objets DateTime à partir d’une chaine de caractères comme « 2023–03–23 », à l’exécution PHP va définir les emplacements « heures », « minutes » et « secondes » aux valeurs instantannées (c’est à dire à l’heure de l’exécution). Or cette heure arbitraire n’est parfois pas ce que nous voulons. Voyons donc comment réinitialiser ces emplacements de temps facilement ! --- # [Talk] [SymfonyCon Disneyland Paris 2022] - Painless authentication with Access Tokens URL: https://blog.welcomattic.com/talks/2022-11-17-painless-authentication-with-access-token/ Date: 2022-11-17 [💬 Slides](https://speakerdeck.com/welcomattic/painless-authentication-with-access-tokens) Via some simple but real scenarios, we will discover the power of the new AccessToken Authenticator shipped in Symfony 6.2. For example, we are developing a SaaS product, which exposes a private API. Our users can register many applications into their accounts, for each one we will generate an API Token that users must inject in their requests. Now, with a pinch of YAML and a dash of PHP, we will be able to authenticate users from their API Token. In this talk, I will show you how it also works with JWT and some other exotic tokens! --- # [Talk] [SymfonyWorld Online 2020] - Internationalize your Symfony application URL: https://blog.welcomattic.com/talks/2020-12-04-symfony-world-2020-internationalize-your-symfony-application/ Date: 2020-12-04 [💬 Slides](https://docs.google.com/presentation/d/1Zz5Oxa-6jedobK4jOhuQ9ggTyVTtGcEP3Un_y0LdBoM/) Translating an application is a big step while you develop an international project. Translation process of an application can be very tough. It can remain laborious even after the first release. So, we have a new type of workmate: dear developers, product owners, designers, let me introduce translators with whom we will have to work efficiently! Together, we will have to make sure that the translations stay up to date on production. How? This is exactly what we will be talking about in this conference. Tools like php-translation bundle, Loco or Transifex will be very useful to your team. We will plug them in Symfony to create an efficient workflow. We will also define responsibilities for developers, translators and designers. Everything to ease the translation process from development environment to production without any pain! At the end of this talk, you will be looking forward to implementing these amazing solutions! --- # [Talk] [SymfonyLive Paris 2019] - Des images au cordeau pour vos applications Symfony URL: https://blog.welcomattic.com/talks/2019-03-29-symfony-live-paris-2019-des-images-au-cordeau-pour-vos-applications-symfony/ Date: 2019-03-29 [📺 Vidéo](https://symfonycasts.com/screencast/paris2019/des-images-au-cordeau-pour-vos-applications-symfony)  |  [💬 Slides](https://slides.com/welcomattic/des-images-au-cordeau-pour-vos-applications-symfony/) Le responsive est une pratique largement répandue dans nos projets depuis quelques années maintenant. De nombreuses techniques CSS permettent d'adapter les layouts de nos sites web à différentes tailles de terminaux. Mais lorsqu'il s'agit d'adapter les images, les techniques sont souvent moins connues, et parfois perçues comme expérimentales. Nous listerons ensemble quels sont les standards actuels permettant de charger la bonne image à la bonne taille et au bon format pour le bon terminal. Puis, nous verrons comment implémenter ces standards au sein d'une application Symfony, quels outils utiliser et comment tirer parti de Twig pour nous faciliter le travail ! --- # [Talk] [PHP Tour Montpellier 2018] - Traduire efficacement une application Symfony URL: https://blog.welcomattic.com/talks/2018-05-18-phptour-montpellier-traduire-efficacement-une-application-symfony/ Date: 2018-05-18 {{< youtube XdG5BZnJXFk >}} [💬 Slides](https://welcomattic.github.io/traduire-efficacement-une-app-symfony/phptour-2018/index.html#/) La traduction d'une application est une étape primordiale dans un projet international. Mais sa mise en place est souvent douloureuse, et ça le reste tout au long de la vie de l'application. Un nouveau type d'intervenant entre en jeu en plus des développeurs, chefs de projets, designers, voici maintenant les traducteurs. Comment faire travailler toutes ces personnes efficacement, et garder l'intégrité des traductions de l'application ? C'est ce que je vous propose d'aborder dans cette conférence. Nous verrons ensemble quels outils externes utiliser pour faciliter la traduction aussi bien pour les développeurs, que les traducteurs, mais aussi pour les intervenants en qualité qui sont chargés de vérifier tout ce travail. Nous verrons également comment brancher ces outils au sein de Symfony pour bénéficier d'un workflow de traduction cohérent et simple d'utilisation d'un environnement local à la production. À la fin de la conférence vous n'aurez qu'une envie, mettre à jour vos projets avec ces outils pour enfin vous épargner de longues journées de mise à jour de traductions ! --- # [Talk] [SymfonyLive Paris 2018] - Traduire efficacement une application Symfony URL: https://blog.welcomattic.com/talks/2018-03-30-symfony-live-paris-2019-traduire-efficacement-une-application-symfony/ Date: 2018-03-30 {{< youtube zP6vbkc-GJY >}} [💬 Slides](https://welcomattic.github.io/traduire-efficacement-une-app-symfony/symfony-live-paris-2018/index.html#/) La traduction d'une application est une étape primordiale dans un projet international. Comment faire travailler toutes ces personnes efficacement, et garder l'intégrité des traductions de l'application ? C'est ce que je vous propose d'aborder dans cette conférence. Nous verrons ensemble quels outils externes utiliser pour faciliter la traduction aussi bien pour les développeurs, que les traducteurs, mais aussi pour les intervenants en qualité qui sont chargés de vérifier tout ce travail. Nous verrons également comment brancher ces outils au sein de Symfony pour bénéficier d'un workflow de traduction cohérent et simple d'utilisation d'un environnement local à la production. À la fin de la conférence vous n'aurez qu'une envie, mettre à jour vos projets avec ces outils pour enfin vous épargner de longues journées de mise à jour de traductions !