Premières classes en POO...

Ancien utilisateur
Invité n'ayant pas de compte PHPfrance

10 mars 2009, 18:18

Bonjour à tous,

La POO fait un tel "buzz" en ce moment que j'ai décidé de m'y mettre. Le soucis c'est que j'aimerais bien savoir si ce que j'ai déjà fait est (vraiment) correct.
C'est un (banal) système de panier, composé de 3 classes. Une construite en Singleton pour gérer les sessions, une autre qui gère le "catalogue" et enfin la dernière pour gérer le panier.

Si vous pouviez me dire ce que vous en pensez (et surtout comment corriger et améliorer) ça serait très gentil.

La classe Sessions (Singleton)

Code : Tout sélectionner

class Sessions { private static $instance = null; private $cart; private function __construct() { session_start(); } public static function getInstance() { if (is_null(self::$instance)) self::$instance = new Sessions(); else return self::$instance; } public function initCart() { if (!isset($_SESSION['cart'])) return $this->cart = array(); else return $this->cart = unserialize($_SESSION['cart']); } public function setCart($items) { $_SESSION['cart'] = serialize($items); } public function __clone() { } public function __destruct() { } }
La classe Store (catalogue)

Code : Tout sélectionner

class Store { protected $db; private $product; public function __construct() { if (is_readable(PRODUCTS_DB)) $this->db = new SimpleXMLElement(file_get_contents(PRODUCTS_DB)); else return exit('Products xml database missing or unreadable.'); } private function productSelect($id) { return $this->db->Xpath('/products/product[id="'.$id.'"]'); } protected function productExist($id) { if (count($this->productSelect($id)) > 0) return true; else return false; } public function productPrice($id) { if ($this->productExist($id)) $this->product = $this->productSelect($id); return $this->product[0]->price; } public function productName($id) { if ($this->productExist($id)) $this->product = $this->productSelect($id); return $this->product[0]->name; } public function viewStore() { return $this->db->product; } }
Et la classe Cart (panier)

Code : Tout sélectionner

class Cart extends Store { protected $items; private $total; public function __construct() { parent::__construct(); $this->items = Sessions::getInstance(); $this->items = Sessions::initCart(); } public function addItem($id) { if (array_key_exists($id, $this->items)) $this->items[$id]++; else $this->items[$id] = 1; } public function delItem($id) { if (array_key_exists($id, $this->items)) if ($this->items[$id] > 1) $this->items[$id]--; else unset($this->items[$id]); } public function emptyCart() { $this->items = array(); } public function nbItems() { return array_sum($this->items); } public function viewCart() { return $this->items; } public function totalCart() { $this->total = 0; foreach ($this->items as $id => $qty) { $this->total += parent::productPrice($id) * $qty; } return $this->total; } public function __destruct() { Sessions::getInstance()->setCart($this->items); } }
Petite précision : au début la classe Sessions n'existait pas, je les gérais dans le constructeur mais on m'a dit que c'était plutôt crade...

Voilà, j'attends avec impatience vos commentaires :)

Merci !

ViPHP
ViPHP | 5924 Messages

10 mars 2009, 23:01

La POO fait un tel "buzz" en ce moment que j'ai décidé de m'y mettre.
Depuis 15 ou 20 ans ouais. :)

La classe Store :
- PRODUCTS_DB : Evite les constantes globales à l'intérieur des classes, passe plutôt cela dans le constructeur…
- productPrice/productName : C'est déconcertant de se voir rendre un résultat valide si l'id n'existe pas…
Petite précision : au début la classe Sessions n'existait pas, je les gérais dans le constructeur mais on m'a dit que c'était plutôt crade...
Oui, en effet.

Ancien utilisateur
Invité n'ayant pas de compte PHPfrance

10 mars 2009, 23:19

Merci pour ta réponse :)
Depuis 15 ou 20 ans ouais. Smile
J'aurais du préciser en PHP5 :)
PRODUCTS_DB : Evite les constantes globales à l'intérieur des classes, passe plutôt cela dans le constructeur…
Ok, donc quand tu dis passer cela dans le constructeur c'est en paramètre ? Ce qui voudrai dire qu'il faut renseigner le nom du fichier à chaque instanciation ?

Code : Tout sélectionner

public function __construc($product_db) { if (is_readable($product_db)) $this->db = new SimpleXMLElement(file_get_contents($product_db)); else return exit('Products xml database missing or unreadable.'); }
Et sinon pour les fonctions productPrice et productName, comment faire pour éviter de mettre un echo 'Message erreur.' parceque ça ne doit pas être top comme gestion des erreurs non ?

