Symfony 5 on va créer un Bundle (Niveau Moyen-Avancé)

Symfony 5 on va créer un Bundle (Niveau Moyen-Avancé)

Catégories : symfony5

Mots-clés : autoload bundle plugin

740 lectures

Auteur : user Pol-doe

Date :

Salut à toi!

Ce qu'on va faire aujourd'hui c'est une librairie.
Et pourquoi ça si tu as suivi mon dernier tuto on à fait une authentification mais imagine toi que tu utilises le même système sur toutes tes applis.
Dans le même sens mais on ne va pas aller jusque là quand tu vas sur https://packagist.org/ tu peux trouver des librairies externes ben nous on va faire la notre!

Dans un premier temps je te propose d'installer un projet symfony: 

  • symfony new my_project_name --full
    ou 
    composer create-project symfony/website-skeleton my_project_name
     
  • Ici  je ne vais pas détailler ces commandes ni les suivantes si tu es là tu les connais .

     


Tu vas aussi créer un contrôleur :
→ bin/console make:controller →HomeController 

Et tu vas modifier templates/home le fichier index.thml.twig 

Tu vas changer cette ligne : <h1>Hello {{ controller_name }}! ✅</h1> 

et tu vas supprimer le Hello donc : <h1> {{ controller_name }}! ✅</h1>

Voilà on va commencer tranquille..

Tu vas créer un répertoire lib comme ici :
→ et dedans tuto/tools-bundle
 

Si tu veux suivre ce tuto respectes exactement les nom ci-dessus!

On va créer le point d'entrés de notre bundle:

Tu vas créer la class : TutoToolsBundle

voici  à quoi ressemble notre arborescence:  


Et dans cette class :
 

namespace Tuto\ToolsBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class TutoToolsBundle extends Bundle
{
}


Bon c'est cool on va aller dans le fichier composer.json à la racine:

Tu vas aller à la clé ‘autoload’ et tu vas faire la modif suivante:
 

    "autoload": {
        "psr-4": {
            "App\\": "src/",
            "Tuto\\ToolsBundle\\": "lib/tuto/tools-bundle"
        }
    }


Donc tu rajoutes la ligne : "Tuto\\ToolsBundle\\": "lib/tuto/tools-bundle"

Respectes exactement ce qui est noté..

Et dans ton terminal tu fais : 

composer dump-autoload

Bon on est sur la bonne voie .
On va maintenant modifier dans config/bundle.php
 

return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
    Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
    Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
    Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
    Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
    Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
    Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
    Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
    
    // Tu rajoutes cette ligne ..
    Tuto\ToolsBundle\TutoToolsBundle::class => ['all' => true],
];

Voilà notre bundle est enregistré..

– Pour ce qui suit il y a plusieurs façon de faire nous on regarde celle-ci..

Bon dans notre dossier tools-bundle tu vas créer les dossiers suivants :

Dans le dossier : DependencyInjection tu vas ajouter la class → TutoToolsExtension

Voici la class :

namespace Tuto\ToolsBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;


class TutoToolsExtension extends Extension implements PrependExtensionInterface
{

    public function load(array $configs, ContainerBuilder $container)
    {
        // TODO: Implement load() method.
    }

    public function prepend(ContainerBuilder $container)
    {
        // TODO: Implement prepend() method.
    }
    
    public function getAlias()
    {
        return parent::getAlias();
    }
}

 

Et pour l'activer →  tu vas dans la class : TutoToolsBundle

Et tu vas faire la modification suivante :

namespace Tuto\ToolsBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Tuto\ToolsBundle\DependencyInjection\TutoToolsExtension;

class TutoToolsBundle extends Bundle
{

    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $ext = new TutoToolsExtension([],$container);

    }
}

On doit continuer à configurer donc on va aller dans le dossier → Resources

Dans ce dossier tu vas rajouter les dossiers suivants :
 

Une fois ajouté → config, public , views

On continu, dans le dossier Resources/config tu vas créer un fichier services.yaml


 



Le voici : 

On va demander à symfony de charger automatiquement nos fichiers et exclure quelqu'un
 

