Dans une application Symfony, les bundles permettent d’installer et utiliser des fonctionnalités déjà développées, sans avoir à les réécrire soi-même.
Pour que que l’on puisse utiliser les fonctionnalités qu’il fournit, le bundle pourra enregistrer des services, des paramètres, des routes, etc…au sein de notre application.
Par exemple, si on veut implémenter une authentification JWT, on peut installer le LexikJWTAuthenticationBundle, qui s’appuie lui-même sur la librairie lcobucci/jwt pour générer, analyser et valider des JWT.
Librairies & Bundles
Concernant l’exemple ci-dessus, on n’installera pas directement la librairie lcobucci/jwt. On pourrait, mais cette librairie n’a pas été conçue spécifiquement pour Symfony. Dans la documentation, c’est la première chose qui est indiquée :
lcobucci/jwtis a framework-agnostic PHP library that allows you to issue, parse, and validate JSON Web Tokens based on the RFC 7519.
“framework-agnostic” signifie que cette librairie n’est spécifique à aucun framework. En réalité, cela peut donc faciliter son intégration dans n’importe quel framework.
Ainsi, tout comme le LexikJWTAuthenticationBundle, conçu pour Symfony, s’appuie sur la librairie lcobucci/jwt, on peut également découvrir dans l’écosystème Laravel que le package tymon/jwt-auth s’appuie sur la même librairie !
Le FrameworkBundle
Le bundle le plus important pour notre application, celui de base, c’est le symfony/framework-bundle : c’est lui qui va regrouper les composants et initialiser tout ce qui est nécessaire à l’application pour pouvoir fonctionner : le container de services, la configuration des autres bundles et des composants au format YAML, le système d’événements, les commandes de la console, etc…
Activation
Chaque bundle est activé par environnement dans une application Symfony, depuis le fichier config/bundles.php. Quand on installe un bundle avec composer require ..., le bundle est automatiquement rajouté au fichier par Symfony Flex.
Exemple : McpBundle
Partons en exploration dans le code du McpBundle, qui permet de créer des serveurs MCP.
Le McpBundle s’appuie sur le SDK MCP. Dans la description du SDK, on peut lire :
[…] It provides a framework-agnostic API for implementing MCP servers and clients in PHP.
Le but du McpBundle n’est donc pas d’implémenter la mécanique requise par le protocole (ça existe déjà dans le SDK), mais de rendre disponible les fonctionnalités du SDK au sein d’une application Symfony.
Point d’entrée
Comme les autres bundles, le McpBundle adopte PSR-4 pour le chargement de ses fichiers.
Le point d’entrée du bundle va se trouver dans la classe qui héritera de la classe AbstractBundle :
<?php
// src/McpBundle.php
//...
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
//...
final class McpBundle extends AbstractBundle
{
//...
}
C’est cette classe qui sera enregistrée et activée dans le fichier config/bundles.php.
Au niveau du nommage, le bundle suit les bonnes pratiques indiquées par Symfony, par exemple, ajouter le suffixe Bundle, ne pas dépasser 2 mots…
Dans cette classe, on va s’intéresser à 3 méthodes : configure, loadExtension et build.
Configuration
La méthode configure permet de définir le format de configuration du bundle (propriétés, valeurs par défaut…).
Dans le McpBundle, on charge le fichier config/options.php qui contient toute la structure qu’on appliquera ensuite dans le fichier YAML config/packages/mcp.yaml, au sein d’une application Symfony.
Lorsqu’on installe le bundle dans une application, il est donc possible de changer divers paramètres qui contrôleront l’exécution de la logique présente dans le bundle.
Une configuration sert à ça : changer les valeurs des paramètres sans toucher au code.
loadExtension
Une fois la configuration du bundle chargée, loadExtension vient quant à elle déclarer les services et les paramètres qui seront intégrés au container.
Le fichier config/services.php est importé et exécuté, il retourne uniquement une fonction qui se charge de la définition des services. Par exemple :
<?php
// config/services.php
//...
use Mcp\Capability\Registry;
//...
return static function (ContainerConfigurator $container): void {
$container->services()
->set('mcp.registry', Registry::class)
->args([service('event_dispatcher'), service('logger')])
->tag('monolog.logger', ['channel' => 'mcp'])
// définition d'autres services
;
};
La classe Registry vient du SDK MCP : c’est ici qu’on déclare le branchement à effectuer avec le framework Symfony. Le container de services, en production, aura donc un service mcp.registry disponible (->set('mcp.registry', Registry::class)). Ce service contiendra tous les outils, les ressources, templates de ressources et prompts qu’on souhaite déclarer dans notre serveur MCP.
Attributs PHP8
Toujours dans la méthode loadExtension, on trouve un appel à une méthode registerMcpAttributes, qui contient le code suivant :
<?php
//...
final class McpBundle extends AbstractBundle
{
//...
private function registerMcpAttributes(ContainerBuilder $builder): void
{
$mcpAttributes = [
McpTool::class => 'mcp.tool',
McpPrompt::class => 'mcp.prompt',
McpResource::class => 'mcp.resource',
McpResourceTemplate::class => 'mcp.resource_template',
];
foreach ($mcpAttributes as $attributeClass => $tag) {
$builder->registerAttributeForAutoconfiguration(
$attributeClass,
static function (ChildDefinition $definition, object $attribute, \Reflector $reflector) use ($tag): void {
$definition->addTag($tag);
}
);
}
}
//...
}
Chacune des classes McpTool, McpPrompt, McpResource et McpResourceTemplate est un attribut PHP8 issu du MCP SDK. Ici, chaque attribut est donc associé à un tag, ce qui permettra d’autoconfigurer les classes qui utilisent ces attributs et les catégoriser dans le container de services.
build
Une fois que les services ont été définis, et les tags appliqués, une passe de compilation est exécutée. C’est la méthode build du bundle qui est chargée de l’enregistrer :
//...
use Symfony\AI\McpBundle\DependencyInjection\McpPass;
//...
final class McpBundle extends AbstractBundle
{
//...
public function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new McpPass());
}
//...
}
C’est ici que les tags appliqués précédemment prennnt sens, à l’exécution de la méthode process :
- Collecte de tous les services qui ont les tags
mcp.tool,mcp.prompt,mcp.resourceetmcp.resource_template - Création d’un service locator qui contiendra tous ces services
- Injection de ce locator dans le
Server Builderdu SDK MCP, en tant que container de services compatible PSR-11
Route
En plus des services, de l’autoconfiguration ou des paramètres enregistrés, le McpBundle vient définir un contrôleur pour gérer les requêtes au serveur MCP :
- Dans
src/Controller/McpController.php, on trouve le contrôleur qui transfère la requête au serveur MCP - Dans
src/Routing/RouteLoader.php, on aura la logique qui permet d’enregistrer ce contrôleur auprès du routeur de Symfony
Le type de route déclaré est simplement mcp, donc dans un projet Symfony, pour activer ce loader, dans config/routes.yaml :
mcp:
resource: .
type: mcp
Cursor
La route est accessible derrière l’URL /_mcp, ainsi, lorsque le serveur est lancé, si on est en mode HTTP (STDIO est également supporté), dans Cursor par exemple :
"Symfony MCP": {
"type": "http",
"url": "http://localhost:8000/_mcp"
}
On verra alors apparaître tous les outils, prompts, ressources et templates de ressources écrits dans l’application Symfony.
En résumé
Il existe de nombreux bundles dédiés à des fonctionnalités diverses et variées, par exemple :
- Intégration de l’ORM Doctrine
- Authentification à 2 facteurs
- Interface d’administration
- Intégration de Tailwind avec le composant
AssetMapper
Chacun d’eux apporte une fonctionnalité supplémentaire à une application, en déclarant des routes, des services, des entités, des paramètres…