FAQ

Dans cette section, nous allons aborder quelques points intéressants qui reviennent souvent dans les questions concernant ce cours.

Les points abordés n’ont pas de relation particulière et peuvent donc être lu dans n’importe quel ordre.

Comment générer des pages HTML depuis l'application Symfony 3 ?

La configuration présentée durant ce cours implique que toute l’application ne génère que des réponses en JSON ou en XML. Cependant, il peut arriver qu’une même application puisse servir des réponses en JSON, en HTML voire en CSV.

Pour ce faire nous pouvons utiliser deux options que propose FOSRestBundle.

Utiliser plusieurs règles dans le format_listener

Dans notre fichier de configuration, nous avions :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fos_rest:
    serializer:
        serialize_null:   true

    routing_loader:
        include_format: false
    view:
        view_response_listener: true
        formats:
            json: true
            xml: true
    format_listener:
        rules:
            - { path: '^/', priorities: ['json', 'xml'], fallback_format: 'json', prefer_extension: false }
configuration du format_listener

Pour générer une page HTML, nous pouvons rajouter une nouvelle règle dans la clé format_listener.rules. L’ordre de déclaration étant important, il faut toujours déclarer les règles les plus spécifiques en premier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    view:
        view_response_listener: true
        formats:
            json: true
            xml: true
        templating_formats:
            html: true
    format_listener:
        rules:
            - { path: '^/route/json', priorities: ['json'], fallback_format: 'json', prefer_extension: false }
            - { path: '^/route', priorities: ['html'], fallback_format: 'html', prefer_extension: false }
            - { path: '^/', priorities: ['json', 'xml'], fallback_format: 'json', prefer_extension: false }
Une nouvelle route pour créer du HTML

Avec cette configuration, toutes les URLs commençant par /route/json renverront du JSON. Par contre, si l’URL commence par /route (mais sans la partie /json, /route/other par exemple) les réponses seront en HTML.

Si nous avions inversé ces deux règles, toutes les URLs /route/json renverraient aussi du HTML car l’expression régulière ^/route englobe aussi ^/route/json.

Comme pour les formats JSON et XML, la génération de réponse au format HTML est déjà supporté par défaut. Mais en rajoutant la clé templating_formats.html, la configuration est plus lisible. De plus, nous utilisons templating_formats au lieu de formats car pour les pages HTML, nous aurons besoin d’un template pour les afficher.

Nous pouvons rajouter autant de règles que nous voulons mais cela peut rapidement montrer ses limites. Nous avons ainsi la possibilité d’utiliser un autre système plus efficace pour isoler la partie API et la partie IHM1 de son application.

Configurer le zone_listener

Il existe un listener de FOSRestBundle que nous n’avons pas abordé qui permet d’isoler la partie API d’une application de manière très simple : le zone_listener.

Le zone_listener est un listener qui nous permet de désactiver toutes les fonctionnalités de FOSRestBundle pour un ensemble d’URLs.

Ajoutons d’abord un préfixe /api à toutes les routes de notre API. La déclaration des routes pourrait ressembler à :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
places:
    type:     rest
    resource: AppBundle\Controller\PlaceController
    prefix: /api

prices:
    type:     rest
    resource: AppBundle\Controller\Place\PriceController
    prefix: /api
...
Ajout d’un préfixe

Tous les appels d’API restent identiques mais sont maintenant préfixés.

La configuration de FOSRestBundle devient maintenant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# src/app/config/config.yml
fos_rest:
    routing_loader:
        include_format: false
    view:
        view_response_listener: true
    zone:
        - { path: ^/api }
    format_listener:
        rules:
            - { path: '^/api', priorities: ['json'], fallback_format: 'json' }
Configuration du zone_listener

La partie zone permet d’activer le bundle que pour les routes commençants par /api. Ainsi, toute requête en dehors de cette zone sera gérée nativement par Symfony. Nous pouvons ainsi faire cohabiter notre API et une IHM complète sans soucis.

