Back to Browse

Apprendre Forelse MCP Server

by Oipnet
Developer ToolsLow Risk10.0MCP RegistryRemote
Free

Server data from the Official MCP Registry

Create shareable links to run Symfony, Laravel or plain PHP code in the browser, no install.

About

Create shareable links to run Symfony, Laravel or plain PHP code in the browser, no install.

Remote endpoints: streamable-http: https://apprendre.forelse.fr/mcp

Security Report

10.0
Low Risk10.0Low Risk

Valid MCP server (1 strong, 0 medium validity signals). No known CVEs in dependencies. Imported from the Official MCP Registry.

3 tools verified · Open access · No issues found

Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.

Permissions Found in Source Code

Found by scanning the linked source code. This listing connects to a hosted endpoint, so none of this runs on your machine: it describes what the server software does where it is hosted.

HTTP Network Access

Connects to external APIs or services over the internet.

database

Applies to the server that hosts this plugin, not to your machine.

file_system

Applies to the server that hosts this plugin, not to your machine.

How to Connect

Remote Plugin

No local installation needed. Your AI client connects to the remote endpoint directly.

Add this to your MCP configuration to connect:

{
  "mcpServers": {
    "io-github-oipnet-php-playground": {
      "url": "https://apprendre.forelse.fr/mcp"
    }
  }
}

Documentation

View on GitHub

From the project's GitHub README.

Forelse · apprendre — le moteur

Une plateforme pour apprendre à développer en codant, sans vidéo ni installation. L'apprenant écrit son code dans le navigateur, où tourne un vrai projet : PHP 8.5 compilé en WebAssembly pour Symfony et Laravel, et des simulateurs pour Docker et Nuxt. L'aperçu se met à jour en direct, et des tests automatiques valident chaque objectif. Rien n'est exécuté sur le serveur.

C'est le moteur de apprendre.forelse.fr, open source (AGPL-3.0) et auto-hébergeable. Les exercices n'en font pas partie : ils vivent dans des packs de contenu, que le moteur charge. Un pack de démonstration est fourni dans examples/packs/demo, et chacun peut écrire les siens. Les parcours de Forelse ne sont pas dans ce dépôt ; une école ou une entreprise peut les utiliser sur sa propre instance, sur devis (en savoir plus).

Architecture

platform/         Application Symfony 8.1 : pages, comptes, progression, API, content:check
    src/Content/Framework/   Ce que le moteur sait de chaque framework (un profil par runtime)
playground/       Îlot TypeScript : runtime PHP WebAssembly, éditeur Monaco, aperçu isolé
packages/           Ce qui peut vivre hors du moteur : contrat et simulateurs
  runtime-contract/  Ce qu'un runtime doit remplir, et ce que le moteur lui fournit (types seuls)
  simulateur-nuxt/   Le runtime Nuxt : simulateur, worker, greffons de build, lanceur de tests
  simulateur-docker/ Le simulateur Docker, bibliothèque PHP du runtime php-wasm
environments/     Projets de base dans lesquels s'exécutent les exercices, et de quoi les empaqueter
                  (bin/build-env.sh) — destiné à devenir son propre dépôt, voir environments/README.md
tools/            Simulateurs Docker (docker-sim) et Nuxt (nuxt-sim), publication d'une version
auto-hebergement/ Guide, compose.yaml et .env.example d'une instance auto-hébergée
deploy/           production et préproduction de l'instance de Forelse, derrière un proxy commun (deploy/README.md)
examples/packs/   Pack de démonstration (format de contenu, tests du moteur)
examples/themes/  Exemple de thème d'instance (marque blanche) : theme.yaml commenté et ses images

Dans le navigateur :

Page exercice (plateforme) ── worker PHP (php-wasm) : instance « aperçu » + instance « tests »
      │  postMessage (origine vérifiée)
      ▼
Relais /sandbox (origine bac à sable) ── Service Worker ── iframe d'aperçu (code de l'apprenant)

Le code de l'apprenant n'est jamais exécuté sur le serveur : PHP tourne dans le bac à sable WebAssembly du navigateur, sans accès au disque ni au réseau. Le playground offre aussi une console du projet (bin/console pour Symfony : Doctrine, debug:router… ; php artisan pour Laravel : migrate, route:list…), exécutée dans ce même bac à sable.

