---
title: "Utilisation de Intro.js avec un projet Symfony"
excerpt: "A la recherche d'idées pour améliorer l'onboarding de vos utilisateurs, je vous propose de découvrir comment utiliser Intro.js avec un projet Symfony"
publishDate: 2022-04-29T00:00:00.000Z
tags: ["symfony", "intro-js", "onboarding"]
canonical: "https://yoandev.co/utilisation-de-intro-js-avec-un-projet-symfony"
---

<YouTube id="aDe3UhIMqBs" />

## Objectifs

Dans cet article nous allons voir ensemble comment mettre en place un onboarding avec [Intro.js](https://introjs.com/) dans une application Symfony.

Nous ferons en sorte que cet onboarding ne soit visible que lors de la première connexion de l'utilisateur.

## Initialisation d'un projet Symfony

* On initie un nouveau projet Symfony

```
symfony new introjs --webapp
cd introjs
npm install --force
npm run build
docker-compose up -d
symfony serve -d
```

* Création d'une entité ```User```

*On prends toute les propositions par défaut*

```bash
symfony console make:user                             
```

* Création d'un controller```HomeController```

```bash
symfony console make:controller Home
```

* Création d'une page de registration

```bash
symfony console make:registration   

 Creating a registration form for App\Entity\User

 Do you want to add a @UniqueEntity validation annotation on your User class to make sure duplicate accounts aren't created? (yes/no) [yes]:
 > yes

 Do you want to send an email to verify the user's email address after registration? (yes/no) [yes]:
 > no

 Do you want to automatically authenticate the user after registration? (yes/no) [yes]:
 > no

 What route should the user be redirected to after registration?:
  [12] app_home
 > 12
```

* Création d'une page de login

```bash
symfony console make:auth 

 What style of authentication do you want? [Empty authenticator]:
  [0] Empty authenticator
  [1] Login form authenticator
 > 1

 The class name of the authenticator to create (e.g. AppCustomAuthenticator):
 > AppAuthenticator

 Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
 > SecurityController

 Do you want to generate a '/logout' URL? (yes/no) [yes]:
 > yes
```

* On modifier le fichier ```src/Security/AppAuthenticator.php```

```php
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
    if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
        return new RedirectResponse($targetPath);
    }
    return new RedirectResponse($this->urlGenerator->generate('app_home'));
}
```

* On modifie le fichier ```config/packages/security.yaml```

```yaml
    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/home, roles: ROLE_USER }
```

* On mets en place Boostrap 5 (via CDN) en modifiant le fichier ```templates/base.html.twig```

```twig
<!doctype html>
<html lang="en">
	<head>
		<title>
			{% block title %}Welcome!{% endblock %}
		</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">

		{% block stylesheets %}
			{{ encore_entry_link_tags('app') }}
			<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
		{% endblock %}

		{% block javascripts %}
			{{ encore_entry_script_tags('app') }}
			<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
		{% endblock %}
	</head>
	<body>
		{% block body %}{% endblock %}
	</body>
</html>
```

* On génére et applique les migrations

```bash
symfony console make:migration
symfony console d:m:m
```

* Et enfin on modifie ```templates/home/index.html.twig```

*On mets en place un modèle de mise en page bateau, histoire de ...*

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

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

{% block body %}
	<header>
		<div class="collapse bg-dark" id="navbarHeader">
			<div class="container">
				<div class="row">
					<div class="col-sm-8 col-md-7 py-4">
						<h4 class="text-white">About</h4>
						<p class="text-muted">Add some information about the album below, the author, or any other background context. Make it a few sentences long so folks can pick up some informative tidbits. Then, link them off to some social networking sites or contact information.</p>
					</div>
				</div>
			</div>
		</div>
		<div class="navbar navbar-dark bg-dark shadow-sm">
			<div class="container">
				<a href="#" class="navbar-brand d-flex align-items-center">
					<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2" viewbox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
					<strong>Album</strong>
				</a>
				<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
					<span class="navbar-toggler-icon"></span>
				</button>
			</div>
		</div>
	</header>

	<main>

		<section class="py-5 text-center container">
			<div class="row py-lg-5">
				<div class="col-lg-6 col-md-8 mx-auto">
					<h1 class="fw-light">Album example</h1>
					<p class="lead text-muted">Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks don’t simply skip over it entirely.</p>
					<p>
						<a href="#" class="btn btn-primary my-2">Main call to action</a>
						<a href="#" class="btn btn-secondary my-2">Secondary action</a>
					</p>
				</div>
			</div>
		</section>

		<div class="album py-5 bg-light">
			<div class="container">

				<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
					<div class="col">
						<div class="card shadow-sm">
							<img src="https://picsum.photos/199" alt="">

							<div class="card-body">
								<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
								<div class="d-flex justify-content-between align-items-center">
									<div class="btn-group">
										<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
										<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
									</div>
									<small class="text-muted">9 mins</small>
								</div>
							</div>
						</div>
					</div>
                    <div class="col">
						<div class="card shadow-sm">
							<img src="https://picsum.photos/200" alt="">

							<div class="card-body">
								<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
								<div class="d-flex justify-content-between align-items-center">
									<div class="btn-group">
										<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
										<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
									</div>
									<small class="text-muted">9 mins</small>
								</div>
							</div>
						</div>
					</div>
                    <div class="col">
						<div class="card shadow-sm">
							<img src="https://picsum.photos/201" alt="">

							<div class="card-body">
								<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
								<div class="d-flex justify-content-between align-items-center">
									<div class="btn-group">
										<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
										<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
									</div>
									<small class="text-muted">9 mins</small>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>

	</main>
{% endblock %}
```

## Installation de Intro.js

Passons enfin à la mise en place de la librairie.

* On install la libraire Intro.js

```
npm install intro.js --save
```

* Créons un fichier ```assets/intro.js```

```js
import 'intro.js/introjs.css';

introJs().setOptions({
    steps: [{
      intro: "Hello world!"
    }, {
      element: document.querySelector('.test'),
      intro: "Ceci est un test",
    }]
  }).start();
```

* Ajoutons notre fichier dans la configuration de Webpack Encore ```webpack.config.js```

```js
.addEntry('intro', './assets/intro.js')
```

* Et utilisons cela dans notre fichier ```templates/base.html.twig```

```twig
<!doctype html>
<html lang="en">
	<head>
		<title>
			{% block title %}Welcome!{% endblock %}
		</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">

		{% block stylesheets %}
			{{ encore_entry_link_tags('app') }}
			{{ encore_entry_link_tags('intro') }}
			<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
		{% endblock %}

		{% block javascripts %}
			{{ encore_entry_script_tags('app') }}
			{{ encore_entry_script_tags('intro') }}
			<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
		{% endblock %}
	</head>
	<body>
		{% block body %}{% endblock %}
	</body>
</html>
```

* Puis modifions le fichier ```templates/home/index.html.twig``` pour tester le bon fonctionnement.

```twig
<section class="py-5 text-center container">
	<div class="row py-lg-5">
		<div class="col-lg-6 col-md-8 mx-auto">
			<h1 class="fw-light">Album example</h1>
			<p class="lead text-muted">Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks don’t simply skip over it entirely.</p>
			<p>
				<a href="#" class="btn btn-primary my-2 test">Main call to action</a>
				<a href="#" class="btn btn-secondary my-2">Secondary action</a>
			</p>
		</div>
	</div>
</section>
```

![](./images/2022-04-28-17-29-57.png)

## Créons notre Onboarding

* Adaptons notre fichier ```assets/intro.js```

```js
import "intro.js/introjs.css";

introJs()
    .setOptions({
        steps: [
            {
                intro: "Bienvenue sur votre espace personnel, nous allons vous guider pour vous aider à prendre en main votre espace personnel.",
            },
            {
                element: document.querySelector(".intro-step1"),
                intro: "Ceci est le nom de votre espace personnel.",
            },
            {
                element: document.querySelector(".intro-step2"),
                intro: "Ceci est le bouton d'action principale de votre espace personnel.",
            },
            {
                element: document.querySelector(".intro-step3"),
                intro: "Ceci est le bouton d'action secondaire de votre espace personnel.",
            },
            {
                element: document.querySelector(".intro-step4"),
                intro: "Ceci est une carte de votre espace personnel.",
            },
            {
                element: document.querySelector(".intro-step5"),
                intro: "Ceci est le bouton View",
            },
        ],
    })
    .start();
```

* Adpatons notre fichier ```templates/home/index.html.twig```

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

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

{% block body %}
	<header>
		<div class="collapse bg-dark" id="navbarHeader">
			<div class="container">
				<div class="row">
					<div class="col-sm-8 col-md-7 py-4">
						<h4 class="text-white">About</h4>
						<p class="text-muted">Add some information about the album below, the author, or any other background context. Make it a few sentences long so folks can pick up some informative tidbits. Then, link them off to some social networking sites or contact information.</p>
					</div>
				</div>
			</div>
		</div>
		<div class="navbar navbar-dark bg-dark shadow-sm">
			<div class="container">
				<a href="#" class="navbar-brand d-flex align-items-center">
					<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2" viewbox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
					<strong>Album</strong>
				</a>
				<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
					<span class="navbar-toggler-icon"></span>
				</button>
			</div>
		</div>
	</header>

	<main>

		<section class="py-5 text-center container">
			<div class="row py-lg-5">
				<div class="col-lg-6 col-md-8 mx-auto">
					<h1 class="fw-light intro-step1">Album example</h1>
					<p class="lead text-muted">Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks don’t simply skip over it entirely.</p>
					<p>
						<a href="#" class="btn btn-primary my-2 intro-step2">Main call to action</a>
						<a href="#" class="btn btn-secondary my-2 intro-step3">Secondary action</a>
					</p>
				</div>
			</div>
		</section>

		<div class="album py-5 bg-light">
			<div class="container">

				<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
					<div class="col intro-step4">
						<div class="card shadow-sm">
							<img src="https://picsum.photos/199" alt="">

							<div class="card-body">
								<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
								<div class="d-flex justify-content-between align-items-center">
									<div class="btn-group">
										<button type="button" class="btn btn-sm btn-outline-secondary intro-step5">View</button>
										<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
									</div>
									<small class="text-muted">9 mins</small>
								</div>
							</div>
						</div>
					</div>
                    <div class="col">
						<div class="card shadow-sm">
							<img src="https://picsum.photos/200" alt="">

							<div class="card-body">
								<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
								<div class="d-flex justify-content-between align-items-center">
									<div class="btn-group">
										<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
										<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
									</div>
									<small class="text-muted">9 mins</small>
								</div>
							</div>
						</div>
					</div>
                    <div class="col">
						<div class="card shadow-sm">
							<img src="https://picsum.photos/201" alt="">

							<div class="card-body">
								<p class="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
								<div class="d-flex justify-content-between align-items-center">
									<div class="btn-group">
										<button type="button" class="btn btn-sm btn-outline-secondary">View</button>
										<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
									</div>
									<small class="text-muted">9 mins</small>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>

	</main>
{% endblock %}
```

* Et vérifions le résultat :

![](./images/2022-04-28-17-43-46.png)

## Onboarding que lors de la première connexion

* Gérons la première connexion en ajoutant un champ à notre entité User :

```bash
symfony console make:entity User

 Your entity already exists! So let's add some new fields!

 New property name (press <return> to stop adding fields):
 > isFisrtConnexion 

 Field type (enter ? to see all types) [boolean]:
 > boolean

 Can this field be null in the database (nullable) (yes/no) [no]:
 > yes

symfony console make:migration
symfony console d:m:m
```
* Refactorons l'import du CSS et du JS d'intro.js en le supprimant de ```templates/base.html.twig``` et en l'ajoutant dans ```templates/home/index.html.twig``` :

*De cette manière Intro.js ne sera chargé que lorsque cela sera nécessaire.*

```twig
{% block body %}
	...
	{% block javascripts %}
		{{ parent() }}
		{% if app.user.isFirstConnexion %}
			{{ encore_entry_script_tags('intro') }}
		{% endif %}
	{% endblock %}
	{% block stylesheets %}
		{{ parent() }}
		{{ encore_entry_link_tags('intro') }}
	{% endblock %}
	...
{% endblock %}
```

* Et enfin gérons le champ ```isFirstConnexion``` lors de l'ouverture de session en adaptant le fichier ```src/Security/AppAuthenticator.php```

```php
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        $user = $token->getUser();
        
        if (true === $user->getIsFirstConnexion()) {
            $user->setIsFirstConnexion(false);
            $this->em->persist($user);
            $this->em->flush();
        }

        if (null === $user->getIsFirstConnexion()) {
            $user->setIsFirstConnexion(true);
            $this->em->persist($user);
            $this->em->flush();
        }

        if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
            return new RedirectResponse($targetPath);
        };

        return new RedirectResponse($this->urlGenerator->generate('app_home'));
    }
```

## Code source

- [Github](https://github.com/yoanbernabeu/intro.js-avec-symfony)
