I. Mise en place de la partie Back-end▲
I-A. Installation d'une application Symfony3▲
Installons tout d'abord la dernière version de Symfony3 via l'installeur prévu à cet effet sur le site officiel :
symfony new api-lab
Puis lançons le serveur PHP interne via cette commande à la racine du projet :
bin/console server:start
I-B. Installation des bundles nécessaires▲
Viennent ensuite l'installation et la configuration de certains bundles incontournables lorsque l'on veut créer une API. Nous sauterons volontairement l'étape du « composer require » et de la déclaration des bundles dans le Kernel de Symfony pour passer directement à la configuration.
I-B-1. FOSRestBundle▲
Ce bundle va nous permettre d'utiliser des routes API automatiques ainsi que de retourner des réponses au format Json à notre client Angular2 avec un minimum de configuration :
2.
3.
4.
5.
fos_rest
:
routing_loader
:
default_format
:
json
view
:
view_response_listener
:
true
2.
3.
4.
app
:
resource
:
"@AppBundle/Controller/"
type
:
rest
prefix
:
/api
I-B-2. NelmioCorsBundle▲
Continuons ensuite avec le Bundle, qui va nous permettre de faire des requêtes Ajax sur l'API, étant donné que nos deux applications se trouvent sur deux domaines différents :
2.
3.
4.
5.
6.
7.
nelmio_cors
:
paths
:
'^/api/'
:
allow_origin
:
[
'http://localhost:4200'
]
allow_headers
:
[
'origin'
, 'content-type'
, 'authorization'
]
allow_methods
:
[
'POST'
, 'PUT'
, 'GET'
, 'DELETE'
]
max_age
:
3600
Nous avons ici autorisé notre future application Angular2 ainsi que le header « authorization » qui nous servira à nous authentifier. Patience, on y est bientôt.
I-B-3. JMSSerializerBundle▲
Ce bundle va nous permettre de sérialiser les données renvoyées par notre API. Aucune configuration n'est nécessaire dans le cadre de ce tutoriel. Nous utiliserons JMSSerializer plus tard, directement dans notre PostController.
I-B-4. LexikJWTAuthenticationBundle▲
Enfin, dernier, mais pas le moindre, le bundle qui va nous servir à sécuriser l'accès à nos données Symfony via un token d'authentification. Je vous laisse lire la documentation officielle qui est très claire. Il vous suffit vraiment de suivre les étapes point par point.
J'ai ajouté deux petites lignes sous l'index « form_login » du security.yml de façon à pouvoir envoyer username & password au lieu de _username et _password pour nous authentifier auprès de notre API. Je vous invite à en faire de même.
username_parameter
:
username
password_parameter
:
password
Nous allons ensuite devoir générer des données de bases pour pouvoir tester notre système.
I-C. Création d'utilisateurs▲
Nous avons besoin d'un utilisateur. Il s'appellera « gary » et aura comme password « pass » (très original…). Pour ce faire, nous n'allons pas mettre en place un système de gestion d'utilisateurs, car ce n'est pas le but de ce tutoriel. Nous allons utiliser le système « user in memory » de Symfony. Je vous propose donc de rajouter un peu de configuration :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
security
:
encoders
:
Symfony\Component\Security\Core\User\User
:
plaintext
providers
:
in_memory
:
memory
:
users
:
gary
:
password
:
pass
roles
:
'ROLE_USER'
I-D. Création d'un jeu de données▲
Nous allons avoir besoin de publications à renvoyer à notre client Angular2. Nous devons créer une entity « Post » qui sera la représentation de nos données.
Nous n'ajouterons qu'une seule propriété « title » à cette entity pour les besoins de ce tutoriel ; même s'il était utile que nos publications aient aussi un auteur, un contenu, une date de création, etc., etc.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
<?php
namespace
AppBundle\Entity;
use
Doctrine\ORM\Mapping as
ORM;
use
JMS\Serializer\Annotation as
Serializer;
/**
* Post
*
* @ORM\Table(name="posts")
* @ORM\Entity
*
* @Serializer\ExclusionPolicy("all")
*/
class
Post
{
/**
*
@var
int
*
* @ORM\Column(name="id", type="
integer
")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private
$id
;
/**
*
@var
string
*
* @ORM\Column(type="
string
", length=
255
)
*
* @Serializer\Expose
*/
private
$title
;
/**
* Get id
*
*
@return
int
*/
public
function
getId()
{
return
$this
->
id;
}
/**
* Set title
*
*
@param
string
$title
*
*
@return
Post
*/
public
function
setTitle($title
)
{
$this
->
title =
$title
;
return
$this
;
}
/**
* Get title
*
*
@return
string
*/
public
function
getTitle()
{
return
$this
->
title;
}
}
Utilisons ensuite le DoctrineFixturesBundle (après l'avoir installé bien sûr !) pour générer deux publications en créant une classe LoadPostData :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
<?php
namespace
AppBundle\DataFixtures\ORM;
use
Doctrine\Common\DataFixtures\FixtureInterface;
use
Doctrine\Common\Persistence\ObjectManager;
use
AppBundle\Entity\Post;
class
LoadPostData implements
FixtureInterface
{
public
function
load(ObjectManager $manager
)
{
$post
=
new
Post();
$post
->
setTitle('post1'
);
$post2
=
new
Post();
$post2
->
setTitle('post2'
);
$manager
->
persist($post
);
$manager
->
persist($post2
);
$manager
->
flush();
}
}
Créons ensuite la base de données, le schéma, et chargeons les données via ces commandes :
2.
3.
4.
5.
6.
7.
8.
# Création de la base
bin/console do
:da:cr
# Création du schéma de données
bin/console do
:sc:cr
# Générations des fixtures
bin/console doctrine:fixtures:load
Enfin, créons notre PostController avec la méthode qui sera le endpoint de notre micro API :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
<?php
namespace
AppBundle\Controller;
use
FOS\RestBundle\Controller\FOSRestController;
use
FOS\RestBundle\Routing\ClassResourceInterface;
use
FOS\RestBundle\Controller\Annotations as
Rest;
class
PostController extends
FOSRestController implements
ClassResourceInterface
{
/**
* @Rest\View()
*/
public
function
cgetAction()
{
return
$this
->
getDoctrine()->
getRepository('AppBundle:Post'
)->
findAll();
}
}
Nous voilà parés ! Vous pouvez dès lors tester la partie back-end en faisant une requête POST vers notre endpoint :
curl -X POST http://localhost:8000
/api/login_check -d username
=
gary -d password
=
pass
Si tout va bien, vous devriez recevoir un token d'authentification.
C'est le cas ? Très bien, nous allons pouvoir commencer la partie front-end.
II. Mise en place de la partie front-end▲
II-A. Création de l'application Angular2 via Angular CLI▲
Installons tout d'abord Angular CLI globalement sur notre machine. Cet outil va nous servir à générer la structure de notre application via une simple commande et à recompiler à la volée nos modifications :
npm install -g @angular/cli
Créons ensuite notre application :
ng new api-lab
Puis lançons notre serveur interne :
cd api-lab &&
ng serve
Maintenant que notre application est lancée, vous pouvez vous rendre sur l'URL indiquée dans votre console pour accéder à votre application :
Vous devriez alors voir apparaître : app works!
II-B. Création des différents composants▲
Nous allons ensuite générer trois composants principaux supplémentaires :
- homepage ;
- authentication ;
- post.
Commençons notre composant « homepage » en lançant la commande suivante :
ng g c homepage
g pour generate et c pour component.
2.
3.
4.
5.
6.
7.
import
{
Component }
from
'@angular/core'
;
@Component
({
selector
:
'app-homepage'
,
templateUrl
:
'./homepage.component.html'
,
}
)
export
class
HomepageComponent {}
Vous remarquerez que nous avons enlevé la déclaration du fichier CSS. En effet, nous inclurons bootstrap pour styliser rapidement notre application.
<h1>Home</h1>
Créons ensuite le composant « authentication » de la même manière :
ng g c authentication
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
import
{
Component }
from
'@angular/core'
;
import
{
FormBuilder,
FormGroup,
Validators }
from
'@angular/forms'
;
import
{
Router }
from
'@angular/router'
;
import
{
AuthenticationService }
from
'./authentication.service'
;
@Component
({
selector
:
'app-authentication'
,
templateUrl
:
'./authentication.component.html'
,
}
)
export
class
AuthenticationComponent {
loginForm
:
FormGroup;
error
:
string
=
''
;
constructor
(
private
formBuilder
:
FormBuilder,
private
authenticationService
:
AuthenticationService,
private
router
:
Router
) {
this
.
loginForm =
formBuilder.group
({
'username'
:
[
''
,
Validators.
required],
'password'
:
[
''
,
Validators.
required]
}
);
}
onSubmit
(
) {
this
.
authenticationService
.authenticate
(
this
.
loginForm.
value)
.subscribe
(
data =>
{
localStorage.setItem
(
'id_token'
,
data.
token);
this
.
router.navigate
([
'post'
]
);
},
error =>
this
.
error =
error.
message
);
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<h1>Login</h1>
<div>
<div [hidden]=
"!error"
class
=
"alert alert-danger"
role
=
"alert"
>
<span class
=
"glyphicon glyphicon-exclamation-sign"
aria-hidden
=
"true"
></span>
<span class
=
"sr-only"
>
Error:</span>
</div>
<form [formGroup]=
"loginForm"
(ngSubmit)=
"onSubmit()"
>
<div class
=
"form-group"
>
<input type
=
"text"
class
=
"form-control"
placeholder
=
"Username*"
formControlName
=
"username"
>
</div>
<div class
=
"form-group"
>
<input type
=
"password"
class
=
"form-control"
placeholder
=
"Password*"
formControlName
=
"password"
>
</div>
<button type
=
"submit"
class
=
"btn btn-success pull-right"
[disabled]=
"!loginForm.valid"
>
Submit</button>
</form>
</div>
Ce composant sera notre formulaire d'authentification vers notre API. Nous utiliserons le module ReactiveFormsModule de Angular2 pour une mise en place plus simple sans utiliser la directive [(ngModel)] très gourmande en ressources et pour pouvoir lui attribuer des validateurs.
Passons ensuite à la création du composant « post » :
ng g c post
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
import
{
Component,
OnInit }
from
'@angular/core'
;
import
{
PostRepository }
from
'./post-repository.service'
;
@Component
({
selector
:
'app-post'
,
templateUrl
:
'./post.component.html'
}
)
export
class
PostComponent implements
OnInit {
posts
:
any
[]
=
[];
error
:
string
=
''
;
constructor
(
private
postRepository
:
PostRepository) {}
ngOnInit
(
) {
this
.
postRepository
.getList
(
)
.subscribe
(
data =>
this
.
posts =
data,
error =>
this
.
error =
error.
message
);
}
}
Pour finir, nous allons ajouter deux services à notre application :
- un service qui nous servira à nous authentifier sur notre API et à gérer le login et le logout ;
- un service qui nous servira à requêter notre endpoint et qui nous renverra notre liste de publications.
Pour ce faire, lancez les commandes suivantes :
ng g s authentication/authentication --flat
g pour generate, s pour service (je sais que vous le saviez !) et -flat pour créer des composants sans dossiers.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
import
{
Injectable }
from
'@angular/core'
;
import
{
Http,
Response,
Headers,
RequestOptions,
URLSearchParams }
from
'@angular/http'
;
import
{
tokenNotExpired }
from
'angular2-jwt'
;
import
'rxjs/add/operator/map'
;
@Injectable
(
)
export
class
AuthenticationService {
constructor
(
private
http
:
Http) {}
authenticate
(
user
:
any
) {
let
url =
'http://127.0.0.1:8000/api/login_check'
;
let
body =
new
URLSearchParams
(
);
body.append
(
'username'
,
user.
username);
body.append
(
'password'
,
user.
password);
let
headers =
new
Headers
({
'Content-Type'
:
'application/x-www-form-urlencoded'
}
);
let
options =
new
RequestOptions
({
headers
:
headers}
);
return
this
.
http
.post
(
url,
body.toString
(
),
options)
.map
((
data
:
Response) =>
data.json
(
));
}
logout
(
) {
localStorage.removeItem
(
'id_token'
);
}
loggedIn
(
) {
return
tokenNotExpired
(
);
}
}
ng g s post/post-repository --flat
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
import
{
Injectable }
from
'@angular/core'
;
import
{
Response }
from
'@angular/http'
;
import
{
AuthHttp }
from
'angular2-jwt'
;
@Injectable
(
)
export
class
PostRepository {
constructor
(
private
authHttp
:
AuthHttp) {}
getList
(
) {
let
url =
'http://127.0.0.1:8000/api/posts'
;
return
this
.
authHttp
.get
(
url)
.map
((
data
:
Response) =>
data.json
(
));
}
}
Nous remarquerons que nous utilisons Http dans l'authentication service alors que nous utilisons AuthHttp dans le post-repository service. Il y a une très bonne raison à cela. En effet, comme il est écrit dans la documentation de la librairie Angular2-jwt :
This library does not have any functionality for (or opinion about) implementing user authentication and retrieving JWTs to begin with. Those details will vary depending on your setup, but in most cases, you will use a regular HTTP request to authenticate your users and then save their JWTs in local storage or in a cookie if successful.
En d'autres termes, cette librairie n'est pas faite pour s'authentifier et stocker notre token. Pour cette étape, il vaut mieux privilégier l'utilisation du module Http basique livré avec Angular2.
II-C. Mise en place d'un système de routing▲
Nous allons maintenant nous occuper du routing. En effet, nous n'avons pour l'instant aucun moyen d'afficher le contenu qui se trouve dans les fichiers HTML de nos composants. Pour configurer le routing de votre application, c'est très simple. Nous allons créer un fichier app.routing.ts à la racine de notre application et indiquer les trois routes de nos composants principaux, ainsi que la route de redirection au cas où nous entrerions une URL qui ne correspond à aucune route :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
import
{
Routes,
RouterModule }
from
'@angular/router'
;
import
{
HomepageComponent }
from
'./homepage/homepage.component'
;
import
{
AuthenticationComponent }
from
'./authentication/authentication.component'
;
import
{
PostComponent }
from
'./post/post.component'
;
import
{
AuthGuard }
from
'./_guard/index'
;
const
APP_ROUTES
:
Routes =
[
{
path
:
''
,
component
:
HomepageComponent
},
{
path
:
'login'
,
component
:
AuthenticationComponent
},
{
path
:
'post'
,
component
:
PostComponent,
canActivate
:
[
AuthGuard]
},
{
path
:
'**'
,
redirectTo
:
''
}
];
export
const
Routing =
RouterModule.forRoot
(
APP_ROUTES);
II-D. Protéger les routes authentifiées avec AuthGuard▲
Pour finir, nous allons mettre en place un système permettant de protéger nos routes sécurisées via Guard. Créons un dossier « _guard » dans le dossier « app » contenant deux fichiers :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
import
{
Injectable }
from
'@angular/core'
;
import
{
Router }
from
'@angular/router'
;
import
{
CanActivate }
from
'@angular/router'
;
import
{
AuthenticationService }
from
'../authentication/authentication.service'
;
@Injectable
(
)
export
class
AuthGuard implements
CanActivate {
constructor
(
private
authentication
:
AuthenticationService,
private
router
:
Router) {}
canActivate
(
) {
if
(
this
.
authentication.loggedIn
(
)) {
return
true
;
}
else
{
this
.
router.navigate
([
'login'
]
);
return
false
;
}
}
}
export
*
from
'./auth.guard'
;
Nous importerons le fichier index.ts dans notre fichier app.module.ts et nous déclarerons AuthGuard en tant que provider, puis nous l'importerons également dans notre fichier app.routing.ts pour protéger notre route « post » via la propriété « canActivate ».
II-E. Authentifier ses requêtes avec Angular2-jwt▲
Pourquoi ai-je choisi d'utiliser cette bibliothèque ? Et bien tout d'abord pour l'essayer. Et puis parce qu'elle va nous simplifier la vie. Enfin du moins l'envoi des requêtes vers notre API dans un premier temps.
En effet, ce wrapper du module Http natif d'Angular2 permet d'inclure directement l'id_token contenu dans le localStorage, dans un header « authorization » compatible avec le format utilisé par notre LexikJwtAuthenticationBundle.
Le deuxième avantage de cette librairie est qu'elle va automatiquement vérifier si le token est valide. Ce qui n'est pas du tout négligeable.
Installons angular2-jwt via npm à la racine du projet en lançant cette commande :
npm install angular2-jwt
Maintenant que nous avons structuré notre application, il nous faut mettre à jour notre composant « app » ainsi que notre fichier app.module.ts avec les imports nécessaires :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
import
{
Component }
from
'@angular/core'
;
import
{
Router }
from
'@angular/router'
;
import
{
AuthenticationService }
from
'./authentication/authentication.service'
;
@Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
}
)
export
class
AppComponent {
constructor
(
private
authenticationService
:
AuthenticationService,
private
router
:
Router) {}
hasAuthToken
(
) {
return
localStorage.getItem
(
'id_token'
) !==
null
;
}
logout
(
) {
this
.
authenticationService.logout
(
);
this
.
router.navigate
([
'home'
]
);
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
<nav class
=
"navbar navbar-inverse navbar-fixed-top"
>
<div class
=
"container"
>
<div class
=
"navbar-header"
>
<button type
=
"button"
class
=
"navbar-toggle collapsed"
data-toggle
=
"collapse"
data-target
=
"#navbar"
aria-expanded
=
"false"
aria-controls
=
"navbar"
>
<span class
=
"sr-only"
>
Toggle navigation</span>
<span class
=
"icon-bar"
></span>
<span class
=
"icon-bar"
></span>
<span class
=
"icon-bar"
></span>
</button>
<img class
=
"pull-left logo"
src
=
"../assets/images/ng-xs.png"
alt
=
"Angular2"
>
<a class
=
"navbar-brand"
[routerLink]=
"['']"
>
Api Lab</a>
</div>
<div id
=
"navbar"
class
=
"collapse navbar-collapse"
>
<ul class
=
"nav navbar-nav"
>
<li><a [routerLink]=
"['']"
>
Home</a></li>
<li><a [routerLink]=
"['post']"
>
Posts</a></li>
</ul>
<ul class
=
"nav navbar-nav pull-right"
>
<li *ngIf
=
"!hasAuthToken()"
><a [routerLink]=
"['login']"
>
Login</a></li>
<li *ngIf
=
"hasAuthToken()"
><a (click)=
"logout()"
href
=
"#"
>
Logout</a></li>
</ul>
</div>
</div>
</nav>
<router-outlet></router-outlet>
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
import
{
BrowserModule }
from
'@angular/platform-browser'
;
import
{
NgModule }
from
'@angular/core'
;
import
{
ReactiveFormsModule }
from
'@angular/forms'
;
import
{
Http,
HttpModule,
RequestOptions }
from
'@angular/http'
;
import
{
AuthHttp,
AuthConfig }
from
'angular2-jwt'
;
import
{
AppComponent }
from
'./app.component'
;
import
{
Routing }
from
'./app.routing'
;
import
{
AuthGuard }
from
'./_guard/index'
;
import
{
AuthenticationComponent }
from
'./authentication/authentication.component'
;
import
{
AuthenticationService }
from
'./authentication/authentication.service'
;
import
{
HomepageComponent }
from
'./homepage/homepage.component'
;
import
{
PostComponent }
from
'./post/post.component'
;
import
{
PostRepository }
from
'./post/post-repository.service'
;
export
function
authHttpServiceFactory
(
http
:
Http,
options
:
RequestOptions) {
return
new
AuthHttp
(
new
AuthConfig
({}
),
http,
options);
}
@NgModule
({
declarations
:
[
AppComponent,
AuthenticationComponent,
HomepageComponent,
PostComponent
],
imports
:
[
BrowserModule,
ReactiveFormsModule,
HttpModule,
Routing
],
providers
:
[
{
provide
:
AuthHttp,
useFactory
:
authHttpServiceFactory,
deps
:
[
Http,
RequestOptions ]
},
AuthGuard,
AuthenticationService,
PostRepository
],
bootstrap
:
[
AppComponent]
}
)
export
class
AppModule {
}
J'ai délibérément inclus la factory authHttpServiceFactory directement dans ce fichier contrairement à ce que dit la documentation, car il y a un bug connu de la team Angular2-jwt lors de la compilation qui peut être résolu de cette manière.
Enfin, pour finaliser notre application, il ne reste plus qu'à lui appliquer un peu de style :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
<!
doctype html
>
<html>
<head>
<meta charset
=
"utf-8"
>
<title>Api Lab</title>
<base href
=
"/"
>
<meta name
=
"viewport"
content
=
"width=device-width, initial-scale=1"
>
<link rel
=
"icon"
type
=
"image/x-icon"
href
=
"favicon.ico"
>
<script src
=
"https://code.jquery.com/jquery-3.1.1.min.js"
integrity
=
"sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
crossorigin
=
"anonymous"
>
</script>
<link href
=
"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
rel
=
"stylesheet"
>
</head>
<body>
<div class
=
"container"
>
<app-root><div class
=
"text-center loading"
>
Loading...</div></app-root>
</div>
<script src
=
"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
></script>
</body>
</html>
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
/* You can add global styles to this file, and also import other style files */
body {
padding-top:
50
px;
}
.loading
{
font-weight:
bold
;
margin-top:
150
px;
}
.logo
{
margin:
5
px;
}
Et voilà le travail !
III. Conclusion▲
Pour conclure, je dirais que cette expérience s'est avérée très enrichissante. Angular2 propose une nouvelle perception du framework front-end en comparaison à la première version. Une façon de développer qui se rapproche plus de la programmation orientée objet et rappelle étrangement les frameworks PHP au niveau de la structure des fichiers. Enfin le CLI, bien que toujours en version bêta, reste un outil très pratique dans la lignée de la console de Symfony. Une très bonne première expérience.
IV. Remerciements▲
Nous remercions Elevenlabs qui nous a autorisés à publier ce tutoriel.
Nous tenons également à remercier Winjerome pour la mise au gabarit et Ced pour la correction orthographique.