Bonjour,

Je possède une table qui regroupe des mots. A chaque mot une "position" lui est appliquée. Selon cette "position" les relations que ce mot entretien avec une autre entité sont différentes. "Position" est un integer. Mon cas est un peu complexe à expliquer.

Pour simplifier la compréhension de mon problème, immaginez une table Users avec un champ "role" qui contient un integer. Selon cet integer l'utilisateur est admin, modo, simple user...
J'aimerais dans un beforeFind() grouper les resultats par le champ "role", puis renommer les valeurs du "role" en mot explicite (admin,modo...).

// Actuellement les resultats sont retournés par un find('list') de cette façon:
[
    1 => [//liste des utilisateurs avec role égal à 1] 
    2 => [//liste des utilisateurs avec role égal à 2] 
    3 => [//liste des utilisateurs avec role égal à 3] 
    ....
]

//Ce que je souhaite obtenir est:
[
    'admins' => [//liste des utilisateurs avec role égal à 1] 
    'modos' =>  [//liste des utilisateurs avec role égal à 2] 
    'users' => [//liste des utilisateurs avec role égal à 3] 
    ....
]

Ainsi dans les autres fonctions de mon application je peux utiliser des mots explicite, plus facile à comprendre, que "des chiffres sorties de nulle part".
Comment peut-on faire ça ?
Je précise que je ne veut pas utiliser une table de jointure. Je souhaite vraiment renommer "position".

Edit: J'utilise cake3

Merci pour votre aide,

8 réponses


Havok
Réponse acceptée

Tu as aussi une autre possibilité, plus simple et plus propre, toujours en gardant ton find('list').
Dans ton Entity, tu définis un virtual field (http://book.cakephp.org/3.0/fr/orm/entities.html#creer-des-champs-virtuels) qui va s'occuper de convertir ton int qui représente ton role en un mot plus user-friendly.

    // Dans ton ton Entity User (ou autre)
    protected function _getNiceRole()
    {
        // On est user par défaut
        $role = 'user';

        // En fonction de la valeur de "role", on a un rôle différent
        switch($user->role) {
              case 1:
                    $role = 'admin';
                    break;
              case 2:
                    $role = 'moderator';
                    break;
        }

        return $role;
    }

D'une, quand tu manipuleras un objet Entity User, tu pourras y accéder facilement comme ça :

// Si $user est un objet entity "User"
echo $user->nice_role;

Mais cela te permet également de l'utiliser comme groupField dans un find('list')

$query = $this->Users->find('list', [
    'keyField' => 'id',
    'valueField' => 'username',
    'groupField' => 'nice_role' // ceci est ton virtual field
]);

debug($query->toArray());

Tu devrais normalement avoir ton tableau comme avec des mots à la place de l'ID, comme tu le souhaites.

Coucou, cakephp 1 2 ou 3 ?

Daniel68
Auteur

Desolé pour l'oubli. je viens d'editer mon post.J'utilise cake3

Alors je connais pas trop cake3, je suis sur cake2 mais peut être que si on communique, il se pourrait de développer des idées :p Tu ne peux pas utiliser des opérateurs ternaires ?

exemple : (admins =1) ? echo 'admin' : 'Error';

Daniel68
Auteur

Merci pour ton aide.
Je peux effectivement modfier les resultats du tableau. Mais ici je suis dans un beforeFind(). Autrement dit je ne peux pas modifier des valeurs que je n'ai pas encore. Il faut que je passe par la requette. Je me demande s'il est possible pour des valeurs de faire un équivalent à "val as admins" par exemple. Mais je ne suis pas sûre qu'en SQL il existe des méthode pour ça (donc pas sur cake).

A la base je voulais utiliser un afterFind() pour grouper et modifier les valeurs dans un foreach. Le problème c'est que afterFind() n'existe plus sur Cake3. A la place il faut utiliser mapReduce(). Mais j'ai beau lire la doc je n'y compris rien. Quelqu'un sait-il comment utiliser cette fonction ? Et faire en sorte qu'elle soit appliqué après chaque find. Sinon je dois faire un finder personnalisé mais c'est pas exactement ce que je veux car je ne pourrais pas modifier cette requette facilement pour different appels. Il faudra donc que je fasse plusieurs finders...

Conclusion. Quelqu'un a-t-il compris comment utiliser mapReduce() ?

Merci pour votre aide.

mapReduce() peut faire l'affaire pour ton cas.
L'avantage c'est que l'exemple que tu donnes est assez simple pour être utilisé pour expliquer la fonction.
La fonction marche avec deux paramètres : le mapper et le reducer.

Le mapper va te servir à classer tes Entity avec la logique que tu décides. Pour reprendre ton exemple, on imagine qu'on a des users avec un champ role qui est un int : si role = 1, alors on est admin ; si role = 2, alors on est moderator ; sinon, on est user.
Ton mapper va en fait boucler sur les résultats (les objets Entity retournés) de la requête sur laquelle tu vas l'attacher.
A chaque appel, tu vas "stocker" l'Entity courante dans un groupe dont tu choisis le nom.
Au final tu auras un tableau comme suit :

        [
            'admin' => [
                0 => User Entity 1
                1 => User Entity 2
            ],
            'moderator' => [
                0 => User Entity 3
            ],
            'user' => [
                0 => User Entity 4
                1 => User Entity 5
            ]
        ];

Tu pourrais avoir un mapper comme suit :

// $user est une Entity User récupéré par le find auquel tu vas binder le mapper
// Cette fonction sera appelé autant de fois que tu as d'Entity dans ton résultat de requête
$mapper = function ($user, $key, $mapReduce) {
            // On est user par défaut
            $role = 'user';

            // En fonction de la valeur de "role", on a un rôle différent
            switch($user->role) {
                case 1:
                    $role = 'admin';
                    break;
                case 2:
                    $role = 'moderator';
                    break;
            }

            // Cet appel va permettre de stocker l'entity courante $user dans un tableau, à la clé $role
            $mapReduce->emitIntermediate($user, $role);
};

Une fois tous tes objets Entity "classés", mapReduce() va appliquer le $reducer.
Le $reducer va te permettre de consolider le résultat de ton $mapper en appliquant une logique supplémentaire.

Le $reducer va parcourir le tableau résultat de ton $mapper (voir le tableau ci-dessus) en passant en paramètres le tableau des objets Entity d'un groupe, le nom du groupe et l'objet MapReduce.
Dans ton cas, tu n'aurais rien besoin de faire de plus :

$reducer = function ($users, $role, $mapReduce) {
    // Va restocker le même résultat tel qu'il est déjà
    $mapReduce->emit($users, $role);
};
    // Tu exécutés ta requête
        $results = $this->Users
            ->find('all')
            ->mapReduce($mapper, $reducer);

        foreach ($results as $role => $users) {
            debug($role);
            debug($users);
        }       

On pourrait imaginer faire des opérations plus complexes cela dit. Il y a quelques exemples dans la doc.
J'espère que ça t'a éclairé sur le fonctionnement de mapReduce.

Concernant ton cas, tu as vraiment besoin que la clé soit traduite ? Tu ne peux pas faire la conversion au moment où tu vas boucler sur ton tableau? C'est impératif que dans ton tableau de résultats possède déjà la clé traduite ?

Daniel68
Auteur

Bonjour.
Mille merci @Havok pour tout le temps que tu as pris à m'expliquer. Vraiment un grand merci.
J'ai mieux compris les mappers. Cependant pour une raison que j'ignore si je reproduis ton code légerement adapté aux noms de mes champs je constate qu'une erreur est retourné lorsque les données sont jointes:
'Nom_table_de_jointure is missing from the belongsToMany results. Results cannot be created.'

Je comptais également essayer une autre idée. L'utilisation de formatResults(). L'idée étant d'overwrite la fonction find() par defaut en fesant:

public function find($type = 'all', $options=[]) {
    $find = parent::find($type,$options);
    return $find->formatResults(function($results) {
        //Mon code
    });
}

Mais je pense que la meilleur solution est celle que tu me propose: Les champs virtuelles.
Alors que je les utilise tout le temps, cette fois je n'y ai même pas pensé.
Je vais tout de suite tester ça et vous fait un retour pour confirmer que groupField fonctionne avec les champs virtuelles.

Edit: Je confirme ça marche très bien :)

Encore une fois, merci.

De rien.

Le formatResults aurait pu faire l'affaire aussi.
Pour info, en interne quand tu fais un find('list'), le résultat est composé avec un formatResults, donc ton intuition sur son utilisation est bonne.

Si tu veux faire un peu d'étude de code : https://github.com/cakephp/cakephp/blob/master/src/ORM/Table.php#L1012-L1054

Mais pour le coup, le virtual field est la solution la plus courte et la plus clean à mettre en place.