Bonjour à tous !
Sur un projet perso que je suis en train de développer pendant mon chômage, je viens d’établir une connexion HTTP REST à une API Web accessible via une bearer authentication. Comme je veux bien faire les choses, je souhaite massivement utiliser les exceptions PHP pour gérer tout ça (chose que je n’ai jamais vue faite dans aucune des deux boîtes dans lesquelles j’ai travaillées, et pourtant j’en ai lu du code ===> ça fait de moi un débutant en la matière car même à la fac, c’était pas une compétence particulièrement explorée).
Je voudrais donc avoir votre avis sur la manière dont j’ai géré les exceptions qui peuvent avoir lieu. Pour ce faire, je vais vous montrer du code et l’expliquer en même temps.
Si vous avez des remarques à faire (exemple : c’est améliorable parce que …, ou bien : c’est faux parce que …), je vous invite à m’en faire part car c’est là le sujet du topic.
Framework PHP utilisé : Laravel v9. Cependant, ça ne devrait aucunement poser problème si vous ne connaissez pas. Seul 1 ou 2 points mineurs pourraient vous échapper, sauf que je les explique à côté du code.
Edit
Une version à jour du code et un peu différente de ce qui suit ici est disponible : https://zestedesavoir.com/forums/sujet/16378/declenchement-et-gestion-exceptions-quoi-en-penser/#p244376 .
Trait
PHP qui éventuellement déclenche une exception
Explications sur la logique des sources
Ce Trait
a pour but d’être appelé dès que l’on veut requêter l’API Web REST (Akeneo, un PIM catalogue de produits que l’on peut requêter pour récupérer les 100 premiers produits ou en ajouter un, par exemple). Ce Trait
permet donc d’établir une connexion avec l’API Web REST (Akeneo) en fournissant les données d’authentification Bearer. La gestion du rafraîchissement de token est aussi gérée (l' access_token
est enregistré dans un fichier dont le nom est le timestamp à l’écriture de ce dernier, si 3000s au moins se sont écoulées entre l’exécution du Trait
et donc de la requête que l’on souhaite faire vers Akeneo et le timestamp "nom du fichier", alors on lance une requête de rafraîchissement de token et l’on remplace les fichiers access_token
et refresh_token
- une autre façon de faire aurait été de mettre ça dans un script de crontab mais bref on s’en fiche).
90% du code du Trait
permet la gestion du rafraîchissement du token comme vous pourrez le lire.
Sources
trait AkeneoConnector {
function sendAkeneoRequest($request_uri, $data = NULL) {
if(empty(Storage::files('akeneo_access_token'))) {
$queried_tokens = Http::withBasicAuth(config('akeneo.authentication.client_id'), config('akeneo.authentication.secret'))->post(config('akeneo.authentication.endpoint'), [
"grant_type" => config('akeneo.authentication.grant_type'),
"username" => config('akeneo.authentication.username'),
"password" => config('akeneo.authentication.password')
])->throw();
} elseif(now()->diffInSeconds(Carbon::createFromTimestamp(pathinfo(Storage::files('akeneo_access_token')[0])['filename'])) > 3000) {
$queried_tokens = Http::withBasicAuth(config('akeneo.authentication.client_id'), config('akeneo.authentication.secret'))->post(config('akeneo.authentication.endpoint'), [
"grant_type" => config('akeneo.authentication.grant_type_refresh'),
"refresh_token" => Storage::get(Storage::files('akeneo_refresh_token')[0]),
])->throw();
}
if(isset($queried_tokens)) {
$now = now();
$queried_tokens_as_object = $queried_tokens->object();
Storage::deleteDirectory('akeneo_access_token');
Storage::put('akeneo_access_token/' . $now->timestamp, $queried_tokens_as_object->access_token);
Storage::deleteDirectory('akeneo_refresh_token');
Storage::put('akeneo_refresh_token/' . $now->timestamp, $queried_tokens_as_object->refresh_token);
}
if(empty(Storage::files('akeneo_access_token'))) {
throw new AkeneoAccessTokenNotFoundException();
}
if(empty(Storage::files('akeneo_refresh_token'))) {
throw new AkeneoRefreshTokenNotFoundException();
}
if(now()->diffInSeconds(Carbon::createFromTimestamp(pathinfo(Storage::files('akeneo_access_token')[0])['filename'])) > 3000) {
throw new AkeneoAccessTokenHasExpiredException();
}
return Http::withToken(Storage::get(Storage::files('akeneo_access_token')[0]))->get($request_uri, $data)->throw();
}
}
Déclenchements potentiels d’exceptions
Voici les endroits qui pourraient déclencher une exception :
-
La fonction Laravel
->throw()
qui est appelée sur le résultat des deux requêtespost
d’authentification (dont une de rafraîchissement) au tout début du code : donc si une exception HTTP Client ou HTTP Server (codes HTTP 400 ou 500 respectivement) doit être déclenchée au niveau de l’authentification ou au niveau du rafraîchissement, et celle appelée sur le résultat de la requêteget
qui est retournée par ceTrait
: si une exception HTTP Client ou HTTP Server doit être déclenchée au niveau de la requête que le script appelant ceTrait
doit effectuer. -
La fonction Laravel
Storage::put
doit également déclencher une exception si besoin car j’ai configuré Laravel FlyStorage pour ce faire : ça arriverait en cas de problème d’écriture des fichiers tokens (pour leaccess_token
comme pour lerefresh_token
). -
Evidemment, mes exceptions custom du genre
throw new AkeneoAccessTokenNotFoundException
(j’aurais peut-être pu en rajouter d’autres mais c’est déjà pas mal et je ne suis même pas sûr que ce soit si utile que ça).
Code appelant ce Trait
Explications sur la logique
Je fais donc ici appel à mon Trait
défini ci-dessus : ainsi je souhaite requêter Akeneo et gérer toutes les exceptions.
Explications sur la gestion des exceptions (catch
)
Je pars du principe que toutes les exceptions doivent être logguées dans mon serveur Laravel : d’où l’appel à la fonction Laravel report()
.
Je pars du principe, également, que toutes les exceptions doivent être portées à la connaissance de l’utilisateur car toute exception implique l’impossibilité de requêter l’API REST Akeneo sur demande de l’utilisateur, donc il faut bien le mettre au courant.
-
Cependant, il n’est pertinent, me semble-t-il, d’adresser à l’utilisateur un message précis que pour une partie de ces exceptions parce qu’elles seraient liées à l’utilisateur : en l’occurrence,
\Illuminate\Http\Client\RequestException
(problème au traitement par Akeneo d’une requête d’authentification/rafraîchissement token/de la vraie requête demandée auTrait
) si et seulement si on a du code HTTP d’erreur 403, 404 et 429 (l’utilisateur est impliqué dans ce problème, d’où un message très en relation avec l’Exception). Si le code HTTP est différent, c’est qu’il y a un problème côté serveur ou avec mon code, l’utilisateur n’est donc pas impliqué, donc je lui affiche un message plus généraliste, moins en relation avec l’Exception. -
C’est aussi le cas pour toute exception de type
\Illuminate\Http\Client\ConnectionException
(problème non pas au traitement par Akeneo de la requête, mais bien en amont : par exemple IP d’Akeneo injoignable). -
Enfin, j’adresse à l’utilisateur un message ENCORE PLUS généraliste pour toute exception de type
\League\Flysystem\UnableToWriteFile | \Exception $e
(qui correspondent à une impossibilité d’écriture d’au moins un fichier token, ou à toute autre exception à laquelle je n’aurais pas pensée à catcher).
function test() {
try {
$response = $this->sendAkeneoRequest(config('akeneo.connections.rest_api.endpoint') . '/products', [
'pagination_type' => 'search_after',
'limit' => 100
]);
}
catch(\Illuminate\Http\Client\RequestException | \Illuminate\Http\Client\ConnectionException $e) {
if($e instanceof \Illuminate\Http\Client\RequestException) {
switch($e->response['code']) {
case 403:
echo __('akeneo.errors.forbidden');
break;
case 404:
echo __('akeneo.errors.not_found');
break;
case 429:
echo __('akeneo.errors.too_many_requests');
break;
default:
echo __('akeneo.errors.connection_problem');
break;
}
} else {
echo __('akeneo.errors.connection_problem');
}
report($e);
}
catch(\League\Flysystem\UnableToWriteFile | \Exception $e) {
echo __('akeneo.errors.unable_to_query_unknown_reason');
report($e);
}
}
Autre question, qui concerne cette façon de gérer les exceptions (catch
)
A la fac on m’a toujours appris de faire les catch
au niveau du script appelant la fonctionnalité qui peut déclencher les exceptions. C’est donc bien ce que j’ai fait ici.
Sauf qu’on se retrouve avec beaucoup de lignes de code de catch
, avec un switch
dedans, un if
, etc. C’est non seulement peu lisible, mais de plus barbant à écrire à chaque fois que je souhaite faire appel à mon Trait
(donc à chaque fois que je souhaite requêter Akeneo, par exemple pour récupérer la liste des produits comme ici).
Comment puis-je me délester de ce poids ?
Je finirais ce topic en vous adressant un grand merci pour le fait de donner votre avis et pour l’aide que vous m’apporterez !
Bonne journée à tous !