Et concernant le Singleton c'est bien comme ça que tu l'aurai fait ? Dédié à la gestion des sessions ou le mieu aurait été qu'il serve juste pour le session_start() ?

Que de questions... c'est frustrant d'avoir l'impression de repartir de zero ou presque en tout cas merci pour ton aide, c'est cool :wink:

ViPHP
ViPHP | 5924 Messages

10 mars 2009, 23:31

PRODUCTS_DB : Evite les constantes globales à l'intérieur des classes, passe plutôt cela dans le constructeur…
Ok, donc quand tu dis passer cela dans le constructeur c'est en paramètre ? Ce qui voudrai dire qu'il faut renseigner le nom du fichier à chaque instanciation ?

Code : Tout sélectionner

public function __construc($product_db) { if (is_readable($product_db)) $this->db = new SimpleXMLElement(file_get_contents($product_db)); else return exit('Products xml database missing or unreadable.'); }
C'est une opinion, mais je pense que c'est plus propre, puisque le but de la POO c'est tout de même d'être réutilisable…
Après j'ai l'impression qu'avec le fonctionnement que tu as prévu, tu vas ouvrir le même fichier plusieurs fois en même temps, et ça c'est ingérable.
Et sinon pour les fonctions productPrice et productName, comment faire pour éviter de mettre un echo 'Message erreur.' parceque ça ne doit pas être top comme gestion des erreurs non ?
Personnellement j'aurais retourné NULL, tout simplement…
On peut imaginer aussi envoyer une exception mais c'est un peu plus lourd.
Et concernant le Singleton c'est bien comme ça que tu l'aurai fait ? Dédié à la gestion des sessions ou le mieu aurait été qu'il serve juste pour le session_start() ?
Non, mais cela ne veut pas dire que ce n'est pas comme cela qu'il faut faire :)
Que de questions... c'est frustrant d'avoir l'impression de repartir de zero ou presque en tout cas merci pour ton aide, c'est cool :wink:
En même temps on ne peut pas dire que je t'ai descendu hein :)

Ancien utilisateur
Invité n'ayant pas de compte PHPfrance

11 mars 2009, 00:15

C'est une opinion, mais je pense que c'est plus propre, puisque le but de la POO c'est tout de même d'être réutilisable…
Oui c'est vrai, je vais faire comme ça.
Après j'ai l'impression qu'avec le fonctionnement que tu as prévu, tu vas ouvrir le même fichier plusieurs fois en même temps, et ça c'est ingérable.
Oui mince... donc la aussi faut le faire en Singleton on dirait...

Le plus sémantiquement correct serait de faire un seul Singleton pour gérer les initialisations de sessions et les ouvertures de fichier ou alors construire la class Cart ainsi que la classe Store en Singleton ?

Personnellement j'aurais retourné NULL, tout simplement…
On peut imaginer aussi envoyer une exception mais c'est un peu plus lourd.
Ah ben après recherche j'étais justement parti sur les exceptions mais c'est vrai... pourquoi faire compliquer quand on peut faire simple :)
Non, mais cela ne veut pas dire que ce n'est pas comme cela qu'il faut faire
Alors on rejoint peut être le paragraphe du dessus ? Comment l'aurais-tu fait ?

En même temps on ne peut pas dire que je t'ai descendu hein Smile
Ca c'est bien vrai et je t'en suis vraiment reconnaissant :)

Punaise j'ai hâte de maitriser... :roll:

ViPHP
ViPHP | 5924 Messages

11 mars 2009, 02:58

Après j'ai l'impression qu'avec le fonctionnement que tu as prévu, tu vas ouvrir le même fichier plusieurs fois en même temps, et ça c'est ingérable.
Oui mince... donc la aussi faut le faire en Singleton on dirait...

