Archives de catégorie : Symfony

[Symfony 4] – Gestion d’événements

On continue notre série de gestion des utilisateurs sans FOSUserBundle, cette fois on va mettre en place un EventListener, c’est à dire un événement qui va se déclencher là où on le voudra.

On va simplement envoyer un mail à l’utilisateur qui vient de s’inscrire afin de lui souhaiter la bienvenue.

Pour cela on va déjà ajouter la librairie qui permet de gérer l’envoi des mails :

composer req symfony/swiftmailer-bundle

Suite à ça vous devez avoir une nouvelle ligne dans votre fichier .env, qu’il faut modifier selon votre serveur, pour moi c’est :

MAILER_URL=smtp://localhost:25?encryption=&auth_mode=

(vous avez aussi la même ligne dans le fichier phpunit.xml.dist si vous voulez envoyer des mails durant vos tests)

On va aussi ajouter quelques paramètres dans nos fichiers de config, comme l’adresse expéditeur :

# services.yaml
parameters:
    locale: 'en'
    app.notifications.email_sender: g.ponty@dev-web.io

services:
   # le nom de votre service
   App\EventSubscriber\RegistrationNotifySubscriber:
        # le nom de la variable que l'on utilisera dans le service
        $sender: '%app.notifications.email_sender%'

Pour mettre en place de bonnes pratiques, on va créer un fichier qui regroupera tous nos events, ça permettra de les avoir tous au même endroit :

// App/Events.php
namespace App;

/**
 * This class defines the names of all the events dispatched in
 * our project. It's not mandatory to create a
 * class like this, but it's considered a good practice.
 *
 */
final class Events
{
    /**
     * For the event naming conventions, see:
     * https://symfony.com/doc/current/components/event_dispatcher.html#naming-conventions.
     *
     * @Event("Symfony\Component\EventDispatcher\GenericEvent")
     *
     * @var string
     */
    const USER_REGISTERED = 'user.registered';
}

On créé maintenant notre Listener, il n’y a rien de compliqué :

<?php
// App\EventSubscriber\RegistrationNotifySubscriber.php
namespace App\EventSubscriber;

use App\Entity\User;
use App\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;

/**
 * Envoi un mail de bienvenue à chaque creation d'un utilisateur
 *
 */
class RegistrationNotifySubscriber implements EventSubscriberInterface
{
    private $mailer;
    private $sender;

    public function __construct(\Swift_Mailer $mailer, $sender)
    {
        // On injecte notre expediteur et la classe pour envoyer des mails
        $this->mailer = $mailer;
        $this->sender = $sender;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            // le nom de l'event et le nom de la fonction qui sera déclenché
            Events::USER_REGISTERED => 'onUserRegistrated',
        ];
    }

    public function onUserRegistrated(GenericEvent $event): void
    {
        /** @var User $user */
        $user = $event->getSubject();

        $subject = "Bienvenue";
        $body = "Bienvenue mon ami.e sur ce tutorial";

        $message = (new \Swift_Message())
            ->setSubject($subject)
            ->setTo($user->getEmail())
            ->setFrom($this->sender)
            ->setBody($body, 'text/html')
        ;

        $this->mailer->send($message);
    }
}

Et enfin on modifie notre Controller pour juste y ajouter le déclenchement de notre event :

<?php
// src/Controller/RegistrationController.php
namespace App\Controller;

use App\Form\UserType;
use App\Entity\User;
use App\Events;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;

class RegistrationController extends Controller
{
    /**
     * @Route("/register", name="user_registration")
     */
    public function registerAction(Request $request, UserPasswordEncoderInterface $passwordEncoder, EventDispatcherInterface $eventDispatcher)
    {
    
        $user = new User();
        $form = $this->createForm(UserType::class, $user);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {

            $password = $passwordEncoder->encodePassword($user, $user->getPassword());
            $user->setPassword($password);

            // Par defaut l'utilisateur aura toujours le rôle ROLE_USER
            $user->setRoles(['ROLE_USER']);

            // On enregistre l'utilisateur dans la base
            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();

            //On déclenche l'event
            $event = new GenericEvent($user);
            $eventDispatcher->dispatch(Events::USER_REGISTERED, $event);

            return $this->redirectToRoute('security_login');
        }

        return $this->render(
            'register.html.twig',
            array('form' => $form->createView())
        );
    }
}