Le bac à sable (/bac-a-sable, /en/playground) a aussi un serveur MCP sur /mcp (HTTP, sans compte), pour les assistants IA (Claude, ChatGPT, Cursor…) : get_starter (projet de départ, chemins modifiables, paquets), create_sandbox (fabrique le lien partagé, rien n'est stocké) et read_sandbox (relit un lien). Voir App\Mcp\PlaygroundTools ; le lien suit le format de playground/src/app/scratch.ts (App\Scratch\ScratchLink). Quota par adresse IP : limiteur mcp. Chaque appel d'outil est un événement Umami envoyé par le serveur (mcp-depart, mcp-lien-cree, mcp-lien-lu : framework, langue, client MCP, jamais le code), voir App\Mcp\McpAnalytics ; les visites venues de ces liens portent utm_source=mcp. Ajout dans Claude Code : claude mcp add --transport http php-playground https://apprendre.forelse.fr/mcp. Documentation publique : /en/playground/ai ; fiche du registre MCP : mcp/server.json.

Démarrer en local

Prérequis : PHP 8.5 (avec pdo_pgsql), Composer, Node.js 22+, PostgreSQL 15+.

Créez une fois le rôle attendu par platform/.env (ou adaptez DATABASE_URL dans platform/.env.local) :

psql -d postgres -c "CREATE ROLE app LOGIN PASSWORD 'app' CREATEDB"
make install   # dépendances, environnements d'exécution, bases PostgreSQL (formation, formation_test)
make dev       # plateforme http://localhost:8000, bac à sable http://127.0.0.1:8001, Vite, Mailpit http://localhost:8025

Ouvrez http://localhost:8000. Pour charger d'autres packs, dans platform/.env.local :

CONTENT_PACKS_PATHS="%kernel.project_dir%/../examples/packs,/chemin/vers/mes-packs"

Tests : make test. Vérification des packs : make check.

make build écrase platform/public/build/.vite/entrypoints.json : la plateforme sert alors les fichiers compilés, même si Vite tourne encore. Après un build, relancez Vite (make dev-vite) pour retrouver le rechargement à chaud — sinon vos modifications front ne s'affichent pas.

Les serveurs de développement tournent avec -d xdebug.mode=off : Xdebug fait planter le serveur interne de PHP (segfault) sur certaines pages, et ralentit tout. content:check fait de même.

L'atelier des auteurs

/atelier écrit les exercices d'un pack depuis le navigateur : les fichiers tels qu'ils sont sur le disque (exercise.yaml, instructions.md, starter/, solution/, tests/), un bouton Vérifier qui lance content:check et affiche son verdict, et un lien pour jouer l'exercice comme un apprenant.

L'atelier écrit sur le disque et exécute le PHP du pack : il est réservé au rôle ROLE_AUTEUR.

⚠️ Sur des packs modifiables, ROLE_AUTEUR est un rôle d'administrateur. Vérifier lance PHPUnit sur les tests que l'auteur vient d'écrire, avec l'utilisateur système de la plateforme : un test peut lire les sessions, APP_SECRET et les variables d'environnement du serveur (clés Stripe et Anthropic, base). Ne le donnez qu'à qui pourrait administrer la machine. Les compose.yaml montent les packs en lecture seule : l'atelier y reste consultable, mais n'écrit rien, et Vérifier ne lance que ce que le dépôt de contenu contient déjà — le même code que sa CI. On écrit alors dans le dépôt de contenu, sur son poste ; ne retirez pas le :ro d'une instance en production.

bin/console app:auteur vous@example.com            # donner le droit
bin/console app:auteur vous@example.com --retirer  # le retirer

Un exercice se supprime depuis son éditeur (dossier et ligne de track.yaml), sauf s'il sert de base à un autre : le fil rouge se casserait.

Un parcours dont le dossier est en lecture seule reste consultable, mais n'est pas modifiable.

Chaque chapitre a sa fiche de cours (voir « Packs de contenu »), éditable depuis l'atelier : markdown à gauche, à droite l'aperçu rendu par le serveur, donc exactement ce que verra l'apprenant. Squelette assemble un point de départ à partir des exercices du chapitre, Rédiger avec l'IA demande un brouillon complet au modèle ; les deux sont proposés, jamais enregistrés sans relecture. Enregistrer une fiche vide la retire du chapitre.

Génération assistée (facultative)

Avec une clé d'API, l'atelier peut proposer un brouillon d'exercice à partir d'un sujet (« tester un voter avec des stubs »), dans l'univers et le style du parcours : consignes, fichiers de départ, solution, tests, indices, liens de documentation. Après un échec de content:check, un bouton Corriger avec l'IA lui renvoie le rapport et propose une correction.

Avec la même clé, un bouton Post LinkedIn apparaît sur la page d'un parcours et sur l'éditeur d'un exercice de Pratique — les deux objets du contenu qui ont une page publique à partager. Il ouvre /atelier/<parcours>/post, où le modèle écrit trois brouillons sous trois angles (annonce, coulisses, pédagogique) à partir du contenu déjà écrit : résumé, chapitres, notions, consignes. Chaque brouillon se relit et se retouche sur place, avec le compte de caractères que LinkedIn impose (3 000 au total, ~200 avant « …voir plus »), puis se copie. Rien n'est publié — l'atelier ne parle jamais à LinkedIn — et rien n'est enregistré : un post vit dans LinkedIn, pas dans le pack.

L'éditeur d'un exercice de Pratique mène aussi à sa version anglaise (/atelier/pratique/<exercice>/anglais) : le même éditeur, sur les fichiers de i18n/en/ (voir « Traduction »). Avec la clé, Traduire avec l'IA propose la traduction entière, en partant de celle qui existe : exercise.yaml réduit aux clés traduisibles, consignes, fichiers de code qui changent, mutants, et l'empreinte source calculée par la plateforme. Rien n'est enregistré avant que l'auteur relise ; Vérifier lance content:check sur la version anglaise. Une traduction en retard sur l'exercice français est signalée dans l'éditeur, avec l'empreinte à recopier.

# platform/.env.local
ANTHROPIC_API_KEY=sk-ant-…
AI_MODEL=claude-sonnet-5        # facultatif
AI_MODEL_POST=claude-opus-5     # facultatif : les posts LinkedIn seulement, vide = AI_MODEL

AI_MODEL vaut pour tout ce qui appelle le modèle ; AI_MODEL_POST ne vaut que pour les brouillons de post. Écrire un post n'est pas relire du code : on peut vouloir un modèle plus fort là où le texte part sous son propre nom, sans payer ce modèle sur chaque revue de code du mentor.

La même clé sert à content:lesson-draft --ia, qui rédige un brouillon de fiche de cours (voir « Packs de contenu »). Sans clé, l'atelier fonctionne à l'identique et ces boutons n'apparaissent pas. Un brouillon reste un brouillon : rien n'est enregistré sans relecture, et c'est content:check qui tranche — tests rouges au départ, verts avec la solution.

Le mentor de l'apprenant

Avec la même clé d'API, l'apprenant connecté dispose d'un mentor dans le playground, qui intervient là où un bon mentor interviendrait, jamais à sa place :

  • Une revue de code, sur demande, quand les tests passent : un regard sur ce qui pourrait être plus idiomatique, plus lisible ou plus sûr, en quatre points au plus, avec la solution de référence comme point de comparaison (jamais citée). Elle est conservée avec la progression.
  • Des erreurs expliquées : sur une page d'exception de l'aperçu, des tests qui plantent ou une commande bin/console en échec, un bouton « Expliquer l'erreur » traduit l'exception en cause probable et en piste (où regarder, quoi vérifier), sans donner la correction.

Les indices, eux, sont écrits par l'auteur dans exercise.yaml et ne coûtent rien. Le mentor est réservé aux comptes et limité à 40 appels par heure et par apprenant (config/packages/rate_limiter.yaml) : chaque appel se paie. Sans clé, les boutons n'apparaissent pas.

La solution de référence est consultable par un apprenant connecté (bouton « Solution »), à côté de son code, en lecture seule. Il est prévenu avant : l'exercice ne rapportera alors pas d'XP, même réussi ensuite avec son propre code. Un invité n'y a pas accès : les solutions font partie du contenu.

Parcours payants

Une seule question décide de ce qu'un apprenant ouvre : a-t-il accès à ce chapitre ? (TrackAccessChecker, utilisé par les pages, les API de l'exercice, de la progression et du mentor, les fiches et le livret).

  1. Sans compte, rien ne se joue : la page d'un exercice reste publique et lisible (consigne, notions), mais l'éditeur, l'exécution et les tests demandent un compte.
  2. Le premier chapitre d'un parcours public est ouvert, gratuitement, à tout compte connecté.
  3. Un administrateur ou un auteur ouvre tout.
  4. Un parcours sans tarif, ou à 0 €, est ouvert à tout compte : une instance qui ne fixe aucun prix reste gratuite.
  5. Un chef de cohorte ouvre les parcours de ses cohortes.
  6. Sinon, il faut un accès actif : acheté, ouvert par une cohorte ou offert (/admin → Accès). Un accès expiré ou révoqué garde la progression, qu'un nouvel accès retrouve intacte.