services:
  _defaults:
    autowire: true      # Automatically injects dependencies in your services.
    autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.


  Tuto\ToolsBundle\:
    resource: '../../'
    exclude:
      - '../../DependencyInjection/'
      - '../../Resources/'
      - '../../Tests/'
      # Tu mets ici les dossiers a exclure simplement...

  #===========================================================================
  # Ici si tu as des problemes et tu veux savoir ce que tu fais si ton arborescence et juste
  # tu ouvres le fichier -> Symfony\Component\Config\Resource\GlobResource
  # Tu vas à la ligne 146 () c'est possible que ca change :-(
  # Et tu rajoutes ca :

  #  if (!$this->recursive || isset($this->excludedPrefixes[str_replace('\\', '/', $path)])) {
  #  continue;
  #   }
  #        // Tu rajoutes juste ces lignes!!
  #  ->    if (!str_contains($path, 'src'))
  #  ->    dd($path);

  #  $files = iterator_to_array(new \RecursiveIteratorIterator(
  #  new \RecursiveCallbackFilterIterator(
  #  new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
  #  function (\SplFileInfo $file, $path) {
  #  return !isset($this->excludedPrefixes[str_replace('\\', '/', $path)]) && '.' !== $file->getBasename()[0];
  #}
  #  ),
  #  \RecursiveIteratorIterator::LEAVES_ONLY
  #  ));
  #
  # Je t'ai mis la methode d'avant et d'après..
  # La ou tu vois les deux flèches c'est juste un test pour exclure le path src (evt. d'autre si tu as d'autre chose)
  # une fois filtré
  # j'obtiens ça -> /var/www/webroot/ROOT/article/lib/tuto/tools-bundle
  # du coup je sais de combien je dois remonter ;-)
  #===========================================================================

Bon on revient dans la class :  TutoToolsExtension 

Je suppose que tu te doutes que la première méthode exécutée c'est prepend puis load et pour getAlias c'est la signature que tu peux modifier

On va loader nos services dans la méthode load voici le code:

    public function load(array $configs, ContainerBuilder $container)
    {
        $loader =new YamlFileLoader($container,new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yaml');
    }  


Bon on fait une pause et on va tester:

On va dans le dossier Service de notre bundle et on va créer la class → HelloService

La voici : 

namespace Tuto\ToolsBundle\Service;

class HelloService
{
    public function sayHello():string
    {
        return 'Hello du bundle';
    }
}

 

Maintenant on va dans le dossier → src de symfony et on ouvre le dossier Controller:

Et la tu vas modifier notre HomeController :

Bon ça se voit mais on injecte le service de notre bundle HelloService dans notre méthode index 

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Tuto\ToolsBundle\Service\HelloService;


class HomeController extends AbstractController
{
    #[Route('/', name: 'home')]
    public function index(HelloService $service): Response
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => $service->sayHello(),
        ]);
    }
}

Voici le résultat :
 


Bon c'est déjà super cool !!!

Mais imaginons que nous on veut un fichier de configuration …

Pas de problème tu vas voir c'est simple :-)

Dans le dossier config de symfony → packages tu vas créer le fichier suivant : tuto_tools.yaml ← EXACTEMENT CE NOM!!!


Ok et dedans tu vas mettre ça :

tuto_tools:
  my_var_string: "je suis heureux de faire un bundle"
  my_array:
    - element1
    - element2
  my_integer: 22


Bon c'est bien mais il faut le charger enfin symfony l'a déjà chargé mais on doit récupérer les données:

Bon je te dis ça de mémoire si tu as un fichier dans le fichier conf.nous on a tuto_tools.yaml
si je me rappelle bien tu n'as plus besoin de cette ligne dans TutoToolsBundle
$ext = new TutoToolsExtension([],$container); // ca devrait fonctionner sans
C'est de mémoire tu essayes tu verras bien..

D'ailleurs tu peux tout virer de mémoire ta class pourrait être comme ça:
 

namespace Tuto\ToolsBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class TutoToolsBundle extends Bundle{}

Bon tu feras tes tests..

Dans le dossier DependencyInjection de notre bundle tu vas créer la class → Configuration



La voici : 

namespace Tuto\ToolsBundle\DependencyInjection;

use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;

class Configuration implements ConfigurationInterface
{


    function getConfigTreeBuilder()
    {
        $builder = new TreeBuilder('tuto_tools');

        $rootNode = $builder->getRootNode();
        $rootNode->children()
            ->scalarNode('my_var_string')
            ->isRequired()
            ->end()
            ->scalarNode('my_var_string_option')
            ->defaultValue('je suis optionnel')
            ->end()
            ->arrayNode('my_array')
            ->isRequired()
            ->scalarPrototype()
            ->end()
            ->end()
            ->integerNode('my_integer')
            ->isRequired()
            ->defaultValue(2)
            ->min(1)
            ->end()

            ->end();

        return $builder;
    }
}

Bon c'est pas difficile :
 

Ici on récupère notre string :
->scalarNode('my_var_string') // son nom 
->isRequired() // est obligatoire
->end()

Même principe mais la var n'est pas obligatoire:

->scalarNode('my_var_string_option')
->defaultValue('je suis optionnel')
->end()

Ici on récupère notre tableau :

->arrayNode('my_array')
->isRequired()
->scalarPrototype()
->end()
->end()

On récupère un entier val minimum 1

->integerNode('my_integer')
->isRequired()
->min(1)
->end()

Bon une fois cela fini on retourne à notre class → TutoToolsExtension et on va modifier  load

    public function load(array $configs, ContainerBuilder $container)
    {
        $loader =new YamlFileLoader($container,new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yaml');

        $config = $this->processConfiguration(new Configuration(), $configs);

        $container->setParameter('tuto_tools.my_var_string', $config['my_var_string']);
        $container->setParameter('tuto_tools.my_array', $config['my_array']);
        $container->setParameter('tuto_tools.my_integer', $config['my_integer']);
        $container->setParameter('tuto_tools.my_var_string_option', $config['my_var_string_option']);
    }


Maintenant on a enregistré nos valeur dans le service container .

On refait une pause on va tester tout ça…

Dans notre bundle dans le dossier Service on va ajouter une class → WithParameterService
 

Et la voici :
 

namespace Tuto\ToolsBundle\Service;

class WithParameterService
{
    public function __construct(private string $str){}

    public function sayHello():string
    {
        return $this->str;
    }
}

 

Maintenant on va aller dans le dossier Controller de symfony et ouvrir notre :  HomeController

Tu vas rajouter une méthode : index2

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Tuto\ToolsBundle\Service\HelloService;
use Tuto\ToolsBundle\Service\WithParameterService;


class HomeController extends AbstractController
{
    #[Route('/', name: 'home')]
    public function index(HelloService $service): Response
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => $service->sayHello(),
        ]);
    }

    #[Route('/test', name: 'home')]
    public function index2(WithParameterService $service): Response
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => $service->sayHello(),
        ]);
    }
}

Bon on va tester → http://ton-serveur/test

Mince ça ne marche pas …

Comme tu peux le comprendre notre service WithParameterService a besoin de paramètres et symfony ne peut pas les deviner
donc on doit modifier le fichier services.yaml de notre bundle. (Resources/config/services.yaml)

Tu vas ajouter :  Tuto\ToolsBundle\Service\WithParameterService: ['Bonjour à tous']

Donc voilà services.yaml

services:
  _defaults:
    autowire: true      # Automatically injects dependencies in your services.
    autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.


  Tuto\ToolsBundle\:
    resource: '../../'
    exclude:
      - '../../DependencyInjection/'
      - '../../Resources/'
      - '../../Tests/'
      # Tu mets ici les dossiers a exclure simplement...

  Tuto\ToolsBundle\Service\WithParameterService: ['Bonjour à tous']


Bon on refait le test → http://ton-serveur/test

Mais imaginons que nous on veut plutôt envoyer une variable de notre fichier de configuration : tuto_tools.yaml

Pas de problème elles sont dans le service container on n'y va :

Modifie dans Resources/config/services.yaml

Tu mets maintenant : Tuto\ToolsBundle\Service\WithParameterService: ['%tuto_tools.my_var_string%']

services:
  _defaults:
    autowire: true      # Automatically injects dependencies in your services.
    autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.


  Tuto\ToolsBundle\:
    resource: '../../'
    exclude:
      - '../../DependencyInjection/'
      - '../../Resources/'
      - '../../Tests/'
      # Tu mets ici les dossiers a exclure simplement...

  Tuto\ToolsBundle\Service\WithParameterService: ['%tuto_tools.my_var_string%']

Et voilà :-)

T'es pas fatigué?

Cool et si on mettait un contrôleur dans notre bundle cool non?

Dans le dossier Controller de notre bundle par exemple : BundleController ou BrouetteController

Bon fais comme moi c'est mieux :
 

namespace Tuto\ToolsBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;



class BundleController extends AbstractController
{
    #[Route('/hello-bundle', name: 'tuto_tools_home')]
    public function index(): Response
    {
        return new Response("Coucou c'est moi" );
    }
}


Bon on fait le test → http://ton-serveur/hello-bundle

Ben ça ne marche pas 😭

Bon je crois qu'il faut dire à symfony comme ça se passe avec les routes…

Alors allons-y dans le dossier config de symfony : config/routes tu vas créer un fichier : tuto_tools.yaml

le voila :

_tools:
  resource: '@TutoToolsBundle/Resources/config/routes.yaml'
  prefix: /my-route

Bon on fait le test → http://ton-serveur/my-route/hello-bundle

→ Ça ne marche toujours pas 😭 😭 😭 😭 😭

Juste pour que tu saches se qui se passe dans config/routes/tuto_tools.yaml on a expliqué à symfony comment joindre le bundle
Mais on doit encore expliquer comment symfony doit lire les routes.

Ok alors on continue dans notre bundle dans le dossier Resource/config tu vas rajouter un fichier routes.yaml
 


Le voilà :

controllers:
  resource: ../../Controller/
  type: annotation


Bon on fait le test → http://ton-serveur/my-route/hello-bundle

Youpi ça marche 😁

Bon tu vas me dire mais avec un template cela serait plus cool alors on va essayer..

Alors on va modifier notre : BundleController
 

class BundleController extends AbstractController
{
    #[Route('/hello-bundle', name: 'tuto_tools_home')]
    public function index(): Response
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => "Hej c'est moi le bundle ",
        ]);
    }
}

Bon on fait le test → http://ton-serveur/my-route/hello-bundle
 

Youpi ça marche 😁

… Mais tu la sens l'embrouille ? 

Et d'où il le prend ce template… ben il le prend de notre contrôleur home
Ouais mais ça va pas ça nous on veut isoler nos templates.

Ok on va créer notre template dans notre bundle tu vas dans :
Resources/views → tu vas créer un dossier : mybundle et dedans → index.html.twig

 

Et dans index.html.twig 

{% extends 'base.html.twig' %}

{% block title %}Hello HomeController!{% endblock %}

{% block body %}
    <style>
        .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
        .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
    </style>

    <div class="example-wrapper">
        <h1>Hello {{ controller_name }}! ✅</h1>

        Je suis dans le Bundle !!!
    </div>
{% endblock %}

Et dans notre contrôleur :  BundleController

 

class BundleController extends AbstractController
{
    #[Route('/hello-bundle', name: 'tuto_tools_home')]
    public function index(): Response
    {
        return $this->render('@tuto_tools/mybundle/index.html.twig', [
            'controller_name' => "Hej c'est moi le bundle ",
        ]);
    }
} 

 

Bon on fait le test → http://ton-serveur/my-route/hello-bundle

→ Ça ne marche  pas 😭 😭 😭 😭 😭

Bon va faire une expérience .
Tu vas dans le dossier config de symfony → config/packages/twig.yaml

Tu rajoutes :

paths:
    '%kernel.project_dir%/lib/tuto/tools-bundle/Resources/views': tuto_tools

twig:
    default_path: '%kernel.project_dir%/templates'
    paths:
        '%kernel.project_dir%/lib/tuto/tools-bundle/Resources/views': tuto_tools

Bon on fait le test → http://ton-serveur/my-route/hello-bundle

Youpi ça marche 😁

Alors on laisse comme ça 😉 mais non je plaisante c'était juste pour info supprime ce qu'on vient de faire : 

Tu supprimes :
paths:
    '%kernel.project_dir%/lib/tuto/tools-bundle/Resources/views': tuto_tools

Donc to ficher twig.yaml et comme avant :

twig:
    default_path: '%kernel.project_dir%/templates'

Si tu te rappelles dans notre bundle DependencyInjection→TutoToolsExtension : prepend est resté vide
Alors on y va on va déclarer nos namespaces:

    public function prepend(ContainerBuilder $container)
    {
        $twigConfig = [];
        $twigConfig['paths'][__DIR__.'/../Resources/views'] = "tuto_tools";
        $twigConfig['paths'][__DIR__.'/../Resources/public'] = "tuto_tools.public";
        $container->prependExtensionConfig('twig', $twigConfig);
    }        

La class complète :

namespace Tuto\ToolsBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;


class TutoToolsExtension extends Extension implements PrependExtensionInterface
{


    public function load(array $configs, ContainerBuilder $container)
    {
        $loader =new YamlFileLoader($container,new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yaml');

        $config = $this->processConfiguration(new Configuration(), $configs);

        $container->setParameter('tuto_tools.my_var_string', $config['my_var_string']);
        $container->setParameter('tuto_tools.my_array', $config['my_array']);
        $container->setParameter('tuto_tools.my_integer', $config['my_integer']);
        $container->setParameter('tuto_tools.my_var_string_option', $config['my_var_string_option']);
    }

    public function prepend(ContainerBuilder $container)
    {
        $twigConfig = [];
        $twigConfig['paths'][__DIR__.'/../Resources/views'] = "tuto_tools";
        $twigConfig['paths'][__DIR__.'/../Resources/public'] = "tuto_tools.public";
        $container->prependExtensionConfig('twig', $twigConfig);
    }

    public function getAlias()
    {
        return parent::getAlias();
    }
}

Bon on a fini!!  le bundle a un code minimum afin d'éviter de parasiter le tuto.

Bon si tu es arrivé jusque là Bravo !!!