Construire votre démarrage Conception d'une API RESTful

Ce que vous allez créer

Ce tutoriel fait partie de la Construire votre démarrage avec la série PHP sur Envato Tuts +. Dans cette série, je vous guide dans le lancement d’une startup du concept à la réalité en utilisant mes Planificateur de réunion application comme exemple de la vie réelle. À chaque étape du processus, je publierai le code de Meeting Planner sous forme d’exemples open source à partir desquels vous pourrez apprendre. Je traiterai également les problèmes liés au démarrage au fur et à mesure qu'ils surviennent.

Pourquoi construire une API pour votre démarrage?

La principale raison pour laquelle j’ajoute une API à Meeting Planner pour le moment est la création d’une fondation pour la création d’une application mobile iOS. L'application mobile utilisera l'API pour enregistrer et connecter les utilisateurs, puis leur permettre de planifier des réunions..

Les API ont également pour effet secondaire de vous aider à repenser et à mieux organiser tout le code que vous avez écrit à ce jour. Il y a certainement des endroits dans le code de Meeting Planner qui sont devenus compliqués. Maintenant, je dois les simplifier à nouveau pour les applications mobiles afin de reproduire les fonctionnalités au-dessus de la mêlée.

Il pourrait y avoir d'autres raisons à l'avenir pour la construction de l'API. Par exemple, je souhaite peut-être permettre aux développeurs tiers d'étendre le type de réunions et d'événements planifiés par Meeting Planner, leur permettant de collecter et de partager des données supplémentaires au cours du processus..

Pour rappel, tout le code de Meeting Planner est fourni en open source et écrit dans le framework Yii2 pour PHP. Une partie importante de cet épisode explique comment utiliser le framework Yii pour prendre en charge une API. Si vous souhaitez en savoir plus sur Yii2, consultez mes séries parallèles Programmer avec Yii2. 

Avant de plonger dans le code de l'API, j'aimerais vous encourager à essayer de planifier votre première réunion pour que vous sachiez de quoi je parle.. 

Si vous avez des questions sur ce tutoriel ou sur l'application elle-même, je participe aux discussions ci-dessous et vous pouvez également me joindre à @lookahead_io sur Twitter. Je suis toujours ouvert aux nouvelles idées de fonctionnalités de Meeting Planner ainsi qu'aux suggestions pour les futurs épisodes de la série..

Concevoir votre API

Alors que je préparais la construction de l’API, je devais comprendre divers concepts. J'ai abordé certaines de ces questions dans Programmation avec Yii2: Construction d'une API RESTful (Envato Tuts +).

Premièrement, je devais créer un point de terminaison pour l'API où tous les appels provenant d'applications mobiles arriveraient. J'ai décidé d'utiliser un troisième arbre indépendant dans le cadre d'application avancée Yii, par exemple. https://api.meetingplanner.io au lieu de https://meetingplanner.io/api/. Cela permet une séparation nette du code API du reste du service..

Deuxièmement, je devais concevoir la sécurité dans l'API. Dans le tutoriel d'aujourd'hui, je vais démontrer la sécurité alpha simple que nous utilisons, mais nous la renforcerons avec le temps, et j'écrirai probablement davantage à ce sujet à l'avenir. Il existe un aspect de la sécurité dans la façon dont nous utilisons et transmettons les clés et les requêtes de l'API, mais il est également important de s'assurer que l'API applique les protocoles de sécurité de l'application. Par exemple, un utilisateur ne peut pas demander aux participants d'une réunion s'ils ne sont ni l'organisateur de la réunion ni l'un de ses participants..

Troisièmement, je voudrais préparer le code de l'API pour la gestion des versions. Par exemple, une ancienne application iOS qui n'a pas été mise à jour peut utiliser API v1.0, tandis qu'une mise à jour ultérieure peut appeler API v2.0. Yii fournit des méthodes pour le faire, mais je ne les ai pas encore implémentées dans la conception actuelle.

Quatrièmement, je voulais me conformer autant que possible aux normes REST. C’est quelque chose que j’ai commencé à faire mais qui nécessitera plus de recherche pour mettre pleinement en œuvre.