and voilà !

Vous pouvez retrouver toutes les sources ici :

https://github.com/gponty/userDemo

[Symfony 4] – Tests unitaires

Rien à voir avecPhpUnit mais on va installer un serveur web pour exécuter notre site :

composer require --dev symfony/web-server-bundle

Puis pour lancer le serveur :

php bin/console server:run

Vous pouvez comme ça accéder à votre application avec : http://127.0.0.1:8000

Ensuite pour les tests, il faut le composant PHPUnit :

composer req symfony/phpunit-bridge

Et pour les tests fonctionnels on va avoir besoin de :

composer req --dev browser-kit
composer req --dev symfony/css-selector

Il faut aussi créer le répertoire (si cela n’a pas était fait automatiquement) qui contiendra tout nos tests, on l’appellera pour être original : « tests »

Et comme on est feignant on va faire appelle au maker pour nous créer nos classes :

php bin/console make:unit-test
php bin/console make:functional-test

On va maintenant faire un peu de config, dans le fichier config/packages/test/framework.yaml, nous allons lui dire que nous allons utiliser les sessions (juste décommenter les 2 dernières lignes) :

framework:
    test: ~
    # Uncomment this section if you're using sessions
    session:
        storage_id: session.storage.mock_file

Et surtout ajouter cette ligne dans le fichier phpunit.xml.dist, c’est sur cette base de données que va se connecter phpunit pour effectuer qui ont besoin de votre base :

<!-- define your env variables for the test env here -->
<env name="DATABASE_URL" value="mysql://root:@127.0.0.1/userDemo" />

ATTENTION : il faut que la base de données soit la même que ce qu’il y a dans votre fichier env.
Ceci implique que vos tests seront réalisés sur la base réelle, l’idéal serait de remplacer userDemo par userDemoTest et utiliser une base dédiée aux tests.
Le problème c’est que je n’ai pas trouvé les commandes en symfony 4 pour créer la base test avec son schéma.

Vous pouvez déjà exécuter les tests par défaut en faisant simplement :

php bin/phpunit

Voici le résultat attendu :

PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

Testing Project Test Suite
..                                                                  2 / 2 (100%)

Time: 12.97 seconds, Memory: 26.00MB

OK (2 tests, 3 assertions)

On va maintenant créer un 1er test qui consiste à vérifier qu’un message d’erreur s’affiche bien lorsque l’utilisateur s’inscrit en ne saisissant pas les 2 mêmes mot de passe :

public function testCheckPassword(){
        $client = static::createClient();

        $crawler = $client->request(
            'GET',
            '/register'
        );

        $form = $crawler->selectButton('S\'inscrire')->form();

        $form['user[email]'] = 'toto@email.com';
        $form['user[username]'] = 'usernametest';
        $form['user[fullName]'] = 'John Doe';
        $form['user[password][first]'] = 'pass1';
        $form['user[password][second]'] = 'pass2';

        $crawler = $client->submit($form);

        //echo $client->getResponse()->getContent();


        $this->assertEquals(1,
            $crawler->filter('li:contains("This value is not valid.")')->count()
        );
    }

Si tout va bien vous devriez obtenir :

#!/usr/bin/env php
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

Testing Project Test Suite
...                                                                 3 / 3 (100%)

Time: 158 ms, Memory: 22.00MB

OK (3 tests, 4 assertions)

 

[Symfony 4] – Gestion des utilisateurs sans FosUserBundle Chapitre 3

@deprecated : nouvelle version => http://blog.dev-web.io/2018/10/30/symfony-4-gestion-utilisateurs-sans-fosuserbundle-v2018-chapitre-1/

