Prise en compte des features flags dans l'API¶
Contexte¶
Dans le projet il existe déjà plusieurs feature flags configuré via le QG. Pour des questions de simplicité il avait été décidé dans un premier temps de ne gérer ces flags que dans l'interface utilisateur en désactivant les éléments liés au flag.
Dans le contexte d'Efalia Safe on souhaite prendre en compte le feature flag pour ne pas déposer des documents dans le coffre si la fonctionnalité est désactivée.
La prise en compte du flag concerne :
- les routes dédiées à la fonctionnalité (ie: la récupérations des conteneurs)
- les actions automatiques (ie: dépôt de fichier au classement)
- la configuration, autant les requêtes que les réponses, dans des objets métier toujours disponibles (ie: le conteneur configuré sur un gabarit de document)
Le choix de l'implémentation doit trouver le meilleur compromis entre simplicité et explicite (alias être sûr qu'on couvre tous les cas).
Décision¶
La configuration n'est pas altérée par la modification du feature flag. Cela permet de garder la documentation de notre API simple, par exemple un conteneur configuré sur un gabarit de document sera toujours retourné par l'API. Cela veut aussi dire que le front doit continuer à nous envoyer la configuration si elle ne veut pas être perdu.
Les routes dédiées retourneront une réponse 503 Service Unavailable avec le code d'erreur métier fonctionnalité_indisponible si le feature flag est désactivé.
L'implémentation en interne de l'API utilise une approche fonctionnelle pour garantir par l'analyse statique qu'on ne peut pas mal utiliser l'api (connecteur configuré mais feature flag désactivé) et qu'on couvre donc tous les cas possibles. Pour ce faire on a besoin de 2 composants tel que suit :
interface Service // ie: EfaliaSafe
{
/**
* @template R
*
* @param callable(API): R $action
*
* @return Maybe<R>
*/
public function __invoke(callable $action): Maybe;
}
La callable passée au service représente l'action qu'on veut faire qui dépend du connecteur et n'est exécutée que si la fonctionnalité est activée et configurée. Puisque l'action peut ne pas être appelée le retour du service est conditionnel représenté par le type Maybe<R> qui nous permet clairement de savoir dans quel cas on est. (On ne peut pas utiliser un retour nullable comme ?R car si l'action elle même retourne null alors l'appelant ne peut pas savoir dans quel cas il se trouve)
Exemple d'implémentation :
final class EfaliaSafe
{
/**
* @template R
*
* @param callable(API): R $action
*
* @return Maybe<R>
*/
public function __invoke(callable $action): Maybe
{
if ($this->featureFlag === false) {
return Maybe::nothing();
}
return Maybe::just($action(Api::of(...$this->args)));
}
}
final class API // ie: EfaliaSafe\API
{
private function __construct() {}
public static function of(/* arguments nécessaire à contacter l'api */): self {}
public function appelA() {}
public function appelB() {}
// etc...
}
Cette classe a un constructeur nommé (avec le __construct en privé) pour empêcher Symfony de pouvoir instancier la classe automatiquement et donc ne pas pouvoir injecter ce service par accident ailleurs dans l'app. C'est donc le Service (défini au dessus) qui aura la responsabilité de créer cet objet.
Dans le cadre du connecteur Efalia Safe on aurait un usage qui ressemblerait à ces exemples :
public function controllerConteneurs(EfaliaSafe $efaliaSafe): Response
{
return $efaliaSafe(function(API $api) {
return $api->conteneurs();
})->match(
static fn($conteneurs) => Response::ok($conteneurs),
static fn() => Response::serviceUnavailable(),
);
}
public function classerDocument(EfaliaSafe $efaliaSafe, File $fichier)
{
$efaliaSafe(function(API $api) use ($fichier) {
// dans cet exemple $this représente le Document côté Efalia Doc
// et si le dépôt ne fonctionne pas `déposer()` va lever une Exception
$objetNumérique = $api->déposer($this->conteneur, $fichier);
$this->objectNumérique = $objetNumérique;
});
// reste du classement côté Efalia Doc
}
Conséquences¶
La non altération de la configuration représente une donnée morte du point de vue technique mais peut être vue comme une fonctionnalité d'un point de vue fonctionnelle puisque le client retrouvera toute sa configuration à la réactivation de la fonctionnalité (même si on peut se poser la question si ce genre de cas peut vraiment arriver).
L'approche fonctionnelle dans l'API demande une période d'adaptation dû aux indirections via les fonctions anonymes. Cependant la garantie par analyse statique qu'on ne peut pas mal utiliser la fonctionnalité aura un bénéfice plus grand dans le temps que le coût d'entrée.