Le plus sémantiquement correct serait de faire un seul Singleton pour gérer les initialisations de sessions et les ouvertures de fichier ou alors construire la class Cart ainsi que la classe Store en Singleton ?
Bah Là pour le coup, quand je disais ça, j'ai bien compris que chaque classe ne serait utilisée qu'une fois, ce n'est pas ça le problème. Le problème, c'est surtout que si tu crées plusieurs classes du même style que Cart, qui seront héritées de Store, tu auras au final plusieurs classes qui accèderont à la même ressource. Il faut donc que tu clarifies ce point. Quel doit être le comportement du système, et en l'occurrence qu'est ce que tu proposes pour empêcher un accès concourant.
A noter que même avec un Singleton sur Store et Cart, tu as le problème, puisqu'avec une instance de Store et une instance de Cart, tu as deux instances qui manipulent la même ressource…
Personnellement j'aurais retourné NULL, tout simplement…
On peut imaginer aussi envoyer une exception mais c'est un peu plus lourd.
Ah ben après recherche j'étais justement parti sur les exceptions mais c'est vrai... pourquoi faire compliquer quand on peut faire simple :)
Bah les exceptions c'est plus propre et plus solide, mais ultra lourd à mettre en place. Et ça doit être mis en place sur l'ensemble d'une application pour que cela vaille le coup. Bref, c'est un choix…
Non, mais cela ne veut pas dire que ce n'est pas comme cela qu'il faut faire
Alors on rejoint peut être le paragraphe du dessus ? Comment l'aurais-tu fait ?
Euh, je ne sais pas trop, je l'ai déjà fait mais je ne me souviens plus comment.
Si je dis cela, c'ets juste que je n'aime pas trop les singleton. Mais comme je le disais, cela ne veut pas dire que ce que tu as fait est faux, c'est juste question de choix.

Quand je parle des histoires de choix de ces deux derniers points, je ne donne aucun détail, c'est voulu. Si je donne des détails, tu pourrais croire que ce que tu as fait est mauvais, et tout changer. :)

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

11 mars 2009, 08:37

Je rebondis juste sur les questions de singleton :
- Une session peut être un singleton puisqu'elle interroge une ressource unique dans l'environnement d'exécution. Donc pas tellement le choix finalement ;)
- Par contre pour Store ou Cart je ne suis pas convaincu qu'un singleton soit une bonne idée, vu qu'en faisant ça tu te fermerais la porte à la possibilité de gérer dans le même processus plusieurs boutiques et/ou plusieurs paniers par boutique (par exemple un panier "en cours" et un panier "pour plus tard", fonctionnalité que je n'ai jamais vu dans aucune boutique mais pourtant je trouverais parfois ça intéressant d'avoir plusieurs paniers, tout comme plusieurs presse-papiers finalement).

Enfin pour la classe Session, je ne suis pas sûr que son rôle soit bien défini : pourquoi une méthode "initCart" dans la session ? Une session c'est un truc générique, elle n'a pas à connaître l'existence du panier. De plus en faisant ça dans la classe Session tu fermes encore une fois la porte au multi-panier alors même qu'il n'y a aucune raison particulière de le faire.

Personnellement je vois la Session comme une classe générique d'accès à des données :
/**
 * Bonus : gestion des "paths"
 * Exemple :
 * $s = Session::getInstance();
 * $s->set('tableau', array('index' => 'valeur'));
 * $s->get('tableau'); // array('index' => 'valeur')
 * $s->get('tableau/index'); // 'valeur'
 * $s->set('tableau/index2', 'valeur2');
 * $s->get('tableau'); // array('index' => 'valeur', 'index2' => 'valeur2')
 */
class Session
{

  protected static $instance = null;

  protected $data = null;

  protected function __construct()
  {
    $this->data = &$_SESSION;
  }

  public static function getInstance()
  {
    if (is_null(self::$instance)) {
      self::$instance = new Session();
    }
    
    return self::$instance;
  }

  public function get($var, $default = null)
  {
    $elements = explode('/', $var);
    $result = null;
    foreach ($elements as $element) {
      if (!isset($result[$element])) {
        return $default;
      }
      $result = $result[$element];
    }

    return $result;
  }

  public function set($var, $value)
  {
    $elements = explode('/', $var);
    $last_key = array_shift($elements);
    $data = &$this->data;
    foreach ($elements as $element) {
      if (!isset($data[$element])) {
        $data[$element] = array();
      }
      $data = &$data[$element];
    }
    $data[$last_key] = $value;
  }

}
C'est une classe extrèmement générique, un simple conteneur de données, qui pourrait traiter n'importe quelle données mais qui en l'occurrence est lié au tableau $_SESSION (tu noteras l'utilisation des affectations par référence dans set() et dans __construct(), qui simplifie énormément ce genre de traitements).

Ensuite dans ton panier j'ai noté que tu allais chercher des données dans la session, mais que tu n'en mettais jamais dedans, je te propose donc de remplir la session automatiquement à la fin de la vie de l'objet :
  public function __construct()
  {
    parent::__construct();

    $this->items = Session::getInstance()->get('cart', array());
  }

...

  /**
   * Sauvegarde automatique dans la session à la destruction de l'objet (automatique à la fin du script)
   */
  public function __destruct()
  {
    Session::getInstance()->set('cart', $this->items);
  }
Enfin, je ne suis pas sûr qu'un Panier soit une instance particulière d'une Boutique. Donc Panier extends Boutique ne me semble pas pertinent ;)

ViPHP
ViPHP | 5924 Messages

