Aller au contenu principal

Documentation API ITN

But de fonctionnement

Ce mode de fonctionnement a été réfléchi afin de pouvoir développer plus rapidement les API, principalement sur la partie recherche (filtres, ordres) et sérialisation (fields, includes, ...)

Si jamais certains éléments trés spécifiques ne réussisent pas à rentrer dans le mode de fonctionnement n'hésitez pas à en parler à loïc sur slack pour en discuter, mais il est tout à fait envisageable de ne pas faire passer toute l'application par ce systéme lorsque cela génére plus de problémes que cela n'en résout.

Les principales raisons de la non utilisation d'API platform sont les suivantes:

  • Pas de possibilité d'includes sans utiliser de serveurs spécifiques à dunglas (vulcain), ou fonctionnement louche à base de serialisation groups spécifique
  • Sérialisation par défaut de tout les champs de l'entité (donc risque de serialisation des champs privés et sérialiser trop de choses)

Ce qu'il manque dans le fonctionnement actuel

  • Des tests !
  • Génération documentation openapi
  • Création d'un repository à part

Configuration des routes

Détail

    <route id="user_detail" path="/users/{id}" methods="GET" controller="App\Core\Controller\DetailController">
<default key="entity">App\Entity\User</default>
</route>

Recherche

    <route id="user_search" path="/users" methods="GET" controller="App\Core\Controller\SearchController">
<default key="entity">App\Entity\User</default>
</route>

Message symfony messenger

    <route id="user_update" path="/users/{id}" methods="PUT" controller="App\Core\Controller\UpdateController">
<default key="message">App\Message\UpdateUser</default>
</route>

Configuration de serialisation d'une entité

EntityNormalizer

Un normalizer sert à indiquer les champs disponibles à la serialisation pour une entité.

Par défaut, un serializer doit implémenter l'interface EntityNormalizer.

namespace App\Normalizer;

use App\Core\Serializer\EntityNormalizer;
use App\Entity\User;

class UserNormalizer implements EntityNormalizer
{

public function getAllowedAttributes(): array
{
return [
"id", "firstName", "lastName"
];
}

public static function getClassName(): string
{
return User::class;
}
}

Sur cet exemple, le normalizer indique qu'il gére la classe User. Lorsque l'utilisateur sera sérialisé, le JSON contiendra son id, son firstName et son lastName.

info

La sérialisation utilise le composant property access de symfony, vous pouvez voir ici les différentes méthodes que va utiliser ce composant pour essayer d'accéder aux attributs.

attention

Le but de getAllowedAttributes est de ne retourner que des éléments scalaires, si vous voulez donner accés à des sous entités, référez vous à la configuration de l'EntityIncludesNormalizer

EntityIncludesNormalizer

Si vous voulez pouvoir rendre disponible certains sous éléments de votre entité, vous pouvez également implémenter l'interface EntityIncludesNormalizer

Si on reprend notre exemple précédent

namespace App\Normalizer;

use App\Core\Serializer\EntityNormalizer;
use App\Core\Serializer\EntityIncludesNormalizer;
use App\Entity\User;

class UserNormalizer implements EntityNormalizer, EntityIncludesNormalizer
{

public function getAllowedAttributes(): array
{
return [
"id", "firstName", "lastName"
];
}
public function getAllowedIncludes() : array
{
return ['preferredMeals'];
}

public static function getClassName(): string
{
return User::class;
}
}

La collection preferredMeals de l'entité User est maitenant disponible en tant qu'include. Cela signifie que faire un appel sur le détail de cet entité en ajoutant en paramétre get includes=preferredMeals incluera également le résultat de la sérailisation de la collection preferredMeals dans la clé preferredMeals du JSON. Si vous voulez ajouter plusieurs includes vous pouvez séparer chaque includes par des ,

Parcours des includes

Vous pouvez accéder aux éléments configuré en include de l'entité en utilisant le .

namespace App\Normalizer;

use App\Core\Serializer\EntityNormalizer;
use App\Core\Serializer\EntityIncludesNormalizer;
use App\Entity\Meal;

