Configuration des packages et premiers développements Symfony - Tuto Symfony - PHP - Partie 2

Configuration des packages et début des développements d’un projet Symfony
Corentin.jpg
Corentin CHEVRETMis à jour le 7 Déc 2020
Configuration des packages et premiers développements Symfony - Tuto Symfony - PHP - Partie 2 | AXOPEN

Après une première partie introductive au Framework Symfony, on se retrouve aujourd’hui pour commencer à coder !

Nous verrons dans cette deuxième partie la configuration des packages de notre projet ainsi que la mise en place et l’utilisation des Controller / Managers / Service / RepositoryC'est l'emplacement où Git va stocker toutes les informations permettant de gérer le versioning et l'historisation d'une application. et Entity.

Configuration des packages d’un projet Symfony

La configuration des packages du projet se base sur le projet initialisé lors de la partie 1 de cette série d’articles. Si vous rencontrez quelques difficultés, nous vous conseillons de vous y référer.

Étape 1 : Auto-génération des fichiers de configuration

Lors de l’ajout de certains packages à l’aide de la commande composer require monpackage, SymfonyFramework PHP permettant de développer des applications web. s’occupe de créer les fichiers YAML de configuration du package, d’activer des bundles et de modifier les variables d’environnement.

Étape 2 : Comprendre et terminer la configuration du package (si nécessaire)

Fichier [bundle.php]

Certains packages ajoutés nécessitent l’activation de bundles dans le fichier bundles.php dans le dossier config de notre projet.

Prenons comme exemple l’installation du package profiler-pack (qui vous sera extrêmement utile pendant votre développement).

Tout d’abord il faut ajouter le package au projet :

composer require symfony/profiler-pack

Une fois l’installation terminée, le fichier bundle.php a été modifié pour ajouter 2 nouveaux bundles :

<?php

return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => [’all’ => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class => [’all’ => true], // new
    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => [’dev’ => true, ’test’ => true], // new
];

On peut remarquer une différence entre le TwigBundle et le WebProfilerBundle :

  • [’all’ => true] : Le TwigBundle est accessible dans n’importe quel environnement de développement. Il permet d’utiliser le moteur de template Twig et est donc toujours nécessaire pour afficher les pages web.
  • [’dev’ => true, ’test’ => true] : Le WebProfilerBundle est accessible uniquement pour les environnements de test et de dev. Ce bundle nous servant à déboguer notre application lors du développement, il ne doit pas être accessible pour l’environnement de production.

Il est donc possible de choisir l’activation du bundle en fonction de l’environnement désiré.

Fichier [.env]

Ce fichier, à la racine du projet, stocke les variables d’environnement nécessaires au fonctionnement de l’application. Certains packages modifient le fichier pour y ajouter leurs variables.

Prenons comme exemple l’installation du package orm-pack (qui nous permet la connexion à la BDD et l’installation de Doctrine).

Tout d’abord, il faut ajouter le package au projet :

composer require symfony/orm-pack

Une fois l’installation terminée, le fichier .env a été modifié pour ajouter la variable d’environnement DATABASE_URL :

...
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7
###< doctrine/doctrine-bundle ###
...

Il vous restera ensuite à renseigner vos identifiants de connexion à la base de données.

Mais où est utilisée cette variable ? Il faut, pour le savoir, se rendre dans le dossier packages du dossier config.

Dossier [packages]

Pour chaque package ajouté nécessitant une configuration, un fichier YAML de config est créé dans ce dossier. 

arbo_config.png

Lors de l’ajout du premier package profiler-pack, un fichier web_profiler.yaml a été créé.

Pour la package orm-pack, ce sont les fichiers doctrine.yaml et doctrine_migration.yaml qui ont été créés. Nous allons nous intéresser au fichier doctrine.yaml :

doctrine:
    dbal:
        url: ’%env(resolve:DATABASE_URL)%’

        # IMPORTANT: You MUST configure your server version,
        # either here or in the DATABASE_URL env var (see .env file)
        #server_version: ’5.7’
    orm:
        auto_generate_proxy_classes: true
        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
        auto_mapping: true
        mappings:
            App:
                is_bundle: false
                type: annotation
                dir: ’%kernel.project_dir%/src/Entity’
                prefix: ’App\Entity’
                alias: App

Nous retrouvons notre variable DATABASE_URL qui est utilisée pour se connecter à la BDD. On remarquera que pour récupérer une variable dans le fichier .env, il faut utiliser la syntaxe %env(resolve:ma_variable)%.

Il vous est ensuite possible de configurer la connexion à la BDD selon vos besoins, et surtout spécifier où se situent vos Entity qui sont utilisés pour faire le lien avec les tables de votre BDD.

Enfin, on remarquera que la configuration des fichiers YAML peut être adapté selon votre environnement. Le fichier doctrine.yaml est utilisé une seconde fois dans le dossier prod, permettant ainsi d’ajouter une configuration spécifique à l’environnement de production.

Enfin, nous allons pouvoir coder un peu avec Symfony !

Dans l’exemple que nous avons préparé, nous allons voir comment récupérer une liste d’utilisateurs en BDD.

Entity : User.php

Nous allons créer notre classe User dans notre fichier User.php du dossier Entity :

<?php

namespace App\Entity;

use DateTime;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="user")
 */
class User
{
    /**
     * @var integer
     *
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(name="id", type="integer")
     */
    private $id;

    /**
     * @var ?string
     *
     * @ORM\Column(name="first_name", type="string")
     */
    private $firstName;

    /**
     * @var ?string
     *
     * @ORM\Column(name="last_name", type="string")
     */
    private $lastName;

    /**
     * @var ?DateTime
     *
     * @ORM\Column(name="user_birthday", type="datetime")
     */
    private $birthday;
    
    /** @var ?string */
    private $description;

    /** @return int */
    public function getId(): int {
        return $this->id;
    }

    /** @param int $id */
    public function setId(int $id): void {
        $this->id = $id;
    }

    /** @return string|null */
    public function getFirstName(): ?string {
        return $this->firstName;
    }

    /** @param string|null $firstName */
    public function setFirstName(?string $firstName): void {
        $this->firstName = $firstName;
    }

    /** @return string|null */
    public function getLastName(): ?string {
        return $this->lastName;
    }

    /** @param string|null $lastName */
    public function setLastName(?string $lastName): void {
        $this->lastName = $lastName;
    }

    /** @return DateTime|null */
    public function getBirthday(): ?DateTime {
        return $this->birthday;
    }

    /** @param DateTime|null $birthday */
    public function setBirthday(?DateTime $birthday): void
    {
        $this->birthday = $birthday;
    }
    
    /** @return string|null */
    public function getDescription(): ?string {
        return $this->description;
    }

    /** @param string|null $description */
    public function setDescription(?string $description): void {
        $this->description = $description;
    }
}

Nous voyons que des décorateurs ont été utilisés pour la classe et nos 4 attributs :

Décorateur Explication
@ORM\Entity Spécifie que notre classe est une Entity. Il est possible d’y rattacher un Repository (nous y reviendrons).
@ORM\Table(name="user") Spécifie le nom de la table en BDD (facultatif).
@ORM\Id Spécifie l’attribut comme PK de la table.
@ORM\GeneratedValue Spécifie la PK comme étant un auto-incrément.
@ORM\Column Spécifie principalement le nom et le type d’une colonne en BDD. Il est possible de rajouter les valeurs par défaut, longueur minimum / maximum et si le champ peut être NULL en BDD selon nos besoins.

Une fois notre UserEntity créé, la structure de notre BDD doit être mise à jour.

Pour cela, il nous faudra générer un fichier de migration et l’exécuter pour appliquer la création de la table user.

Générer le fichier de migration

La commande suivante permet la création d’un fichier de migration contenant le SQLLangage permettant de communiquer avec une base de données. appliqué en BDD :

