Bonjour à tous,

Je me lance dans la création d'un forum avec cakephp 3, histoire de peaufiner mon apprentissage de celui-ci.

Mon problème :

Pour faire simple, sur la page d'accueil de mon forum, je n'arrive pas à sélectionner le dernier sujet pour chaque forum présent dans sa catégorie.

Exemple :

Cliquez sur le lien forum de ce site et vous verrez que dans le forum "Formation ", vous aurez le dernier sujet publié dans celui-ci. Idem pour chaque forum présent dans la catégorie "Détente", "Les questions - Logiciels ", etc.

Structure des tables :

Suite à la lecture du tutoriel Système de sujets lus / non lus , j'ai gardé cette structure.

Structure :

La structure du forum va être simple :

  • Une table categories pour organiser nos différents forums (hasMany Forum)
  • Une table forums (belongsTo Categorie, hasMany Topic)
  • Une table topics (belongsTo User, belongsTo Forum, hasMany Post)
  • Une table posts qui contiendra les messages de notre forum (belongsTo Topic, belongsTo User)
  • L'incontournable table users pour sauvegarder les utilisateurs (hasMany Topic, hasMany Post)

Requête :

Voici la requête situé dans ForumsController, permettant d'afficher l'accueil de mon forum :

public function index()
    {
        $this->loadModel('ForumCategories');

        $categories = $this->ForumCategories
            ->find()
            ->contain([
                'ForumForums' => function ($q) {
                    return $q->order(['ForumForums.position' => 'asc']);
                },
                'ForumForums.ForumTopics' => function ($q) {
                    return $q
                        ->order(['ForumTopics.created' => 'desc']);
                }
            ])
            ->order([
                'ForumCategories.position' => 'asc'
            ]);

        $this->set(compact('categories'));
    }

Avec un debug() dans la vue, cela me donne :

object(Cake\ORM\Entity) {

    'new' => false,
    'accessible' => [
        '*' => true
    ],
    'properties' => [
        'id' => (int) 2,
        'title' => 'Catégorie 2',
        'position' => (int) 1,
        'forum_forums' => [
            (int) 0 => object(App\Model\Entity\ForumForum) {

                'new' => false,
                'accessible' => [
                    '*' => true
                ],
                'properties' => [
                    'id' => (int) 2,
                    'category_id' => (int) 2,
                    'title' => 'Forum 2',
                    'slug' => 'forum-2',
                    'description' => 'Bla bla',
                    'position' => (int) 1,
                    'topic_count' => (int) 0,
                    'message_at' => null,
                    'forum_topics' => [
                        (int) 0 => object(App\Model\Entity\ForumTopic) {

                            'new' => false,
                            'accessible' => [
                                '*' => true
                            ],
                            'properties' => [
                                'id' => (int) 6,
                                'forum_id' => (int) 2,
                                'user_id' => (int) 1,
                                'title' => 'Topic 6',
                                'content' => 'Bla bla',
                                'sticky' => false,
                                'post_count' => (int) 0,
                                'message_at' => null,
                                'created' => object(Cake\I18n\Time) {

                                    'time' => '2015-04-11T05:00:00+0000',
                                    'timezone' => 'UTC',
                                    'fixedNowTime' => false

                                },
                                'modified' => null
                            ],
                            'dirty' => [],
                            'original' => [],
                            'virtual' => [],
                            'errors' => [],
                            'repository' => 'ForumTopics'

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

                            'new' => false,
                            'accessible' => [
                                '*' => true
                            ],
                            'properties' => [
                                'id' => (int) 3,
                                'forum_id' => (int) 2,
                                'user_id' => (int) 1,
                                'title' => 'Topic 4',
                                'content' => 'Bla bla',
                                'sticky' => false,
                                'post_count' => (int) 0,
                                'message_at' => null,
                                'created' => object(Cake\I18n\Time) {

                                    'time' => '2015-04-09T00:00:00+0000',
                                    'timezone' => 'UTC',
                                    'fixedNowTime' => false

                                },
                                'modified' => null
                            ],
                            'dirty' => [],
                            'original' => [],
                            'virtual' => [],
                            'errors' => [],
                            'repository' => 'ForumTopics'

                        }
                    ]
                ],
                'dirty' => [],
                'original' => [],
                'virtual' => [],
                'errors' => [],
                'repository' => 'ForumForums'

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

                'new' => false,
                'accessible' => [
                    '*' => true
                ],
                'properties' => [
                    'id' => (int) 3,
                    'category_id' => (int) 2,
                    'title' => 'Forum 3',
                    'slug' => 'forum-3',
                    'description' => 'Bla bla',
                    'position' => (int) 2,
                    'topic_count' => (int) 0,
                    'message_at' => null,
                    'forum_topics' => [
                        (int) 0 => object(App\Model\Entity\ForumTopic) {

                            'new' => false,
                            'accessible' => [
                                '*' => true
                            ],
                            'properties' => [
                                'id' => (int) 7,
                                'forum_id' => (int) 3,
                                'user_id' => (int) 1,
                                'title' => 'Topic 7',
                                'content' => 'Bla bla',
                                'sticky' => true,
                                'post_count' => (int) 0,
                                'message_at' => null,
                                'created' => object(Cake\I18n\Time) {

                                    'time' => '2015-04-12T00:00:00+0000',
                                    'timezone' => 'UTC',
                                    'fixedNowTime' => false

                                },
                                'modified' => null
                            ],
                            'dirty' => [],
                            'original' => [],
                            'virtual' => [],
                            'errors' => [],
                            'repository' => 'ForumTopics'

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

                            'new' => false,
                            'accessible' => [
                                '*' => true
                            ],
                            'properties' => [
                                'id' => (int) 2,
                                'forum_id' => (int) 3,
                                'user_id' => (int) 1,
                                'title' => 'Topic 2',
                                'content' => 'Bla bla',
                                'sticky' => false,
                                'post_count' => (int) 0,
                                'message_at' => null,
                                'created' => object(Cake\I18n\Time) {

                                    'time' => '2015-04-10T00:00:00+0000',
                                    'timezone' => 'UTC',
                                    'fixedNowTime' => false

                                },
                                'modified' => null
                            ],
                            'dirty' => [],
                            'original' => [],
                            'virtual' => [],
                            'errors' => [],
                            'repository' => 'ForumTopics'

                        }
                    ]
                ],
                'dirty' => [],
                'original' => [],
                'virtual' => [],
                'errors' => [],
                'repository' => 'ForumForums'

            }
        ]
    ],
    'dirty' => [],
    'original' => [],
    'virtual' => [],
    'errors' => [],
    'repository' => 'ForumCategories'

}