class MealNormalizer implements EntityNormalizer, EntityIncludesNormalizer
{

public function getAllowedAttributes(): array
{
return [
"name"
];
}
public function getAllowedIncludes() : array
{
return ['ingredients'];
}

public static function getClassName(): string
{
return Meal::class;
}
}

En ajoutant cette configuration au projet, cela signifie qu'il sera possible d'indiquer includes=preferredMeals.ingredients pour que soit retourné dans le json les preferredMeals ainsi que les ingredients de chaque preferredMeals.

attention

Pensez bien à créer un EntityNormalizer pour toutes les entités que vous mettez dans votre allowed includes

info

La sérialisation utilise le composant property access de symfony, vous pouvez voir ici les différentes méthodes que va utiliser ce composant pour essayer d'accéder aux attributs.

attention

Si jamais vous devez renvoyer une liste préfiltrée, référez vous à la configuration de l'EntityCustomIncludesAccessorNormalizer

EntityCustomIncludesAccessorNormalizer

Si vous vous rettrouvez dans un cas ou uniquement appeller des méthodes de l'entité n'est pas suffisant(Récupération via une autre api, requétes DQL complexe, récuération de la valeur via une autre classe dans le container, ...), vous pouvez implémenter l'interface EntityCustomIncludesAccessorNormalizer[https://github.com/ITNetworkParis/ilec-crm/blob/6a4baad76a870d329db1e903cfe73712957ce09e/src/Core/Normalizer/EntityCustomIncludesAccessorNormalizer.php].

Si on reprend notre exemple précédent:

namespace App\Normalizer;

use App\Core\Serializer\EntityNormalizer;
use App\Core\Serializer\EntityCustomIncludesAccessorNormalizer;
use App\Entity\User;

class UserNormalizer implements EntityNormalizer, EntityCustomIncludesAccessorNormalizer
{
private UserMoodGuesser $userMoodGuesser;

public function __construct(UserMoodGuesser $userMoodGuesser)
{
$this->userMoodGuesser = $userMoodGuesser;
}

public function getAllowedAttributes(): array
{
return [
"id", "firstName", "lastName"
];
}

public function getIncludesAccessor(): array
{
return [
'mood' => fn(User $user) => $this->userMoodGuesser->guess($user)
]
}

public static function getClassName(): string
{
return User::class;
}
}

Cela signifie que faire un appel sur le détail de cet entité en ajoutant en paramétre get includes=mood incluera également le résultat de l'appel à la méthode guess du service userMoodGuesser.

Afin de pouvoir configurer rapidement la recherche d'une entité, il faut implémenter l'interface EntitySearch

Voici un exemple basique:

namespace App\Search;
use App\Core\Doctrine\Search\EntitySearch;

class UserSearch implements EntitySearch
{
public function getAllowedFilters(): array
{
return [
"status" => ExactMatchFilter::class,
];
}

public function getAllowedOrders(): array
{
return [
"firstName" => BasicOrder::class,
"lastName" => BasicOrder::class,
];
}

public static function getEntityClassName(): string
{
return User::class;
}
}

Cette exemple permet d'avoir la possibilité d'ordonner par firstName et lastName ainsi que de filtrer par active

Order

Comme vu plus haut, Les ordres sont déclarés via la méthode getAllowedOrders.

La clé du tableau associatif est le nom de l'ordre, la valeur est le type d'ordre.

Ainsi, dans l'exemple donné, ajouter orders=firstName dans la query string permettra de trier par firstName par ordre ascendant. Si vous voulez trier par ordre descendant, il faut préfixer par - le nom du filtre (orders=-firstName)

info

Comme pour les includes, il est possible d'indiquer plusieurs ordres en les séparant par des ,

Filter

Comme vu plus haut, Les ordres sont déclarés via la méthode getAllowedFilters.

Dans l'exemple donné, ajouter filters[status]=disabled permettra d'avoir dans la recherche uniquement les utilisateurs ayant le champ status ayant la valeur disabled