Les prix (TTC, avec prix fondateur limité par une date ou un quota) se fixent dans /admin → Tarifs ou avec bin/console app:tarif. L'achat passe par Stripe Checkout et Stripe Tax (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_TAX_CODE) ; seul le webhook /paiement/stripe/webhook ouvre l'accès, à vie. Les événements reçus sont journalisés et se rejouent avec bin/console app:stripe:rejouer. Un administrateur rembourse depuis /admin → Achats (l'accès est révoqué).

Une cohorte est financée par l'établissement (ses apprenants reçoivent l'accès aux parcours choisis, aux dates de la cohorte ; l'estimation du devis se règle dans config/services.yaml, app.cohort_quote.*) ou par ses apprenants (chacun achète, au tarif de la cohorte s'il est fixé).

Un chef de cohorte peut organiser des soutenances de fin de parcours (/cohorte/{id} → Soutenances) : il ouvre des créneaux en ligne (lien de visio) ou en présentiel (adresse, salle), un apprenant qui a réussi tous les exercices du parcours en réserve un depuis la page du parcours et reçoit sa convocation avec une invitation d'agenda (.ics). L'évaluateur (chef de la cohorte ou administrateur) lit le dossier de l'apprenant (code rendu, indices, solutions consultées, revue du mentor), remplit la grille (un niveau par chapitre) et donne le résultat, envoyé par email. Les résultats s'exportent en CSV.

Mise à jour d'une instance vers la 0.8 : après la migration, lancer une fois bin/console app:acces:initialiser (les cohortes et les apprenants existants gardent ce qu'ils avaient), puis seulement fixer des prix.

Packs de contenu