Enfin, pour l’instant, je devais aborder l’ensemble des fonctionnalités offertes par l’API. Initialement, pour le développement d'applications mobiles, je me suis concentré sur la création de fonctionnalités en lecture seule. Le didacticiel et le code d’aujourd’hui porteront principalement sur la fonctionnalité d’application en lecture seule, c’est-à-dire sur les réunions de l’utilisateur. Mais cela inclut également l'enregistrement de l'utilisateur. Dans un avenir proche, nous ajouterons d'autres fonctions d'écriture, telles que créer une réunion, ajouter un participant, ajouter un lieu de réunion, finaliser une invitation, etc..

Considérez donc ce tutoriel comme un premier pas vers une API de services complète et robuste pour notre application.. 

Construire l'API

Création de l'arbre de service de l'API

Meeting Planner utilise l'infrastructure Yii Advanced Application, qui inclut une arborescence frontale pour l'application et une arborescence dorsale pour le composant administratif. Nous allons créer une troisième arborescence pour l'API.. 

J'ai décrit la procédure à suivre précédemment dans Programmation avec Yii2: Construction d'une API RESTful (Envato Tuts +).. 

Tout d'abord, j'ai dupliqué l'arborescence du back-end et les paramètres d'environnement associés:

$ cp -R backend api $ cp -R environnements / dev / backend / environnements / dev / api $ cp -R environnements / prod / backend / environnements / prod / api

Et j'ai ajouté l'alias @api à /common/config/bootstrap.php:

Ensuite, nous allons commencer à construire la fonctionnalité principale.

Sécuriser l'API

J'ai créé une sécurité de base lors de la création et du test de l'application iOS. Je vais rendre cela plus robuste à l'avenir.

Tous les appels d’API devront connaître un app_id et app_secret. Ceux-ci seront transmis sous une forme de HTTPS. Toutefois, rien ne garantit que nous pourrons les protéger. Nous devons donc finalement concevoir l'application de manière à ce que ces clés ne soient pas découvertes..

Pour le moment, j'ai développé le fichier mp.ini dans / var / secure pour inclure ceux-ci:

… Sentry_key_public = "xxxxxxxx" sentry_key_private = "xxxxxx" sentry_id = "nnnnnn" app_id = "xnxnxnxxxxxx" app_secret = "xnxnxnxnxxxxxxxx 

Ensuite, j'ai créé un modèle Service.php pour gérer la vérification de ces clés. Au fur et à mesure que nous rendrons cela plus robuste, il ne me restera plus qu'à modifier un morceau de code.

Le service de classe étend le modèle fonction statique publique verifyAccess ($ app_id, $ app_secret) if ($ app_id == Yii :: $ app-> params ['app_id'] && $ app_secret == Yii :: $ app-> params [ 'app_secret']) Yii :: $ app-> params ['site'] ['id'] = SiteHelper :: SITE_SP; retourne vrai;  else return false;  

Ensuite, je mets en place un avantAction dans tous les contrôleurs d’API pour réutiliser la méthode ci-dessus:

fonction publique beforeAction ($ action) // votre code personnalisé ici, si vous souhaitez que le code soit exécuté avant les filtres d'action, // qui sont déclenchés lors de l'événement [[EVENT_BEFORE_ACTION]], par exemple. PageCache ou AccessControl if (! Parent :: beforeAction ($ action)) return false;  if (Service :: verifyAccess (Yii :: $ app-> getRequest () -> getQueryParam ('app_id'), Yii :: $ app-> getRequest () -> getQueryParam ('app_secret'))) return true ;  else echo 'vos touches d'api sont du côté obscur'; Yii :: $ app-> end ();  

Les faiblesses principales sont que les clés de sécurité sont transmises à chaque appel et que les paramètres de requête ne sont pas signés. Leur transmission via HTTPS aide, mais ce n'est pas totalement sécurisé. Je vais améliorer cela à l'avenir.

Inscription et connexion