Dans la vue, après avoir listé mes forums dans une catégorie avec

<?php foreach ($category->forum_forums as $forum): ?>

Je dois utiliser actuellement $forum->forum_topics[0]['title'] pour afficher le dernier topic dans chaque forum. Vous comprendrez aisément que si j'ai 1000 topics par forum, la requête sera énorme et peu performante. J'ai donc pensé à rajouter ->first() dans mon contain ForumForums.ForumTopics pour sélectionner le dernier topic, mais j'ai une erreur fatale :

Error: Call to undefined method App\Model\Entity\ForumTopic::all()
File ../vendor/cakephp/cakephp/src/ORM/Association/ExternalAssociationTrait.php
Line: 114 

Ma question :

Comment résoudre en une seule requête ce problème ? Dans divers topic, certains disent qu'une seule requête suffit sans donner de piste. J'ai également pensé à créer une autre table du genre LastTopic, mais il faudrait faire de même avec LastPost. En effet, le problème est le même lorsqu'on va dans le forum "Détente", on liste tous les sujets et pour chaque sujet, on indique le dernier message de celui-ci.

Auriez vous une idée, une solution ? Car pour le moment, le résultat est là, mais les performances/optimisations.. pas du tout. Merci d'avance.

8 réponses


ker0x
Réponse acceptée

Dans ta table Forums, rajoute un champ last_topic_id et fais une relation belongsTo vers ta table topics

$this->belongsTo('LastTopic', [
    'className' => 'ForumTopics',
    'foreignKey' => 'last_topic_id'
]);

Ensuite dans ton ForumsController rajoute la liaison vers la table Topics dans ta requête Forums

public function index()
    {
        $this->loadModel('ForumCategories');
        $categories = $this->ForumCategories->find()->contain([
            'ForumForums' => function ($q) {
                return $q->contain([
                    'LastTopic' => [
                        'fields' => ['title', 'created_at']
                    ]
                ])->order(['ForumForums.position' => 'asc']);
            }
        ])->all();

        $this->set('categories', $categories);
    }

