Bonjour,

Je découvre CakePhp depuis qques semaines, et c'est un monde de beauté féérique et rationnelle qui s'ouvre à moi. Ce framework est abolument génial !!!!

Cependant, mon enthousiasme est douché par un incompréhensible (pour moi) problème de sauvegarde d'une table de jointure.

Etat des lieux : des utilisateurs (nom, prenom, etc.) et des animaux de compagnie (chiens, chats, poissons rouge, iguanes, etc.) L'animal d'un utilisateur à un nom.
La table de jointure récupère l'id de l'utilisateur, l'id du type d'animal et le nom donné au bestiau par le propriétaire.
Mes tables :

  • users => les utilisateurs
  • animals => les animaux (le nom n'est pas heureux, mais j'avais pas envie de m'embêter avec les pluriels de la langue française)
  • animalsusers => la table de jointure

Mes modèles (tout comme il est dit dans la documentation) :

  • dans class UsersTable extends Table
    $this->belongsToMany('Animals', [
    'through' => 'AnimalsUsers'
    ]);
  • dans class AnimalsTable extends Table
    $this->belongsToMany('Users', [
    'through' => 'AnimalsUsers',
    ]);
  • dans class AnimalsusersTable extends Table
    $this->belongsTo('Users');
    $this->belongsTo('Animals');

Le problème : Dans l'édition du formulaire, tout va bien ! (merci) Mais lorsque j'essaie de patcher mon entité user avant de la sauvegarder, les données de la table de jointure ne sont pas mises à jour ...

Mon request->data :

[
    'nom' => 'DOE',
    'prenom' => 'John',
    'animals' => [
        (int) 0 => [
            '_joinData' => [
                'id' => '1',
                'nom' => 'Rintintin',
                'user_id' => '16',
                'animal_id' => '1'
            ]
        ],
            (int) 1 => [
            '_joinData' => [
                'id' => '2',
                'nom' => 'Felix',
                'user_id' => '16',
                'animal_id' => '2'
            ]
        ]
    ]
]

Mon objet user après ```
$user = $this->Users->patchEntity($user, $this->request->data, ['associated'=> ['Animals._joinData']]);



object(App\Model\Entity\User) {

    'id' => (int) 16,
    'nom' => 'DOE',
    'prenom' => 'John',
    'animals' => [
        (int) 0 => object(App\Model\Entity\Animal) {

            'id' => (int) 1,
            'nom' => 'chien',
            '_joinData' => object(App\Model\Entity\Animalsuser) {

                'animal_id' => (int) 1,
                'id' => (int) 1,
                'user_id' => (int) 16,
                'nom' => 'Médor',
                '[new]' => false,
                '[accessible]' => [
                    'user_id' => true,
                    'animal_id' => true,
                    'nom' => true,
                    'user' => true,
                    'animal' => true
                ],
                '[dirty]' => [],
                '[original]' => [],
                '[virtual]' => [],
                '[errors]' => [],
                '[repository]' => 'AnimalsUsers'

            },
            '[new]' => false,
            '[accessible]' => [
                'nom' => true,
                'animalsusers' => true,
                '_joinData' => true
            ],
            '[dirty]' => [],
            '[original]' => [],
            '[virtual]' => [],
            '[errors]' => [],
            '[repository]' => 'Animals'

        },
        (int) 1 => object(App\Model\Entity\Animal) {

            'id' => (int) 2,
            'nom' => 'chat',
            '_joinData' => object(App\Model\Entity\Animalsuser) {

                'animal_id' => (int) 2,
                'id' => (int) 2,
                'user_id' => (int) 16,
                'nom' => 'Felix',
                '[new]' => false,
                '[accessible]' => [
                    'user_id' => true,
                    'animal_id' => true,
                    'nom' => true,
                    'user' => true,
                    'animal' => true
                ],
                '[dirty]' => [],
                '[original]' => [],
                '[virtual]' => [],
                '[errors]' => [],
                '[repository]' => 'AnimalsUsers'

            },
            '[new]' => false,
            '[accessible]' => [
                'nom' => true,
                'animalsusers' => true,
                '_joinData' => true
            ],
            '[dirty]' => [],
            '[original]' => [],
            '[virtual]' => [],
            '[errors]' => [],
            '[repository]' => 'Animals'

        }
    ],
    '[new]' => false,
    '[accessible]' => [
        'nom' => true,
        'prenom' => true,
    ],
    '[dirty]' => [],
    '[original]' => [],
    '[virtual]' => [],
    '[errors]' => [],
    '[repository]' => 'Users'
}

Mon request->data me semble formaté convenablement, le patchEntity inclu l'association avec le  \_joinData, et pourtant, l'objet user ne reflète pas la modification ('Rintintin' au lieu de 'Médor') ... 
Bref, 3 jours la-dessus !
Si vous avez une idée ....

7 réponses


PierreMasclet
Auteur
Réponse acceptée

Merci de mansaychai pour ton idée. Effectivement, le problème est bien dans le formulaire !!!
(Par contre, à mon avis, il n'y a rien à gagner à passer par le AnimalsController. On se retrouverai avec les même liaisons relous mais sur Users au lieu de Animals ... Peut-être à partir du AnimalsUsersController ???....)

Voici mes corrections :
-> Dans les tables mysql, tout est ok
-> Dans les Model >Table : pour UserTable et AnimalsTable, j'ai gardé le through mais avec le nom de l'instance plutot que ce lui de la table (ie. 'through'=>'AnimalsUsers'). Mais je sais pas si ça fait le moindre changement...
-> Dans les Model> Entity : j'ai ajouté à l'accessible de Animal le '__*joinData'=> true. Les autres sont inchangés
-> Dans le UsersController : tout était bon (comme le cochon)

dans src\model\table\userstable.php
class UsersTable extends Table
{
    public function initialize(array $config)
    {
        $this->table('users');
        $this->displayField('id');
        $this->primaryKey('id');
        $this->belongsToMany('Animals', [
            'foreignKey' => 'user_id',
            'targetForeignKey' => 'animal_id',
            'joinTable' => 'animals_users',
            'through' => 'AnimalsUsers',
        ]);
    }
}
dans src\model\table\animalstable.php
class AnimalsTable extends Table
{
    public function initialize(array $config)
    {
        $this->table('animals');
        $this->displayField('id');
        $this->primaryKey('id');
        $this->belongsToMany('Users', [
            'foreignKey' => 'animal_id',
            'targetForeignKey' => 'user_id',
            'joinTable' => 'animals_users',
            'through' => 'AnimalsUsers',
        ]);
    }
}
dans src\model\entity\animals.php
class Animal extends Entity
{
    protected $_accessible = [
        'genre' => true,
        'users' => true,
        '_joinData' => true,
    ];
}
dans src\controller\userscontroller.php
public function add()
    {
        $user = $this->Users->newEntity();
        if ($this->request->is('post')) {
            $user = $this->Users->patchEntity($user, $this->request->data,['associated' => ['Animals._joinData']]);
            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));
                return $this->redirect(['action' => 'index']);
            } else {
                $this->Flash->error(__('The user could not be saved. Please, try again.'));
            }
        }
        $animals = $this->Users->Animals->find('list', ['keyField' => 'id', 'valueField' => 'genre']);
        $this->set(compact('user', 'animals'));
        $this->set('_serialize', ['user']);
    }

    public function edit($id = null)
    {
        $user = $this->Users->get($id, ['contain' => ['Animals']]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $user = $this->Users->patchEntity($user, $this->request->data,['associated'=>['Animals._joinData']]);
            if ($this->Users->save($user, ['associated' => ['Animals._joinData']])) {
                $this->Flash->success(__('The user has been saved.'));
                return $this->redirect(['action' => 'index']);
            } else {
                $this->Flash->error(__('The user could not be saved. Please, try again.'));
            }
        }
        $animals = $this->Users->Animals->find('list', ['keyField' => 'id', 'valueField' => 'genre']);
        $this->set(compact('user', 'animals'));
        $this->set('_serialize', ['user']);
    }

Et maintenant, les formulaires Add et Edit :

dans src\template\users\add.ctp
<div class="users form large-10 medium-9 columns">
    <?= $this->Form->create($user) ?>
    <fieldset>
        <legend><?= __('Add User') ?></legend>
        <?php
        echo $this->Form->input('nom',['label'=>__('Nom du propriétaire')]);
        echo $this->Form->input('animals.0.id', ['type' => 'select', 'label' => __('Genre d\'animal'), 'options' => $animals, 'value' => '']);
        echo $this->Form->input('animals.0._joinData.nom', ['label' => 'Nom de l\'animal']);
        ?>
    </fieldset>
    <?= $this->Form->button(__('Enregistrer')) ?>
    <?= $this->Form->end() ?>
</div>

dans src\template\users\edit.ctp
<div class="users form large-10 medium-9 columns">
    <?= $this->Form->create($user) ?>
    <fieldset>
        <legend><?= __('Edit User') ?></legend>
        <?= $this->Form->input('nom') ?>
        <?php foreach($user->animals as $key=>$row) : ?>
        <fieldset>
            <legend><?=__('Animal N°').($key+1) ?></legend>
            <?= $this->Form->input('animals.'.$key.'.id',['type'=>'select','label'=>__('Genre d\'animal'),'options'=>$animals]) ?>
            <?= $this->Form->input('animals.'.$key.'._joinData.nom') ?>
        </fieldset>
        <?php endforeach;
        $nextKey = count($user->animals);
        ?>
        <fieldset>
            <legend><?=__('Nouvel animal N°').($nextKey+1) ?></legend>
            <?= $this->Form->input('animals.'.$nextKey.'.id',['type'=>'select','label'=>__('Genre d\'animal'),'options'=>$animals,'value'=>'']) ?>
            <?= $this->Form->input('animals.'.$nextKey.'._joinData.nom') ?>
        </fieldset>
    </fieldset>
    <?= $this->Form->button(__('Enregistrer')) ?>
    <?= $this->Form->end() ?>
</div>

Il s'agissait donc de faire l'input du animals.0.id, animals.1.id, etc. et non pas de celui qui est dans la jointure (animals.0._joinData.id)

Au final, j'arrive à créer de nouveaux animaux pour les propriétaires (pour l'instant un seul par edition du formulaire, mais un petit tour de jquery et j'aurai un bouton "Ajouter" pour insérer les éléments appropriés dans le DOM...), et je peux changer le nom du bestiau quand il existe déjà dans la jointure.

Reste maintenant à : 1/ changer le type d'animal pour une jointure , 2/ supprimer un animal de la jointure (je tiens à préciser qu'aucun animal ne sera maltraité dans ce script)

On commence à voir la lumière au bout du tunnel .....

Bonjour,

Tu peux essayer de changer le nom de ta table d'association en 'animals_users'.
Par convention, les tables d'asso' doivent contenir le nom des deux tables, séparées par un undescore, dans l'ordre alphabétique croissant.

Ca se trouve ce n'est que ça !

ahhhhhhh !!!..... dommage !.... :'-(
Désolé mansachai, ton idée me paraissait très prometteuse et je viens de "standardiser" mes tables et mes champs avec les règles de nommage, j'obtiens pas de meilleur résultat. (tout n'est pas perdu, j'aurai au moins appris à nommer mes champs)
Mais je me demande si la récupération de la jointure a le moindre intéret.
Il me semble que si l'utilisateur ajoute ou supprime des animaux, il me faudra ajouter/supprimer des enregistrements de jointure. Cake ne va pas le faire tout seul (enfin, je crois).
Du coup, puisque je ne peux pas me fier aux indices de mon _joinData, pour insérer, updater ou supprimer, je vais devoir me peler la mise à jour du "animals_users" à la main dans un aftersave (j'ai vu passer un tuto grafikart la-dessus, mais en cakePhp2... à adapter).

Merci de cette idée qui m'aura permis de me coucher moins idiot.

Booooonnnnnnnn ... j'avance un peu ... et sans l'avoir fait exprès.

Explication :
En fait, je ne souhaite pas gérer des animaux de compagnie. Je n'avais développé cet exemple que pour me simplifier les formulaires...
En vrai, j'ai des utilisateurs, qui peuvent avoir plusieurs moyens de contact (tél. domicile, tél. bureau, mobile, email perso, etc.) et des réseaux sociaux.
De plus ces utilisateurs, appartiennent à des structures (des établissements) qui ont elles aussi des moyens de contact et des réseau sociaux.
Donc, j'ai une table users et une table structures, un table contacts et une table socials (tjrs ces #!%$$... pluriels français) où je mets les types de contact et de réseau, et 4 tables de jointure contactsUsers, socialsUsers, contactsStructure et socialsStructures.
Eh b'en, je sais pas pourquoi, alors que les jointures users<-> contacts et users<->socials ne se font pas dans le patchentity, les jointures users->structures<->contacts et users->structures-socials, elles marchent vachement bien !!!! ..... et j'ai même le sentiement que Cake serait capable de m'ajouter/updater/supprimer mes jointures, tout seul, comme un grand qu'il est...
Je pense qu'il y a une subtilité qui m'échappe encore dans l'écriture de mon formulaire...
Vu l'heure, j'arrête de bosser pour ce soir et je laisse reposer jusqu'à demain.
La solution est proche.... (ou pas ...)

Bonjour,
Dans ton cas, on évite de remplir les tables d'association "à la main", si tout fonctionne comme il faut ça doit se faire tout seul.
Et effectivement, une fois que tout est OK, Cake gère tout seul les assos' .

Peux tu nous montrer un exemple de formulaire qui ne marche pas ?
Sinon, tu peux aussi vérifier la structure de tes tables et en particuliers tes clées étrangères. Elles doivent ressembler à ça : nom-de-la-table-au-singulier_ id. Si c'est OK ce sera déjà un problème en moins.

Je viens de remarquer un truc bizare dans ton exemple :

'_joinData' => object(App\Model\Entity\Animalsuser)

Ton entitée devrait s'appeler AnimalsUser et pas Animalsuser. Ici, Cake ne peut pas comprendre qu'il s'agit d'une table d'association.

Du coup, ce serait pas mal qu'on puisse aussi voir ce que tu as fait dans tes tables.

Bonjour,
Afin de simplifier et clarifier la situation, j'ai créé un nouveau projet avec juste le nécessaire :
Les tables :

CREATE TABLE users (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
nom varchar(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;

CREATE TABLE animals (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
genre varchar(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;

CREATE TABLE IF NOT EXISTS animals_users (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
user_id int(10) unsigned NOT NULL,
animal_id int(10) unsigned NOT NULL,
nom varchar(255) NOT NULL,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY animal_id (animal_id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ;

J'ai généré la totalité du CRUD avec CakePhp
Les Modeles Tables:

class UsersTable extends Table
{
public function initialize(array $config)
{
$this->table('users');
$this->displayField('id');
$this->primaryKey('id');
$this->belongsToMany('Animals', [
'foreignKey' => 'user_id',
'targetForeignKey' => 'animal_id',
'joinTable' => 'animals_users',
'through' => 'animals_users',
]);
}
}
class AnimalsTable extends Table
{
public function initialize(array $config)
{
$this->table('animals');
$this->displayField('id');
$this->primaryKey('id');
$this->belongsToMany('Users', [
'foreignKey' => 'animal_id',
'targetForeignKey' => 'user_id',
'joinTable' => 'animals_users',
'through' => 'animals_users',
]);
}
}
class AnimalsUsersTable extends Table
{
public function initialize(array $config)
{
$this->table('animals_users');
$this->displayField('id');
$this->primaryKey('id');
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
'joinType' => 'INNER'
]);
$this->belongsTo('Animals', [
'foreignKey' => 'animal_id',
'joinType' => 'INNER'
]);
}
}

Je vous ai fait grâce des validators qui n'ont rien de particuliers. La seule modification que j'ai apporté ce sont les conditions 'through'=> 'animals__*users' que j'ai ajouté conformément à la doc (http://book.cakephp.org/3.0/fr/orm/associations.html#utiliser-l-option-through)
Les Modeles Entity

class User extends Entity
{
    protected $_accessible = [
        'nom' => true,
        'animals' => true,
    ];
}
class Animal extends Entity
{
    protected $_accessible = [
        'genre' => true,
        'users' => true,
    ];
}
class AnimalsUser extends Entity
{
    protected $_accessible = [
        'user_id' => true,
        'animal_id' => true,
        'nom' => true,
        'user' => true,
        'animal' => true,
    ];
}

Rien à signaler, sauf que .... Je ne comprend pas le accessible sur 'animals' dans User. A mon sens, 'animals' ne peut (et ne doit) pas être modifié directement à partir de User. En changeant ça, j'obtiens des résultats plus cohérents dans le patchage de l'entité user ( mais pas concluants de toute façon).

Le edit du controleur User :

class UsersController extends AppController
{
...
public function edit($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => ['Animals']
        ]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $user = $this->Users->patchEntity($user, $this->request->data,['associated'=>['Animals._joinData']]);
            debug($this->request->data);
            debug($user); 
            die();

            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));
                return $this->redirect(['action' => 'index']);
            } else {
                $this->Flash->error(__('The user could not be saved. Please, try again.'));
            }
        }
        $animals = $this->Users->Animals->find('list', ['keyField' => 'id', 'valueField' => 'genre', 'limit' => 200]);
        $this->set(compact('user', 'animals'));
        $this->set('_serialize', ['user']);
    }
    ....

Le formulaire edit :
( je pars sur celui-là, car il est plus facile de voir si les données requétées remplacent, ou pas, les données extraitent de la bdd)

<div class="users form large-10 medium-9 columns">
    <?= $this->Form->create($user) ?>
    <fieldset>
        <legend><?= __('Edit User') ?></legend>
        <?= $this->Form->input('nom') ?>
        <fieldset>
            <legend><?=__('Animal N°1') ?></legend>
            <?= $this->Form->input('animals.0._joinData.user_id', ['type'=>'hidden']) ?>
            <?= $this->Form->input('animals.0._joinData.nom') ?>
            <?= $this->Form->input('animals.0._joinData.animal_id', ['label'=>__('Genre d\'animal'),'options' => $animals]) ?>
        </fieldset>
    </fieldset>
    <?= $this->Form->button(__('Enregistrer')) ?>
    <?= $this->Form->end() ?>
</div>

J'ai repris le _joinData.user_id en hidden. Des fois que ça servirai ...
Enfin, le résultat du debug de request->data et de l'entité du get après le patchage, AVEC $_accessible { 'animals' => true} dans le model entity User :

 \src\Controller\UsersController.php (line 77)
[
    'nom' => 'john doe',
    'animals' => [
        (int) 0 => [
            '_joinData' => [
                'user_id' => '1',
                'nom' => 'Garfield',
                'animal_id' => '1'
            ]
        ]
    ]
]

\src\Controller\UsersController.php (line 77)
object(App\Model\Entity\User) {
    'id' => (int) 1,
    'nom' => 'john doe',
    'animals' => [
        (int) 0 => object(App\Model\Entity\Animal) {
            '[new]' => true,
            '[accessible]' => [
                'genre' => true,
                'users' => true
            ],
            '[dirty]' => [],
            '[original]' => [],
            '[virtual]' => [],
            '[errors]' => [
                'genre' => [
                    '_required' => 'This field is required'
                ]
            ],
            '[repository]' => 'Animals'
        }
    ],
    '[new]' => false,
    '[accessible]' => [
        'nom' => true,
        'animals' => true
    ],
    '[dirty]' => [
        'animals' => true
    ],
    '[original]' => [
        'animals' => [
            (int) 0 => object(App\Model\Entity\Animal) {
                'id' => (int) 1,
                'genre' => 'chat',
                '_joinData' => object(App\Model\Entity\AnimalsUser) {
                    'animal_id' => (int) 1,
                    'id' => (int) 1,
                    'user_id' => (int) 1,
                    'nom' => 'Felix',
                    '[new]' => false,
                    '[accessible]' => [
                        'user_id' => true,
                        'animal_id' => true,
                        'nom' => true,
                        'user' => true,
                        'animal' => true
                    ],
                    '[dirty]' => [],
                    '[original]' => [],
                    '[virtual]' => [],
                    '[errors]' => [],
                    '[repository]' => 'animals_users'       
                },
                '[new]' => false,
                '[accessible]' => [
                    'genre' => true,
                    'users' => true,
                    '_joinData' => true
                ],
                '[dirty]' => [],
                '[original]' => [],
                '[virtual]' => [],
                '[errors]' => [],
                '[repository]' => 'Animals'     
            }
        ]
    ],
    '[virtual]' => [],
    '[errors]' => [],
    '[repository]' => 'Users'
}

Le même, SANS $_accessible { 'animals' => true} dans le model entity User :

 \src\Controller\UsersController.php (line 77)
[
    'nom' => 'john doe',
    'animals' => [
        (int) 0 => [
            '_joinData' => [
                'user_id' => '1',
                'nom' => 'Garfield',
                'animal_id' => '1'
            ]
        ]
    ]
]

\src\Controller\UsersController.php (line 77)
object(App\Model\Entity\User) {
    'id' => (int) 1,
    'nom' => 'john doe',
    'animals' => [
        (int) 0 => object(App\Model\Entity\Animal) {
            'id' => (int) 1,
            'genre' => 'chat',
            '_joinData' => object(App\Model\Entity\AnimalsUser) {
                'animal_id' => (int) 1,
                'id' => (int) 1,
                'user_id' => (int) 1,
                'nom' => 'Felix',
                '[new]' => false,
                '[accessible]' => [
                    'user_id' => true,
                    'animal_id' => true,
                    'nom' => true,
                    'user' => true,
                    'animal' => true
                ],
                '[dirty]' => [],
                '[original]' => [],
                '[virtual]' => [],
                '[errors]' => [],
                '[repository]' => 'animals_users'
            },
            '[new]' => false,
            '[accessible]' => [
                'genre' => true,
                'users' => true,
                '_joinData' => true
            ],
            '[dirty]' => [],
            '[original]' => [],
            '[virtual]' => [],
            '[errors]' => [],
            '[repository]' => 'Animals'
        }
    ],
    '[new]' => false,
    '[accessible]' => [
        'nom' => true
    ],
    '[dirty]' => [],
    '[original]' => [],
    '[virtual]' => [],
    '[errors]' => [],
    '[repository]' => 'Users'
}

Ce dernier semble plus conforme à mes attentes, mais 'Garfield' remplace toujours pas 'Felix' ...

Bref, y un truc qui est pas clair ! En tout cas dans mon esprit, et peut-être dans la doc...

Ok, cool on y voit plus clair !
Les Tables on l'air OK, tu peux enlever le 'throught', je pense que ça ne sers à rien ici.
Laisse les entitées comme elles sont avec le $accessible comme il a été généré.

Dans le controller je ferais plutôt comme ça :

    // le patchEntity
    $user = $this->Users->patchEntity($user, $this->request->data(),['associated'=>['Animals']]);

    // le save
    $this->Users->save($user, ['associated' => ['Animals']]));

Si ça ne marche pas mieux tu sauras que le problème vient du formulaire.

Essaie peut être de rentrer les inputs à la mano en vérifiant dans le debug ce que ça fait.
Dans l'idée ce serait quelque chose comme ça :

         <?= $this->Form->input('animals.user_id', ['type'=>'hidden']) ?>
         <?= $this->Form->input('animals.nom') ?>
         <?= $this->Form->input('animals.genres.animal_id', ['label'=>__('Genre d\'animal'),'options' => $animals]) ?>   

Dernière chose, es-tu bien sur que tes clées étrangères sont en integer ?

Sinon par curiosité, pourquoi ne gères tu pas tes animaux avec le AnimalsController ? Ca pourrait éviter quelques liaisons relous à gérer !
Courage :D