Nous allons voir comment un utilisateur peut s’inscrire sur notre site et ainsi accéder à certaines fonctions du site réservé aux inscrits.

On va tout d’abord ajouter le composant form à notre projet :

composer require form

Ensuite nous allons modifier notre class User pour y ajouter 2 contraintes :

  • Un login unique
  • Un mail unique

Ceux qui veut dire que si un utilisateur essaie de s’inscrire avec un username ou login déjà existant, il ne pourra pas le faire mais en plus on lui affichera un message d’erreur.

// /src/Entity/User.php

// Ne pas oublier
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @ORM\Table(name="user")
 * @UniqueEntity(fields="email", message="Email déjà pris")
 * @UniqueEntity(fields="username", message="Username déjà pris")
 */
class User implements UserInterface, \Serializable

Et on va ajouter d’autres contrôles :

  • Username
  • Email renseigné
  • Mot de passe renseigné

On verra plus tard comment contrôler le mot de passe, voici ce que nous donne notre classe User mis à jour :

<?php
// /src/Entity/User.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;


/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @ORM\Table(name="user")
 * @UniqueEntity(fields="email", message="Email déjà pris")
 * @UniqueEntity(fields="username", message="Username déjà pris")
 */
class User implements UserInterface, \Serializable
{
    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="string")
     * @Assert\NotBlank()
     */
    private $fullName;

    /**
     * @var string
     *
     * @ORM\Column(type="string", unique=true)
     * @Assert\NotBlank()
     */
    private $username;

    /**
     * @var string
     *
     * @ORM\Column(type="string", unique=true)
     * @Assert\NotBlank()
     * @Assert\Email()
     */
    private $email;

    /**
     * @var string
     *
     * @ORM\Column(type="string", length=64)
     */
    private $password;

    /**
     * @var array
     *
     * @ORM\Column(type="json")
     */
    private $roles = [];

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

    public function setFullName(string $fullName): void
    {
        $this->fullName = $fullName;
    }

    // le ? signifie que cela peur aussi retourner null
    public function getFullName(): ?string
    {
        return $this->fullName;
    }

    public function getUsername(): ?string
    {
        return $this->username;
    }

    public function setUsername(string $username): void
    {
        $this->username = $username;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): void
    {
        $this->email = $email;
    }

    public function getPassword(): ?string
    {
        return $this->password;
    }

    public function setPassword(string $password): void
    {
        $this->password = $password;
    }

    /**
     * Retourne les rôles de l'user
     */
    public function getRoles(): array
    {
        $roles = $this->roles;

        // Afin d'être sûr qu'un user a toujours au moins 1 rôle
        if (empty($roles)) {
            $roles[] = 'ROLE_USER';
        }

        return array_unique($roles);
    }

    public function setRoles(array $roles): void
    {
        $this->roles = $roles;
    }

    /**
     * Retour le salt qui a servi à coder le mot de passe
     *
     * {@inheritdoc}
     */
    public function getSalt(): ?string
    {
        // See "Do you need to use a Salt?" at https://symfony.com/doc/current/cookbook/security/entity_provider.html
        // we're using bcrypt in security.yml to encode the password, so
        // the salt value is built-in and you don't have to generate one

        return null;
    }

    /**
     * Removes sensitive data from the user.
     *
     * {@inheritdoc}
     */
    public function eraseCredentials(): void
    {
        // Nous n'avons pas besoin de cette methode car nous n'utilions pas de plainPassword
        // Mais elle est obligatoire car comprise dans l'interface UserInterface
        // $this->plainPassword = null;
    }

    /**
     * {@inheritdoc}
     */
    public function serialize(): string
    {
        return serialize([$this->id, $this->username, $this->password]);
    }

    /**
     * {@inheritdoc}
     */
    public function unserialize($serialized): void
    {
        [$this->id, $this->username, $this->password] = unserialize($serialized, ['allowed_classes' => false]);
    }
}

