[SWIFT] Class, Token et fonction recursive ?

Holà tout le monde ^^

J'essaie d'optimiser le fonctionnement de mon code dès le départ et il me manque quelques connaissances pour faire fonctionner tout ça..

Je vais essayé de m'expliquer clairement (pcq ça ne l'est pas toujours dans ma tête ^^)

Pour chaque requête que je fais sur le serveur, j'ai un token à passer.
Le token expire toute les X minutes.
Quand le token expire, je dois faire une demande de nouveau token en lui passant un "tokenRefresh" (appelons le comme ça).

J'ai donc fait un premier test et j'aimerais transformer ce "test" en fonction que je peux appeler de n'importe où et qui me renvoi un json (là j'ai un soucis de requête synchrone, qui n'attends pas le résultat avant de me le renvoyer).
J'utilise Alamofire + Alamofire_Synchronous.

Voici le début de mon code :

let parametres = ["IdSociete": "1000"]
let urlString = monUrl+"/myAccount"
let headers: [String : String] = ["X-AUTH-TOKEN": DB.store["token"] ?? ""] // recupère le token
let request = Alamofire.request(urlString, method: .post, parameters : parametres as Parameters, encoding : JSONEncoding.default, headers: headers)

request.responseJSON { response in
    switch(response.result){

            case .success:
                if let value = response.result.value {
                    let json = JSON(value) // SwiftyJSON
                    if(json["success"].intValue == 1){  // Token valide
                        // Token OK
                        // Retourner le json quand ça sera transformer en fonction ?
                    }
                    else{ // Token expiré, refresh token, relance de la requete initiale
                        if(json["message"].stringValue == "needrefreshtoken"){
                            // Requete imbriquée pour rafraichir le token
                            // envoi le "refreshToken"
                            let parametresToken = ["_refresh_token": DB.store["refresh_token"]!] 
                            let requestRefreshToken = Alamofire.request(monUrl+"/refresh_token", method: .post, parameters : parametresToken as Parameters)
                            requestRefreshToken.responseJSON { response in
                                switch(response.result){

                                case .success:
                                    if let value = response.result.value {
                                        let jsonTokenExpire = JSON(value) // SwiftyJSON
                                        if(jsonTokenExpire["token"].boolValue == true){
                                            // met à jour le nouveau token
                                            DB.store["token"] = jsonTokenExpire["token"].stringValue 
                                            // Ici le token est actualisé
                                            // Refaire la requête initiale avec le nouveau token... 
                                            // faire une fonction de ce code et l'appelé lui même ?
                                        }
                                        else{
                                            // Le token n'a pas été actualisé !
                                        }
                                    }
                                    break;

                                case .failure:
                                    break;
                                }
                            }
                        }
                        else{  
                             // On est pas dans le cas du needrefreshtoken.. mais que ce passe-t-il ?
                        }
                    }
                }
            break;


            case .failure:
            break;
}

Comment faire pour transformer ce code en class ? fonction ? En un truc pas trop chiant à réutiliser partout en fait ^^

Afin de pouvoir l'utiliser n'importe où et lui passer que les paramètres nécessaires ? (Une fois que j'aurai résolu ce soucis, je pense que ça sera pas compliqué d'intégrer la récursivité dedans).

Je vais surement avoir d'autre question mais chaque problème en son temps ^^

Merci de votre aide ^^

PS: @Pyroh On est bon sur les noms de variables là ? :p

Réponses

  • PyrohPyroh Membre

    Bon pour commencer les noms de variables collent plus aux best-practices. C'est assez important quand tu partages du code et les gens peuvent avoir un mauvais a priori quand les conventions de naming ne sont pas respectées. Tu peux aussi te voir refuser des pull requests sur ce motif.
    Une dernière chose sur le sujet et après je te laisse tranquille avec ça : choisi une langue et tiens toi-y. Tu mixes français et anglais dans tes noms de variable, je te conseille l'anglais, surtout si tu partage ton code. J'ai déjà certaines tables dans une DB chez un client avec des noms de colones tantôt en allemand, en anglais ou en français. C'est tout bonnement abjecte.

    Sinon pour ton soucis en lui-même je vais commencer par te conseiller d'oublier les appels réseau synchrones ou alors dans un thread séparé. Et il y a peu de cas réels où tu dois effectuer des appels réseau synchrones dans la partie applicative.
    Pour ce qui est de gérer un token d'accès c'est plus compliqué. Normalement tu dois avoir une info sur la validité de ton token qui t'es renvoyée par le backend quand tu le génère. Ce qui te donne une idée approximative du moment où il va expirer.

    C'est cette information que tu dois conserver et vérifier si le token est encore théoriquement valide au moment où tu veux faire ta requête sinon tu le regénère. Il faut prendre une marge d'erreur de quelques secondes en compte pour ne pas être charrette sur les requêtes proches de la fin de validité. De toute manière il va falloir que tu gère les erreurs.

    Une dernière chose : ["X-AUTH-TOKEN": DB.store["token"] ?? ""] ça veut dire que tu stocke en DB un token de connexion temporaire ? Si oui tu dois avoir quelques faiblesses architecturales dans ton projet. Tu peux nous en dire un peu plus sur la nature de l'objet qui est chargé des appels réseau ?

  • InsouInsou Membre
    31 janv. modifié #3

    C'est noté pour la langue, j'vais essayé de m'y tenir ^^

    Pour le token, je n'ai pas le choix, je suis obligé de faire comme ça.
    A la connexion, je récupère 2 tokens, un token pour toute les requêtes et un token de "refresh" pour re-générer un token si le mien à expiré.
    Nos autres applications/sites fonctionnent comme ça, du coup je ne peux pas modifier ce fonctionnement.

    Pour les appels synchrones, je vais te faire un exemple de ce à quoi je veux arriver et le soucis que je rencontre, ça sera peut être plus clair..

    En gros, je veux faire une fonction ? (une class ?) qui gère mes appels aux serveurs, je ne vais pas faire des gros copier/coller de mon code plus haut pour chaque appel au serveur..
    Dans ma fonction, j'intègre directement la vérification du token et la regénération du token si il n'est plus valide, comme ça, tout se fait tout seul et je ne m'inquiète plus de ça... ça, ça fonctionne, c'est réglé.

    A chaque appel serveur, je veux juste faire appel à cette fonction, lui passer l'url et un tableau de paramètres et hop, il me retourne le résultat du serveur... et c'est là que je coince.. voici où j'en suis dans mon code :

    Supposons que j'ai un bouton qui déclenche une requête :

    @IBAction func actionBouton(_ sender: Any) {
        let parametres = ["IdSociete": "1000"]
        let info = Connexion().req(url: monUrl, parametres: parametres)
        print(info)
    }
    

    Ma class "connexion" avec ma fonction "req" :

    import Foundation
    import UIKit
    import Alamofire
    import SwiftyJSON
    
    class Connexion : UIViewController {
    
    func req(url: String, parametres: [String : String?]) -> JSON{
    
        var blabla = JSON(["Juste un retour", "en json"]) // ma variable de retour
    
        let headers: [String : String] = ["X-AUTH-TOKEN": DB.store["token"] ?? ""]
        let request = Alamofire.request(url, method: .post, parameters : parametres as Parameters, encoding : JSONEncoding.default, headers: headers)
    
        request.responseJSON { response in
    
            switch(response.result){
    
            case .success:
                if let value = response.result.value {
                    let json = JSON(value)
                    if(json["success"].intValue == 1){  // Token valide
                        blabla = json // le résultat que je veux dans mon retour 
                    }
                    else{ // Token expiré, refresh token, relance de la requete initiale
    
                        if(json["message"].stringValue == "needrefreshtoken"){
                            // Requete imbriquée pour rafraichir le token
                            let ParamètresToken = ["_refresh_token": DB.store["refresh_token"]!]
                            let requestRefreshToken = Alamofire.request(urlMessenger+"/refresh_token", method: .post, parameters : ParamètresToken as Parameters)
                            requestRefreshToken.responseJSON { response in
                                switch(response.result){
    
                                case .success:
                                    if let value = response.result.value {
                                        let jsonTokenExpire = JSON(value) // Recupère le nouveau token
                                        if(jsonTokenExpire["token"].boolValue == true){
                                            DB.store["token"] = jsonTokenExpire["token"].stringValue // met à jour le nouveau token
    
                                            self.req(url: url, parametres: parametres) // je relance la requête avec le nouveau token valide
                                        }
                                        else{
                                            // le token n'a pas été actualisé
                                        }
                                    }
                                    break;
    
                                case .failure:
                                    break;
                                }
                            }
                        }
                        else{  
                             // On est pas dans le cas du needrefreshtoken.. 
                        }
                    }
                }
                break;
    
    
            case .failure:
                break;
            }
    
        }
    
        return blabla
    
    }
    }
    

    Le soucis est que dans mon print(info) (à l'appuie sur mon bouton), je me retrouve avec :

    ["Juste un retour","en json"]
    

    Ma variable initiale et pas le résultat après avoir attendu le retour du serveur..
    Du coup, c'est là que je bloque..
    Ce qui m'intéresse, c'est le retour de cette ligne :

    blabla = json
    

    Une idée de comment m'en sortir ? ^^

  • PyrohPyroh Membre
    31 janv. modifié #4

    Donc on a bien un soucis d'architecture.
    Pour commencer : class Connexion : UIViewController. Il n'y a aucune raison —absolument aucune— pour qu'une classe qui s'occupe uniquement de gérer une connexion avec un backend étende UIViewController. Ta classe ne doit à la limite étendre aucune classe standard, à la limite implémenter des protocols mais c'est tout.

    On continue avec : Connexion().req(url: monUrl, parametres: parametres). Tu crées une instance de Connexion à chaque fois que tu veux faire un appel réseau. Niveau optimisation c'est catastrophique parce qu'instancier une classe est coûteux en terme de performances. Sur un processeur moderne ça n'est pas dramatique mais si tu fais ça partout dans ton code ça va commencer à avoir un impact sur la batterie.
    En faisant ça ton interface de connexion devient totalement stateless ce qui veut dire qu'elle n'a aucune connaissance de ce qui s'est passé avant et n'a aucune chance de voir ce qu'il se passer après. Or dans un cas comme ça c'est ici que tu devrais stocker les tokens au lieu de le faire en DB.

    Maintenant on va parler des solutions.

    Ta classe Connexion ne doit plus rien avoir à faire avec UIViewController. Ça ne t'apporte rien et ça bouffe de la mémoire sans aucune raison valable d'autant plus que tu n'utilises pas les méthodes de UIViewController.

    Tu n'instancie une seule fois ta classe et tu garde une référence à l'objet pour pouvoir le réutiliser. Tu peux aussi faire un singleton si tu veux. Ça va te permettre d'avoir un connecteur statefull qui va gérer tes tokens et leur renouvellement.

    Abordons un peu l'asynchronicité, c'est assez commun de ne pas vouloir s'y frotter quand on débute. C'est normal, ce n'est pas sale. Sauf que c'est se tirer une balle dans le pieds d'essayer de tout synchroniser. Si les appels réseaux sont asynchrones c'est pour une bonne raison : ils prennent du temps. À ce titre ils ne doivent pas s'effectuer sur le main thread parce que sinon ça bloque l'UI et les gens n'aiment pas et ils quittent l'application.
    C'est pourquoi tu as des completion blocks. Si on reprend ton code la signature de ta fonction req devrait être la suivante :

     func req(url: String, parametres: [String : String?], completion: (JSON) -> ()) throws 
    

    Ce qui veut dire qu'au lieu de renvoyer quelque chose elle appelle la closure que tu lui passe en paramètre avec le JSON tant attendu et provoque une erreur si quelque chose se passe mal.
    L'appel est alors comme suit :

    @IBAction func actionBouton(_ sender: Any) {
        let parametres = ["IdSociete": "1000"]
        do {
            try self.connexion.req(url: monUrl, parametres: parametres) { (json) in
                print(json)
            }
        } catch {
            // gestion de l'erreur
        }
    }
    

    Bien entendu tu auras pris soin d'instancier un objet Connexion dans une propriété connexion de ton contrôleur.
    Je te laisse te documenter pour trouver comme faire tout ça 😉

  • InsouInsou Membre

    Pour commencer : class Connexion : UIViewController. Il n'y a aucune raison —absolument aucune— pour qu'une classe qui s'occupe uniquement de gérer une connexion avec un backend étende UIViewController. Ta classe ne doit à la limite étendre aucune classe standard, à la limite implémenter des protocols mais c'est tout.

    Yes, ça je savais que c'était une connerie mais au moins là je sais pourquoi ^^

    On continue avec : Connexion().req(url: monUrl, parametres: parametres). Tu crées une instance de Connexion à chaque fois que tu veux faire un appel réseau. Niveau optimisation c'est catastrophique parce qu'instancier une classe est coûteux en terme de performances. Sur un processeur moderne ça n'est pas dramatique mais si tu fais ça partout dans ton code ça va commencer à avoir un impact sur la batterie.

    C'est noté.
    J'ai donc créer une variable..

    var maConnexion = Connexion()
    

    ..et c'est celle là que j'utiliserai à chaque fois que je ferai un appel réseau.

    J'ai fais plusieurs tests et je n'arrive pas à récupérer mon json..
    J'ai pas bien compris comment le renvoyer vu que la signature de ma fonction a changée..

    Cette phrase n'est pas encore clair dans ma tête :

    Ce qui veut dire qu'au lieu de renvoyer quelque chose elle appelle la closure que tu lui passe en paramètre avec le JSON tant attendu

    Du coup, voici les premières corrections de ma classe :

    class Connexion {
    
    func req(url: String, parametres: [String : String?], completion: (JSON) -> ()) throws  {
    
    let headers: [String : String] = ["X-AUTH-TOKEN": DB.store["token"] ?? ""]
    let request = Alamofire.request(url, method: .post, parameters : parametres as Parameters, encoding : JSONEncoding.default, headers: headers)
    
    request.responseJSON { response in
    
        switch(response.result){
    
        case .success:
            if let value = response.result.value {
                let json = JSON(value)
                if(json["success"].intValue == 1){  // Token valide
                    // C'est ici que je ne comprends pas, je suis censé renvoyer le json obtenu ?
                    return json // Unexpected non-void return value in void function
                }
                else{ // Token expiré, refresh token, relance de la requete initiale
    
                    if(json["message"].stringValue == "needrefreshtoken"){
                        // Requete imbriquée pour rafraichir le token
                        let ParamètresToken = ["_refresh_token": DB.store["refresh_token"]!]
                        let requestRefreshToken = Alamofire.request(url+"/refresh_token", method: .post, parameters : ParamètresToken as Parameters)
                        requestRefreshToken.responseJSON { response in
                            switch(response.result){
    
                            case .success:
                                if let value = response.result.value {
                                    let jsonTokenExpire = JSON(value) // Recupère le nouveau token
                                    if(jsonTokenExpire["token"].boolValue == true){
                                        DB.store["token"] = jsonTokenExpire["token"].stringValue // met à jour le nouveau token
    
                                        // Pour la récursivité, en attente pour l'instant
                                    }
                                    else{
                                        // le token n'a pas été actualisé
                                    }
                                }
                                break;
    
                            case .failure:
                                break;
                            }
                        }
                    }
                    else{  
                         // On est pas dans le cas du needrefreshtoken.. 
                    }
                }
            }
            break;
    
    
        case .failure:
            break;
        }
    }
    }
    }
    

    Merci de ton aide en tout cas, il y a plein de mauvais mécanisme que je fais, qui fonctionne mais qui ne sont pas optimisé et avec tes explications, certains points sont beaucoup plus clairs ^^

  • LarmeLarme Membre

    C'est quoi la signature de responseJSON() exactement ?
    Car en réalité, c'est ce que tu mimiques.

    À un moment donné, quand tu estimes que tu as ton JSON, tu dois faire completion(json).

  • InsouInsou Membre

    @Larme aaaahhh bah voila.. c'est ça que je cherchais ^^
    J'étais parti sur un **return **mais ça coincait.. maintenant c'est good, j'arrive à avoir le retour en json lorsque je clic sur mon bouton.

    Bon bah je pense que tout est bon maintenant pour ce problème là.
    Merci de votre aide ^^

    (et je reviens avec un nouveau problème dans peu de temps concernant les scrollView, je termine quelques tests histoire d'être sûr que je ne m'en sort pas :D)

Connectez-vous ou Inscrivez-vous pour répondre.