php bin/console doctrine:migrations:diff

Un fichier Version[datetime].php est créé dans le dossier migrations. Tous les fichiers de migration sont triés par date pour pouvoir les exécuter les un après les autres dans le bon ordre.

Pour appliquer le fichier de migration, il suffit d’exécuter la commande :

php bin/console doctrine:migrations:execute --up DoctrineMigrations\Version[datetime]

La table user est créée en BDD !

Si vous voulez annuler l’insertion de la table, vous pouvez tout à fait utiliser la commande :

php bin/console doctrine:migrations:execute --down DoctrineMigrations\Version[datetime]

Alimenter la table user avec des fixtures

Il faut tout d’abord installer le package doctrine-fixtures-bundle :

composer require --dev doctrine/doctrine-fixtures-bundle

Ensuite, pour générer de fausses données utilisateur, un package existe et le fera à notre place :

composer require --dev fzaninotto/faker

Dans le fichier AppFixtures.php du dossier DataFixtures, nous allons créer 20 utilisateurs :

<?php

namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Faker\Factory;
use Exception;

class AppFixtures extends Fixture
{
    /**
     * @param ObjectManager $manager
     * @throws Exception
     */
    public function load(ObjectManager $manager)
    {
        $faker = Factory::create();
        // création de 20 utilisateurs
        for ($i = 0; $i < 20; $i++) {
            $user = new User();
            $user->setFirstName($faker->firstName);
            $user->setLastName($faker->lastName);
            $user->setBirthday($faker->dateTime);
            $manager->persist($user);
        }

        $manager->flush();
    }
}

Il nous reste plus qu’à insérer les fixtures en BDD :

php bin/console doctrine:fixtures:load

Repository : UserRepository.php

Nous allons générer un fichier UserRepository.php dans le dossier Repository avec un exemple de queryBuilder :

<?php

namespace App\Repository;

use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

/**
 * @method User|null find($id, $lockMode = null, $lockVersion = null)
 * @method User|null findOneBy(array $criteria, array $orderBy = null)
 * @method User[]    findAll()
 * @method User[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class UserRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, User::class);
    }
    
    /**
     * @param \DateTime $datetime
     * @return User[]
     */
    public function findByBirthdayMoreThan(\DateTime $datetime) {
        return $this->createQueryBuilder(’u’)
            ->where(’u.birthday > :datetime’)
            ->setParameter(’datetime’, $datetime)
            ->getQuery()
            ->getResult()
        ;
    }
}

Il faut savoir qu’un Repository doit être le seul à accéder à la BDD au travers de queryBuilder ou requêtes natives.

Enfin, il ne faut surtout pas oublier de lier le Repository avec l’Entity User, sinon impossible d’aller requêter sur la table user :

<?php
/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository") // ajouter le Repository
 * @ORM\Table(name="user")
 */
class User

Service : UserService.php

Nous allons créer un fichier UserService.php dans le dossier Service avec une fonction qui va modifier la description des utilisateurs :

<?php

namespace App\Service;

use App\Entity\User;

class UserService {

    /**
     * @param User $user
     * @return User
     */
    public function updateUserDescription(User $user) {
        $description = ’Je suis ’ . ucfirst($user->getFirstName()) . ’ ’ . 
            mb_strtoupper($user->getLastName()) . ’, mon anniversaire est le ’ . 
            $user->getBirthday()->format(’d/m/Y’);
        $user->setDescription($description);
        return $user;
    }
}

Il faut savoir qu’un Service va s’occuper de tout le traitement des données reçues depuis un repository.

Manager : UserManager.php

Nous allons créer un fichier UserManager.php dans le dossier Manager avec des fonctions qui iront chercher un seul ou tous les utilisateurs avec leur description :

<?php

namespace App\Manager;

use App\Entity\User;
use App\Repository\UserRepository;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use Exception;

class UserManager
{
    /** @var EntityManagerInterface */
    private $em;