En utilisant ce système de zone, il ne faut pas oublier de reconfigurer toute la partie liée au pare-feu de Symfony et à notre système de sécurité pour prendre en compte le préfixe.

La configuration finale serait donc :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# app/config/security.yml
security:
    firewalls:
        main:
            pattern: ^/api
            stateless: true
            simple_preauth:
                authenticator: auth_token_authenticator
            provider: auth_token_user_provider
            anonymous: ~

La clé pattern prend en compte le préfixe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
# src/AppBundle/Security/AuthTokenAuthenticator.php
public function createToken(Request $request, $providerKey)
{
    $targetUrl = '/api/auth-tokens';
    // Si la requête est une création de token, aucune vérification n'est effectué
    if ($request->getMethod() === "POST" && $this->httpUtils->checkRequestPath($request, $targetUrl)) {
        return;
    }
  // ...
}
// ...
}
Prise en compte du pattern dans nos contrôleurs

L’URL dans targetUrl contient maintenant notre préfixe /api.

Il est quand même utile de souligner qu’il est préférable d’utiliser une application à part pour générer ses pages HTML et avoir une application dédiée pour son API. L’intérêt de REST est d’avoir une architecture orientée service et donc de séparer les différents composants.


  1. Interface Homme Machine, dans notre cas la page qui s’affiche dans le navigateur. 

Comment autoriser l'accès à certaines urls avec notre système de sécurité ?

Comme vous l’avez sans doute remarqué, une fois le système de sécurité est activé, seule la requête de création de token est autorisée. Mais dans les faits, il est assez courant d’avoir plusieurs appels d’API accessible sans authentification.

Par exemple, comment autoriser les utilisateurs à s’inscrire ?

Actuellement cela est impossible mais nous pouvons corriger le tir très facilement.

Pour rappel, l’authentification est géré par la classe AuthTokenAuthenticator dont voici un extrait du code :

 1
 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
<?php
# src/AppBundle/Security/AuthTokenAuthenticator.php

namespace AppBundle\Security;

// ...

class AuthTokenAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
    /**
    * Durée de validité du token en secondes, 12 heures
    */
    const TOKEN_VALIDITY_DURATION = 12 * 3600;

    protected $httpUtils;

    public function __construct(HttpUtils $httpUtils)
    {
        $this->httpUtils = $httpUtils;
    }

    public function createToken(Request $request, $providerKey)
    {
        $targetUrl = '/auth-tokens';
        // Si la requête est une création de token, aucune vérification n'est effectué
        if ($request->getMethod() === "POST" && $this->httpUtils->checkRequestPath($request, $targetUrl)) {
            return;
        }

        $authTokenHeader = $request->headers->get('X-Auth-Token');

        if (!$authTokenHeader) {
            throw new BadCredentialsException('X-Auth-Token header is required');
        }

        return new PreAuthenticatedToken(
            'anon.',
            $authTokenHeader,
            $providerKey
        );
    }
    // ...
}
Le nouvel Authenticator

Pour vérifier si une requête a été faite sur une certaine URL, la méthode checkRequestPath peut utiliser une route comme nous l’avons spécifié dans l’extrait de code ci-dessus, mais aussi le nom d’une route.

Le code peut donc être simplifié en utilisant directement le nom pour la route auth-tokens : post_auth_tokens.

 1
 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
<?php
# src/AppBundle/Security/AuthTokenAuthenticator.php

namespace AppBundle\Security;

// ...

class AuthTokenAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
    /**
    * Durée de validité du token en secondes, 12 heures
    */
    const TOKEN_VALIDITY_DURATION = 12 * 3600;

    protected $httpUtils;

    public function __construct(HttpUtils $httpUtils)
    {
        $this->httpUtils = $httpUtils;
    }

    public function createToken(Request $request, $providerKey)
    {
        $targetUrl = 'post_auth_tokens';
        // Si la requête est une création de token, aucune vérification n'est effectué
        if ($request->getMethod() === "POST" && $this->httpUtils->checkRequestPath($request, $targetUrl)) {
            return;
        }

        $authTokenHeader = $request->headers->get('X-Auth-Token');

        if (!$authTokenHeader) {
            throw new BadCredentialsException('X-Auth-Token header is required');
        }

        return new PreAuthenticatedToken(
            'anon.',
            $authTokenHeader,
            $providerKey
        );
    }
    // ...
}