Les deux seuls appels d'API qui reposent entièrement sur des clés d'API sont l'enregistrement et la connexion. Les utilisateurs mobiles peuvent s'inscrire via OAuth et nous envoyer leurs jetons de service OAuth, ou ils peuvent nous fournir directement leur adresse électronique..

Une fois reçu, chaque utilisateur reçoit un jeton unique, qui sécurise les futurs appels d'API de cet utilisateur..

Je dois également faire plus pour améliorer la sécurité, mais je ne couvrirai pas cela aujourd'hui non plus..

Voici le code initial pour enregistrer un utilisateur via l'API et créer un jeton:

Fonction statique publique signupUser ($ email, $ prénom = ", $ nom =") $ nomutilisateur = $ nom complet = $ prénom. ". $ dernier nom; si ($ nomutilisateur ==") $ nomutilisateur = 'ios'; if (isset ($ username) && User :: find () -> Où (['username' => $ username]) -> existe ()) $ username = User :: generateUniqueUsername ($ username, 'ios') ;  $ password = Yii :: $ app-> security-> generateRandomString (12); $ user = nouvel utilisateur (['username' => $ username, // $ attributs ['login'], 'email' => $ email, 'password' => $ password, 'status' => Utilisateur :: STATUS_ACTIVE ,]); $ user-> generateAuthKey (); $ user-> generatePasswordResetToken (); $ transaction = $ user-> getDb () -> beginTransaction (); if ($ user-> save ()) $ ut = new UserToken (['user_id' => $ user-> id, 'token' => Yii :: $ app-> security-> generateRandomString (40),] ) if ($ ut-> save ()) User :: completeInitialize ($ user-> id); UserProfile :: applySocialNames ($ user-> id, $ prénom, $ dernier nom, $ nom complet); $ transaction-> commit (); return $ user-> id;  else print_r ($ auth-> getErrors ());  else $ transaction-> rollBack (); print_r ($ user-> getErrors ()); 

UserToken est une chaîne aléatoire unique à 40 chiffres, ce qui rend encore plus difficile à deviner que de croire que l'Amérique choisirait Donald Trump pour les diriger..

$ ut = new UserToken (['user_id' => $ user-> id, 'token' => Yii :: $ app-> security-> generateRandomString (40),]); 

Le contrôleur de réunion

Maintenant, regardons les appels pour une zone spécifique de l'API, demandant des informations sur les réunions. Voici la partie initiale de /api/controllers/MeetingController.php:

 ['class' => VerbFilter :: className (), 'actions' => ['delete' => ['POST'],],],];  public function beforeAction ($ action) // votre code personnalisé ici, si vous souhaitez que le code soit exécuté avant les filtres d'action, // qui sont déclenchés lors de l'événement [[EVENT_BEFORE_ACTION]], par exemple. PageCache ou AccessControl if (! Parent :: beforeAction ($ action)) return false;  if (Service :: verifyAccess (Yii :: $ app-> getRequest () -> getQueryParam ('app_id'), Yii :: $ app-> getRequest () -> getQueryParam ('app_secret'))) return true ;  else echo 'vos touches d'api sont du côté obscur'; Yii :: $ app-> end ();  

Remarquez ci-dessus comment chaque action vérifie que les jetons sont corrects.

Ensuite, chaque appel d'API pour les réunions est structuré de la même manière, comme indiqué ci-dessous. (félicite ma tentative de discipline):

fonction publique actionList ($ app_id = ", $ app_secret =", $ token = ", $ status = 0) Yii :: $ app-> réponse-> format = Response :: FORMAT_JSON; renvoyer MeetingAPI :: meetinglist ($ token , $ status); public function actionHistory ($ app_id = ", $ app_secret =", $ token = ", $ meeting_id = 0) Yii :: $ app-> response-> format = Response :: FORMAT_JSON; return MeetingAPI :: history ($ token, $ meeting_id);  fonction publique actionMeetingplaces ($ app_id = ", $ app_secret =", $ token = ", $ meeting_id = 0) Yii :: $ app-> response-> format = Response :: FORMAT_JSON; renvoyer MeetingAPI :: meetingplaces ($ jeton, $ meeting_id); fonction publique actionMeetingtimes ($ app_id = ", $ app_secret =", $ token = ", $ meeting_id = 0) Yii :: $ app-> response-> format = Response :: FORMAT_JSON; return MeetingAPI :: meetingtimes ($ token, $ meeting_id);  fonction publique actionMeetingplacechoices ($ app_id = ", $ app_secret =", $ token = ", $ meeting_place_id = 0) Yii :: $ app-> response-> format = Response :: FORMAT_JSON; retourner MeetingAPI :: meetingplacechoices ($ jeton, $ meeting_place_id); fonction publique actionMeetingtimechoices ($ app_id = ", $ app_secret =", $ token = ", $ meeting_time_id = 0) Yii :: $ app-> response-> format = Response :: FORMAT_JSON; return MeetingAPI :: meetingtimechoices ($ token, $ meeting_time_id);  fonction publique actionNotes ($ app_id = ", $ app_secret =", $ token = ", $ meeting_id = 0) Yii :: $ app-> response-> format = Réponse :: FORMAT_JSON; renvoyer MeetingAPI :: notes ($ jeton, $ meeting_id); public function actionSettings ($ app_id = ", $ app_secret =", $ token = ", $ meeting_id = 0) Yii :: $ app-> response-> format = Response :: FORMAT_JSON; return MeetingAPI :: settings ($ token, $ meeting_id);  public function actionCaption ($ app_id = ", $ app_secret =", $ token = ", $ meeting_id = 0) Yii :: $ app-> response-> format = Réponse :: FORMAT_JSON; renvoyer MeetingAPI :: caption ($ jeton, $ meeting_id); fonction publique actionDetails ($ app_id = ", $ app_secret =", $ token = ", $ meeting_id = 0) Yii :: $ app-> response-> format = Response :: FORMAT_JSON; return MeetingAPI :: details ($ token, $ meeting_id);  fonction publique actionReminders ($ app_id = ", $ app_secret =", $ token = ") Yii :: $ app-> réponse-> format = Response :: FORMAT_JSON; renvoyer MeetingAPI :: rappels ($ token); 

Pour le moment, chaque appel comprend le $ app_id, la $ app_secret et le $ jeton pour l'utilisateur connecté. Je vais changer cela pour la sécurité dans un proche avenir. C'est sécurisé, mais pas solidement sécurisé.

Regardons liste d'action, qui répertorie les réunions de l'utilisateur filtrant par le $ status argument pour les filtrer:

fonction publique actionList ($ app_id = ", $ app_secret =", $ token = ", $ status = 0) Yii :: $ app-> réponse-> format = Response :: FORMAT_JSON; renvoyer MeetingAPI :: meetinglist ($ token , $ status); 

Finalement, l’API peut limiter le nombre de demandes de réunion pour chaque statut, c’est-à-dire me montrer les 15 dernières réunions en mode planification de cet utilisateur..

Toutes les méthodes de réunion sont intégrées au modèle MeetingAPI. Voici le code pour le liste de réunion () méthode:

joinWith ('participants') -> where (['owner_id' => $ user_id]) -> orWhere (['participant_id' => $ user_id]) -> andWhere (['meeting.status' => $ queryStatus]) -> distinct () -> orderBy (['created_at' => SORT_DESC]) -> all (); $ meetings = []; foreach ($ meeting_list as $ m) $ x = new \ stdClass (); $ x-> id = $ m-> id; $ x-> owner_id = $ m-> owner_id; $ x-> meeting_type = $ m-> meeting_type; $ x-> sujet = $ m-> sujet; $ x-> message = $ m-> message; $ x-> identifiant = $ m-> identifiant; $ x-> status = $ m-> status; $ x-> created_at = $ m-> created_at; $ x-> log_at = $ m-> log_at; $ x-> sequence_id = $ m-> sequence_id; $ x-> cleared_at = $ m-> cleared_at; $ x-> site_id = $ m-> site_id; if ($ status> = Réunion :: STATUS_CONFIRMED) $ x-> selectedTime = Réunion :: getChosenTime ($ m-> id); $ x-> caption = $ m-> friendlyDateFromTimestamp ($ x-> selectedTime-> start, $ timezone, true, true). ". $ m-> getMeetingParticipants (); $ x-> selectedPlace = Meeting :: getChosenPlace ( $ m-> id); if ($ x-> ChoisiePlace! == false) $ x-> Place = $ x-> ChoisiePlace-> Place; $ x-> gps = $ x-> ChoisiePlace-> Place- > getLocation ($ x-> selectedPlace-> place-> id); $ x-> noPlace = false; autre $ x-> place = false; $ x-> noPlace = true; $ x-> gps = false ; else $ x-> ChoisiTime = 0; $ x-> ChoisiPlace = 0; $ x-> caption = $ m-> getMeetingParticipants (); $ réunions [] = $ x; unset ($ x);  return $ meetings; 

Tout d'abord, la méthode vérifie que le jeton appartient à l'utilisateur:

$ user_id = UserToken :: lookup ($ token); if (! $ user_id) return Service :: fail ('jeton invalide');  

Voici le code pour UserToken :: lookup ():

fonction publique statique lookup ($ token) // jeton de recherche pour id_utilisateur $ ut = UserToken :: find () -> où (['jeton' => jeton $]) -> un (); if (! is_null ($ ut)) return $ ut-> user_id;  else return false;  

Ensuite, nous vérifions le filtre pour $ status et chercher l'utilisateur $ fuseau horaire:

if ($ status == Meeting :: STATUS_PLANNING || $ status == Meeting :: STATUS_SENT) $ queryStatus = [Meeting :: STATUS_PLANNING, Meeting :: STATUS_SENT];  else $ queryStatus = $ status;  // Obtenir le fuseau horaire de l'utilisateur appelant $ timezone = MiscHelpers :: fetchUserTimezone ($ user_id); 

Enfin, nous interrogeons une liste des réunions de l'utilisateur et les transposons manuellement dans un tableau d'objets:

$ meeting_list = Meeting :: find () -> joinWith ('participants') -> où (['owner_id' => $ user_id]) -> ouWhere (['participant_id' => $ user_id]) -> etWhere ([ 'meeting.status' => $ queryStatus]) -> distinct () -> orderBy (['created_at' => SORT_DESC]) -> all (); $ meetings = []; foreach ($ meeting_list as $ m) $ x = new \ stdClass (); $ x-> id = $ m-> id; $ x-> owner_id = $ m-> owner_id; $ x-> meeting_type = $ m-> meeting_type; $ x-> sujet = $ m-> sujet; $ x-> message = $ m-> message; $ x-> identifiant = $ m-> identifiant; $ x-> status = $ m-> status; $ x-> created_at = $ m-> created_at; $ x-> log_at = $ m-> log_at; $ x-> sequence_id = $ m-> sequence_id; $ x-> cleared_at = $ m-> cleared_at; $ x-> site_id = $ m-> site_id; if ($ status> = Réunion :: STATUS_CONFIRMED) $ x-> selectedTime = Réunion :: getChosenTime ($ m-> id); $ x-> caption = $ m-> friendlyDateFromTimestamp ($ x-> selectedTime-> start, $ timezone, true, true). ". $ m-> getMeetingParticipants (); $ x-> selectedPlace = Meeting :: getChosenPlace ( $ m-> id); if ($ x-> ChoisiePlace! == false) $ x-> Place = $ x-> ChoisiePlace-> Place; $ x-> gps = $ x-> ChoisiePlace-> Place- > getLocation ($ x-> selectedPlace-> place-> id); $ x-> noPlace = false; autre $ x-> place = false; $ x-> noPlace = true; $ x-> gps = false ; else $ x-> ChoisiTime = 0; $ x-> ChoisiPlace = 0; $ x-> caption = $ m-> getMeetingParticipants (); $ réunions [] = $ x; unset ($ x);  retourne $ réunions;

Bien qu'il puisse exister un moyen plus simple de mapper les résultats de la base de données à renvoyer dans l'API, la transposition manuelle de la table la plus complexe, Meeting, me permet de contrôler ce que les résultats de l'API fournissent aux programmeurs. C'est en fait une chance pour moi d'améliorer et de simplifier l'API par rapport au code d'origine et aux propriétés de la base de données..

Par exemple, Meeting Planner doit générer du code dans l'interface utilisateur pour générer des sous-titres qui ne sont pas stockés dans la base de données. Plutôt que de demander à l'application iOS de dupliquer ce code complexe, nous générons simplement le sous-titre et le renvoyons dans les résultats de l'API..

Faire des appels API

Voici un moyen préliminaire de faire et de tester les appels d'API. Par exemple, si je lance l'appel URL suivant:

http://apix.meetingplanner.io/meeting/list/?app_id=xxx&app_secret=xxxxx&token=yyyy

Ça marchera. Mais, pour le tester et le voir en action, j'ai utilisé Postman, une extension d'application Chrome, qui est extrêmement utile..

Voici comment vous pouvez créer un appel API avec Postman UX:

Et voici à quoi ressemblent les résultats:

C'est un moyen simple de parcourir le résultat brut de mon serveur de développement en affichant toutes mes réunions:

["id": 207, "owner_id": 1, "réunion_type": 0, "sujet": "Nouveau Mtg à tester", "message": "", "identificateur": "dAefqLGi", "statut": 20, "created_at": 1475285105, "log_at": 1476642100, "sequence_id": "0", "cleared_at": 1475780470, "site_id": 0, "ChoisiTime": 0, "ChoisiPlace": 0, "Légende": "avec Jeff Reifman et [email protected]", "id": 206, "owner_id": 1, "type_reunion": 150, "sujet": "Ignorer - juste tester", "message": "", "identifiant": "ITJpSmlo", "status": 20, "created_at": 1474706654, "log_at": 1474706702, "sequence_id": "0", "cleared_at": 1474706732, "site_id": 0, "selectedTime": 0, "selectedPlace": 0, "caption": "avec Jeff Reifman et [email protected]", "id": 205, "owner_id": 1, "meeting_type": 110, "subject": "Our Prochaine réunion Test "," message ":" "," identifiant ":" vkVPWVmH "," statut ": 20," created_at ": 1474677013," log_at ": 1474921968," sequence_id ":" 0 "," cleared_at ": 1474920744, "site_id": 0, "Choisi Heure": 0, "ChoisiPlace": 0, "caption": "avec Jeff Reifman a nd [email protected] ",… 

C'est tout pour le moment. Vous pouvez parcourir la version dans l'arborescence de l'API et afficher de nombreuses autres méthodes. Alors que je mets à niveau la sécurité et améliore les fonctionnalités de l'API, je vais essayer d'écrire plus à ce sujet..

Regarder vers l'avant

J'espère que vous avez apprécié le tutoriel d'aujourd'hui. De toute évidence, l’API se développera et changera à mesure que notre développement mobile se poursuivra. Comme je l’ai dit plus tôt, je renforcerai la sécurité et élargirai les fonctionnalités.

Encore une fois, si vous ne l'avez pas encore fait, allez planifier votre première réunion avec Meeting Planner maintenant.! 

Vous pouvez également me contacter à @lookahead_io. Je suis toujours ouvert aux nouvelles idées de fonctionnalités et suggestions de sujets pour les prochains tutoriels. Ou essayez notre service d'assistance et ouvrez un rapport de bogue ou un ticket de demande de fonctionnalité. 

Restez à l'affût de tout cela et de plusieurs autres tutoriels à venir en consultant la série Construire son démarrage avec PHP.

Liens connexes

  • Planificateur simple et planificateur de réunion
  • Suivre le profil de financement de Meeting Planner
  • Programmation avec la série Yii2 (Envato Tuts +)
  • Programmation avec Yii2: Construction d'une API RESTful (Envato Tuts +)
  • Postman et l'extension Postman pour Chrome