<pack>/pack.yaml                       id, titre, version, licence, moteur attendu, parcours
<pack>/tracks/<parcours>/track.yaml    environnement, chapitres → exercices
<pack>/tracks/<parcours>/about.md      présentation du parcours (Markdown, facultative) : ce qu'on y apprend, pour qui
<pack>/tracks/<parcours>/chapters/<chapitre>/
    lesson.md         fiche de cours de fin de chapitre (Markdown, facultative)
<pack>/tracks/<parcours>/exercises/<exercice>/
    exercise.yaml     titre, XP, accès, fichiers éditables / en lecture seule, objectifs, indices
    instructions.md   consignes (Markdown)
    starter/          fichiers de départ, superposés à l'environnement
    tests/            tests PHPUnit cachés ; chaque objectif = une méthode de test
    solution/         solution de référence (jamais envoyée au navigateur, sauf en dev)
<pack>/practice/<exercice>/            exercice de Pratique, hors parcours (même contenu qu'un exercice)
<pack>/versions/<version>.md           intro d'une page de nouveautés (Markdown, facultative)
<pack>/notions/<notion>.md             intro d'une page de notion (Markdown, facultative ; sans elle, la page ne s'indexe pas)
<pack>/notions/en/<notion>.md          intro d'une page de notion anglaise (/en/concepts/<notion>, nommée par son slug anglais)
<pack>/notions/themes.yaml             rangement de l'index /notions : par framework, des thèmes et leurs notions (facultatif)
<pack>/notions/en/themes.yaml          rangement de l'index /en/concepts, par les libellés anglais des notions (facultatif)
<pack>/articles/<slug>.md              article du blog (en-tête YAML, puis Markdown)
<pack>/articles/en/<slug>.md           sa traduction anglaise (même format, plus translation_of)
  • duration: (dans exercise.yaml, facultative) : la durée estimée de l'exercice, en minutes, pour le public du parcours (duration: 25). La carte du parcours l'affiche par exercice, par chapitre (« ≈ 3 h 15 ») et pour le parcours (« environ 46 h de pratique »), ainsi que l'accueil et le JSON-LD du cours (timeRequired). Un chapitre ou un parcours dont un exercice n'a pas de durée n'en affiche pas : une somme partielle promettrait trop peu.

  • Pratique (<pack>/practice/<exercice>/) : de courts exercices à part des parcours, pour se servir d'une fonctionnalité d'un framework, souvent une nouveauté d'une version, parfois un point précis. Pas de chapitre, pas de fiche, pas d'XP, pas d'histoire : le code de départ est écrit à l'ancienne, les tests vérifient le comportement et l'usage de la fonctionnalité (par réflexion, par exemple). Le moteur découvre les dossiers de practice/ tout seul, sans liste dans pack.yaml. exercise.yaml garde son format, avec en plus :

    • environment: (obligatoire : il n'y a pas de parcours dont l'hériter) ;
    • published: AAAA-MM-JJ (obligatoire) : la liste /pratique va du plus récent au plus ancien. Une date à venir programme l'exercice : visible des seuls administrateurs (badge « Programmé »), il est publié automatiquement ce jour-là ;
    • summary: (obligatoire), une phrase affichée dans la liste ;
    • version: '8.1' (facultative, entre guillemets) : la version du framework qui apporte la fonctionnalité ; elle affiche le badge « Nouveauté » et content:check échoue si l'environnement a une version plus ancienne (lue dans son composer.lock) ;
    • pull_request: (URL https, facultative), proposée à la réussite ;
    • visibility: admin (facultative), comme pour un parcours : un exercice en préparation.

    access, base et xp y sont refusés : la Pratique demande toujours un compte et ne rapporte pas d'XP. Les identifiants sont uniques parmi les exercices de Pratique de tous les packs, et un parcours ne peut pas s'appeler pratique. La liste /pratique est publique, filtrable par framework, par notion (concepts:) et par nouveauté ; l'exercice se joue sur /pratique/<exercice>. À la réussite, le playground propose un « Avant / après » (le code de départ face à celui de l'apprenant). L'atelier crée un exercice de Pratique en préparation (visibility: admin) dans un pack modifiable.

    Traduction (<exercice>/i18n/en/) : l'exercice en anglais, joué sur /en/practice/<exercice>. La Pratique anglaise ne montre que les exercices traduits ; un exercice non traduit y répond 404. Le dossier contient :

    • exercise.yaml, avec les seules clés traduites : title, summary, objectives, hints, requests, docs, mutants (obligatoires dès que l'exercice français les a, sinon le texte français s'afficherait), concepts, preview et readonly (facultatives ; readonly quand un fichier montré en lecture seule a son équivalent dans l'environnement de la langue, lang/en/validation.php au lieu de lang/fr/validation.php). Les mutants traduits gardent leurs ids, que les objectifs citent ; leurs search visent les lignes du code traduit. Le reste (environnement, fichiers, date, version…) vient de l'exercice français ; une autre clé est refusée ;
    • instructions.md, la consigne complète ;
    • au besoin, des fichiers de starter/, solution/ et tests/ qui remplacent ceux de l'exercice français au même chemin, quand le code contient du français (messages, noms de méthodes de test). Un chemin que l'exercice français n'a pas est refusé.

    content:check vérifie chaque traduction comme un exercice à part (pratique/<exercice> [en]). La clé source recopie l'empreinte de l'exercice français : quand il change, content:check signale la traduction en retard et donne la nouvelle empreinte à recopier une fois la traduction à jour. Une réussite en anglais est revérifiée avec les tests anglais. Les pages de notions et de nouveautés restent en français : la page anglaise n'y renvoie pas.

    Traduction d'un parcours : chaque exercice se traduit comme en Pratique (<exercice>/i18n/en/, sans summary), et le parcours lui-même dans <parcours>/i18n/en/ :

    • track.yaml, avec title, description et chapters, qui associe à l'id de chaque chapitre son title (et sa description si le chapitre français en a une) ; source recopie l'empreinte du parcours français (track.yaml, fiches de cours et présentation) ;
    • chapters/<chapitre>/lesson.md, la fiche de cours traduite de chaque chapitre qui en a une ;
    • about.md, la présentation traduite, si le parcours français en a une.

    Un parcours ne paraît en anglais qu'entièrement traduit : il ne fait pas passer l'apprenant d'une langue à l'autre. Ses exercices peuvent se traduire avant lui, un par un : content:check les vérifie déjà. Une traduction devenue incomplète (un exercice ou un chapitre ajouté en français) ne bloque rien : le parcours anglais disparaît, et content:check dit ce qui manque. Le parcours de démo decouverte est traduit en exemple. Un parcours traduit a ses pages sous /en/tracks/<parcours> ; ailleurs, l'adresse anglaise renvoie au français.

  • Blog (<pack>/articles/<slug>.md) : des articles sur les coulisses du contenu et du moteur, publiés sur /blog/<slug>. Le nom du fichier est l'adresse de l'article (minuscules, chiffres et tirets). Le fichier commence par un en-tête YAML entre deux lignes ---, suivi du Markdown (rendu comme une fiche de cours : HTML brut supprimé, code coloré ; un # Titre en tête est retiré, la page affiche le sien) :

    ---
    title: Faire tourner Symfony dans le navigateur       # obligatoire : le h1 et le title de la page
    description: Comment un vrai projet Symfony tourne…   # obligatoire, sans HTML : la meta description
    published: 2026-10-06                                 # obligatoire ; une date à venir programme l'article
    updated: 2026-10-20                                   # facultative : dernière révision du fond
    visibility: admin                                     # facultative : en préparation
    practice: laravel-13-26-read-through                  # facultative : l'exercice de Pratique dont part l'article
    course: symfony-pour-dev-php                          # facultative : le parcours proposé en fin d'article
    author: grace-hopper                                  # facultative : un des « authors » du thème (sinon « person »)
    tags: [Symfony, Sécurité]                             # facultative : les sujets, filtres de la liste
    ---
    

    Toute autre clé est refusée (une faute de frappe ne passe pas inaperçue). Un article programmé ou en préparation n'est ni dans la liste /blog ni dans le sitemap ; seuls les administrateurs le lisent à son adresse. Une instance dont aucun pack n'a d'article paru n'a pas de blog : /blog répond 404 et le pied de page n'y mène pas. Chaque article porte ses données structurées (BlogPosting) ; sa date dans le sitemap est updated, à défaut published.

    La liste met l'article le plus récent à la une, puis range les autres par mois. Deux articles du même jour se départagent par une heure (published: 2026-10-28 14:00:00, en UTC ; sans heure, minuit) : le plus tardif passe devant. L'heure ne fait qu'ordonner, l'article paraît dès le matin de son jour. Les sujets (tags:, des libellés courts, tels qu'ils s'affichent) donnent les filtres de la liste, du plus fourni au moins fourni : /blog?sujet=securite ne montre que les articles « Sécurité » (page suivie, non indexée). Une traduction écrit ses propres sujets, dans sa langue. Dans le flux RSS, les sujets deviennent des <category> : dev.to, qui importe le flux, en fait les tags de l'article (quatre au plus, sans espace ni accent).

    Chaque article a son image, dessinée par le moteur comme celles de la Pratique (share_card du thème) : « Blog · », le titre et le site, en 1200 × 630 sur /blog/<slug>/image-de-partage. Elle sert d'aperçu sur les réseaux, d'image des données structurées et de vignette dans le flux RSS (enclosure et media:content). dev.to ignore les images du flux : collez l'adresse de l'image dans « Cover image » avant de publier le brouillon.

    Chaque article se termine par une invitation à essayer dans le navigateur. Avec practice: (l'identifiant d'un exercice de Pratique, qui doit exister), elle mène à cet exercice, et une ligne sous le titre y invite aussi ; sans, elle mène à la liste des exercices de Pratique. Une traduction sans practice: reprend celle de l'article français ; si l'exercice n'est pas traduit, elle invite à la Pratique anglaise. Les clics se comptent dans Umami (événements article-exercice, avec sa position, et article-pratique).

    L'encart « Aller plus loin » propose un parcours, dans la colonne collante sur un écran large (au-dessus des contenus proches) et sous l'invitation à essayer sur un téléphone : celui de course: (l'identifiant d'un parcours, qui doit exister), sinon le parcours du framework de l'exercice de practice:, comme après un exercice de Pratique. Un parcours en préparation ou non traduit dans la langue de la page n'est pas proposé. Une traduction sans course: reprend celui de l'article français. Les clics se comptent dans Umami (article-parcours, avec le parcours et la position : colonne ou fin).

    Un article d'au moins trois intertitres (##) affiche son sommaire en tête ; chaque intertitre a son ancre. Les blocs de code ont un bouton « Copier ». Sous l'article, l'encart « Écrit par » présente la personne du thème quand elle déclare bio ou links (voir « Thème » plus bas).

    Un bloc de code peut s'ouvrir dans le bac à sable : après son langage, sandbox=php, sandbox=symfony ou sandbox=laravel (le fichier ouvert à l'arrivée du bac à sable), ou sandbox=symfony:src/Controller/MenuController.php pour un autre fichier modifiable. Le bloc devient tout le fichier, il doit donc se suffire (namespace, use…). Dessous, un lien « Essayer ce code dans le bac à sable » emporte le code dans l'adresse (rien n'est stocké) ; content:check refuse un bac à sable ou un fichier qu'il n'accepte pas. Les clics se comptent dans Umami (article-essayer, avec le framework).

    ```php sandbox=symfony:src/Controller/MenuController.php
    <?php
    namespace App\Controller;
    …
    ```
    

    Un article se traduit en anglais dans <pack>/articles/en/<slug>.md, publié sur /en/blog/<slug> avec sa propre adresse (running-symfony-in-the-browser.md). Même en-tête, plus translation_of:, le nom de l'article français qu'il traduit (une traduction par article). Le blog anglais a sa liste, son flux (/en/blog/rss.xml) et ses entrées du sitemap ; l'interface autour des articles passe en anglais, mais ses liens mènent aux pages françaises. Les deux versions parues se nomment l'une l'autre (hreflang, la française en x-default) et chaque article propose de lire l'autre. Une traduction programmée ou en préparation suit les mêmes règles qu'un article. Un moteur antérieur ignore le dossier en/.

  • moteur: (dans pack.yaml) déclare la ou les versions du moteur avec lesquelles le pack fonctionne, avec la syntaxe de Composer (^0.1, >=1.2 <2.0). Un moteur qui ne la satisfait pas refuse de charger le pack et le dit, plutôt que de le casser en silence. La clé est facultative — un pack livré avec le moteur n'en a pas besoin — mais un pack distribué à part a tout intérêt à la déclarer (voir « Versionnage »).

  • access est dépréciée (sans effet depuis la 0.8.0, signalée par content:check) : le premier chapitre de chaque parcours public est gratuit pour tout compte, la suite dépend de l'accès au parcours (voir « Parcours payants »).

  • editable: accepte des motifs (migrations/*.php ; * ne franchit pas un « / ») pour les fichiers qu'une commande va créer, dont le nom est imprévisible. Tout ce qu'une commande de la console crée, modifie ou supprime sous src/, migrations/, templates/, config/, tests/ ou translations/ (par exemple doctrine:migrations:diff) est reporté dans l'éditeur — un fichier modifiable s'ouvre aussitôt —, dans l'explorateur, dans l'instance de tests et dans le brouillon. « Réinitialiser » retire ces fichiers générés. Quand l'exercice a un motif, le bouton + de l'explorateur crée un fichier qu'il couvre, avec le bon namespace. Les fichiers déjà présents qu'un motif couvre (src/Entity/*.php) ne s'ouvrent pas tous au démarrage : l'apprenant les ouvre depuis l'explorateur. open: doit toujours désigner un fichier, jamais un motif ; ce fichier peut être couvert par un motif s'il existe au départ (content:check le vérifie). Les commandes externes lancées par PHP (proc_open, comme le php-cs-fixer de MakerBundle) se terminent sans rien faire : make:entity, make:controller… fonctionnent dans la console du navigateur.

  • Les tests d'exercices disposent d'outils fournis par l'environnement, dans tests/Formation/ : BaseDeDonnees (recréer la base depuis les entités, enregistrer des objets), FileDeMessages (file Messenger, worker) et MigrationsDeLaBase (vider la base, jouer les migrations jusqu'à une version, comparer le schéma aux entités).

  • setup: liste des commandes bin/console lancées au chargement de l'aperçu, par exemple doctrine:schema:update --force : la base de l'aperçu (SQLite) vit en mémoire et repart de zéro à chaque chargement.

  • requests: propose des requêtes toutes faites dans l'onglet « Requêtes » du playground, un petit client HTTP pour tester une API : {title, method, path, headers, body}. Un body écrit en objet YAML est envoyé en JSON, et {{ date:+7 }} y est remplacé par la date du jour + 7 jours, pour que les exemples ne se périment pas.

  • Les exercices dont l'application crée une base SQLite (var/*.db, database/*.sqlite) ont en plus un onglet « Base » dans le playground : les tables et leurs premières lignes, lues directement dans le fichier. Une valeur chiffrée ou binaire s'y montre en hexadécimal. Rien à déclarer : l'onglet apparaît dès que la base existe.

  • Exercices où l'apprenant écrit ses tests : ses fichiers éditables tests/…Test.php sont ses tests, notés par deux sortes d'objectifs.

    • own-tests: pass : ses tests passent tous sur l'application correcte, avec au moins un test.
    • mutant: <id> : ses tests échouent sur un mutant, une version volontairement cassée de l'application, déclarée dans mutants: [{id, label, changes: [{file, search, replace}]}].

    Chaque mutant est appliqué, testé puis retiré, de la même façon dans le navigateur et dans content:check. content:check vérifie que chaque texte à remplacer existe, et que la solution détecte chaque mutant.

  • Exercices à étapes (steps:), pour avancer au rythme du TDD, rouge, vert, refacto : l'apprenant garde son code d'une étape à l'autre. Chaque étape déclare {id, phase, title, editable, open?, objectives} ; sa consigne est dans steps/<id>/instructions.md (sous l'instructions.md de l'exercice, qui présente le tout) et sa solution dans steps/<id>/solution/, posée par-dessus celles des étapes d'avant. objectives, editable, open et hints ne se règlent plus pour tout l'exercice : les fichiers modifiables de l'exercice sont ceux de toutes ses étapes, et ses objectifs (ceux que rejoue la vérification d'une réussite) ceux de la dernière. Pendant une étape, les fichiers modifiables d'une autre étape restent visibles, verrouillés : les tests au vert et au refacto, le code au rouge.

    steps:
      - id: rouge-bidon-vide
        phase: red                 # red, green ou refactor
        title: Un bidon vide ne rapporte rien
        editable: [tests/PaieDuLaitTest.php]
        objectives:
          - {own-tests: fail, label: Votre test échoue sur votre code}
          - {own-tests: reference, label: … et passe sur le code attendu}
      - id: vert-bidon-vide
        phase: green
        title: Zéro centime
        editable: [src/PaieDuLait.php]
        objectives:
          - {own-tests: pass, label: Votre test passe}
          - {test: testVerifieUnBidonVide, label: Un bidon vide rapporte 0 centime}
    

    Une étape rouge a deux objectifs à elle : own-tests: fail, un test de l'apprenant au moins échoue sur son code (le message de PHPUnit s'affiche, lire l'échec fait partie de l'étape), et own-tests: reference, ses tests passent tous sur le code attendu, l'état du projet à la fin de l'étape verte qui suit (obligatoire) : un test qui échoue pour une mauvaise raison ne valide pas l'étape. Ce code attendu part au navigateur avec l'exercice, comme les tests cachés ; il ne donne que le code de l'étape verte suivante. Un test caché ne porte jamais le nom d'un test de l'apprenant (content:check le refuse). content:check vérifie les étapes une à une : au départ d'une étape, ses objectifs ne sont pas tous atteints (un simple avertissement pour un refacto) ; avec sa solution, ils le sont. La progression retient l'étape atteinte ; « Voir la solution » donne celle de l'étape en cours, et « Réinitialiser » ramène à la première. Les étapes se traduisent dans i18n/<langue>/exercise.yaml (steps: avec les mêmes ids, dans le même ordre : title et objectives) et i18n/<langue>/steps/<id>/. Un moteur antérieur à la 3.34 refuse un exercice à étapes (aucun objectif) : déclarez moteur: '>=3.34'.

  • docs: liste la documentation qui aide à résoudre l'exercice, {title, url} (URL http(s) uniquement), affichée sous les objectifs. bin/console content:links [pack|parcours|parcours/exercice|pratique|pratique/exercice] vérifie que chaque page répond et que chaque ancre (#…) existe encore (accès réseau requis). Les liens des fiches de cours et les pull_request de la Pratique sont vérifiés en même temps.

  • Fiche de cours : chapters/<chapitre>/lesson.md résume ce qu'un chapitre a enseigné ; l'apprenant y accède une fois le chapitre réussi. Elle commence à ## (le moteur affiche le titre du chapitre en #), utilise des blocs de code avec leur langue (php, twig, yaml…) et des liens https:// ; le HTML brut n'est pas rendu. Structure conseillée : « Ce que vous avez appris », une section par concept avec l'extrait de code clé, « Les pièges », « Pour aller plus loin ». content:check signale (sans échouer) les chapitres qui n'en ont pas. Pour ne pas partir d'une page blanche, bin/console content:lesson-draft <parcours>/<chapitre> écrit un squelette assemblé à partir des exercices du chapitre (concepts, sections « Rappel » des consignes, liens de documentation) ; avec --ia et une clé d'API (voir l'atelier), le modèle rédige un brouillon complet, à relire. --force remplace une fiche existante. La fiche se lit en HTML (/parcours/<parcours>/chapitre/<chapitre>) et se télécharge en PDF, établi au nom de l'apprenant (dompdf, en PHP pur : aucune dépendance système). Une fois le parcours terminé, /parcours/<parcours>/livret.pdf regroupe toutes ses fiches derrière une page de garde et un sommaire.

  • Environnements : environments/<id>/environment.yaml décrit un projet de base (id, title, php), empaqueté par environments/bin/build-env.sh [<id>] (sans argument : tous) en archive zip servie au navigateur, avec un index de complétion généré par Reflection. Tout ce qui concerne les environnements vit dans ce dossier — les projets et leur empaquetage —, prêt à partir dans son propre dépôt : il ne dépend d'aucun code du moteur, et le moteur n'y entre que par ENVIRONMENTS_DIR et ce script (voir environments/README.md). framework: choisit le runtime (symfony par défaut, laravel, docker, nuxt) : sa console, l'organisation de son projet, ses caches, son lanceur de tests. framework: docker bascule sur le simulateur Docker (voir plus bas) ; framework: laravel sur les conventions de Laravel : console artisan, dossiers (app/, routes/, resources/…), vues Blade compilées dans storage/framework/views vidées entre deux runs de tests (cache: pour un autre dossier). extends: <id> prolonge un autre environnement : le projet joué est la superposition de la chaîne, du plus général au plus particulier, et l'environnement ne porte que ce qu'il ajoute ou remplace (symfony-8-app est passé de 45 à 26 fichiers, symfony-8-doctrine de 33 à 14). Les clés d'environment.yaml s'héritent sauf title ; vendor/ ne se superpose pas — il vient du seul dossier de la chaîne qui déclare un composer.json, si bien qu'un environnement sans dépendance propre n'a ni composer.json ni composer.lock à porter. Détails et règles dans environments/README.md. Un fichier .archiveignore (motifs zip -x, un par ligne) allège l'archive, par exemple des traductions de vendor/ inutiles à l'apprenant. Les environnements Laravel forcent APP_ENV=testing et APP_RUNNING_IN_CONSOLE=true dans phpunit.xml : Laravel n'exempte les tests de la vérification CSRF que si les deux moitiés de runningInConsole() && runningUnitTests() sont vraies, et la première se déduit de PHP_SAPI, qui vaut wasm dans le navigateur (ni cli ni phpdbg) — sans cela, tout POST d'un test de fonctionnalité répond 419. L'aperçu, lui, vérifie le jeton pour de vrai. Leur .env est livré tel quel : rien n'y est secret, le projet ne quitte jamais le bac à sable.

  • Runtimes : tout ce que le moteur sait d'un framework est déclaré une fois, dans un profil (platform/src/Content/Framework/Profiles/) : nom, console et commande d'exemple, dossiers du projet et du code, caches à vider, dossiers masqués, racines de namespace, lanceur de tests (PHPUnit ou Vitest), paquet dont la version fait foi pour version:, langages des fiches de cours et conventions données au modèle qui rédige. Le navigateur le reçoit dans la charge utile de l'exercice au lieu de le réécrire : l'éditeur ne contient plus de table de frameworks, seulement le code propre à chaque runtime (le worker, les snippets, le script de la console).

    Ajouter un framework : fournir un FrameworkProfileProvider, une classe découverte par son étiquette de service. Rien d'autre, tant qu'il s'exécute avec un runtime existant — Laravel et le simulateur Docker tournent tous deux sur php-wasm, comme Symfony.

    Ajouter un runtime — une autre façon d'exécuter un projet dans le navigateur — se fait en installant un paquet, sans modifier un fichier du moteur. Le paquet déclare dans son package.json :

    { "forelse": { "runtime": "./src/browser/forelse.ts", "vite": "./src/node/vite.ts" } }
    

    runtime exporte le manifeste (identifiant, libellé, fabrique) ; vite, facultatif, ce que le runtime demande au build — greffons, constantes, alias. Le playground parcourt ses dépendances au build, trouve les paquets qui portent ce champ et les enregistre seuls (discover-runtimes.ts). Aucune liste à tenir à jour. Côté serveur, le profil du framework déclare runtime: 'mon-runtime' et arrive par l'étiquette de service app.framework.

    Le simulateur Nuxt est le premier à passer par ce chemin : @forelse/simulateur-nuxt porte son runtime, son worker, ses greffons Vite et son lanceur de tests, et playground/vite.config.ts ne nomme plus Nuxt nulle part. Les types que les deux côtés partagent vivent dans @forelse/runtime-contract.

    Les tests côté serveur suivent la même règle. content:check doit rejouer les tests d'un exercice avec le même runner que le navigateur, sinon les deux verdicts divergent. Un framework dont les tests ne sont pas du PHPUnit déclare donc testModule dans son profil — un spécificateur de paquet, jamais un chemin :

    testModule: '@forelse/simulateur-nuxt/tests',
    

    Le moteur demande à Node de le résoudre depuis playground/, là où les paquets de runtime sont installés : il ne sait ni où ce paquet vit, ni qu'il existe. Le module reçoit le dossier du projet puis les fichiers de test, et écrit sur la sortie standard un JSON {cases: [{name, status, file, message}], output}.

Documentation truncated — see the full README on GitHub.

Reviews

No reviews yet

Be the first to review this server!