Il est possible d'évoir plusieurs filtres utilisé dans la query string de cette manière filters[status]=disabled&filters[hairColor]=gray

Les filtres fonctionnent en ET, cette query string signifie donc que le résultat de la recherche ne contiendra que les utilisateurs désactivés ayant les cheveux gris.

info

La notation montrée en exemple est une manière simplifiée de déclarer les filtres. Cela correspond à la même chose que ce fonctionnement:

public function getAllowedFilters(): array
{
return [
"status" => new Search(
ExactMatchFilter::class,
),
];
}
Filtre sur des sous éléments

Si vous voulez filtrer sur des sous éléments de votre entité nécessitant une jointure, vous pouvez utiliser ce mode de fonctionnement

public function getAllowedFilters(): array
{
return [
"location.zipCode" => new Search(
searchFilter: ExactMatchFilter::class,
neededRelations: new NeededRelations(["location"]),
),
];
}

L'objet NeededRelations sert à déclarer les éléments de jointure qui devront être ajoutés dans le query builder pour effectuer la requète.

:::

Type de filtres

Il existe plusieurs types de filtre de base permettant de s'adapter à différents cas.

ExactMatchFilter

Ce filtre permet de faire une vérification stricte sur un champ donné, comme vu dans l'exemple précédent.

DateFilter

Ce filtre permet de filtrer par date, il est nécessaire de fournir dans sa configuration:

  • l'attribut de l'entité sur lequel on filtre
  • le type d'opérateur (liste présente ici)
public function getAllowedFilters(): array
{
return [
"createdAtFrom" => new Search(
DateFilter::class,
new DateFilterConfig("createdAt", DateFilterConfig::GTE)
),
"createdAtTo" => new Search(
DateFilter::class,
new DateFilterConfig("createdAt", DateFilterConfig::LT)
),
];
}

Cet exemple permet de trouver les utilisateurs ayant leur date de création aprés une certaine et/ou avant une certaine date.

Par exemple pour avoir tous les utilisateurs créé entre le 1er janvier 2023 et le 1er février 2023, la query string correspondante est filters[createdAtFrom]=2023-01-01&filters[createdAtTo]=2023-01-02

InFilter

Ce filtre permet de filtrer par éléments présents dans une liste donné dans le filtre, il est nécessaire de fournir dans sa configuration:

  • l'attribut de l'entité sur lequel on filtre
public function getAllowedFilters(): array
{
return [
"idsIn" => new Search(
InFilter::class,
new InFilterConfig("id")
),
];
}

Cet exemple permet de trouver les utilisateurs ayant les ids indiqué dans le filtre.

Par exemple pour avoir les utilisateurs ayant l'id 1 ou l'id 2 la query string correspondante est filters[idsIn]=1,2

NotInFilter

Ce filtre est l'inverse du InFilter et permet d'exclure les entités ayant leur attribut correspondant à au moins un des éléments donné dans le filtre.

public function getAllowedFilters(): array
{
return [
"idsNotIn" => new Search(
NotInFilter::class,
new NotInFilterConfig("id")
),
];
}

Cet exemple permet de trouver les utilisateurs n'ayant pas un des ids indiqué dans le filtre.

Par exemple pour avoir les utilisateurs n'ayant pas l'id 1 ou l'id 2 la query string correspondante est filters[idsNotIn]=1,2

IsNullFilter

Ce filtre permet de filtrer par élément null.

public function getAllowedFilters(): array
{
return [
"stateIsNull" => new Search(
IsNullFilter::class,
new IsNullFilterConfig("state")
),
];
}

Cet exemple permet de trouver les utilisateurs ayant leur état étant null en ajoutant le filtre filters[stateIsNull]= dans la query string

IsNotNullFilter

Ce filtre fonctionne à l'inverse de IsNullFilter (trouve les élements non null au lieu de null)

JsonbContainsFilter

TODO: trouver un exemple d'usage de ce filtre. Ce filtre permet de filtrer par jsonb query de postgresql.