Et enfin, dans la fonction add de ton TopicsController, sauvegarde l'ID du nouveau topic un fois le topic sauvegardé

if ($topic = $this->ForumTopics->save($topic)) {
    // Update last_topic_id field
    $forum = $this->ForumForums->patchEntity($forum, ['last_topic_id' => $topic->id]);
    $this->ForumForums->save($forum);
}
Xeta
Réponse acceptée

Tu as testé de faire ta requête sans faire de SELECT sur les tables ? (En sélectionnant tout quoi) Car j'ai comme une impression que tu oublie de sélectionner certains fields qui servent à faire les relations.
A la place de :

'LastPost' => function ($q) {
    return $q->select(['id', 'user_id', 'created']);
 },
'LastPost.Users' => function ($q) {
    return $q->find('short');
 },
'Users' => function ($q) {
    return $q->find('short');
 }

Test ça :

'LastPost',
'LastPost.Users',
'Users'

Et dit nous se que ça fait.

Xeta
Réponse acceptée

C'est le topic_id qui manquais (et peut être l'id de l'user dans la relation 'LastPost.Users'):

'LastPost' => function ($q) {
    return $q->select(['id', 'topic_id', 'user_id', 'created']);
},
JeremyB
Auteur

Ça faisait une bonne semaine que je galérais et tournais en rond pour trouver ce fichu problème alors que.. ça coulait de source. Je n'y ai pas du tout pensé, le boulet. Bon, je vais faire de même pour le listing des sujets avec le dernier message pour chaque sujet.

Grand merci à toi en tout cas, problème résolu et une sacrée optimisation de faite ! ;)

