Questionnement sur l'architecture logicielle

Mammouth du PHP | 1776 Messages

19 févr. 2014, 17:55

Hello @ll !

Je me pose une petite question depuis quelques temps. Je me suis formé à JAVA ces derniers temps, et j'ai pu découvrir l'ORM Hibernate. Sans vouloir aller jusque là, et ne pouvant pas implémenter d'ORM sur ma base actuelle (PHP/Oracle), je cherche à m'adapter au mieux.

Je me pose alors des questions (existentielles ?) sur l'architecture que je dois mettre en oeuvre.

Actuellement, j'ai découpé mon application en plusieurs morceaux :
- Codeigniter pour la partie IHM MVC
- une couche Business qui est appelée par le contrôleur. Dans celle-ci, je mets en forme mes données avec en entrée les données brutes et en sortie un objet
- une couche DAO en CRUD qui est appelée par le business avec en paramètre de mes fonctions soit un objet, soit l'id unique de la ligne (dans le cas d'un read).

Visuellement ça ressemble à ça :

Mon model
<?php

class Model_direction extends MY_Model {

    /* Attributs */
    protected $id;
    protected $libelle;
    protected $division;

    /* Constructeur */
    public function __construct($id = '', $libelle = '') {
        
        $this->setId($id);
        $this->setLibelle($libelle);
        
        parent::__construct();
    }

    /* Id */
    public function getId() {
        return $this->id;
    }

    public function setId($id) {
        $this->id = $id;
    }

    /* Libellé */
    public function getLibelle() {
        return $this->libelle;
    }

    public function setLibelle($libelle) {
        $this->libelle = $libelle;
    }
    
    /* Division */
    public function getDivision() {
        return $this->division;
    }

    public function setDivision(Model_division $division) {
        $this->division = $division;
    }
}
Mon Business
class Business_direction extends CI_Business {

    /**
     * Recherche des Model_direction suivant des paramètres
     * 
     * @param String $id est l'identifiant de la direction
     * @param String $libelle est le libellé de la direction
     * @param String $id_division est l'identifiant de la division
     * @return false si échec
     * @return array(Model_direction) si réussite
     */
    function rechercher($id = '', $libelle = '', $id_division = '') {

        /* On crée l'objet de recherche */
        $dr = new Model_direction($id, $libelle);
        if (strlen($id_division) > 0) {
            $dr->setDivision(new Model_division($id_division));
        }
        
        /* On lance la recherche */
        $result = Dao_direction::search($dr);

        return $result;        
    }

    /**
     * Récupération d'un Model_direction suivant son id
     * 
     * @param String $id est l'identifiant de la direction
     * @return false si échec
     * @return Model_direction si réussite
     */
    function lire($id) {

        /* On crée l'objet de recherche */
        $direction = new Model_direction($id);
        
        /* On lance la recherche */
        $result = Dao_direction::read($direction);

        if (!($result instanceof Model_direction) {
            return false;
        }
        return $result;        
    }
}
Mon DAO
class Dao_direction extends CI_Dao {

    /**
     * Recherche des Model_direction suivant un objet Model_direction
     * 
     * @param Model_direction $direction
     * @return Array d'objets Model_direction
     */
    public function search(Model_direction $direction, $profondeur = 0) {

        /* Construction de la requête */
        $this->db->select('DR.ID, DR.ID_DIVISION, DR.LIBELLE');
        $this->db->from('DIRECTION DR');

        /* Filtre id */
        if (strlen($direction->getId()) != 0) {
            $this->db->like('DR.ID_DR', $direction->getId(), 'none');
        }

        /* Filtre libellé */
        if (strlen($direction->getLibelle()) != 0) {
            $this->db->like('DR.LIBELLE', $direction->getLibelle());
        }
        
        if ($direction->getDivision() instanceof Model_division) {
            $division = $direction->getDivision();
            
            /* Filtre division */        
            if (strlen($division->getId()) != 0) {
                $this->db->where('DR.ID_DIVISION', $division->getId());
            }
        }
        
        $this->db->order_by('DR.LIBELLE', 'asc');
        
        /* On lance la requête */
        $query = $this->db->get();

        /* On crée la liste des directions */
        $result = array();
        foreach ($query->result_array() as $element) {
            /* On crée l'objet Model_direction */
            $direction = new Model_direction($element['ID'], $element['LIBELLE']);
            if ($profondeur > 0) {
                $direction ->setDivision(
                        Dao_division::read($element['ID_DIVISION'], $profondeur-1));
            }
            $result[] = $direction;
        }
        
        return $result;
    }