public function getAllowedFilters(): array
{
return [
"roles" => JsonbContainsFilter::class
];
}

Cet exemple permet de trouver les roles ayant leur json query correspondant à une valeur.

Par exemple, pour avoir les utilisateurs ayant le rôle ROLE_MANAGER (en considérant que le json est un tableau), la query string correspondante est: filters[roles]=

info

Pour plus d'informations sur le fonctionnement des filtres jsonB, consultez la documentation postgresql

attention

Pour pouvoir utiliser ce filtre, vous devez également activer l'extension doctrine JsonbContains

StringFilter

Ce filtre permet de filtrer par texte en ignorant la casse et les accents.

public function getAllowedFilters(): array
{
return [
"firstName" => StringFilter::class
];
}

Cet exemple permet de trouver les utilisateurs ayant leur prénom correspondant à la valeur recherché en ignorant les accents.

Ainsi pour trouver l'utilisateur ayant le prénom José différentes recherches permettront de l'avoir dans les résultats.

  • filters[firstName]=Jose
  • filters[firstName]=josé
  • filters[firstName]=jose
attention

Pour pouvoir utiliser ce filtre, vous devez:

  • Activer les extensions doctrine ILike et Unaccent
  • Activer Unaccent (CREATE EXTENSION unaccent sur votre BDD)
TextSearchFilter

Ce filtre permet de filtrer par recherche approximative sur un ensemble de champs.

Chaque mot de la recherche en lowercae et asciifolding doit correspondre à au moins un morceau des champs recherchés dans l'entité également passé en lowercase et asciifolding

public function getAllowedFilters(): array
{
return [
"firstName" => new Search(
TextSearchFilter::class,
new TextSearchFilterConfig(["firstName", "lastName", "email"]),
)
];
}

Cet exemple permet de trouver les utilisateurs ayaant le nom, le prénom ou l'email correspondant à la recherche.

Ainsi pour trouver l'utilisateur ayant le prénom José, le nom dasilva et l'email monemail@yahoo.fr, différentes recherches permettront de l'avoir dans les résultats.

  • filters[search]=os
  • filters[search]=Silva yaho
  • filters[search]=email yah jo
Implémentation custom

Si vous ne pouvez pas faire ce que vous voulez avec les filtres ou que vous souhaitez avoir un filtre réutilisable correspondant à une régle métier, vous pouvez tout à fait créer votre propre filtre.


<?php

namespace App\Core\Doctrine\Search\Filters;

use App\Core\Doctrine\QueryBuilder\JoinRelations;
use App\Core\Doctrine\Search\Filters\FilterConfig\FilterConfig;
use Doctrine\ORM\QueryBuilder;

class UserHasMoreThanThreePreferedMealsFilter implements SearchFilter
{
/**
* @throws Exception
*/
public function apply(
QueryBuilder $queryBuilder,
JoinRelations $joinRelations,
array|string $value,
string $property,
?FilterConfig $filterConfig = null
)
{
$joinRelations->addJoins(['preferredMeals'])

$queryBuilder
->Andhaving('Count(' . $joinRelations->getJoinPathFromPropertyPath('preferredMeals') . ') > 3')
}
}

queryBuilder: Le queryBuilder qui exécutera la requète. joinRelations: Les relations supplémentaires à join pour la requète. value: la valeur du filter property: la propriété à filtrer filterConfig: Vous pouvez créer votre propre classe de FilterConfig si vous avez besoin de donner des instructions suplémentaires (exemple ici avec le TextSearchFilter).

info

Pour plus d'informations sur le fonctionnement de la classe joinRelations, consultez sa documentation

EntitySearchDefaultFilters

Lorsque vous souhaitez mettre des filtres par défaut, vous pouvez implémenter l'interface EntitySearchDefaultFilters sur votre classe de recherche.

namespace App\Search;
use App\Core\Doctrine\Search\EntitySearch;
use App\Core\Doctrine\Search\EntitySearchDefaultFilters;