Et je rajouterais, pour compter le nombre de topic/posts, utilise le behavior counterCache. (Si tu ne l'a pas déjà fait bien sûr.)

JeremyB
Auteur

Oui Xeta, je l'ai implémenté dans mon code. :)

Par contre, pour le listing des topics avec le dernier message, tout marche sauf la liaison avec user.

Dans ForumTopicsTable.php, j'ai bien rajouté :

        $this->belongsTo('LastPost', [
            'className'  => 'ForumPosts',
            'foreignKey' => 'last_post_id'
        ]);

Dans mon ForumsController.php, la requête :

public function topics()
    {
        $this->loadModel('ForumForums');

        $forum = $this->ForumForums
            ->find()
            ->select(['title', 'description'])
            ->where([
                'ForumForums.id' => $this->request->id
            ])
            ->first();

        $this->loadModel('ForumTopics');
        $this->paginate = [
            'maxLimit' => 25
        ];

        $topics = $this->ForumTopics
            ->find()
            ->contain([
                'LastPost' => function ($q) {
                    return $q->select(['id', 'user_id', 'created']);
                },
                'LastPost.Users' => function ($q) {
                    return $q->find('short');
                },
                'Users' => function ($q) {
                    return $q->find('short');
                }
            ])
            ->where([
                'ForumTopics.forum_id' => $this->request->id,
                // 'ForumTopics.thread_open !=' => 2
            ])
            ->order([
                'ForumTopics.sticky'  => 'DESC',
                'ForumTopics.created' => 'DESC' //message_at
            ]);

        $topics = $this->paginate($topics);

        $this->set(compact('forum', 'topics'));
    }

Je fais une liaison de user avec les topics, et le dernier post avec son user.

Sauf qu'en résultat, sur le last_post, le user n'est pas correte et se met dans un last_post null :

object(Cake\ORM\ResultSet) {

    'query' => object(Cake\ORM\Query) {

        'sql' => 'SELECT ForumTopics.id AS `ForumTopics__id`, ForumTopics.forum_id AS `ForumTopics__forum_id`, ForumTopics.last_post_id AS `ForumTopics__last_post_id`, ForumTopics.user_id AS `ForumTopics__user_id`, ForumTopics.title AS `ForumTopics__title`, ForumTopics.content AS `ForumTopics__content`, ForumTopics.sticky AS `ForumTopics__sticky`, ForumTopics.post_count AS `ForumTopics__post_count`, ForumTopics.message_at AS `ForumTopics__message_at`, ForumTopics.created AS `ForumTopics__created`, ForumTopics.modified AS `ForumTopics__modified`, LastPost.id AS `LastPost__id`, LastPost.user_id AS `LastPost__user_id`, LastPost.created AS `LastPost__created`, Users.first_name AS `Users__first_name`, Users.last_name AS `Users__last_name`, Users.username AS `Users__username`, Users.slug AS `Users__slug` FROM forum_topics ForumTopics LEFT JOIN forum_posts LastPost ON LastPost.id = (ForumTopics.last_post_id) LEFT JOIN users Users ON Users.id = (ForumTopics.user_id) WHERE ForumTopics.forum_id = :c0 ORDER BY ForumTopics.sticky DESC, ForumTopics.created DESC LIMIT 20 OFFSET 0',
        'params' => [
            ':c0' => [
                'value' => '1',
                'type' => 'integer',
                'placeholder' => 'c0'
            ]
        ],
        'defaultTypes' => [
            'ForumTopics.id' => 'integer',
            'id' => 'integer',
            'ForumTopics.forum_id' => 'integer',
            'forum_id' => 'integer',
            'ForumTopics.last_post_id' => 'integer',
            'last_post_id' => 'integer',
            'ForumTopics.user_id' => 'integer',
            'user_id' => 'integer',
            'ForumTopics.title' => 'string',
            'title' => 'string',
            'ForumTopics.content' => 'text',
            'content' => 'text',
            'ForumTopics.sticky' => 'boolean',
            'sticky' => 'boolean',
            'ForumTopics.post_count' => 'integer',
            'post_count' => 'integer',
            'ForumTopics.message_at' => 'datetime',
            'message_at' => 'datetime',
            'ForumTopics.created' => 'datetime',
            'created' => 'datetime',
            'ForumTopics.modified' => 'datetime',
            'modified' => 'datetime'
        ],
        'decorators' => (int) 0,
        'executed' => true,
        'hydrate' => true,
        'buffered' => true,
        'formatters' => (int) 0,
        'mapReducers' => (int) 0,
        'contain' => [
            'LastPost' => [
                'queryBuilder' => object(Closure) {

                },
                'Users' => [
                    'queryBuilder' => object(Closure) {

                    }
                ]
            ],
            'Users' => [
                'queryBuilder' => object(Closure) {

                }
            ]
        ],
        'matching' => [],
        'extraOptions' => [
            'whitelist' => [
                (int) 0 => 'limit',
                (int) 1 => 'sort',
                (int) 2 => 'page',
                (int) 3 => 'direction'
            ]
        ],
        'repository' => object(App\Model\Table\ForumTopicsTable) {

            'registryAlias' => 'ForumTopics',
            'table' => 'forum_topics',
            'alias' => 'ForumTopics',
            'entityClass' => 'App\Model\Entity\ForumTopic',
            'associations' => [
                (int) 0 => 'users',
                (int) 1 => 'forumforums',
                (int) 2 => 'lastpost',
                (int) 3 => 'forumposts'
            ],
            'behaviors' => [
                (int) 0 => 'Timestamp',
                (int) 1 => 'Sluggable'
            ],
            'defaultConnection' => 'default',
            'connectionName' => 'default'

        }

    },
    'items' => [
        (int) 0 => object(App\Model\Entity\ForumTopic) {

            'new' => false,
            'accessible' => [
                '*' => true
            ],
            'properties' => [
                'id' => (int) 5,
                'forum_id' => (int) 1,
                'last_post_id' => (int) 0,
                'user_id' => (int) 1,
                'title' => 'Topic 5',
                'content' => 'Bla bla',
                'sticky' => true,
                'post_count' => (int) 0,
                'message_at' => null,
                'created' => object(Cake\I18n\Time) {

                    'time' => '2015-04-10T01:00:00+0000',
                    'timezone' => 'UTC',
                    'fixedNowTime' => false

                },
                'modified' => null,
                'user' => object(App\Model\Entity\User) {

                    'new' => false,
                    'accessible' => [
                        '*' => true
                    ],
                    'properties' => [
                        'first_name' => 'Jérémy',
                        'last_name' => 'B',
                        'username' => 'Jeremy',
                        'slug' => 'jeremy'
                    ],
                    'dirty' => [],
                    'original' => [],
                    'virtual' => [],
                    'errors' => [],
                    'repository' => 'Users'

                },
                'last_post' => object(App\Model\Entity\ForumPost) {

                    'new' => false,
                    'accessible' => [
                        '*' => true
                    ],
                    'properties' => [
                        'id' => null,
                        'user_id' => null,
                        'created' => null,
                        'user' => object(App\Model\Entity\User) {

                            'new' => false,
                            'accessible' => [
                                '*' => true
                            ],
                            'properties' => [
                                'first_name' => 'Jérémy',
                                'last_name' => 'B',
                                'username' => 'Jeremy',
                                'slug' => 'jeremy'
                            ],
                            'dirty' => [],
                            'original' => [],
                            'virtual' => [],
                            'errors' => [],
                            'repository' => 'Users'

                        }
                    ],
                    'dirty' => [],
                    'original' => [],
                    'virtual' => [],
                    'errors' => [],
                    'repository' => 'LastPost'

                }
            ],
            'dirty' => [],
            'original' => [],
            'virtual' => [],
            'errors' => [],
            'repository' => 'ForumTopics'

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

            'new' => false,
            'accessible' => [
                '*' => true
            ],
            'properties' => [
                'id' => (int) 1,
                'forum_id' => (int) 1,
                'last_post_id' => (int) 1,
                'user_id' => (int) 1,
                'title' => 'Topic 1',
                'content' => 'Bla bla',
                'sticky' => false,
                'post_count' => (int) 0,
                'message_at' => null,
                'created' => object(Cake\I18n\Time) {

                    'time' => '2015-04-11T00:00:00+0000',
                    'timezone' => 'UTC',
                    'fixedNowTime' => false

                },
                'modified' => null,
                'user' => object(App\Model\Entity\User) {

                    'new' => false,
                    'accessible' => [
                        '*' => true
                    ],
                    'properties' => [
                        'first_name' => 'Jérémy',
                        'last_name' => 'B',
                        'username' => 'Jeremy',
                        'slug' => 'jeremy'
                    ],
                    'dirty' => [],
                    'original' => [],
                    'virtual' => [],
                    'errors' => [],
                    'repository' => 'Users'

                },
                'last_post' => object(App\Model\Entity\ForumPost) {

                    'new' => false,
                    'accessible' => [
                        '*' => true
                    ],
                    'properties' => [
                        'id' => (int) 1,
                        'user_id' => (int) 1,
                        'created' => object(Cake\I18n\Time) {

                            'time' => '2015-04-16T00:00:00+0000',
                            'timezone' => 'UTC',
                            'fixedNowTime' => false

                        },
                        'user' => null
                    ],
                    'dirty' => [],
                    'original' => [],
                    'virtual' => [],
                    'errors' => [],
                    'repository' => 'LastPost'

                }
            ],
            'dirty' => [],
            'original' => [],
            'virtual' => [],
            'errors' => [],
            'repository' => 'ForumTopics'

        }
    ]

}

Je ne vois pas pourquoi il indique le bon user au last_post qui n'existe pas
et pas au vrai last_post. Pourquoi cette inversion ?

Moi je sauvegarde, le dernier id du topic pour les forums, ou du post pour les topics, mais je save aussi le lastuserid dans les tables forums et topics. C'est un champ de plus, mais c'est quand même plus simple, et ça peut te permettre d'éviter 1 requête SQl dans certains cas.

Voilà à quoi ressemble mes 3 tables si ça peut aider:

Ma table Forum:

Ma table Topics

Ma table Posts

JeremyB
Auteur

@ Xeta : là, ça marche très bien. Lorsque le last_post existe, il me donne le bon user. Et lorsque last_post est null, aucun user est donné. Donc ça, c'est réglé. Maintenant, me reste plus qu'à trouver quel élément me manquait dans le select pour qu'il ne me fasse pas la bonne liaison.

EDIT : en faisant ceci, j'ai le bon résultat.

$topics = $this->ForumTopics
            ->find()
            ->contain([
                'LastPost' => function ($q) {
                    return $q->select(['id', 'topic_id', 'user_id', 'created']);
                },
                'LastPost.Users' => function ($q) {
                    return $q->select(['id', 'username']);
                },
                'Users' => function ($q) {
                    return $q->find('short');
                }
            ])
            ->where([
                'ForumTopics.forum_id' => $this->request->id
            ])
            ->order([
                'ForumTopics.sticky'  => 'DESC',
                'ForumTopics.created' => 'DESC' //message_at
            ]);

@ Jean-christophe Pires : merci pour m'avoir montré tes tables, cela m'a donné des idées pour la suite (fonctionnalités). Rien à voir, mais tu es sur HeidiSQL ? Dommage qu'il n'y ait pas quelque chose d'aussi joli sur Mac en utilisant MAMP.