11 mars 2009, 10:38

En effet, après traduction, c'est plus choquant :)

Ancien utilisateur
Invité n'ayant pas de compte PHPfrance

11 mars 2009, 11:25

Super ! Merci beaucoup pour ces précisions, c'est vrai que la logique POO est parfois difficile à appréhender et que tout compte fait mon truc n'est pas vraiment bien pensé.

Pour gérer les accès concurrents je peut faire ça avec flock(), si quand tu parlais d'accès concurrents c'était bien dans ce sens.
Ou alors je fais une classe Singleton dédiée à la gestion de la communication avec les fichiers...

J'ai un peu de mal à comprendre la classe Session de naholyr même si le principe est saisi (du moins j'espère).

Il ne reste plus qu'à digérer tous vos conseils et à refaire quelque chose de mieux.

Merci beaucoup pour votre aide ! :)

EDIT : Vachement bien le coup des "paniers presse-papier" !

Re-EDIT : Je viens d'essayer de faire une classe Session en suivant le modèle de naholyr mais j'ai l'impression de me fourvoyer...

Code : Tout sélectionner

class Sessions { private static $instance = null; protected $data; private function __construct() { session_start(); $this->data = &$_SESSION; } public static function getInstance() { if (is_null(self::$instance)) { self::$instance = new Sessions(); } return self::$instance; } public function get($name) { if (!isset($this->data[$name])) return array(); else return unserialize($this->data[$name]); } public function set($name, $value) { $this->data[$name] = serialize($value); } public function __clone() { } public function __destruct() { $_SESSION = $this->data; } }
Pour les transmissions par référence aussi j'ai du mal à saisir, normalement quand on manipule un objet c'est par référence par défaut non ?

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

11 mars 2009, 20:53

$_SESSION est un tableau, pas un objet, donc il faut faire un passage par référence pour modifier ses valeurs sans le nommer directement ;)
$this->data =& $_SESSION;
$this->data['truc'] = 'toto';
echo $_SESSION['truc']; // toto

Ancien utilisateur
Invité n'ayant pas de compte PHPfrance

11 mars 2009, 23:14

Ah oui tiens... :) Oublié de réfléchir 2 secondes...

Bon j'en suis à la classe qui gère la lecture des fichiers xml. D'après ce que j'ai compris le but c'est de faire des classes les plus génériques possibles. Donc voici ce que ça donne, vous serait il possible de me dire ce que vous en pensez ? Niveau structure sémantique et tout le toutim...
class Xmldb
{

	private static $instance = null;
	protected $db;
	private $tmp = null;
	
	private function __construct($file)
	{
		if (is_readable($file))
			$this->db = new SimpleXMLElement(file_get_contents($file));
		else
			return exit('Xml file missing or unreadable.');
	}
	
	public static function getInstance($file)
	{
		if (is_null(self::$instance)) {
			self::$instance = new Xmldb($file);
		}
		return self::$instance;
	}
	
	public function selectByid($id, $item)
	{
		return $this->db->Xpath('//'.$item.'[id="'.$id.'"]');
	}
	
	public function existByid($id, $item)
	{
		if (count($this->selectByid($id, $item)) > 0)
			return true;
		else
			return false;
	}
	
	public function itemByid($id, $item, $node)
	{
		if ($this->existByid($id, $item)) {
			$this->tmp = $this->selectByid($id, $item);	
			return $this->tmp[0]->$node;
		} else {
			return null;
		}
	}
	
	public function viewAll($item)
	{
		return $this->db->Xpath('//'.$item);
	}
	
	public function __clone()
	{
	}
	
}
ça progresse au moins ? :oops:

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

11 mars 2009, 23:44

À vue de nez je trouve cette classe très bien conçue. En effet tu l'as rendue générale, l'idée c'est :
1. Tu crées les outils dont tu as besoin pour manipuler des données (BDD XML, Sessions).
2. Tu utilises ces outils pour répondre à un besoin, via des objets représentant les données à manipuler (Panier, Boutique).

En fonctionnant ainsi tu vas petit à petit te constituer une petit librairie réutilisable de tous les outils que tu auras mis en place ;)

Ancien utilisateur
Invité n'ayant pas de compte PHPfrance

12 mars 2009, 11:25

Cool ! :)

Après une bonne semaine on dirait que je suis enfin sur la bonne voie.

Merci infiniment à vous deux pour vos précieux conseils :) Sans ça... c'était cuit !

Parceque c'est quand même pas évident de prime abord de saisir l'intérêt de la chose, de trouver des tutos conformes de bout en bout ou pire de bien capter la logique.

Encore merci !

:)