class UserSearch implements EntitySearch, EntitySearchDefaultFilters
{
//

public function getDefaultSearchFilters(): array
{
return [
new SearchValue(
"archivedAt",
null,
new Search(IsNullFilter::class)
),
];
}
}

Cet exemple permet de toujours exclure les utilisateurs archivés de la recherche.

EntitySearchDefaultOrders

Lorsque vous souhaitez mettre des ordres par défaut, vous pouvez implémenter l'interface EntitySearchDefaultOrders sur votre classe de recherche.

namespace App\Search;
use App\Core\Doctrine\Search\EntitySearch;
use App\Core\Doctrine\Search\EntitySearchDefaultOrders;
use App\Core\Doctrine\Search\BasicOrder;

class UserSearch implements EntitySearch, EntitySearchDefaultOrders
{
//

public function getDefaultSearchOrders(): array
{
return [
new OrderValue(
new Order("createdAt", Direction::asc),
BasicOrder::class,
),
];
}
}

Cet exemple permet de trier par date de création lorsque aucun ordre n'a été donné.

Symfony messenger

Configuration de la route

En pmlus de l'élément obligatoires indiqués ici (message symfony à traiter), d'autres paramétres peuvent étre envoyés au contrôleur

entity
   <default key="entity">App\Entity\User</default>

Permet d'indiquer l'entité sur laquelle l'action est effectuée (l'entité User dans cet exemple)

security
        <default key="security">ROLE_DSI</default>

Permet d'indiquer l'entité la régle de sécurité présence du ROLE_DSI sur l'utilsateur autentifié dans cet exemple.

Création du message

Afin de pouvoir passer dans le MessengerController, un message doit implémenter l'interface Creatable (si c'est une création) ou Identifiable (pour toute autre action sur un élément).

L'id de l'élément mis à jour/créé devra être celui retourné par la méthode getId du message.

Classes utiles

JoinRelations

L'object JoinRelations sert à déclarer les jointures nécessaires pour l'exécution d'une requéte DQL

    public function addJoins(array $neededJoins): array

Cette méthode permet d'indiquer les relations nécessaires dans une requéte.

$joinRelations = new JoinRelations($queryBuilder);

$joinRelations->addJoins(['preferredMeals']);

Cet exemple permet d'indiquer que la réquéte DQL va nécesssiter de faire un LEFT JOIN sur user.preferredMeals

EntityFinder

Lorsque vous souhaitez rechercher des entités, cette classe vous permet d'avoir accés à plusieurs méthodes de base.

find
    public function find(string $entityClassName, string $id): ?object;

Permet de faire une recherche par identifiant d'une classe.

getReference
    public function getReference(string $entityClassName, string $id): mixed;

Permet de récupérer la référence d'une classe.

matchAll
    public function matchAll(
string $entityClassName,
array $searchValues = [],
array $orderValues = []
): array;

Permet de trouver tous les résultats d'une recherche en fonction de certains critéres


$users = $this->entityFinder->matchAll(User::class, [
new SearchValue(
"id",
['3', '4', '5'],
new Search(
InFilter::class, new InFilterConfig("id")
)
),
],
[
new OrderValue(
new Order("createdAt", Direction::asc),
BasicOrder::class,
),
]
);

Cet exemple permet de trouver tous les utilisateurs ayant les id 3, 4 ou 5 et de trier le résultat par date de création.

matchOne
    public function matchOne(
string $entityClassName,
array $searchValues = []
): null|object;

Permet de trouver un élément corespondant à la recherche

            $user = $this->entityFinder->matchOne(User::class, [
new SearchValue(
"resetToken",
"myresettoken",
new Search(ExactMatchFilter::class),
),
]);

Cet exemple permet de trouver l'utilisateur ayant le resetToken avec la valeur myresettoken

EntityPersisterInterface

Cette classe permet de pouvoir persister ou supprimmer une entité sans avoir à injecter l'entity manager dans le MessageHandler.

Il est également possible de flush les opérations présentes dans l'Unit Of Work de doctrine.