Nous allons aussi avoir besoin du formulaire qui va permettre à l’utilisateur de s’inscrire. Pour cela on va utilise un type form qui reprendra pratiquement toutes les zones de notre class User, vous noterez que le mot de passe sera à saisir 2 fois afin de contrôler qu’il soit bien identique :

<?php
// /src/Form/UserType.php
namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('fullName', TextType::class)
            ->add('email', EmailType::class)            
            ->add('username', TextType::class)
            ->add('password', RepeatedType::class, array(
                'type' => PasswordType::class,
                'first_options'  => array('label' => 'Password'),
                'second_options' => array('label' => 'Repeat Password'),
            ))
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => User::class,
        ));
    }
}

Bien évidemment on va avoir besoin d’un controller et d’un template :

<?php
// src/Controller/RegistrationController.php
namespace App\Controller;

use App\Form\UserType;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class RegistrationController extends Controller
{
    /**
     * @Route("/register", name="user_registration")
     */
    public function registerAction(Request $request, UserPasswordEncoderInterface $passwordEncoder)
    {
        $user = new User();
        $form = $this->createForm(UserType::class, $user);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {

            $password = $passwordEncoder->encodePassword($user, $user->getPassword());
            $user->setPassword($password);

            // Par defaut l'utilisateur aura toujours le rôle ROLE_USER
            $user->setRoles(['ROLE_USER']);

            // On enregistre l'utilisateur dans la base
            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();

            return $this->redirectToRoute('security_login');
        }

        return $this->render(
            'register.html.twig',
            array('form' => $form->createView())
        );
    }
}
{# templates/register.html.twig #}

{{ form_start(form) }}
    {{ form_row(form.fullName) }}
    {{ form_row(form.username) }}
    {{ form_row(form.email) }}
    {{ form_row(form.password.first) }}
    {{ form_row(form.password.second) }}

    <button type="submit">S'inscrire !</button>
{{ form_end(form) }}

Voilà pour l’inscription d’un utilisateur, vous pourrez par vous-même envoyer un mail de confirmation si l’inscription est valide.

Les sources se trouvent ici : https://github.com/gponty/userDemo

Dans le prochain chapitre nous verrons comment l’utilisateur peur modifier son mot de passe et le modifier si il l’a oublié.

 

[Symfony 4] – Gestion des utilisateurs sans FosUserBundle Chapitre 2

@deprecated : nouvelle version => http://blog.dev-web.io/2018/10/30/symfony-4-gestion-utilisateurs-sans-fosuserbundle-v2018-chapitre-1/

Un court chapitre où nous allons juste implémenter la fonction de connexion d’un utilisateur.

Pour cela nous allons avoir besoin de faire encore un peu de configuration, nous allons tout d’abord mettre à jour le firewall principal du fichier secrity.yaml :

security:
    encoders:
        App\Entity\User: bcrypt

    providers:
        database_users:
            entity: { class: App\Entity\User, property: username }

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            # les urls auxquels s'appliquent ce firewall, dans ce cas, ce sont toutes les urls
            pattern: ^/

            # La connexion n'est pas requise sur toutes les pages
            # par exemple la page d'accueil
            anonymous: true

            form_login:
                # Le nom de la route de la page de connexion
                check_path: security_login
                # Le nom de la route où se trouve le formulaire de connexion
                # Si un utilisateur tente d'acceder à une page protégée sans en avoir les droits
                # il sera redirigé sur cette page
                login_path: security_login
                # Securisation des formulaires
                csrf_token_generator: security.csrf.token_manager
                # La page par defaut apres une connexion reussie
                default_target_path: admin

            logout:
                # La route où se trouve le process de deconnexion
                path: security_logout
                # La route sur laquelle doit etre rediriger l'utilisateur apres une deconnexion
                target: index

    access_control:
        # Les regles de securité
        # Là dans ce cas seul les utilisateurs ayant le rôle ROLE_ADMIN
        # peuvent acceder à toutes les pages commençant par /admin
        - { path: '^/admin', roles: ROLE_ADMIN }

On va aussi modifier le fichier framework.yaml pour y ajouter la gestion des sessions et la protection xcrf

framework:
    secret: '%env(APP_SECRET)%'
    #default_locale: en
    csrf_protection: { enabled: true }
    #http_method_override: true

    # uncomment this entire section to enable sessions
    session:
        # With this config, PHP's native session handling is used
        handler_id: ~

    #esi: ~
    #fragments: ~
    php_errors:
        log: true

On va créer notre controller de login et logout, rien de compliqué, dans App/Controller/SecurityController.php :

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends AbstractController
{
    /**
     * @Route("/login", name="security_login")
     */
    public function login(AuthenticationUtils $helper): Response
    {
        return $this->render('Security/login.html.twig', [
            // dernier username saisi (si il y en a un)
            'last_username' => $helper->getLastUsername(),
            // La derniere erreur de connexion (si il y en a une)
            'error' => $helper->getLastAuthenticationError(),
        ]);
    }

    /**
     * La route pour se deconnecter.
     * 
     * Mais celle ci ne doit jamais être executé car symfony l'interceptera avant.
     *
     *
     * @Route("/logout", name="security_logout")
     */
    public function logout(): void
    {
        throw new \Exception('This should never be reached!');
    }
}

Si ce n’est pas déjà fait, on va créer 1 controller qui contiendra 2 routes : index et admin

La page index sera la landing page de notre site, la page admin sera la partie sécurisée où seul les admins seront autorisés à y accéder :

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class IndexController extends Controller
{
    /**
     * @Route("/", name="index")
     */
    public function index()
    {
        return $this->render('index.html.twig');
    }

    /**
     * @Route("/admin", name="admin")
     */
    public function admin()
    {
        return $this->render('Admin/index.html.twig');
    }

}

Et les templates qui vont bien :

index.html.twig

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

{% block body %}
    <a href="{{ path('security_login')}}"> Connexion</a>

        
{% endblock %}
Admin/index.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    Bienvenue {{ app.user.username }} !
        
{% endblock %}
On va aussi créer un template pour notre formulaire de connexion, Security/login.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    {% if error %}
        <div class="alert alert-danger">
            {{ error.messageKey }}
        </div>
    {% endif %}

    <div class="row">
        <div class="col-sm-5">
            <div class="well">
                <form action="{{ path('security_login') }}" method="post">
                    <fieldset>
                        <legend><i class="fa fa-lock" aria-hidden="true"></i> Connexion</legend>
                        <div class="form-group">
                            <label for="username">Username</label>
                            <input type="text" id="username" name="_username" value="{{ last_username }}" class="form-control"/>
                        </div>
                        <div class="form-group">
                            <label for="password">Mot de passe</label>
                            <input type="password" id="password" name="_password" class="form-control" />
                        </div>
                        <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"/>
                        <button type="submit" class="btn btn-primary">
                            <i class="fa fa-sign-in" aria-hidden="true"></i> On entre
                        </button>
                    </fieldset>
                </form>
            </div>
        </div>

        
{% endblock %}

A ce point et si on a rien oublié on doit pouvoir se connecter en accédant à la page /login et en saisissant les identifiants qui sont dans votre fichier fixtures.

Dans le prochain chapitre nous verrons comment un utilisateur peut s’inscrire sur le site.
Vous pouvez retrouver les sources complètes ici : https://github.com/gponty/userDemo

[Symfony 4] Gestion utilisateurs sans FosUserBundle

@deprecated : nouvelle version => http://blog.dev-web.io/2018/10/30/symfony-4-gestion-utilisateurs-sans-fosuserbundle-v2018-chapitre-1/

Après « Comment utiliser FosUserbundle« , comment se passer de FosUserBundle !

Mais à l’heure où j’écris ces lignes il n’y a de toute façon pas le choix puisque FosUserBundle n’est pas encore compatible avec Symfony 4.

Plutôt que d’attendre qu’il le soit, j’aurai pu y contribuer, mais j’ai préféré me retrousser les manches et mettre les mains dans le cambouis pour développer ma propre solution de gestion des utilisateurs.

Ceci afin de ne pas être dépendant de FOSUserBundle et devoir attendre qu’il soit compatible avec les dernières version de Symfony, mais surtout de bien comprendre les mécanismes de sécurité de Symfony (et aussi parce que ça faisait longtemps que j’avais écrit sur ce blog).

D’abord on prend un papier et un stylo et on répond à la question : Que doit faire un système de gestion des utilisateurs ?

On va commencer par des réponses simples :

  • Inscription d’un utilisateur
  • Connexion de l’utilisateur
  • Affecter des droits (rôles)
  • Récupérer son mot de passe
  • Remplacer son mot de passe

Dans cette 1ere partie nous allons juste voir la création de notre entité User.

Afin de mettre en place tout ça on va commencer par créer un projet Symfony 4 (pour ça il faut absolument php 7.1, sinon ce sera du symfony 3.4)  :

composer create-project symfony/skeleton UserDemo

Puis ajouter tous les composants que l’on aura besoin :

Doctrine et maker pour générer des controllers, entity :

composer require doctrine maker

Gestion de la sécurité :

composer require symfony/security-bundle

Moteur de template Twig :

composer require twig

Toolbar debug :

composer require web-profiler-bundle

Annotations :

composer require annotations

Validation :

composer require validator

Gestion de fixtures :

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

Pour créer la base de donnée, on met à jour notre config dans le fichier .env :

DATABASE_URL=mysql://root:@127.0.0.1:3306/userdemo

puis pour la créer dans mysql :

php bin/console doctrine:database:create

On va aussi creer notre 1er controller :

php bin/console make:controller

On va maintenant créer notre Entité User, c’est celle ci qui contiendra toutes les données de l’utilisateur (username, email, mot de passe, …) :

php bin/console make:entity
On a donc un nouveau fichier dans src/Enity/User.php :
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @ORM\Table(name="user")
 */
class User implements UserInterface, \Serializable
{
    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="string")
     */
    private $fullName;

    /**
     * @var string
     *
     * @ORM\Column(type="string", unique=true)
     */
    private $username;

    /**
     * @var string
     *
     * @ORM\Column(type="string", unique=true)
     */
    private $email;

    /**
     * @var string
     *
     * @ORM\Column(type="string")
     */
    private $password;

    /**
     * @var array
     *
     * @ORM\Column(type="json")
     */
    private $roles = [];

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

    public function setFullName(string $fullName): void
    {
        $this->fullName = $fullName;
    }

    public function getFullName(): string
    {
        return $this->fullName;
    }

    public function getUsername(): string
    {
        return $this->username;
    }

    public function setUsername(string $username): void
    {
        $this->username = $username;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setEmail(string $email): void
    {
        $this->email = $email;
    }

    public function getPassword(): string
    {
        return $this->password;
    }

    public function setPassword(string $password): void
    {
        $this->password = $password;
    }

    /**
     * Retourne les rôles de l'user
     */
    public function getRoles(): array
    {
        $roles = $this->roles;

        // Afin d'être sûr qu'un user a toujours au moins 1 rôle
        if (empty($roles)) {
            $roles[] = 'ROLE_USER';
        }

        return array_unique($roles);
    }

    public function setRoles(array $roles): void
    {
        $this->roles = $roles;
    }

    /**
     * Retour le salt qui a servi à coder le mot de passe
     *
     * {@inheritdoc}
     */
    public function getSalt(): ?string
    {
        // See "Do you need to use a Salt?" at https://symfony.com/doc/current/cookbook/security/entity_provider.html
        // we're using bcrypt in security.yml to encode the password, so
        // the salt value is built-in and you don't have to generate one

        return null;
    }

    /**
     * Removes sensitive data from the user.
     *
     * {@inheritdoc}
     */
    public function eraseCredentials(): void
    {
        // Nous n'avons pas besoin de cette methode car nous n'utilions pas de plainPassword
        // Mais elle est obligatoire car comprise dans l'interface UserInterface
        // $this->plainPassword = null;
    }

    /**
     * {@inheritdoc}
     */
    public function serialize(): string
    {
        return serialize([$this->id, $this->username, $this->password]);
    }

    /**
     * {@inheritdoc}
     */
    public function unserialize($serialized): void
    {
        [$this->id, $this->username, $this->password] = unserialize($serialized, ['allowed_classes' => false]);
    }
}

On met la base à jour :

php bin/console doctrine:schema:update --force

Nous allons tout de suite créer un utilisateur en utilisant les fixtures (c’est-à-dire des exemples de données), il faut d’abord ajouter un encoder dans le fichier security.yaml :

security:
    encoders:
        App\Entity\User: bcrypt
Créer le fichier src/DataFixtures/AppFixtures.php
namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class AppFixtures extends Fixture
{
    private $passwordEncoder;

    public function __construct(UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->passwordEncoder = $passwordEncoder;
    }

    public function load(ObjectManager $manager)
    {
        foreach ($this->getUserData() as [$fullname, $username, $password, $email, $roles]) {
            $user = new User();
            $user->setFullName($fullname);
            $user->setUsername($username);
            $user->setPassword($this->passwordEncoder->encodePassword($user, $password));
            $user->setEmail($email);
            $user->setRoles($roles);

            $manager->persist($user);
            $this->addReference($username, $user);
        }

        $manager->flush();
    }

    private function getUserData(): array
    {
        return [
            // $userData = [$fullname, $username, $password, $email, $roles];
            ['Jane Doe', 'jane_admin', 'kitten', 'jane_admin@symfony.com', ['ROLE_ADMIN']],
            ['Tom Doe', 'tom_admin', 'kitten', 'tom_admin@symfony.com', ['ROLE_ADMIN']],
            ['John Doe', 'john_user', 'kitten', 'john_user@symfony.com', ['ROLE_USER']],
        ];
    }


}

Il n’y a plus qu’à charger les fixtures :

php bin/console doctrine:fixtures:load

Et si tout va bien vous devez avoir dans votre base 3 utilisateurs :

Dans le prochain chapitre nous verrons comment se connecter.

En attendant vous pouvez retrouver l’ensemble des sources ici : https://github.com/gponty/userDemo

[Symfony 4] Generate entities

Si comme moi vous aviez l’habitude de creer vos getters et setters à l’aide de la commande :

 php bin/console doctrine:generate:entities 

Sachez que vous pouvez encore l’executer dans Symfony 4 mais qu’elle ne vous renverra qu’une erreur. Néanmoins la plupart des éditeurs de texte ont une option ou extension pour le faire.

Source : https://github.com/doctrine/DoctrineBundle/issues/729

Tri des oneToMany

Lorsque vous voulez trier une relation OneToMany d’une entité, par exemple :

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

/**
* @ORM\OneToMany(targetEntity="AppBundle\Entity\Animal", mappedBy="personne")
*/
protected $animaux;

...
}

Il suffit d’ajouter un @ORM/OrderBy({« attribute » = « ASC », « attribute2 » = « DESC »}), ce qui donnera pour notre exemple :

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

/**
* @ORM\OneToMany(targetEntity="AppBundle\Entity\Animal", mappedBy="personne")
* @ORM\OrderBy({"nom_animal" = "ASC"})
*/
protected $animaux;

...
}

Pratique et indispensable !

DQL MEMBER OF

Lorsque vous avez une relation ManyToMany du style :

/**
* @ORM\ManyToMany(targetEntity="Societe", inversedBy="dossiersSecondaire" , cascade={"persist"})
* @ORM\JoinTable(name="dossier_societe")
*/
private $societeSecondaire;

Et que vous voulez ramener toutes les lignes contenant ce critère, il suffit de faire  :

->where(':societeATrouver MEMBER OF d.societeSecondaire')