    public function read($id, $profondeur = 0) {

        $direction = new Model_direction($id);
                
        $result = self::search($direction, $profondeur);
        if (count($result) == 1) {
            return $result[0];
        } else {
            return false;
        }
    }

}
Ça fonctionne bien, mais ça a ses limites :
- La relation 1-1 est parfaite, mais la relation 1-n est assez tordue pour moi. Je ne vois pas comment la mettre en place. Par exemple, si l'on se base non plus sur la direction mais sur la division, on peut donc avoir plusieurs direction par division. Le schématiser est assez complexe. Avez-vous une solution à cette problématique ? Des exemples ?
- La recherche profonde est très complexe à mettre en oeuvre. Si l'on se base sur la table direction et que l'on veut faire un filtre 3 tables au dessous, c'est actuellement impossible, à part de filtrer les retours en php : trop gourmand en mémoire, processeur et bande passante pour des données qui ne serviront pas.

En bref, j'aimerai pouvoir me promener facilement dans des objets sans avoir ces problématiques, et sans devoir dupliquer mon code ou faire à chaque fois des requêtes spécifiques. Avez-vous déjà rencontré un tel problème ? Comment l'avez-vous résolu ?

Merci ;)

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

19 févr. 2014, 22:12

salut,

l'accès statique c'est le mal, surtout quand les méthodes ne sont pas déclarées static (regarde du coté de l'injection de dépendance).

ton problème vient de ton orm (ou ce qui y s'en rapproche vu le code) qui ne te permet pas de réaliser une jointure.

ensuite il faut qu'il puisse savoir quel champs il doit mettre dans quel objet.
à la limite, si tu as des objets qui correspondent aux tables, avec l’introspection des pojos il est possible de savoir si un champs de la requête va à tel ou tel objet (voir plusieurs dans le cas de la clef de jointure).

vu que tu parle de Java tu peux regarder le fonctionnement de MyBatis qui permet d'utiliser de faire la liaison entres des bases "hétéroclite" et l'appli java (qui peux l'être aussi :) ).

Pourquoi ne peux tu pas utiliser d'orm ?
as tu regardé / testé doctrine (http://fr.openclassrooms.com/informatiq ... - doctrine). fonctionne avec oracle (PDO_oci)
ou alors propel

et surement d'autre (Par exemple le framework laravel utilise un orm qui s'appel eloquent)

@+
Il en faut peu pour être heureux ......

Mammouth du PHP | 1776 Messages

20 févr. 2014, 10:54

Les appels en statique se font sur des classes statiques, c'est juste que j'ai pris une ancienne classe de l'appli pour l'exemple. Je repasse sur chacune d'elle en ce moment pour corriger ça.
Par contre, je ne comprends pas ta reco sur l'injection de dépendance. Le passage en statique m'évite de charger ma mémoire avec des objets inutiles qui n'ont pas besoin d'exister, mais juste d’exécuter une routine. Mes méthodes dans les classes DAO sont ainsi appelées sans avoir besoin de créer à chaque requête un objet. Ou alors je n'ai pas compris ta reco :oops: ?

Pour l'introspection, c'est très compliqué. Les tables ne sont ni propres, ni optimisées. De nombreuses tables n'ont pas d'ID ou en ont un mais en CHAR. Je repasse là dessus aussi au fur et à mesure, mais ce n'est pas la priorité de mes tâches. À l'heure actuelle, j'essaie de concilier bonnes pratiques (réutilisabilité et souplesse du code) et existant.
De par ces mêmes problématiques, Doctrine ne peut pas être installé. De plus, les avis qui traînent sur le net sur Doctrine avec Oracle ne sont pas fameux, et il est fait part de nombreux problèmes (instable).

Si aujourd'hui je veux poursuivre mon mini ORM, et afin de lier les différents objets, si je ne dis pas de bêtises mon Model_Direction aura comme attribut un Model_division, et mon Model_Division aura comme attribut un array de Model_Direction. C'est clean ?
Par contre, pour filtrer plus profondément mes requêtes, je suis paumé. J'ai l'impression qu'il faut que je crée un outil de création de requêtes à la volée via un schéma de mes tables, et non comme je fais actuellement.

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

20 févr. 2014, 12:16

injection de dépendance (la c'est pas le constructeur,tu peux utiliser des "setter" mais bon ça pas la chose obligatoire ;) )
<?php
class toto {
    private $sgbdConn;
   
    public function __construct(\PDO $connexion){
        if($connexion !=null){
            $this->sgbdConn = $connexion;
        }else {
            // je voulais un IllegalArgumentException ....
            throw new InvalidArgumentException('Objet PDO obligatoire');
        }
    }
    
    // etc
}
Les appels en statique se font sur des classes statiques,
c'est pas le cas de la dao en tout cas (pas de "static" dans le code).

de plus tu pense que l'objet n'est pas chargé et que tu ne va avoir seulement l'appel a la partie du code qui t'intéresse ?
quid de l'initialisation de la classe, de l'accès base etc etc.

si tu gère correctement l'injection des objet tu n'auras pas besoin de multiplier les instances en mémoires.
Avec l'injection de dépendance tu créer une fois pour toute ta connexion sgbd (ou tes connexions, tu peux gérer un pool, ou plusieurs accès à différents sgbd) à l'initialisation du contrôleur et tu fournit la connexion utile aux objet que tu instancie et ainsi de suite.

Un seul objet sera en mémoire.

Dans le genre, (même si ce n'est pas forcément parfait) tu peux regarder l'injection de dépendance de spring (en java).




quand au reste direction ou division ne sont pas parlante, sans modèle de base ou autre ce n'est pas simple de te répondre.

pour l'introspection ce n'est pas simple effectivement, mais cela peux te permettre de dresse une carte des objets que tu as et ainsi affecter les bonnes infos aux bon endroits.
Par exemple le fetchAll de PDOStatement avec l'argument PDO::FETCH_CLASS: ou pourquoi pas PDO::FETCH_FUNC offre des possibilités importante par contre pas de gestion d'objets enfants d'un autre la c'est a toi de la coder.

Donc dans un orm, dynamiquement tu pas vraiment d'autre choix que de savoir a l'avance la constitution des objets (donc xml ou introspection pour dresser la cartographiques des objets correspondants à la requête).
si je ne dis pas de bêtises mon Model_Direction aura comme attribut un Model_division, et mon Model_Division aura comme attribut un array de Model_Direction. C'est clean ?
Non parce que la il y a une référence circulaire (une direction contient une division qui va contenir des direction qui elle même contiennent des division etc etc etc).
S'il s'agit de relation "parents - enfants" tu ne devrais pas avoir d'arbre trop profond, mais il ne me semble pas logique dans des parents dans un enfant (après je ne connais pas le fonctionnel de l'appli donc c'est difficile de juger la chose).


pour ce qui est des ressources employées (processeurs,, mémoire, bande passantes pourquoi taille disque etc) c'est effectivement une chose à prendre en compte en cas d'optimisation mais il faut quand même relativiser les choses. Les machines sont assez puissantes pour ne pas trop s'en soucier. (ou devrait l'être après si ton appli est hébergée sur un pentium 200 ... :mrgreen: )
au dela de ces considération il est (je pense) absurde de retourner 500 lignes d'une bases de données (requête complexe ou non) car généralement l'utilisateur est perdu avant (riende plus chiant que d'éplucher un truc super long ou tu finis par ne plus savoir où tu en es quand tu lit.
du coup => pagination, cela complexifie un poil les requêtes (rowid between etc) mais tu gagne sur tout le reste et le sgbd encaisse la chose (il est fait pour ça hein).

d'ailleurs dans le clan optimisation il y a souvent des optimisation à faire coté requête SQL (utilisation de with par exemple etc etc).

bref ce n'est pas parce que tu va avoir 50 ou 500 objet en mémoire que tu va mettre ton serveur en croix :D

En partant de ce principe tu peux facilement te dire que tu va demande les enfants voir les petits enfants et rarement plus profond.

Dans ce cas effectivement tu auras des "gens" qui auront des enfants (donc une liste de gens) etc etc.
Après tu ne pourras pas avoir toutes les branches de l'arbres en une seule requête et il te faudra une méthode récursive pour la recherche "en profondeur" de l'arbre

par exemple un truc comme ça (c'est fait rapide sans test ni gestion d'erreur)
class Gens {
    private $nom;
    private $prenom;
    private $enfants = [];
    private $id;

    function __construct($nom = null, $prenom = null, $id = null) {
        $this->id = $id;
        $this->nom = $nom;
        $this->prenom = $prenom;
    }

    /**
     * @return mixed
     */
    public function getEnfants() {
        return $this->enfants;
    }

    /**
     * @param mixed $enfants
     */
    public function setEnfants($enfants) {
        $this->enfants = $enfants;
    }

    /**
     * @return mixed
     */
    public function getNom() {
        return $this->nom;
    }

    /**
     * @param mixed $nom
     */
    public function setNom($nom) {
        $this->nom = $nom;
    }

    /**
     * @return mixed
     */
    public function getPrenom() {
        return $this->prenom;
    }

    /**
     * @param mixed $prenom
     */
    public function setPrenom($prenom) {
        $this->prenom = $prenom;
    }

    /**
     * @return null
     */
    public function getId() {
        return $this->id;
    }

    /**
     * @param null $id
     */
    public function setId($id) {
        $this->id = $id;
    }

    public function addEnfant(Array $enfant) {
        foreach ($enfant as $e) {
            $this->enfants[] = $e;
        }
    }
}

class DaoGens {
    private $connexion;

    function __construct(\PDO $connexion) {
        if ($connexion == null) {
            throw new InvalidArgumentException('Connexion sgbd obligatoire');
        }
        $this->connexion = $connexion;
    }

    function search($id) {
        $sql = 'select * from gens where id = ' . $id;
        $result = $this->connexion->query($sql);
        $gens = new Gens($result->nom, $result->prenom, $result->id);
        $result->closeCursor();
        return $this->searchDescendance($gens);
    }

    /**
     * Recherche de la descendance
     * @param Gens $gens
     */
    function searchDescendance(Gens $gens) {
        if (count($gens->getEnfants()) == 0) {
            $gens->addEnfant($this->getEnfantDirect($gens));
        }
        $child = $gens->getEnfants();
        foreach ($child as $index => $e) {
            $enf = $this->getEnfantDirect($e);
            if (count($enf) > 0) {
                $e->addEnfant($enf);
                $g = $this->searchDescendance($e);
                $child[$index]->addEnfant($g);
            }
        }
        $gens->setEnfants($child);
        return $gens;
    }

    /**
     * @param Gens $gens
     */
    public function getEnfantDirect(Gens $gens) {
        $sql = 'select * from gens where idparent=' . $gens->getId();
        $result = $this->connexion->query($sql);
        $enfants = $result->fetchAll(PDO::FETCH_CLASS, 'Gens');
        $result->closeCursor();
        return $enfants;
    }

}
si tu veux gagner en perf pense aux requêtes préparées.


@+
Il en faut peu pour être heureux ......

Mammouth du PHP | 1776 Messages

20 févr. 2014, 15:33

Merci pour ta réponse ;)

En clair, pour l'instant, la base de données n'est pas assez propre et correctement structurée pour subir ce genre de traitement. Je vais donc continuer sur ma voie, en mettant un minimum de code dégueulasse pour faire tourner mon bouzin. Et en parallèle, je vais remettre en forme mes tables pour faire cette optimisation via un ORM.