Accueil / Articles

Automatiser le développement Laravel avec des agents IA : créer, tester, corriger, répéter

Utilisez Laravel Boost et l’OpenCode CLI pour exécuter un cycle de compilation, de test et de correction grâce à des tâches en file d’attente, un orchestrateur, des limites de tentative et une révision humaine avant que quoi que ce soit ne soit déployé.

Éditeur : REACTAPP.TOP14 min de lecture3315 mots

Sources: Build → Test → Fix → Repeat: Automating Laravel Development with AI Agents

La course infinie du copier-coller

Comptez combien de fois une journée typique avec un assistant de codage IA se déroule ainsi : on écrit une demande, on copie le résultat dans l’éditeur, on exécute php artisan test, on constate une erreur. On copie le texte d’erreur, on le colle à nouveau dans l’IA en demandant « veuillez corriger ceci », puis on recommence depuis l’étape de copie.

Certains développeurs répètent ce cycle une douzaine de fois par jour. On qualifie cela de développement assisté par l’IA, mais celui qui travaille le plus dans ce cycle n’est pas l’IA : c’est la personne qui transmet les messages entre le modèle et le terminal, en décidant quand exécuter les tests, en lisant les erreurs, en jugeant s’une autre correction est nécessaire, et en copiant tout d’un bout à l’autre. L’IA reste inactive jusqu’à ce que la prochaine instruction arrive.

Une meilleure organisation élimine le messager humain. Un petit système peut gérer seul le cycle de création, de test, de correction et de répétition jusqu’à ce que le travail soit véritablement terminé, et une personne n’intervient que pour la révision finale. Les outils utilisés ici sont Laravel Boost et OpenCode CLI, deux outils adaptés à l’écosystème Laravel actuel. Les explications sont volontairement lentes afin que toute personne n’ayant jamais utilisé aucun de ces outils puisse suivre.

Vocabulaire

Quelques termes reviennent fréquemment :

  • Agent : une intelligence artificielle à qui ont été confiées une tâche définie ainsi qu’un ensemble d’outils définis, comparable à un employé ayant un rôle clairement défini par écrit.
  • MCP (Model Context Protocol) : un connecteur permettant à l’intelligence artificielle de poser des questions directement à votre système, que ce soit la base de données ou les journaux, ce qui l’empêche de deviner en se basant sur ce que vous avez tapé par hasard.
  • CLI (interface en ligne de commande) : logiciel de gestion qui fonctionne par l’entrée de commandes dans une invite de terminal, au lieu d’utiliser un écran graphique.
  • Tâches et file d’attente : manière dont Laravel gère les tâches en arrière-plan, une par une, afin que aucun utilisateur ne soit obligé d’attendre indéfiniment.
  • Orchestrateur : le contrôleur qui définit l’ordre des tâches, suit à qui revient son tour et sait où reprendre en cas d’échec.
  • La question a évolué au-delà de « l’IA peut-elle écrire du code ? »

    Tout ceux qui ont déjà utilisé Claude Code, Cursor ou un outil similaire avec Laravel savent que l’IA peut générer des migrations, des modèles, des politiques et des tests. Le problème restant concerne le flux de travail manuel associé :

    Developer → AI → Developer → Test → Developer → AI → Developer → Test → ...
    

    Chaque flèche représente une personne qui transfère manuellement le contexte d’une étape à l’autre, décidant qu’il est temps de tester ou qu’une erreur doit être renvoyée à l’IA.

    Comparez avec une version automatisée :

    Developer → Orchestrator → Build → Test → (failed? → Fix → Test again) → Review → Done
    

    L’écart semble petit, mais il entraîne de nombreux changements. Il existe désormais un orchestrateur, une couche qui connaît l’ordre des tâches, le moment de réessayer et celui d’arrêter, sans qu’il soit nécessaire que quelqu’un surveille constamment la situation tous les trente secondes.

    Laravel évolue également dans cette direction. Laravel Boost a été conçu pour que les agents de codage basés sur l’IA reçoivent un contexte réel concernant une application Laravel, au lieu de deviner en se basant sur des modèles génériques.

    Les deux outils principaux

    Laravel Boost : un intermédiaire entre l’IA et l’application

    Même si une intelligence artificielle est forte en général pour Laravel, elle peut rester complètement incapable de gérer votre projet spécifique. Elle ne sait pas quelle version de Laravel vous utilisez, comment votre base de données est réellement structurée, si vous employez Livewire ou Inertia, ni quelles conventions de nommage votre équipe a adoptées.

    Boost est un paquet Composer qui permet à l’IA de prendre connaissance de la situation réelle du projet. Il fonctionne via MCP, ce qui permet à l’IA de consulter directement l’application : examiner le schéma de la base de données, visualiser les routes existantes, voir l’erreur la plus récente, exécuter des commandes Artisan et rechercher des informations dans les documents Laravel correspondant aux versions réellement installées du paquet.

    L’installation nécessite deux commandes :

    composer require laravel/boost --dev
    
    php artisan boost:install
    

    Le deuxième est interactif. Il demande quel IDE ou outil d’IA vous utilisez et installe les ressources correspondantes.

    Il convient de préciser clairement que Boost traduit le contexte sans prendre de décisions. Il ne dira pas « exécutez les tests maintenant » ni « lancez l’agent de correction en cas d’échec ». Sa seule fonction est d’empêcher les agents d’ignorer l’état réel de l’application.

    OpenCode : où l’agent effectue le travail

    Si Boost traduit le contexte, OpenCode constitue l’atelier : une interface en ligne de commande conçue spécifiquement pour le codage par intelligence artificielle. Contrairement à un chatbot ordinaire, il vous permet de définir plusieurs agents ayant des responsabilités différentes, par exemple un qui écrit du code et un autre qui lit les résultats des tests, chacun disposant de ses propres instructions et limites.

    OpenCode peut également se connecter à des serveurs MCP, y compris Boost. Une fois connecté, tout ce que Boost propose devient disponible pour l’agent qui s’exécute à l’intérieur d’OpenCode.

    Pratique : connecter Boost à OpenCode

    Commencez par essayer les choses manuellement dans le terminal, avant d’automatiser quoi que ce soit.

    1. Diriger OpenCode vers Boost

    Avec Boost installé, OpenCode doit savoir où se trouve son serveur MCP. Ces informations sont stockées dans un fichier opencode.json situé à la racine du projet :

    {
      "$schema": "https://opencode.ai/config.json",
      "mcp": {
        "laravel-boost": {
          "type": "local",
          "command": ["php", "artisan", "boost:mcp"],
          "enabled": true
        }
      }
    }
    

    La commande exacte peut varier en fonction de la version de Boost, il faut donc suivre celle générée par php artisan boost:install plutôt que de la copier aveuglément. C’est l’un des éléments les plus susceptibles de changer entre les versions.

    Vérifiez que OpenCode peut voir le serveur :

    opencode mcp list
    

    Si laravel-boost est indiqué comme connecté, les agents ont accès à tout ce que Boost met à disposition.

    2. Créer des agents avec des responsabilités distinctes

    Au lieu d’un seul agent pour toutes les tâches, divisez les fonctions. Créez un fichier .opencode/agent/builder.md dans le projet :

    ---
    description: Build Agent for new features
    mode: primary
    tools:
      write: true
      edit: true
    ---
    You are the Build Agent.
    Use Laravel Boost tools to understand the application structure
    before writing any code. Never assume the database structure -
    always check the schema first. Write tests for every new behavior.
    At the end of your work, return ONLY JSON in this format:
    {"status": "success", "files_changed": [...], "summary": "..."}
    

    La ligne tools.write: true permet à cet agent de modifier des fichiers. Un agent ultérieur recevra write: false, ce qui le renda uniquement en lecture et incapable de modifier quoi que ce soit, et la raison importante sera expliquée bientôt.

    Énumérez les agents enregistrés :

    opencode agent list
    

    3. Exécutez-le manuellement en premier

    opencode run --agent builder "Add a task assignment feature to TaskFlow according to the requirements in TASK.md"
    

    Quelques notes sur cette commande :

    • --agent permet de choisir quel agent s’exécute, que ce soit le constructeur, le testeur ou tout autre agent défini.
    • L’ajout de --format json génère des données brutes au lieu d’un affichage destiné aux utilisateurs. C’est ce que l’appel PHP utilisera par la suite.
    • --session et --continue permettent à l’agent de se souvenir des conversations précédentes au lieu de recommencer à zéro à chaque fois.

    Exécutez-la manuellement plusieurs fois pour vous familiariser avec son fonctionnement avant de l’automatiser.

    Un cas concret : l’affectation de tâches dans TaskFlow

    Un exemple concret aide à bien comprendre le concept. Imaginez une application simple de gestion de tâches appelée TaskFlow qui a besoin de la fonctionnalité suivante : les utilisateurs peuvent assigner une tâche à un autre utilisateur, mais uniquement au sein d’un projet auquel ils ont tous deux accès. Décomposé en exigences :

    1. A task has an assignee column (belongs to a User).
    2. A user can only assign tasks within a project they're a member of.
    3. A user must not assign or access tasks from other projects.
    4. All of the above must be covered by automated tests.
    

    C’est une exigence courante, ce qui en fait une bonne démonstration. Des cas comme celui-ci nécessitent souvent deux ou trois tentatives avant que la question de savoir qui peut accéder à quoi, techniquement appelée autorisation, ne soit résolue de manière définitive. C’est précisément là que le boucle automatisée trouve son utilité.

    Pourquoi le processus a besoin d’un journal

    Lorsque le travail s’exécute en arrière-plan, personne ne peut compter sur sa mémoire pour se rappeler « quel était déjà cet erreur ? ». Tout doit être enregistré, et cela signifie ici l’utilisation d’une table de base de données servant de journal des progrès :

    Schema::create('ai_tasks', function (Blueprint $table) {
        $table->id();
        $table->string('type');
        $table->string('status')->default('pending');
        $table->text('prompt');
        $table->json('result')->nullable();
        $table->unsignedTinyInteger('iteration')->default(0);
        $table->timestamps();
    });
    

    Le modèle :

    class AiTask extends Model
    {
        protected $fillable = [
            'type', 'status', 'prompt', 'result', 'iteration',
        ];
    
        protected function casts(): array
        {
            return ['result' => 'array'];
        }
    }
    

    La colonne status a une importance capitale. Elle passe par ces étapes :

    pending → building → testing → (fixing → testing)* → reviewing → completed / failed
    

    Pourquoi pas simplement un drapeau done ? Parce que l’orchestrateur doit savoir à quelle étape se trouve le travail, et non seulement s’il est terminé. Le suivi des étapes permet également de reprendre une exécution en cas de panne d’un travailleur au milieu du processus, et rend visible l’avancement en temps réel.

    Intégrer OpenCode dans PHP

    Beaucoup de gens sautent cette étape : il ne faut pas disperser les commandes OpenCode dans de nombreux fichiers. Si l’outil est remplacé plus tard, des dizaines d’endroits ne devraient pas nécessiter de modifications. Créez un point d’entrée unique, une interface :

    interface AgentRunner
    {
        public function run(string $agent, string $prompt): array;
    }
    

    Puis la mise en œuvre réelle, qui appelle OpenCode depuis PHP :

    class OpenCodeAgentRunner implements AgentRunner
    {
        public function run(string $agent, string $prompt): array
        {
            $result = Process::timeout(600)->run([
                'opencode', 'run',
                '--agent', $agent,
                '--format', 'json',
                $prompt,
            ]);
    
            if (! $result->successful()) {
                return [
                    'status' => 'error',
                    'error' => $result->errorOutput(),
                ];
            }
    
            return json_decode($result->output(), true) ?? [
                'status' => 'error',
                'error' => 'Agent output was not valid JSON',
            ];
        }
    }
    

    Lier cela à un fournisseur de services :

    $this->app->bind(AgentRunner::class, OpenCodeAgentRunner::class);
    

    Avec ce modèle, chaque tâche ne communique qu’avec AgentRunner et se moque du fait que ce soit OpenCode ou un autre élément qui se trouve derrière.

    Une tâche par étape

    Pour ceux qui découvrent Laravel Jobs : une tâche est une opération qui s’exécute en arrière-plan via une file d’attente, de sorte que personne n’a à attendre qu’elle soit terminée.

    La tâche de compilation

    class BuildFeature implements ShouldQueue
    {
        public function __construct(protected AiTask $task) {}
    
        public function handle(AgentRunner $agent): void
        {
            $result = $agent->run(
                agent: 'builder',
                prompt: $this->task->prompt,
            );
    
            $this->task->update([
                'status' => 'testing',
                'result' => $result,
            ]);
    
            RunTests::dispatch($this->task);
        }
    }
    

    Ici, l’agent de compilation s’exécute, son résultat est enregistré, et le flux passe directement au job suivant (RunTests) sans aucune action manuelle.

    Le job de test

    Ce job est délibérément conçu de manière simple. Il n’a pas besoin de comprendre le code. Il exécute la suite de tests et lit les résultats.

    class RunTests implements ShouldQueue
    {
        public function __construct(protected AiTask $task) {}
    
        public function handle(): void
        {
            $result = Process::timeout(300)->run('php artisan test --compact');
    
            if ($result->successful()) {
                $this->task->update(['status' => 'reviewing']);
                ReviewFeature::dispatch($this->task);
                return;
            }
    
            $this->task->update(['status' => 'failed']);
            AnalyzeFailure::dispatch($this->task, $result->output());
        }
    }
    

    Le processus est simple :

    Test
     ├── passed → move to Review
     └── failed → move to Analyze
    

    Ne transmettez pas d’erreurs brutes à l’agent

    Cette étape est la plus facile à sauter, mais elle a l’impact le plus important sur la qualité. Indiquer simplement à l’agent qu’un test a échoué et lui demander de le corriger ne suffit pas. Il doit tout reprendre à zéro, relire chaque fichier et deviner ce qui a mal tourné.

    Structurez d’abord l’échec :

    {
      "status": "failed",
      "failed_tests": [
        {
          "name": "user_cannot_assign_task_from_other_project",
          "error": "Expected response status code [403] but received 200."
        }
      ]
    }
    

    Puis transmettez-le à un agent dont la seule fonction est le diagnostic, appelons-le l’Agent de débogage, en lui donnant des instructions qui restreignent strictement ce qu’il peut faire :

    You are the Debug Agent.
    
    Analyze the failed tests below. DO NOT modify any files.
    Inspect: relevant models, policies, migrations, and tests.
    Return JSON with:
    - root_cause
    - affected_files
    - recommended_fix
    - risk_of_regression
    

    Pourquoi séparer le diagnostic de la correction ? Pour créer un écart entre la compréhension du problème et l’exécution de la réparation. Si l’agent se voit simplement demander de corriger les choses, la trace montrant pourquoi un fichier donné a changé disparaît, et l’examen final devient beaucoup plus difficile à vérifier.

    Pour TaskFlow, la sortie de l’agent de débogage ressemblerait probablement à ceci :

    {
      "status": "analyzed",
      "root_cause": "TaskPolicy does not verify project membership before allowing assignment",
      "affected_files": ["app/Policies/TaskPolicy.php"],
      "recommended_fix": "Add a project membership check inside the assign() method",
      "next_action": "fix"
    }
    

    Ce serait une découverte crédible, car il s’agit de l’un des bugs d’autorisation les plus courants : une politique qui ne vérifie que si l’utilisateur est connecté, et non s’il fait réellement partie du projet.

    Résolvez le problème, puis testez à nouveau ; ne vous contentez jamais de résoudre sans tester.

    class FixFeature implements ShouldQueue
    {
        public function __construct(protected AiTask $task, protected array $analysis) {}
    
        public function handle(AgentRunner $agent): void
        {
            $result = $agent->run(
                agent: 'fixer',
                prompt: json_encode($this->analysis),
            );
    
            $this->task->increment('iteration');
            $this->task->update([
                'status' => 'testing',
                'result' => $result,
            ]);
    
            RunTests::dispatch($this->task);
        }
    }
    

    Remarquez que, après la correction, l’état revient à testing au lieu de passer directement à completed. Le principe est clair : un agent n’a pas le droit de se déclarer terminé simplement parce qu’un fichier a changé. La seule preuve valable de correction est un ensemble de tests qui a réellement été exécuté à nouveau, et non l’affirmation de l’agent lui-même.

    L’orchestrateur en tant que contrôleur de trafic

    Au début, il semble tentant d’enchaîner les tâches en ligne droite :

    Bus::chain([
        new BuildFeature($task),
        new RunTests($task),
        new ReviewFeature($task),
    ])->dispatch();
    

    Mais ce n’est qu’une ligne. Dès qu’apparaît une branche, comme le retour pour corriger un échec de test, une chaîne linéaire s’avère insuffisante. Ce dont on a besoin ressemble à un contrôleur de trafic qui, chaque fois qu’il est sollicité, vérifie l’état actuel et choisit la prochaine action en conséquence :

    class AgentOrchestrator
    {
        public function next(AiTask $task): void
        {
            match ($task->status) {
                'pending'   => BuildFeature::dispatch($task),
                'testing'   => RunTests::dispatch($task),
                'failed'    => AnalyzeFailure::dispatch($task),
                'fixing'    => RunTests::dispatch($task),
                'reviewing' => ReviewFeature::dispatch($task),
                'completed', 'stopped' => null,
                default => throw new LogicException("Unknown status: {$task->status}"),
            };
        }
    }
    

    Le changement de mentalité est significatif. L’orchestrateur ne se demande plus quel est le prochain travail à effectuer. Il se demande quelle transition est valide compte tenu de l’état actuel. Cela facilite le débogage, et une nouvelle étape, comme une revue de sécurité, peut être ajoutée sans perturber le flux global.

    Limitez les tentatives de réessai

    En l’absence de limite, un scénario comme celui-ci peut se produire :

    Fix → Test fails → Fix → Test fails → Fix → Test fails → ...
    

    Cela gaspille du temps, et chaque appel à l’IA a également un coût. La solution réside dans une limite maximale :

    class RunTests implements ShouldQueue
    {
        protected const MAX_ITERATIONS = 5;
    
        public function handle(): void
        {
            if ($this->task->iteration >= self::MAX_ITERATIONS) {
                $this->task->update(['status' => 'needs_human_review']);
                return;
            }
            // ... run tests as usual
        }
    }
    

    Si cinq tentatives de correction ne suffisent pas, le système s’arrête et marque la tâche pour être traitée par un humain, au lieu de continuer indéfiniment. Cela ne signifie pas nécessairement que l’IA est mauvaise dans sa tâche. Parfois, les exigences sont ambiguës, ou la décision nécessite réellement une intervention humaine.

    Les tests réussis ne signifient pas pour autant que la fonctionnalité est terminée

    Il est facile d’oublier cela une fois que le système qui fonctionne seul semble impressionnant. Supposons que l’agent écrive quelque chose comme :

    public function assign(User $user, Task $task): bool
    {
        return $user->isAdmin();
    }
    

    Tous les tests pourraient passer, car ils ne couvrent que les scénarios imaginés à l’époque. La exigence réelle stipulait que l’utilisateur devait être membre du projet, et non qu’il devait être administrateur. La logique est erronée même si tous les tests restent positifs.

    C’est pourquoi ce flux est dangereux :

    Build → Tests pass → Deploy immediately
    

    Celle qui est plus sûre ressemble à ceci :

    Build → Test → (if failed: Debug → Fix → Test again) → Human review → Deploy
    

    La phase d’examen doit toujours impliquer une personne, au minimum pour tout ce qui concerne les droits d’accès. Pour des modifications sensibles sur le plan de la sécurité comme celle-ci, il est judicieux d’exiger un examen humain avant fusionnement, aussi performant que puisse paraître le processus automatisé.

    Agents parallèles et conflits de fichiers

    L’idée d’exécuter simultanément un agent de sécurité et un agent de recherche présente un piège. Si deux agents peuvent écrire dans le même fichier en même temps, une condition de course apparaît, ce qui signifie que les deux processus écrasent le travail de l’autre.

    Agent A → writes Task.php
    Agent B → reads Task.php (nearly at the same time)
    Agent A → writes Task.php again
    

    La règle de base consiste à répartir les responsabilités en fonction de ceux qui peuvent écrire et de ceux qui ne peuvent que lire :

    Research Agent   → read-only
    Review Agent     → read-only
    Security Agent   → read-only
    Build Agent      → write access
    Fix Agent        → write access
    

    Cela explique également la ligne tools.write: true dans le fichier builder.md précédent. Elle détermine quels agents peuvent modifier des fichiers et quels agents ne peuvent que les consulter.

    Résistez au super-agent

    La plus grande tentation une fois que cela fonctionne est d’avoir un seul agent chargé de planifier, de construire, de tester, de déboguer, de corriger, de réviser et de déployer. Cela a tendance à avoir des effets contraires aux escomptés. Plus un agent assume de responsabilités, plus d’informations il doit traiter, plus d’outils il possède, et plus il est susceptible de prendre une mauvaise décision à un moment donné. Diviser les tâches en fonction des responsabilités est plus efficace :

    Planner   → designs the approach
    Builder   → writes the new feature's code
    Tester    → runs the test suite
    Debugger  → diagnoses failures (read-only)
    Fixer     → executes the fix
    Reviewer  → final quality check (read-only)
    

    C’est le même principe que celui qui consiste à éviter une seule classe géante gérant tout dans une application. Évitez également de créer un seul agent qui fait tout.

    Une liste de contrôle minimale avant utilisation réelle

    Pour des expérimentations dans un projet réel, ces éléments doivent être en place dès le premier jour et non ajoutés ultérieurement :

    • un plafond strict sur les tentatives de réessai, afin que la boucle de correction ne puisse pas fonctionner indéfiniment
    • un délai limité pour chaque commande exécutée par l’agent
    • des restrictions d’accès aux fichiers adaptées au rôle de l’agent, lecture seule ou écriture
    • des tests qui doivent exister et réussir avant que le statut ne puisse passer en phase d’examen
    • une approbation humaine avant que quoi que ce soit ne soit mis en production
    • un enregistrement de chaque décision prise par l’agent, et non seulement du résultat final
    • une protection pour les fichiers sensibles tels que .env, afin qu’un agent ne les lise jamais par accident

    L’élément final est souvent sous-estimé. Une fois qu’un agent peut exécuter des commandes sur le système, grâce aux appels Process::run() mentionnés ci-dessus, il peut en fait effectuer presque n’importe quelle action sur ce serveur. Les restrictions d’accès sont une exigence dès le départ, et non une option.

    Rôle réel de Boost

    Voici le tableau complet :

    Developer (us)
        │
        ▼
    Orchestrator (traffic controller, running on Laravel Queue)
        │
        ├── Build Agent
        ├── Test Agent
        ├── Debug Agent
        └── Fix Agent
              │
              ▼
          OpenCode (where the agent does its work)
              │
              ▼
        Laravel Boost (context translator, via MCP)
              │
              ▼
        Our Laravel application
    

    Boost ne décide jamais de lancer les tests ni d’activer l’agent de correction en cas d’échec. Il fait une seule chose, et c’est essentielle : s’assurer que chaque agent du projet comprend bien l’état réel de l’application et ne se base pas sur des schémas généraux. Les questions concernant le fait de savoir qui agit, à quel moment, et quand tout s’arrête restent du ressort de l’orchestrateur basé sur Laravel Queue.

    La question à se poser maintenant

    Il y a quelques années, la question intéressante était de savoir si l’IA pouvait écrire du code Laravel correct. Elle le peut clairement, et ses capacités s’améliorent constamment. La question plus pertinente aujourd’hui est de savoir si l’IA peut mettre en place son propre cycle de feedback comprenant la compilation, les tests, les échecs, l’analyse des raisons de ces échecs, la correction et de nouveaux tests, jusqu’à ce que le travail soit véritablement terminé, sans avoir besoin d’un humain pour intervenir manuellement à chaque étape.

    Le scénario TaskFlow suggère que la réponse est oui, à condition que trois éléments ne soient pas oubliés. Il faut un enregistrement clair des progrès, une limite stricte aux tentatives de réessai, et les tests réussis doivent être considérés comme un signal, et non comme la ligne d’arrivée.

    Boost aide les agents à comprendre le contexte de l’application. OpenCode leur fournit un endroit où travailler. Laravel Queue devient l’élément central qui organise tout. De plus, les développeurs peuvent monter en niveau, passant d’opérateurs manuels à chaque étape à des personnes chargées de vérifier le résultat final.

    Vibe coding accélère l’écriture du code. Une telle orchestration permet au processus de codage de se dérouler automatiquement, et c’est là que les expériences de codage par IA deviennent intéressantes.