IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Langage PHP Discussion :

Modélisation d'une relation 1 à plusieurs


Sujet :

Langage PHP

  1. #1
    Membre du Club
    Homme Profil pro
    Développeur multimédia
    Inscrit en
    Avril 2008
    Messages
    39
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France

    Informations professionnelles :
    Activité : Développeur multimédia
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Avril 2008
    Messages : 39
    Points : 40
    Points
    40
    Par défaut Modélisation d'une relation 1 à plusieurs
    Bonjour,

    Je m'interroge sur la meilleure façon de modéliser une relation "1 à plusieurs" classique.

    Prenons par exemple le blog dans lequel un billet peut avoir plusieurs commentaires (avec une classe Billet et une classe Commentaire).

    Solution 1 :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    $billet = new Billet(15);
    $billet->addComment('commentaire');
    Solution 2 :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    $billet = new Billet(15);
    $commentaire = new Commentaire();
    $commentaire->add($billet, 'commentaire');
    La première solution semble plus séduisante, mais ça m'agace un peu que la classe Billet aille trifouiller dans la table des commentaires.

    De plus, ça me semble plus dans le concept objet de passer l'objet Billet à l'objet Commentaire : de cette façon la classe Billet ne s'occupe que la table Billet et la classe Commentaire de la table commentaires.

    Qu'en dites-vous ?

    Merci d'avance.

    Franck.

  2. #2
    Expert éminent
    Avatar de Benjamin Delespierre
    Profil pro
    Développeur Web
    Inscrit en
    Février 2010
    Messages
    3 929
    Détails du profil
    Informations personnelles :
    Âge : 36
    Localisation : France, Alpes Maritimes (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Février 2010
    Messages : 3 929
    Points : 7 762
    Points
    7 762
    Par défaut
    Hello

    En effet, au sens objet, la relation 1-n se caractérise par une aggregation (un objet porte l'instance de plusieurs autres). Au sens d'une base de données relationnelles, ça se caractérise par une clé étrangère (avec eventuellement une contrainte sur cette clé).

    Moi je verrai ça comme ça:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
     
    class Billet {
       // SplObjectStorage
       protected $_commentaires;
     
       public static function obtenirCommentaires () {
       }
     
       public function ajouterCommentaire (Commentaire $commentaire) {
           $this->_commentaires->attach($commentaire);
       }
     
       public function retirerCommentaire (Commentaire $commentaire) {
           $this->_commentaires->detach($commentaire);
       }
     
       public function sauvegarder () {
          foreach ($this->_commentaire as $commentaire) {
             $commentaire->save();
          }
       }
    }
     
    class Commentaire { 
    //...
    }
    Je te recommande d'utiliser un factory au niveau de tes classes modèles d'une manière générale (utilise une classe abstraite Model pour ça). Ce factory pourra fonctionner de la façon suivante:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
     
    $commentaire  = Commentaire::find($id);
    ça m'agace un peu que la classe Billet aille trifouiller dans la table des commentaires.
    Tout à fait, tu t'appercevra vite que définir les responsabilités d'une classe c'est de loin le plus difficile Eclate au maximum tes entitées, les relations apparaitront d'elles-mêmes.

  3. #3
    Membre du Club
    Homme Profil pro
    Développeur multimédia
    Inscrit en
    Avril 2008
    Messages
    39
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France

    Informations professionnelles :
    Activité : Développeur multimédia
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Avril 2008
    Messages : 39
    Points : 40
    Points
    40
    Par défaut
    Salut Benjamin,

    Alors là, ya quelque chose qui m'échappe complètement : j'ai bien compris que SplObjectStorage allait me permettre de stocker les objets commentaires dans mon objet billet, mais je n'en vois pas du tout l'utilité !

    Car je dois créer un objet commentaire qui doit être passé à $billet->ajouterCommentaire(), hors, pour créer un objet commentaire j'ai besoin de l'ID du billet concerné.

    Ce qui donnerait ceci :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    $billet = Billet(15);
    $commentaire = new Commentaire($billet, 'commentaire'); // Pour récupérer l'ID du billet concerné
    $billet->ajouterCommentaire($commentaire); // Ce qui ne sert plus à rien
    $billet->save();
    Je vois bien l'utilisation d'un tableau d'objets pour $billet->obtenirCommentaires() mais j'ai du mal à saisir l'utilité de ajouterCommentaire() et retirerCommentaire().

    Merci d'avance.

    Franck.

  4. #4
    Expert éminent
    Avatar de Benjamin Delespierre
    Profil pro
    Développeur Web
    Inscrit en
    Février 2010
    Messages
    3 929
    Détails du profil
    Informations personnelles :
    Âge : 36
    Localisation : France, Alpes Maritimes (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Février 2010
    Messages : 3 929
    Points : 7 762
    Points
    7 762
    Par défaut
    Tu m'a dis dans ton premier post que tu modélisait une relation 1-n. Donc dans le cadre des classes Billet et Commentaire, ça s'exprime par "un billet à plusieurs commentaire" et "un commentaire porte sur un et un seul billet".

    Donc la classe Billet doit se munir d'une méthode obtenirCommentaires (ou getComments) et la classe Commentaire peut se munir d'une méthode obtenirBillet (ou getArticle). Effectivement, ça marche dans les deux sens, mais fait attention au nombre d'instances:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    $billet = new Billet(15);
    $commentaire = $billet->getFirstcomment(); // par exemple
    $billet2 = $commentaire->getBillet();
    Dans cet exemple, les objets référencés par $billet et $billet2 sont identiques mais ils sont dupliqués ce qui peut poser des problèmes de collision: si tu fais des opérations sur l'un, l'autre ne sera pas à jour.
    Le pattern IdentityMap peut t'aider à résoudre ce genre de problèmes: martinfowler.com/eaaCatalog/identityMap.html

    Alors là, ya quelque chose qui m'échappe complètement : j'ai bien compris que SplObjectStorage allait me permettre de stocker les objets commentaires dans mon objet billet, mais je n'en vois pas du tout l'utilité !
    C'est pratique pour l'affichage et pour la gestion des commentaires. Personnellement, je trouve plus cohérent de faire $billet->ajouterCommentaire($commentaire);
    que $commentaire = new Commentaire($billet, 'un commentaire');
    Ainsi, la classe commentaire n'a pas besoin de savoir sur quel billet elle porte, c'est la classe Billet qui s'occupe de ses propres commentaires.

    Pour résumer, je penche davantage pour l'approche "un billet porte n commentaire" plutôt que "un commentaire porte sur un billet" car c'est plus simple de gérer des billets que des commentaires.

    Au niveau des vues ça fait sens:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    <h1><?=$billet->title?></h1>
    <p><?=$billet->body?></h1>
    <?php foreach ($billet->obtenirCommentaires as $comment): ?>
    <div><?=$comment->body?>
    <?php endforeach; ?>
    Sinon, comment ferais-tu pour récupérer tes commentaires ?

  5. #5
    Membre du Club
    Homme Profil pro
    Développeur multimédia
    Inscrit en
    Avril 2008
    Messages
    39
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France

    Informations professionnelles :
    Activité : Développeur multimédia
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Avril 2008
    Messages : 39
    Points : 40
    Points
    40
    Par défaut
    Le problème est que pour pouvoir faire $billet->ajouterCommentaire($commentaire), je dois instancier un objet $commentaire auparavant.

    Et donc faire $commentaire = new Commentaire($billet, 'un commentaire')

    Que penses-tu de passer les données au lieu de l'objet à la fonction, comme ceci :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    $billet->ajouterCommentaire('commentaire', 'auteur').

  6. #6
    Membre expert Avatar de RunCodePhp
    Profil pro
    Inscrit en
    Janvier 2010
    Messages
    2 962
    Détails du profil
    Informations personnelles :
    Localisation : Réunion

    Informations forums :
    Inscription : Janvier 2010
    Messages : 2 962
    Points : 3 947
    Points
    3 947
    Par défaut
    Salut

    Si l'ajout d'un commentaire doit avoir un ID de billet et un contenu, alors faut juste renseigner ces 2 données, non ?
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    $billet = Billet(15);
    $commentaire = new Commentaire(array($billet->getID(), 'commentaire'));
    $billet->ajouterCommentaire($commentaire);
    Ou plus directement :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    $billet = Billet(15);
    $billet->ajouterCommentaire(new Commentaire(array($billet->getID(), 'commentaire')));
    Ne faut il pas faire comme ceci par exemple, passer en argument un tableau, et la classe Commentaire se charge de créer coté Bdd le nouveau commentaire (ça à l'air d'être le cas).

    En tout cas, si 1 billet doit avoir une collection d'Objets Commentaire, Billet::ajouterCommentaire() devient obligatoire, car dans ton exemple de code, $commentaire est totalement isolé, ne fait pas partie de $billet.


    Disons que j'interviens aussi pour dire que je viens de découvrir SplObjectStorage grâce à ce topic, je pense que ça va m'être utile.

  7. #7
    Membre du Club
    Homme Profil pro
    Développeur multimédia
    Inscrit en
    Avril 2008
    Messages
    39
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France

    Informations professionnelles :
    Activité : Développeur multimédia
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Avril 2008
    Messages : 39
    Points : 40
    Points
    40
    Par défaut
    J'ai également découvert SplObjectStorage grâce à Benjamin. Je vais y jeter un oeil, enfin, quand j'en aurais finis avec ce topic, grrr.

    Ta solution première solution me semble la plus efficace, et la plus simple RunCodePhp.

    Mais une fois que tu as appelé new Commentaire(array($billet->getID(), 'commentaire')), ton commentaire est lié au billet (dans la base).

    La fonction $billet->ajouterCommentaire($commentaire) me semble donc superflue.

    Il suffirait juste d'une fonction $billet->getCommentaires() et le tour est joué.

    Qu'en dites vous ?

  8. #8
    Expert éminent
    Avatar de Benjamin Delespierre
    Profil pro
    Développeur Web
    Inscrit en
    Février 2010
    Messages
    3 929
    Détails du profil
    Informations personnelles :
    Âge : 36
    Localisation : France, Alpes Maritimes (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Février 2010
    Messages : 3 929
    Points : 7 762
    Points
    7 762
    Par défaut
    Je rebondis sur vos proposition.

    Je pense que Billet::ajouterCommentaire peut avoir les deux signatures; ça se modélise comme ça:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
     
    class Billet extends SplObjectStorage {
    	/**
    	 * @var SplObjectStorage
    	 */
    	protected $_comments;
     
    	/**
    	 * Ajouter un commentaire.
    	 * 
    	 * Cette méthode supporte deux signatures:
    	 *    Billet::ajouterCommentaire(Commentaire $comment);
    	 *    Billet::ajouterCommentaire('title','body');
    	 * 
    	 * @param Commentaire $comment
    	 * @return Commentaire
             */
    	public function ajouterCommentaire () {
    		$argv = func_get_args();
    		$argc = func_num_args();
    		if ($argc === 1 && is_a($argv[0], 'Commentaire')) {
    			return $this->_comments->attach($argv[0]);
    		}
    		elseif ($argc === 2) {
    			return $this->_comments->attach(new Commentaire($argv[0],$argv[1]);
    		}
    		else {
    			throw new BadMethodCallException(__METHOD__ . " attends 1 ou 2 paramètres, $argc fournis");
    		}
    	}
    }
    Comme ça tu peux faire indifférement:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    $billet = new Billet(15);
    $billet->ajouterCommentaire('titre', 'foobar');
    // Equivaut à 
    $commentaire = new Commentaire('titre','foobar');
    $billet->ajouterCommentaire($commentaire);
    Dans les deux cas, ton commentaire t'es retourné par Billet::ajouterCommentaire.

  9. #9
    Expert éminent
    Avatar de Benjamin Delespierre
    Profil pro
    Développeur Web
    Inscrit en
    Février 2010
    Messages
    3 929
    Détails du profil
    Informations personnelles :
    Âge : 36
    Localisation : France, Alpes Maritimes (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Février 2010
    Messages : 3 929
    Points : 7 762
    Points
    7 762
    Par défaut
    Appendum: SplObjectStorage est une super classe pour gérer des objets mais ce n'est pas une hashmap !! Si votre but est de retrouver vos petits par un système clé valeur, l'idéal reste le bon vieux tableau.

    Vous remarquerez par ailleurs que SplObjectStorage est un itérateur, ça va bien nous servir pour créer des fonctions de filtrage par exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     
    class GenericFilterIterator extends FilterIterator {
    	/**
    	 * @var array
    	 */
    	protected $_callbacks;
     
    	public function accept () {
    		$current = $this->getInnerIterator()->current();
    		foreach ($this->_callbacks as $callback) {
    			if (!$callback($current)) return false;
    		}
    		return true;
    	}
     
    	public function attach ($closure) {
    		if (is_callable($closure)) {
    			$this->_callbacks[] = $closure;
    			return true;
    		}
    		return false;
    	}
    }
    et on va l'utiliser comme ça (PHP 5.3 only mais adaptable à des version antérieures):
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
     
    class FooBar {
    	public $_name;
    	public $_value;
    	public function __construct ($name, $value) {
    		$this->_name = $name;
    		$this->_value = $value;
    	}
    	public function __toString () { return "{$this->_name} {$this->_value}"; }
    }
     
    $object_storage = new SplObjectStorage();
    $object_storage->attach(new FooBar('hello','there'));
    $object_storage->attach(new FooBar('hello','roger'));
    $object_storage->attach(new FooBar('degage','roger'));
     
    $filter = new GenericFilterIterator($object_storage);
    $filter->attach(function ($object) {
    	return $object->_name == 'hello';
    });
     
    foreach ($filter as $object) {
    	echo $object . '<br />';
    }
    Renseignez vous sur http://www.php.net/~helly/php/ext/spl/
    il existe tout un tas d'itérateur pour tous les usages, filtres, limites, XML et j'en passe...

    Pour pouvoir utiliser cette merveille de technologie que nous envie la Nasa, il faut que votre objet Traversable implémente l'interface Iterator.
    Dans le cas d'un tableau associatif, vous pouvez passer par un ArrayIterator ou un ArrayObject comme ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    $arr = array(1,2,3);
    $iterator = new ArrayIterator($arr);
    $iiterator = new LimitIterator($iterator, 0,1);
    foreach ($iiterator as $value) { echo $value; }

  10. #10
    Membre expert Avatar de RunCodePhp
    Profil pro
    Inscrit en
    Janvier 2010
    Messages
    2 962
    Détails du profil
    Informations personnelles :
    Localisation : Réunion

    Informations forums :
    Inscription : Janvier 2010
    Messages : 2 962
    Points : 3 947
    Points
    3 947
    Par défaut
    Mais une fois que tu as appelé new Commentaire(array($billet->getID(), 'commentaire')), ton commentaire est lié au billet (dans la base).

    La fonction $billet->ajouterCommentaire($commentaire) me semble donc superflue.

    Il suffirait juste d'une fonction $billet->getCommentaires() et le tour est joué.
    Si j'ai bien compris, Billet::ajouterCommentaire() est loin d'être superflue, comme je l'avais dit, elle est obligatoire, car c'est toi qui a défini que tout Objet Billet DOIT contenir 1 ou plusieurs commentaire.

    Quand on appel : new Commentaire(...), le commentaire et certes créé coté Bdd, mais ne fait absolument pas partie de l'Objet $billet en court.
    Du coup, si tu appel Billet::getCommentaires(), ça va rien renvoyer du tout vu qu'aucun Objet Commentaire à été ajouté, je dis bien Objet.

    C'est peut être ça qui te turlupine : Billet::getCommentaires() renvoie uniquement des Objets Commentaires, donc coté traitements n'effectue pas de requêtes SQL pour récupérer les commentaires.
    Disons que ce serait dans ce cas présent, après l'ajout d'1 commentaire, des requêtes inutiles (superflux).


    Disons qu'il faudrait peut être 2 méthodes coté Billet : Une qui récupère les Commentaires en Bdd qui les rajouterait au Billet, une autre renvoie les Objets Commentaires (getCommentaires()).
    Faut voir ...
    Benjamin a peut être un avis ... plus avisé.

  11. #11
    Membre du Club
    Homme Profil pro
    Développeur multimédia
    Inscrit en
    Avril 2008
    Messages
    39
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France

    Informations professionnelles :
    Activité : Développeur multimédia
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Avril 2008
    Messages : 39
    Points : 40
    Points
    40
    Par défaut
    Merci Benjamin,

    Dernière chose, si j'utilise le premier appel :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    $billet = new Billet(15);
    $billet->ajouterCommentaire('titre', 'foobar');
    De quelle manière ma fontion ajouterCommentaire() devrait elle faire l'ajout dans la base ?

    Directement en SQL :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    public function ajouterCommentaire($comment, $auteur)
    {
      $query = "INSERT INTO commentaires VALUES...";
    }
    Avec l'instanciation d'un objet Commentaire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    public function ajouterCommentaire($comment, $auteur)
    {
      $commentaire = new Commentaire();
      $commentaire->add($this->id, $comment, $auteur);
    }
    Ou via une fonction statique :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    public function ajouterCommentaire($comment, $auteur)
    {
      Commentaire::add($this->id, $comment, $auteur);
    }

  12. #12
    Membre du Club
    Homme Profil pro
    Développeur multimédia
    Inscrit en
    Avril 2008
    Messages
    39
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France

    Informations professionnelles :
    Activité : Développeur multimédia
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Avril 2008
    Messages : 39
    Points : 40
    Points
    40
    Par défaut
    Citation Envoyé par RunCodePhp Voir le message
    C'est peut être ça qui te turlupine : Billet::getCommentaires() renvoie uniquement des Objets Commentaires, donc coté traitements n'effectue pas de requêtes SQL pour récupérer les commentaires.
    Oui, c'est ça qui me turlupinait, ça commence à s'éclaircir !

    Merci.

  13. #13
    Expert éminent
    Avatar de Benjamin Delespierre
    Profil pro
    Développeur Web
    Inscrit en
    Février 2010
    Messages
    3 929
    Détails du profil
    Informations personnelles :
    Âge : 36
    Localisation : France, Alpes Maritimes (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Février 2010
    Messages : 3 929
    Points : 7 762
    Points
    7 762
    Par défaut
    De quelle manière ma fontion ajouterCommentaire() devrait elle faire l'ajout dans la base ?
    Billet::ajouterCommentaire n'a pas à savoir comment insérer un commentaire dans la base de données. C'est le rôle de la classe Commentaire de fournir une méthode pour l'insertion (Commentaire::creer(xxx,yyy) par exemple).

    La méthode Billet::ajouterCommentaire doit donc soit
    - appeller Commentaire::creer et récupérer son retour (une instance de Commentaire) pour l'insérer dans sa liste de commentaire (Billet::$_commentaires)
    - laisser le contrôleur de commentaire insérer en DB (s'il en est capable), là encore, tu peux faire un constructeur avec plusieurs signature comme je l'ai montré plus haut (une signature pour la création et une signature pour la réccupération de données).

    Histoire de garder ça propre et clair, je te recommande de ne pas abuser des signatures multiples: PHP n'étant pas fait pour à la base, ça devient complexe pour manipuler classes et objets...

    Voici ce que je te recommande pour la classe Commentaire (on part de l'hypothese que tu dispose d'un singleton Database pour PDO, si tu ne sais pas les faire, je t'en fournirai un, c'est très pratique à utiliser):
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
     
    abstract class Model {
     
    	protected $_data;
    	protected static $_statements;
    	protected static $_initialized;
     
    	protected function __construct ($data) {
    	    $this->_data = $data;
    	}
     
    	public function __get ($key) {
    		if (isset($this->_data[$key])) {
    			return $this->_data[$key];
    		}
    		else {
    			throw new OutOfRangeException("Cannot find $key in model's data");
    		}
    	}
     
    	public function __set ($key, $value) {
    		if ($key == 'id') {
    			throw new LogicException('Cannot change id in Model instances');
    		}
     
    		if (isset($this->_data[$key])) {
    			$this->_data[$key] = (string)$value;
    			$this->update();
    		}
    		else {
    			throw new OutOfRangeException("Cannot find $key in model's data");
    		}
    	}
     
    	protected static function _init () {
    	    if (func_num_args() == 0) throw BadMethodCallException(__METHOD__ . ' expects at least 1 parameter');
    	    $statements = func_get_arg(0);
     
    		if (isset($statements['create'], $statements['retrieve'], $statements['update'], $statements['delete'])) {
    			static::$_statements['create'] = Database::prepare($statements['create']);
    			static::$_statements['retrieve'] = Database::prepare($statements['retrieve']);
    			static::$_statements['update'] = Database::prepare($statements['update']);
    			static::$_statements['delete'] = Database::prepare($statements['delete']);
    			return static::$_initialized = true;
    		}
    		else
    			throw new InvalidArgumentException("Statements are missing");
    	}
     
    	public static function find ($id) {
    		if (!static::$_initialized && !static::_init()) return false;
     
    		if (static::$_statements['retrieve']->execute(array(':id' => $id))) {
    		    return new static (static::$_statements['retrieve']->fetch(PDO::FETCH_ASSOC));
    		}
    		else
    		    return false;
    	}
     
    	public static function create ($values) {
    		if (!static::$_initialized && !static::_init()) return false;
     
    		if (static::$_statements['create']->execute($values)) {
    			$id = Database::lastInsertId();
    			return static::find($id);
    		}
    		else {
    			return false;
    		}
    	}
     
    	public function delete () {
    		return static::$_statements['delete']->execute(array(':id' => $this->_data['id']));
    	}
     
    	protected function update () {
    		return static::$_statements['update']->execute($this->_data);
    	}
    }
    Avec le commentaires du développeur (moi tout seul )
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
     
    <?php
     
    /**
     *
     * Cette class est abstraite car elle ne permet
     * pas d'elle même la création d'une instance de modèle.
     * On verra plus loin de quoi elle a besoin pour fonctionner
     * correctement.
     *
     * Dans cette classe, on part du principe que les tables en
     * base de données qui porteront les valeur possède une clé
     * primaire nommée 'id'. C'est nécéssaire pour se simplifier
     * la vie des deux cotés et c'est une recommandation de ma
     * part.
     *
     */
    abstract class Model {
     
        /**
         * On va stocker dans cette variable un tableau
         * qui caractérise les données de notre objet.
         * En fait, ce seront les données renvoyées par le
         * 'select'.
         */
    	protected $_data;
     
    	/**
    	 * On va conserver les PDOStatement qui nous serviront
    	 * à effectuer les opérations CRUD (Create, Retrieve,
    	 * Update, Delete) qui nous permettront de manager
    	 * conjointement les données de la l'objet et leur
    	 * valeurs en base de données.
    	 * Cette propriété doit rester statique car elle
    	 * est dépendante d'une classe modèle donnée et non
    	 * pas d'un objet, il est donc innutile de délcarer
    	 * 4 PDOStatement par instance car chaque instance à
    	 * le même job.
    	 * Les données renvoyées par ces statements sont en
    	 * revanche dépendantes de l'instance en cours donc
    	 * elles sont dans la propriété d'instance _data.
    	 */
    	protected static $_statements;
     
    	/**
    	 * On conserve ici un flag qui nous indique si les
    	 * propriété statique (de classe si tu préfères) ont
    	 * été correctement initialisées.
    	 */
    	protected static $_initialized;
     
    	/**
    	 * On choisit de garder le constructeur protéger et
    	 * de ne donner un accès aux instances qu'au travers
    	 * des méthodes 'find' et 'create' (voir plus bas).
    	 * ça signifie entre autre que faire un new sur une
    	 * classe dérivée de modèle échoura systématiquement.
    	 *
    	 * Ce constructeur par défaut mets à jour
    	 * les données de l'objet (_data) avec le tableau
    	 * qu'il reçoit en paramètre.
    	 */
    	protected function __construct ($data) {
    	    $this->_data = $data;
    	}
     
    	/**
    	 * On définit ici une méthode magique pour accéder
    	 * directement au contenu de _data.
    	 * ça permet de faire
    	 * $instance_modele->nom_de_champ
    	 */
    	public function __get ($key) {
    		if (isset($this->_data[$key])) {
    			return $this->_data[$key];
    		}
    		else {
    			throw new OutOfRangeException("Cannot find $key in model's data");
    		}
    	}
     
    	/**
    	 * On définit ici une méthode magique
    	 * pour éditer le contenu de _data.
    	 * Ainsi, à chaque edition de l'objet
    	 * de travail, les champs correspondant
    	 * en database dont mis à jour instantanment.
    	 * On appelle cela 'ActiveRecord' ou les objets
    	 * et leurs données en DB sont mis à jour ensemble.
    	 */
    	public function __set ($key, $value) {
     
    	    /**
    	     * on interdit formellement de changer
    	     * l'id utilisé dans la base de données !!!
    	     * Sinon ça pourrait découpler l'objet et
    	     * sa représentation mysql ce qui violerait
    	     * le design pattern active record et créerait
    	     * des doublons partout.
    	     */
    		if ($key == 'id') {
    			throw new LogicException('Cannot change id in Model instances');
    		}
     
    		/**
    		 * Si la clé existe
    		 */
    		if (isset($this->_data[$key])) {
    		    /**
    		     * on la modifie
    		     */
    			$this->_data[$key] = (string)$value;
     
    			/**
    			 * et on met à jour dans la base de données
    			 */
    			$this->update();
    		}
    		else {
    			throw new OutOfRangeException("Cannot find $key in model's data");
    		}
    	}
     
    	/**
    	 * Dans cette méthode, on va initialiser les
    	 * PDOStatement nécéssaire à toutes les operations
    	 * CRUD. Elle reste protégé pour que les classes
    	 * filles puissent surcharger son comportement
    	 * (en fait elles DOIVENT le surcharger) et que
    	 * de l'exterieur de la classe on ne puiss pas
    	 * l'appeller.
    	 * Note: cette méthode devrait porter un paramètre
    	 * $statements mais elle n'en porte aucun.
    	 * On procède de cette manière pour que les classes
    	 * filles puisent porter une signature sans paramètres
    	 * qui sera appellée par les méthodes statiques
    	 * find et create qui ne doivent pas lui passer
    	 * de paramètres.
    	 * On utilisera à cet effet le late static binding
    	 * dans les méthodes find et create qui permettront
    	 * d'appeller _init dans la fille qui se chargera
    	 * de définir les statement puis les passera à
    	 * _ini de la mère (Model).
    	 */
    	protected static function _init () {
    	    if (func_num_args() == 0) throw BadMethodCallException(__METHOD__ . ' expects at least 1 parameter');
     
    	    /**
    	     * les statements sont dans le premier paramètre
    	     */
    	    $statements = func_get_arg(0);
     
    	    /**
    	     * On verifie qu'on a tout ce qu'il nous faut
    	     */
    		if (isset($statements['create'], $statements['retrieve'], $statements['update'], $statements['delete'])) {
     
    		    /**
    		     * on créé tous les statements CRUD
    		     */
    			static::$_statements['create'] = Database::prepare($statements['create']);
    			static::$_statements['retrieve'] = Database::prepare($statements['retrieve']);
    			static::$_statements['update'] = Database::prepare($statements['update']);
    			static::$_statements['delete'] = Database::prepare($statements['delete']);
    			return static::$_initialized = true;
    		}
    		else
    			throw new InvalidArgumentException("Statements are missing");
    	}
     
    	/**
    	 * Cette méthode est, avec create, le seul moyen
    	 * d'instancier des objets Model.
    	 */
    	public static function find ($id) {
    	    /**
    	     * Si la classe (attention, pas l'instance! )
    	     * n'est pas initialisée, on appelle _init dans
    	     * le contexte de la fille de Model.
    	     * On utilise pour ça le mot clé static.
    	     * CF late static binding.
    	     */
    		if (!static::$_initialized && !static::_init()) return false;
     
    		/**
    		 * on execute le statement de récupération des données
    		 */
    		if (static::$_statements['retrieve']->execute(array(':id' => $id))) {
     
    		    /**
    		     * on vérifie qu'on trouve bien quelque chose
    		     */
    		    if (static::$_statements['retrieve']->rowCount()) {
     
        		    /**
        		     * et on appelle le constructeur DE LA FILLE toujours avec
        		     * le mot clé static en lui passant le retour de la requête SQL.
        		     * voir le constructeur.
        		     * Et on renvoie l'instance ainsi crée.
        		     */
        		    return new static (static::$_statements['retrieve']->fetch(PDO::FETCH_ASSOC));
    		    }
    		}
     
    	    /**
    	     * on s'est gaufré, il n'y a pas d'objet
    	     */
    	    return false;
    	}
     
    	/**
    	 * Create est avec find la seule méthode permettant de 
    	 * réccupérer des instances, elle attends en paramètres
    	 * un tableau associatif clé valeur dont les clés 
    	 * correspondent aux éléments présents dans la requête 
    	 * préparée.
    	 */
    	public static function create ($values) {
    	    /**
    	     * Toujours pareil, on tente d'initialiser si ce n'est pas
    	     * défjà fait
    	     */
    		if (!static::$_initialized && !static::_init()) return false;
     
    		/**
    		 * On tente l'insertion
    		 */
    		if (static::$_statements['create']->execute($values)) {
     
    		    /**
    		     * Si c'est ok, on réccupère l'id inséré
    		     */
    			$id = Database::lastInsertId();
     
    			/**
    			 * et on renvoie l'objet en passant par find
    			 * (pattern DRY: Don't Repeat Yourself)
    			 */
    			return static::find($id);
    		}
    		else {
    			return false;
    		}
    	}
     
    	/**
    	 * Contrairement à find et create qui 
    	 * sont des méthodes de classe, delete
    	 * est une méthode d'instance car elle
    	 * a besoin que l'objet existe pour 
    	 * le supprimer (logique).
    	 * On ne fait donc qu'appelle le 
    	 * statement de suppression avec l'id 
    	 * présent dans les données de l'objet
    	 */
    	public function delete () {
    		return static::$_statements['delete']->execute(array(':id' => $this->_data['id']));
    	}
     
    	/**
    	 * Contrairement à find et create,
    	 * update est une méthode d'instance
    	 * car elle a besoin des données de l'objet
    	 * pour le mettre à jour en base (logique).
    	 * Comme tu le vois dans __set, cette
    	 * méthode est appellée à chaque modification
    	 * de ton objet.
    	 * On ne fais donc qu'appeller le statement
    	 * d'update pour mettre les donénes de
    	 * l'objet à jour dans la db.
    	 */
    	protected function update () {
    		return static::$_statements['update']->execute($this->_data);
    	}
    }
    Maintenant, on va pouvoir créer notre classe:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     
    class TestModel extends Model {
     
        public static function _init () {
            $statements = array(
                'create' => 'INSERT INTO `test` (`name`,`value`) VALUES (:name, :value)',
                'retrieve' => 'SELECT `id`,`name`,`value` FROM `test` WHERE `id`=:id',
                'update' => 'UPDATE `test` SET `name`=:name, `value`=:value WHERE `id`=:id',
                'delete' => 'DELETE FROM `test` WHERE `id`=:id',
            );
            return parent::_init($statements);
        }
     
        public static function create ($name, $value) {
            return parent::create(array(':name' => $name, ':value' => $value));
        }
    }
    Et on peut l'utiliser comme ceci (dans un de mes contrôleurs, pense à ajouter de la sécurité):
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
     
    class SummaryController extends BaseController {
     
         public static function index () {
     
         }
     
         public static function create () {
             $test = TestModel::create('time', time());
             return compact('test');
         }
     
         public static function retrieve () {
             $test = TestModel::find(self::$_request->id);
             return compact('test');
         }
     
         public static function delete () {
             $founds = self::retrieve();
             $test = $founds['test'];
             $result = $test->delete();
             return compact('test', 'result');
         }
     
         public static function update () {
             $test = TestModel::find(self::$_request->id);
             $test->value = self::$_request->value;
             return compact('test', 'result');
         }
    }
    Ensuite dans une vue, on peut faire:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    <h1>Object created: <?=$test->id?></h1>
    <span>Name: <?=$test->name?></span><br />
    <span>Value: <?=$test->value?></span>
    C'est In Ze Pocket si j'ose dire

  14. #14
    Expert éminent
    Avatar de Michel Rotta
    Homme Profil pro
    DPO
    Inscrit en
    Septembre 2005
    Messages
    4 954
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 61
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : DPO
    Secteur : Distribution

    Informations forums :
    Inscription : Septembre 2005
    Messages : 4 954
    Points : 8 486
    Points
    8 486
    Par défaut
    Si je peux me permettre d'intervenir, un peu tard dans votre conversation.

    Structurellement ce n'est pas à l'objet billet d'ajouter un commentaire, mais à l'objet commentaire de lier un billet. Donc c'est dans l'objet commentaire qu'il faut prévoir une méthode setBillet() avec deux paramètres possibles :

    • soit l'id du billet a lier qui sera alors stockée dans le champ correspondant du commentaire en mémoire, et, a terme, sauvegardé
    • soit l'objet billet en lui même duquel il faudra récupérer l'id et faire le traitement ci-dessus.

    Il convient aussi de prévoir un objet permettant de gérer une collection de commentaire. On pourra alors prévoir une méthode pour ton objet billet qui va recevoir en paramètre la collection de commentaires à ajouter au billet. Il suffira, pour chaque commentaire, d'utiliser la méthode setBillet() avec l'objet billet concerné pour faire l'ajout.


    Accessoirement, ce que vous travaillez ici et ce type d'accès est déjà utilisé dans de nombreux ORM, il serait peut-être intéressant d'y jeter un œil avant de réinventer la roue.

    Je me retire sur la pointe des pieds.

  15. #15
    Expert éminent
    Avatar de Benjamin Delespierre
    Profil pro
    Développeur Web
    Inscrit en
    Février 2010
    Messages
    3 929
    Détails du profil
    Informations personnelles :
    Âge : 36
    Localisation : France, Alpes Maritimes (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Février 2010
    Messages : 3 929
    Points : 7 762
    Points
    7 762
    Par défaut
    Pas envie de prendre un ORM, j'aurais l'impression d'annihiler des fourmis à la bombe atomique.
    Cela étant, je trouve que Michel Rotta à raison dans un sens: c'est effectivement la table "commentaire" qui porte l'information "id_billet". Dons si on s'en tiens à une stricte modélisation objet, c'est effectivement au commentaire de connaitre son billet...
    Cependant, je maintiens qu'il est plus pratique d'avoir une classe Billet capable de retrouver ses commentaires (et de les stocker dans sa map au passage), quite à appeller une méthode statique de Commentaire pour ramasser des billet.

    Est ce que ça serait convenable :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    class Commentaire extends Model {
    //....
    const BILLET_ID = 1;
    const COMMENTAIRE_ID = 2;
    public static function find ($id, $type = self::COMMENTAIRE_ID) {
    //...
    }
    ça devrait permettre de trouver les commentaires soit par leurs ID soit par le billet dont ils dépendent.

    -- EDIT: Edwin on the rock!

  16. #16
    Expert éminent
    Avatar de Michel Rotta
    Homme Profil pro
    DPO
    Inscrit en
    Septembre 2005
    Messages
    4 954
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 61
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : DPO
    Secteur : Distribution

    Informations forums :
    Inscription : Septembre 2005
    Messages : 4 954
    Points : 8 486
    Points
    8 486
    Par défaut
    Ce n'est que ce que j'ai décris plus haut.

    Ton billet peut récupérer par une méthode un objet collection d'objets commentaire (il n'y a pas d'erreur sur les s).

    Par contre, si tu crées un nouveau commentaire, il convient d'instantier un nouvel objet commentaire, de le compléter y compris avec l'objet billet associé et de le sauvegarder. C'est la seul bonne méthode pour garder une indépendance entre des entités dans ton modèle. Ici tu n'en as que 2, tu peux peut-être jongler un peu, imagine avec une structure plus habituel et entre 50 et 100 tables...


    Pour ce qui est de tuer des fourmis avec des bombes atomique, j'ai entendu dire qu'elles étaient résistantes aux radiations...

    J'en reviens à l'ORM, jette un œil attentif du côté de Doctrine 2, il a une approche très similaire à la tienne et pourrait te faciliter grandement la vie. Même pour deux tables. Justement en ne réinventant pas la roue.

    Reste a savoir si ton projet est pour apprendre à manipuler les données (et alors ton approche est la bonne) ou a faire un site en exploitation (et alors l'ORM est une option à prendre sérieusement en considération).

    A partir d'ici la disgression sur les ORM, Framework et patterne continue ici.

Discussions similaires

  1. Réponses: 25
    Dernier message: 16/02/2011, 17h40
  2. Réponses: 5
    Dernier message: 08/04/2009, 17h39
  3. Réponses: 2
    Dernier message: 01/04/2009, 16h44
  4. Modélisation d'une relation
    Par loganblack dans le forum Schéma
    Réponses: 4
    Dernier message: 10/07/2007, 11h26
  5. [ADO.NET]Comment réaliser une relation sur plusieurs champs?
    Par kleomas dans le forum Accès aux données
    Réponses: 3
    Dernier message: 13/03/2006, 12h40

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo