JeuWeb - Crée ton jeu par navigateur
Méthodes magiques, classe statique en instance - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : Méthodes magiques, classe statique en instance (/showthread.php?tid=7313)

Pages : 1 2 3 4


Méthodes magiques, classe statique en instance - Max72 - 06-02-2015

Oui, le nom du sujet n'est pas très clair mais je ne voyais pas trop comment nommer ce que je vais décrire ci-dessous.

Je me suis essayé à faire un petit tuto en POO.
Y'a beaucoup de blabla pour pas grand chose, et le tout est pas forcément top. Mais c'est là Wink

________________________________________________


Je m'explique : J'ai une classe qui peut être instanciée plusieurs fois ( Town). Ainsi, je peux créer plusieurs cités et les faire interagir (A donne de l'or à B, C attaque D, etc).
Mon problème est que j'ai besoin d'une instance de Town qui soit unique, et surtout qui soit accessible absolument partout (controllers, vues, ..)
Cette instance est la Cité Courante, la cité du joueur en cours. J'appellerai cette cité 'CurrTown'.

2 approches s'offraient à moi :
- Créer un objet Town global, mais je n'aime pas du tout cette pratique
- Créer un singleton Town. Cette méthode aurait pu me convenir, mais je n'avais pas envie de faire des get_instance à tout va.

J'ai donc créé une 3ème possibilité, une classe statique qui me permet de travailler comme si je travaillais sur une classe Town, à quelques détails près.

Pour info, voici le code de ma classe Town (celle qui peut être instanciée), épuré pour le post :

Code PHP :
<?php 
class Town

{
   
   
// Ressources pour le calcul
   private
       $time_ellapse
= 0,
       $gold_per_hour = 0,
       $products_per_hour = 0,
       $population = 0,
       $taken_population = 0,
       $taken_boxes= 0,
       $free_boxes = 0;
   
   
// Attributs de la cité (or, products etc provenant de la DB)
   private $attributs = array();
   
   public
function __construct($id = false)
   {

       if (!$id) return false;
       // Récupération de la cité en DB
       $town = Towns::retrieveById($id, ORM::FETCH_ONE);

       if ($town == NULL) return false;

       // Parcours de tous les attributs de la cité, qu'on place dans $this->attributs
       foreach (get_object_vars($town) as $key => $value)
       {
           $this->attributs[$key] = $value;
       }
   }

   private function SaveTown()
   {
       // Enregistrement des attributs en BDD
    }

   public function __isset($name)
   {
       return (isset($this->attributs[$name])) ? true : false;
   }
   
   public
function __get($name)
   {
       return (isset($this->attributs[$name])) ? $this->attributs[$name] : false;
   }
   
   public
function __set($name, $value)
   {
       $this->attributs[$name] = $value;
   }
}

La partie intéressante dans ce code est la présence du tableau 'attributs'.
Dans le __construct, on récupère la cité stockée en DB, et on stocke dans l'attribut 'attributs' de la classe toutes les valeurs de la cité.

Attention à $town = Towns::retrieveById($id, ORM::FETCH_ONE);
La classe Towns est le modèle qui me permet de récupérer mes cités par l'ORM, rien à voir avec ce post.


Sorti du construct, $this->attributs contient à peu près :
'id' => 25,
'name' => 'Cité de Max',
'gold' => 100,
'products' => 50,
 etc etc

Je ne détaillerai pas les __isset() et autres méthodes magiques, le tout est de savoir que tout attribut inaccessible est recherché et/ou défini dans l'array 'attributs'.

Sinon, pour instancier la classe, rien de plus facile :
Code PHP :
<?php 
$town
= new Town($id_de_la_cite);

Si maintenant je veux récupérer l'id de la cité, il me suffit de faire :
Code PHP :
<?php 
$id
= $town->id;

Enfin pour modifier le nombre d'or :
Code PHP :
<?php 
$town
->gold = 500;


C'est bien, mais on a toujours pas parlé de la cité courante.
Passons maintenant à CurrTown.

Voilà ma classe toute simple :

Code PHP :
<?php 
class CurrTown

{
   // Instance de Town de la cité courante
   private static $town;
   
   public
static function Init(Town $town)
   {
       self::$town = $town;
   }
}

La fonction Init() sert de constructeur. Elle accepte en argument une instance de la classe Town, créée ci-dessus.
En gros, pour initialiser cette classe, ça se passe comme ça :

Code PHP :
<?php 
$town
= new Town($id_cite);
CurrTown::Init($town);


Comme je l'ai dit, je souhaite créer une classe accessible partout (en statique), et pas un objet que je trimballerai de classes en classes.
Mais pour que le tout soit dynamique, et ne pas avoir à gérer des dizaines et des dizaines d'attributs dans ma classe, j'aimerai que tous les attributs soient gérés et créés automatiquement.

Le problème est qu'avec les attributs statiques, on ne peut pas utiliser de méthode magique __isset(), __set() etc.

Heureusement, il y a __callstatic().
Cette méthode est appelée lorsqu'on invoque des méthodes inaccessibles dans un environnement statique.
Des méthodes, mais rien n'existe pour les attributs.
Il va donc falloir palier à ce problème avec la méthode magique :

Code PHP :
<?php 
public static function __callStatic($name, $arguments)

{


}

J'explique :
CurrTown travaillant dans un contexte statique, à chaque fois qu'une méthode inexistante sera lancée, la méthode __callstatic sera appelée. Y seront transmis 2 arguments :
$name : Nom de la méthode (string)
$arguments : Arguments passés à la méthode (array)

Aussi, si je tente :
Code PHP :
<?php 
CurrTown
::Calcul(5, 'ok');
La méthode __callstatic sera appelée et :
$name prendra la valeur de 'Calcul '
$arguments sera égal à 0 => 5, 1 => 'ok'

Maintenant, nous souhaitons avoir accès aux attributs de la cité.
Pour rappel, ces attributs sont dynamiques et stockées dans 'attributs' de la classe Town (ouais faut suivre ^^ ).

Nous allons donc implémenter dans la méthode __callstatic() un moyen de retourner un attribut de l'instance Town (s'il existe) :
Code PHP :
<?php 
   
if (isset(self::$town->$name))
       return self::$town->$name;

Si j'essaye de lancer :
Code PHP :
<?php 
echo CurrTown::gold();
l'attribut 'gold ' existe dans l'instance Town, donc on retourne le nombre d'or (grâce au __get() de la classe Town ).

Maintenant, si je souhaite lancer une méthode de la cité ?
Implémentons ça, toujours dans notre __callstatic() :
Code PHP :
<?php 
   
if (method_exists(self::$town, $name))
        return call_user_func_array(array(self::$town, $name), $arguments);

A partir de là, je peux lancer sur CurrTown toutes les méthodes existantes dans la classe Town :
Code PHP :
<?php 
CurrTown
::SaveTown();

Un peu plus difficile à présent. Comment assigner une valeur à un attribut de la cité courante ?
...
J'ai bricolé ceci :
Code PHP :
<?php 
   if
(strpos($name, 'set_') === 0)

   {
      $name = substr($name, 4);
      if (isset(self::$town->$name))
        {
           list($value) = $arguments;
           self::$town->$name = $value;
           return $value;
         }
   }

Ainsi, pour définir l'or de la cité en cours, il me suffit de faire :
Code PHP :
<?php 
CurrTown
::set_gold(150);

Il y aurait bien des meilleures façons, mais celle-ci fonctionne très bien.

Je vous donne à présent le code complet de ma classe CurrTown :

Code PHP :
<?php 
class CurrTown
{
   // Instance de Town de la cité courante
   private static $town;
   
   public
static function Init(Town $town)
   {
       self::$town = $town;
   }

   public static function __callStatic($name, $arguments)

   {
       //Setter magique
       if (strpos($name, 'set_') === 0)
       {
          $name = substr($name, 4);
          if (isset(self::$town->$name))
            {
               list($value) = $arguments;
               self::$town->$name = $value;
               return $value;
             }
       }

        // Attributs de Town
        if (isset(self::$town->$name))
           return self::$town->$name;

        // Methodes de Town
        if (method_exists(self::$town, $name))
            return call_user_func_array(array(self::$town, $name), $arguments);

        echo 'CurrTown::' . $name . ' not exist';
   }
}

Le 'echo ' à la fin indique si l'attribut ou méthode demandé n'existe pas.

_______________________________________________________________

Lisez-ceci avec un peu de recul, je m'essaye doucement à l'écriture ^^
Je ne dis pas non plus que c'est la bonne parole. Comme dis ailleurs, je ne suis pas une référence Wink

N'hésitez pas si vous avez des questions (j'ai essayé de bien expliquer) ou des remarques.


RE: Méthodes magiques, classe statique en instance - Xenos - 06-02-2015

Pour la méthodologie, je ne vois pas bien pourquoi faire un tel barouf sachant que la méthode de Singleton CurTown::getInstance() serait plus standardisée (pattern Singleton), souple (on fait renvoyer ce qu'on veut à cette fonction) et rapide à l'exec... Si jamais c'est gonflant d'écrire CurTown::getInstance(), alors il suffit d'un alias

function CTG() {
return CurTown::getInstance();
}

Cela évitera les méthodes magiques qui sont impossibles à déboguer (l'IDE aura un mal de chien à t'aider pour tout ce qui est Find usage ou Goto Declaration / Implementation).



Sinon, point de vue forme, je te conseillerai d'être attentif aux changements de sujets: "Mon" en début de tuto, "Nous" au milieu, "Me" à la fin...
Perso, un tuto rédigé en "Je" me semble mal venu: au début du sujet, j'ai cru qu'il s'agit d'un problème de code que tu exposais et non d'un tutoriel / guide. Il m'a fallu attendre le milieu du message (et zieuter la catégorie du sujet) pour le comprendre.

Et dernier petit point, je dirai que les morceaux de code sont trop nombreux et trop courts. Mais, là, c'est peut-etre plus un avis perso Smile



Note:
la généralisation de ce système serait plus intéressante que son écriture pour un cas précis. C'est à dire que disposer d'une classe (abstraite?) qui serait en charge de rendre un singleton accessible facilement, et que l'on extends ensuite en un CurTown, un CurDB ou un CurUser aurait plus d'intérêt. Je pense que lire le code de Frameworks connus (type Symphony, phpCake ou autres; je les cite au pif car je ne les connais pas) sera instructive: ces Frameworks ont déjà surement ce type de classes dans leurs tiroirs.

Aussi, si la recopie des attributs te gonfle, il y a la possibilité de générer automatiquement le code que tu souhaites, ou bien par l'IDE (ou tout autre script exécuté en local, en dec; c'est préférable à mon sens), ou bien par d'autres codes PHP.


RE: Méthodes magiques, classe statique en instance - niahoo - 06-02-2015

Au contraire je trouve la démarche très intéressante. Je te suggère de regarder les Façades de Laravel.

Ce qui serait très intéressant ce serait un tuto qui explique comment utiliser le composant IoC de Laravel 5 (attention la doc en fr est sur 4.1) et les façades dans un projet qui n'est pas basé sur laravel.

edit: cependant tu n'indiques pas à quel moment tu charges ton instance dans ta Facade. Où se fait l'appel à "Init" (les noms de fonctions devraient être en minuscules). Il manque le code qui charge automatiquement l'instance (si ce n'est déjà fait) lors d'un appel.


RE: Méthodes magiques, classe statique en instance - Max72 - 06-02-2015

Alors, dans l'ordre :
Pour la méthodologie, je trouvais intéressant de montrer en application comment se servir de ces méthodes magiques.
Dans mon projet, j'ai utilisé ce système car je n'aime pas, comme dit plus haut, devoir utiliser les singleton (bien que ça en soit presque un). Et surtout, j'apprends. Alors pourquoi pas essayer de l'expliquer à d'autres.

Et dans un cas plus général, il ne faut pas s'arrêter au tuto (mais peut-être que j'aurai dû en parler) :
Dans mon exemple, J'utilise ce système pour 2 raisons :
- Comme dit, pas de get_instance, il est partout.
- Surtout, les attributs de ma CurrTown sont publics. Donc je peux les lire depuis mes vues etc, mais si je veux, je peux enlever la possibilité à tout autre classe de modifier mon CurrTown. Il me suffit pour cela de retirer mon 'Setter magique'.

Et il y a bien d'autres cas à pouvoir utiliser ce système.

Pour la forme, je prends note. C'est vrai que je ne savais pas trop comment me lancer, et mon esprit a dû rester hésitant Wink

Sinon, pourquoi je ne recopie pas mes attributs dans ma classe :
Il s'agit de colonnes présentes dans ma DB et qui sont suceptibles d'évoluer. De plus, certains attributs seront dynamiques, toujours.
Par exemple pour mes ressources (plusieurs dizaines différentes), elles sont stockées dans une autre table et non dans Towns. Elles évolueront au fil du jeu, et je n'ai pas envie de copier/coller plus de 50 attributs juste pour mon IDE, et de devoir mettre à jour ma classe à chaque MAJ du jeu.
Faire du dynamique, c'est pour moi un des avantages de la POO.

niahoo : Mon code s'approche involontairement de plus en plus à ce pattern. Par exemple, mes controllers ne travaillent pas avec mes models directement :
Ils passent par une classe qui fait le boulot plus complexe, pour avoir un code utilisateur (dans le controller) le plus facile à comprendre possible.
C'est de l'abstraction en plus, sûrement plus dur à maintenir mais j'adore ce concept.

Concernant Laravel, je ne le connais pas du tout.

Enfin, j'instancie CurrTown dans la classe Mere de mes controllers.
Donc à chaque controller qui est instanciée, celui-ci lance l'Init() selon si le joueur a choisi sa cité ou pas (en session).
Voilà en gros le code épuré :


Code PHP :
<?php 
// Classe Mere
class MY_Controller

{
   function __construct()
   {
    // Utilisateur non connecté

        if (!CurrUser::$IsLoggedIn || !CurrUser::$server)
        {
            // Si URL non accessible au public, on le redirige vers l'accueil
            if ($this->uri->segment(1) != 'home' && $this->uri->segment(1) != false)
            {
                redirect (site_url());
            }
        
       
// Chargement auto des models
        $this->load_Home_Models();
        }
    else
    
{
        // Utilisateur connecté
        $this->load_Game_Models();
        if ($this->session->town_id)
        {
           // Le joueur a déjà choisi sa cité
           $town = new Town($this->session->town_id);
           CurrTown::Init($town); // <----------------------------------- Ici !
        }
    }
 }
}

// Mon controller
class Game extends MY_Controller
{

    function __construct()
    {
       parent::__construct();
    }
    
   
function index()
    {
        var_dump(CurrTown::gold());
    }
}

Edit : Désolé l'indentation ne rend pas génial.

Merci de vos commentaires Smile


RE: Méthodes magiques, classe statique en instance - Akira777 - 07-02-2015

Ah tiens Max72, j'en vois un qui fait son jeu avec CodeIgniter (mon nouveau jeu est fait avec)...

Pour ma part, j'ai mis toutes les classes de mon jeu dans un namespace.

Concernant l'instance de mon joueur connecté, j'ai bâti ça autour d'une classe Player. Son rôle est de livrer un singleton de l'instance du joueur, puis de faire un lazyLoad des classes liées au jeu. Mon Player charge donc dynamiquement toutes les classes du joueur.

Ex :
Code PHP :
<?php 
Player
::Account->connected(); // charge dynamiquement \Game\Account puis execute la méthode connected()
Player::Ninja->maximum_life; // charge dynamiquement \Game\Ninja puis récupère l'attribut "maximum_life"

Les objets de l'instance sont donc chargés dynamiquement. Le driver SQL met en cache la plupart des requêtes et bien souvent l'objet entier est déjà stocké serializé via Redis.
Pour les interactions entre les joueurs (ou plus généralement entre les entités), j'utilise un service entre les deux.

Ex : X veut transférer de la tune à un autre joueur
Code PHP :
<?php 
$trade
= new \Game\Trade\PlayerToPlayer(Player, new \Game\Service\Player($receiver_id), $amount);
if (
$trade->possible())
{
$trade->commit();
}



RE: Méthodes magiques, classe statique en instance - niahoo - 07-02-2015

Tu n'utilises pas composer pour l'autoloading ?


RE: Méthodes magiques, classe statique en instance - Akira777 - 07-02-2015

Ca peut paraître étrange mais là, non.
Sur ce projet je n'utilise rien d'autre que des libs "maisons" ou des outils qui sont composer-less. (Malgré le fait que le framework que j'utilise l'intégre...). Un spl_autoload_register(array('GameAutoloader', 'register')) me suffit pour ce projet précis.


RE: Méthodes magiques, classe statique en instance - niahoo - 07-02-2015

Ok, ok, on est d'accord que ce n'est pas indispensable Smile


RE: Méthodes magiques, classe statique en instance - Max72 - 08-02-2015

Akira : Pourrais-tu nous filer un bout de code de ta classe Player qui montre comment tu charges en auto les classes dépendantes ?
Concernant le cache SQL, j'espère pouvoir me passer de Redis et construire moi-même un petit gestionnaire de cache SQL pour mon ORM.
D'ailleurs j'hésite de plus en plus à me remettre sur mon framework maison.....

Concernant composer, perso je ne l'utilise dans rien du tout. Si une lib n'a pas d'autre choix d'installation, je cherche autre chose, nanméoh ! ^^
Rien que pour ça, je me détourne de pas mal de frameworks. Oui je sais, c'est un comportement de vieux qui ne veut pas évoluer Smile
Faut dire que je n'aime pas aller dans le sens du courant (suffit de voir que j'utilise un framework dépassé et que j'utilise Qwant pour toutes mes recherches Smile ).

A ce sujet, certains tournent-ils sous Yii ou Kohana ?


RE: Méthodes magiques, classe statique en instance - Akira777 - 08-02-2015

Hum, c'est dommage de se passer de Composer, ou du moins pas de lui directement mais surtout des librairies construites "pour" (qui suivent PSR-0). Presque toutes les "meilleures" (c'est très objectif) se sont accordées à la norme.

Enfin bon, il suffit de faire un "composer install", puis de penser à inclure le dossier "vendor" dans ton git. Après un autoloader de ce style fera l'affaire sans problème :

Code PHP :
<?php 
function __autoload($className) {
$extensions = array(".php", ".class.php", ".inc");
$paths = explode(PATH_SEPARATOR, get_include_path());
$className = str_replace("_" , DIRECTORY_SEPARATOR, $className);
foreach (
$paths as $path) {
$filename = $path . DIRECTORY_SEPARATOR . $className;
foreach (
$extensions as $ext) {
if (
is_readable($filename . $ext)) {
require_once
$filename . $ext;
break;
}
}
}
}