    /** @var UserService */
    private $userService;

    function __construct(EntityManagerInterface $em, UserService $userService) {
        $this->em = $em;
        $this->userService = $userService;
    }

    /**
     * Rechercher d’un utilisateur avec sa description
     *
     * @param int $id
     * @return User|false
     * @throws Exception
     */
    public function findOneWithDescription(int $id) {
        $user = $this->getRepo()->find($id);
        if (!$user) {
            throw new Exception(’Utilisateur introuvable !’, 422);
        }
        return $this->userService->updateUserDescription($user);
    }

    /**
     * Liste des utilisateurs avec leur description
     *
     * @return User[]
     */
    public function findAllWithDescription() {
        $usersWithDescription = [];
        $users = $this->getRepo()->findAll();
        foreach ($users as $user) {
            $usersWithDescription[] = $this->userService->updateUserDescription($user);
        }
        return $usersWithDescription;
    }

    /**
     * Récupération de notre Repository UserRepository
     *
     * @return UserRepository
     */
    public function getRepo(): UserRepository
    {
        return $this->em->getRepository(User::class);
    }
}

Il faut savoir qu’un Manager sert seulement à rediriger depuis un Controller vers un Service ou directement un Repository.

Controller : UserController.php

Dans cette seconde partie, nous verrons comment utiliser un Controller et renvoyer une réponse en JSON, à la manière dont le ferait l’application si celle-ci était une API. L’utilisation des templates Twig se fera dans une prochaine partie.

Pour pouvoir transformer nos objets User en JSON, nous devons rajouter un Serializer à notre application. Il suffit d’utiliser la commande suivante :

composer require symfony/serializer symfony/property-access

Nous allons créer un fichier UserController.php dans le dossier Controller et ajouter 2 points d’entrée :

<?php

namespace App\Controller;

use App\Manager\UserManager;
use Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

class UserController extends AbstractController
{
    /** @var Serializer */
    private $serializer;

    function __construct() {
        $this->serializer = new Serializer(
            [new DateTimeNormalizer([’datetime_format’ => ’Y-m-d\TH:i:s.u\Z’]), new ObjectNormalizer()],
            [’json’ => new JsonEncoder()]
        );
    }

    /**
     * @Route("/users", name="get-users", methods={"GET"})
     *
     * @param UserManager $userManager
     * @return Response
     */
    public function getUsers(UserManager $userManager)
    {
        $users = $userManager->findAllWithDescription();

        return new Response($this->serializer->serialize($users, ’json’));
    }

    /**
     * @Route("/users/{id}", name="get-user", requirements={"id"="\d+"}, methods={"GET"})
     *
     * @param UserManager $userManager
     * @param int $id
     * @return Response
     * @throws Exception
     */
    public function getOneUser(UserManager $userManager, int $id)
    {
        $user = $userManager->findOneWithDescription($id);
        return new Response($this->serializer->serialize($user, ’json’));
    }
}

Testons nos services !

Il existe plusieurs façon de récupérer nos utilisateurs :

  • Ouvrir le navigateur et taper l’url http://localhost:8000/users (fonctionne uniquement pour la méthode GET et si aucune authentification n’est nécessaire)
  • Utiliser les logiciels Postman ou Insomnia qui permettront de créer une collection pour enregistrer tous les services de notre API (et plein d’autres choses très utiles).

Voici un exemple de JSON qui doit être retourné si l’on accède au service http://localhost:8000/users/1 de notre application Symfony 5 :

{
    "id": 1,
    "firstName": "Fidel",
    "lastName": "Sipes",
    "birthday": "2020-05-30T02:31:10.000000Z",
    "description": "Je suis Fidel SIPES. Mon anniversaire est le 30\/05\/2020"
}

La suite d’un projet Symfony

La prochaine partie nous amènera sur le fonctionnement des assets, du moteur de template Twig et des Forms Symfony. Nous verrons, entre autres, comment lister et modifier nos utilisateurs.

À BIENTÔT ♥