Pour obtenir la liste des routes, nous pouvons utiliser la commande php bin/console debug:router.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 get_places                 GET      ANY      ANY    /places
 get_place                  GET      ANY      ANY    /places/{id}
 post_places                POST     ANY      ANY    /places
 remove_place               DELETE   ANY      ANY    /places/{id}
 update_place               PUT      ANY      ANY    /places/{id}
 patch_place                PATCH    ANY      ANY    /places/{id}
 get_prices                 GET      ANY      ANY    /places/{id}/prices
 post_prices                POST     ANY      ANY    /places/{id}/prices
 get_themes                 GET      ANY      ANY    /places/{id}/themes
 post_themes                POST     ANY      ANY    /places/{id}/themes
 get_users                  GET      ANY      ANY    /users
 get_user                   GET      ANY      ANY    /users/{id}
 post_users                 POST     ANY      ANY    /users
 remove_user                DELETE   ANY      ANY    /users/{id}
 update_user                PUT      ANY      ANY    /users/{id}
 patch_user                 PATCH    ANY      ANY    /users/{id}
 get_user_suggestions       GET      ANY      ANY    /users/{id}/suggestions
 post_auth_tokens           POST     ANY      ANY    /auth-tokens
 remove_auth_token          DELETE   ANY      ANY    /auth-tokens/{id}
 get_preferences            GET      ANY      ANY    /users/{id}/preferences
 post_preferences           POST     ANY      ANY    /users/{id}/preferences

Avec le système de nommage de FOSRestBundle, nous avons des noms simples et surtout qui décrivent aussi le verbe HTTP associé à la route. Dés lors pour autoriser une action, nous pouvons nous baser uniquement sur le nom de la route correspondante (le verbe HTTP est vérifiée indirectement).

Ainsi pour autoriser la création d’utilisateurs et de tokens d’authentification, nous pouvons simplement utiliser respectivement les routes : post_users et post_auth_tokens.

Le code peut devenir :

 1
 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
<?php
# src/AppBundle/Security/AuthTokenAuthenticator.php

namespace AppBundle\Security;

// ...

class AuthTokenAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
    /**
    * Durée de validité du token en secondes, 12 heures
    */
    const TOKEN_VALIDITY_DURATION = 12 * 3600;

    public function __construct()
    {
    }

    public function createToken(Request $request, $providerKey)
    {
        $autorisedPaths = [
            'post_users', // Création d'un utilisateur (inscription)
            'post_auth_tokens' // Création d'un token (connexion)
        ];

        $currentRoute = $request->attributes->get('_route');

        if (in_array($currentRoute, $autorisedPaths)) {
            return;
        }

        $authTokenHeader = $request->headers->get('X-Auth-Token');

        if (!$authTokenHeader) {
            throw new BadCredentialsException('X-Auth-Token header is required');
        }

        return new PreAuthenticatedToken(
            'anon.',
            $authTokenHeader,
            $providerKey
        );
    }
    // ...
}

Le service HttpUtils étant maintenant inutile, nous pouvons même le retirer de la configuration des services.

1
2
3
4
5
6
# app/config/services.yml

auth_token_authenticator:
    class:     AppBundle\Security\AuthTokenAuthenticator
    # arguments: ["@security.http_utils"] # à supprimer ou à commenter
    public:    false
Désactivation du service HTTPUtils

Bien sur, libre à vous de gérer la liste de routes autorisées comme bon vous semble (en injectant un paramètre configurable dans le service, en ayant une liste dans une variable statique, etc.).