En terme de modélisation, on a une triviale relation 1-n entre billet et commentaire.
Cela se représente en base de données par une FK not null de commentaire vers billet :
Code : Tout sélectionner
billet:
id: ...
...
commentaire:
billet_id: foreign_key(billet, id)
...
Et cela se représente en terme d'objet par une méthode "Billet::getCommentaires()" pour récupérer la liste des commentaires d'un billet, et éventuellement une méthode "Commentaire::getBillet()" pour récupérer le billet "parent".
Ainsi qu'une méthode "Billet::addCommentaire()" et éventuellement "Commentaire::setBillet()" pour lier un commentaire et un billet.
Et évidemment on ajoutera une méthode "Billet::removeCommentaire()" qui casse ce lien.
Le plus compliqué étant de maintenir bien à jour ces relations sans forcément surcharger la base de requêtes. Et surtout de savoir profiter de ce lien entre commentaire et billet pour maintenir un modèle cohérent grâce à des sauvegardes en cascade.
Je te laisse avec ces bouts de code (non testés, et qui de toute façon ne marchent pas tel quel du fait de l'utilisation d'une hypothétique classe d'abstraction SQL, ce n'est que pour te donner une idée) pour voir un exemple concret (*) :
class Billet
{
...
// Liste des commentaires
protected $commentaires = null;
// Retourner la liste des commentaires
public function getCommentaires()
{
if (is_null($this->commentaires)) {
$this->commentaires = Commentaire::selectWhere('billet_id = :id', array('id' => $this->getId()));
}
return $this->commentaires;
}
// Ajouter un commentaire à la liste
public function addCommentaire(Commentaire $commentaire)
{
if (!is_null($this->commentaires)) {
// Si on a déjà interrogé la liste des commentaires, on la maintient à jour
$this->commentaires[] = $commentaire;
}
$commentaire->setBillet($this);
}
// Retirer un commentaire de la liste
public function removeCommentaire(Commentaire $commentaire, $delete = true)
{
if (!is_null($this->commentaires)) {
foreach ($this->commentaires as $i => $a_commentaire) {
if ($commentaire === $a_commentaire) {
unset($this->commentaires[$i]);
}
}
}
$commentaire->setBillet(null);
if ($delete) {
$commentaire->delete();
}
}
}
class Commentaire
{
...
protected $billet = null;
public function getBillet()
{
if (is_null($this->billet)) {
$this->billet = Billet::selectOneWhere('id = :id', array('id' => $this->getBilletId()));
}
return $this->billet;
}
public function setBillet(Billet $billet = null)
{
if ($this->getBillet()) {
// je suis déjà lié à un billet : rompre ce lien
$this->getBillet()->removeCommentaire($this, false);
// Note : c'est le genre de test qu'il faudrait faire dans la méthode "delete()" aussi
}
$this->billet = $billet;
$this->setBilletId(is_null($billet) ? null : $billet->getId());
}
}
(*) Dans cet exemple je considère que tes objets ont déjà le minimum vital pour travailler en corrélation avec la base de données :
- Des méthodes statiques pour interroger la base de données (ici au pif un selectWhere() qui renvoie une liste d'objets, et un selectOneWhere() qui renvoie un seul objet).
- Des accesseurs (pour chaque champ "bidule" on a une méthode getBidule() et setBidule()).
- Une méthode save() qui insère ou met à jour l'objet selon s'il est "nouveau" ou pas (en gros, si sa primary key est défini on update, et sinon on insère) et qui renvoie la valeur de sa primary key.
- Une méthode delete() qui fait un delete dans la base, et marque l'objet comme "supprimé" (par exemple un objet marqué comme supprimé ne peut pas être sauvegardé).
Exemple :
class Billet
{
/*** Elements génériques de manipulation de l'objet dans la base ***/
// L'objet est-il "virtuel" ou a-t-il été déjà sauvé dans la base ?
protected $_saved = true;
// Liste des colonnes modifiées par rapport à la version en base
protected $_modifiedColumns = array();
// Marqué pour suppression ?
protected $_deleted = false;
public function isDeleted()
{
return $this->_deleted;
}
public function delete()
{
if ($this->isDeleted()) {
return false;
}
SQL::exec('DELETE FROM billet WHERE id=:id', array('id' => $this->getId()));
$this->_deleted = true;
return true;
}
public function isNew()
{
return !$this->_saved;
}
public function isModified()
{
return count($this->_modifiedColumns) >= 0;
}
public function save()
{
if ($this->isNew()) {
SQL::exec('INSERT INTO billet (titre, ...) VALUES (:titre, ...)', array('titre' => $this->getTitre(), ...));
$this->setId(SQL::lastInsertId());
} elseif ($this->isModified()) {
$changes = array();
$values = array('id' => $this->getId());
foreach ($this->_modifiedColumns as $column) {
$changes[] = $column . ' = :' . $column;
$values[$column] = call_user_func(array($this, 'get'.$column));
}
SQL::exec('UPDATE billet SET '.implode(', ', $changes).' WHERE id=:id', $values);
}
// Histoire de se simplifier la vie : ici on pourrait parcourir les commentaires attachés mais
// pas encore sauvegardés, et les sauver
foreach ($this->getCommentaires() as $commentaire) {
if ($commentaire->isNew() || $commentaire->isModified()) {
$commentaire->save();
}
}
// Note : dans Commentaire::save() on fera la même vérification mais au début de la méthode
// save() car le billet est représentée par une colonne billet_id not null :
/*
Commentaire::save():
if ($this->getBillet() && ($this->getBillet()->isNew() || $this->getBillet()->isModified())) {
$this->setBilletId($this->getBillet()->save());
}
...
*/
// Remise à 0 des modifications
$this->_saved = true;
$this->_modifiedColumns = array();
return $this->getId();
}
/*** Elements de l'objet représentant une ligne de la table "billet" ***/
protected $id = null;
// billet.id
public function getId()
{
return $this->id;
}
// setId n'est pas public : on considère qu'on doit avoir un auto_increment et que c'est via "save" qu'elle doit être définie, et pas manuellement
protected function setId($v)
{
$this->id = $v;
}
protected $titre = null;
public function getTitre()
{
return $this->titre;
}
public function setTitre($v)
{
if ($v !== $this->titre) {
$this->titre = $v;
if (!in_array('titre', $this->_modifiedColumns)) {
$this->_modifiedColumns[] = 'titre';
}
}
}
...
/*** Méthodes de requêtage ***/
public static function selectWhere($whereClause, array $values = array(), $fromClause = null)
{
$query = 'SELECT id, titre, ... FROM billet';
if (!is_null($fromClause)) {
$query .= $fromClause;
}
$query .= ' WHERE ' . $whereClause;
$rows = SQL::exec($query, $values);
$billets = array();
foreach ($rows as $row) {
$billet = new Billet();
foreach ($row as $column => $value) {
call_user_func(array($this, 'set'.ucfirst($column)), $value);
}
$billets[] = $billet;
}
return $billets;
}
public static function selectOneWhere($whereClause, array $values = array(), $fromClause = null)
{
$billets = self::selectWhere($whereClause . ' LIMIT 1', $values, $fromClause);
return reset($billets);
}
}
C'est une version raccourcie et simplifiée de ce que font des
ORM comme
Propel ou
Doctrine.
Il faudrait évidemment y ajouter des méthodes de sélection avec jointure pour récupérer en une seule requête un billet et ses commentaires, ce que font très bien ces deux librairies
À toi de voir si tu te sens d'attaque de réimplémenter ça "à ta manière" à partir des pistes que j'ai pu te fournir ici, ou si tu veux directement utiliser ces librairies qui ont fait leurs preuves. S'il s'agit d'un projet perso tu peux en profiter pour explorer plusieurs pistes dont le "fait maison" pour comprendre comment ça marche. S'il s'agit d'un projet professionnel : utilise l'une des deux librairies qui sont mures et bien maintenues.