Salut à tous !

Voilà je tourne en rond depuis un moment car je n'arrive à choisir la meilleure façon de modeliser les relations entre class User et Language

Pour commencer je m'explique, j'ai une entité User, celui-ci à plusieurs propriétés dont ses langues parlées ( ex : anglais, allemand, italien).
Language à un id et code (code de la langue, ex = EN)

Le truc c'est que ces langues et leur niveau (de 1 à 3 pour débutant, bon, expert) peuvent se décomposer comme ça =>

1) Langue native ( exemple FR) (niveau max par défaut = 3)
2) EN avec un niveau à (bon = 2 par exemple)
3)Eo avec niveau à (débutant = 1 )
4) etc..

Donc, est-ce que User doit avoir une proprieté langue native et langue apprisent, ou seulement languages (array) qui prendra les langues de l'user sans distinction en langue native et langue ?

Les relations, un User peut avoir 0 (car pas demander à l'inscription sinon 1 mini) ou plusieurs langues, et Language peut appartenir a 0 ou * User

Je ne sais pas si je dois faire une relation manyToMany avec une classe intermédiaire pour ajouter le niveau de la langue au passage ou juste une propriété languages en array qui contient0 à * Language (instance de) et du coup cette propriété languages serait juste un OneToMany..

Bref je m'y prend surement pas de la bonne façon, et mélange un peu tout, c'est pour ça que je post ici pour avoir une vision extérieure, des idées de comment arriver au résultat souhaité..

Merci.

16 réponses


laplumaencre
Auteur
Réponse acceptée

J'arrete pour ce soir, il faudrait que je te laisse un accès visiteur sur mon code sur bitbucket pour que tu vois plus clair à mon histoire :-)
,sur discord de grafikart j'ai le meme pseudo, ça pourrait être plus pratique que d'utiliser le forum, qui à remettre tout ça dessus apres...

Ben... Tu sais que tu peux faire autant de liens entre deux tables que nécessaires? Je vois deux solutions, perso :

  • Tu fais une seule relation (0, ) <-> (0, ), donc une table Language_User qui contient un booléen "native" et tu gères avec des triggers pour que le lvl soit forcément à 3 si native
  • Tu fais deux relations : (0, 1) <-> (0, *) pour native et (0, n) <-> (0, n) pour le reste!

Le deuxième cas cristalise cependant l'idée que l'on ne peut avoir qu'un seul langage natif! Or, si nos parents ou nos proches lors de la petite enfance en maitrisent et parlent plusieurs, ça ne marche plus...

La grande question sera tout de même : "est-il vraiment nécessaire de s'emmerder autant avec un petit problème de tables dont les relations contiennent toutes un 0..!?"

Et pour cette histoire de lvl, tu pourrais décider que le lvl 4 correspond à "native", ça supprimerait un champ ;-p

Bon courage!

Salut Psylozoff, et merci.
Pour l'idée du level 4 pour dire natif, c'est carrément une bonne idée !

Donc je serait partisans du systeme User 0_->UserLanguage -> 0 Language

Voilà ce que j'ai commencé à faire =>
User

<?php
/**
 * @Class User
 */
namespace App\Entity;

use App\Entity\Image;
use App\Entity\Language;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @ORM\Table(name="user")
 * @UniqueEntity(fields="username")
 */
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(name="id",type="integer")
     */
    private $id;

    /**
     * username is email for login/register
     * 
     * @ORM\Column(type="string", length=255, unique=true)
     * @Assert\Email
     * @var $username
     */
    private $username;

    /**
     * @ORM\Column(name="password", type="string", length=255)
     * @var $password
     */
    private $password;

    /**
     * @ORM\Column(name="nickname", type="string", length=255)
     * @Assert\Length(min=3)
     * @var $nickname
     */
    private $nickname;

    /**
     * @ORM\Column(name="languages") 
     */
    private $languages;

    /**
     * @ORM\ Column(name="presentation", type="string", length=400, nullable=true)
     * @var $presentation 
     */
    private $presentation;

    /**
     * @ORM\ Column(name="country", type="string", length=100, nullable=true)
     * @var $country 
     */
    private $country;

    /**
     * @ORM\ Column(name="city", type="string", length=100, nullable=true)
     * 
     * @var $city 
     */
    private $city;

    /**
     * @ORM\ Column(name="gender", type="string", length=1, nullable=true)
     * @Assert\Length(max=1)
     * @var $gender gender ( "m" or "f")
     */
    private $gender;

    /**
     * @ORM\Column(name="interest", type="array", length=255, nullable=true)
     * @var  $interest Array 
     */
    private $interest;

    /**
     * @ORM\OneToOne(targetEntity="App\Entity\Image", cascade={"persist"})
     * @var $picture array
     */
    private $picture;

    /**
     * @ORM\ Column(name="first_connection", type="datetime")
     * @ORM\Column(type="datetime")
     * @Assert\DateTime()
     * @var $firstConnection date subscribe
     */
    private $firstConnection;

    /**
     * @ORM\ Column(name="last_connection", type="datetime")
     * @ORM\Column(type="datetime")
     * @Assert\DateTime()
     * @var $lastConnection
     */
    private $lastConnection;

    /**
     * @ORM\Column(name="is_active", type="boolean", options={"default":false})
     */
    private $isActive;

    /**
     * @ORM\Column(name="roles", type="array")
     */
    private $roles = array();

    /**
     * locale user give the language of Site
     * @ORM\Column(name="locale", type="string", length=2)
     */
    private $locale;

    /**
     * token for confirm registration
     * @ORM\Column(name="token_register", type="string", length=255)
     */
    private $tokenRegister;

/**
 ****************************** Methods **************************
 */

    /**
     * @method array __construct
     */
    public function __construct()
    {
        $this->isActive = true;
        $this->roles = array("ROLE_USER");   
        $this->languages = new ArrayCollection(); 
        $this->interest = new ArrayCollection();    
    }

    public function getSalt() { return null; }

    public function getRoles() { return $this->roles; }

    public function eraseCredentials() {}

    /** @see \Serializable::serialize() */
    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
            $this->nickname,
            $this->languages,
            $this->presentation,
            $this->country,
            $this->city,
            $this->gender,
            $this->interest,
            $this->picture,
            $this->firstConnection,
            $this->lastConnection,
            $this->firstConnection,
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password,
            $this->nickname,
            $this->languages,
            $this->presentation,
            $this->country,
            $this->city,
            $this->gender,
            $this->interest,
            $this->picture,
            $this->firstConnection,
            $this->lastConnection,
            $this->firstConnection,
        ) = unserialize($serialized, ['allowed_classes' => false]);
    }

    public function removeLanguages($language)
    {
        $this->languages->removeElement($language);
    }

    /** 
     **************************** GETTERS & SETTERS************************
     */

    /**
     * Get the value of id
     */ 
    public function getId()
    {
        return $this->id;
    }

    /**
     * Get password
     *
     * @return  self
     */ 
    public function getPassword()
    {
        return $this->password;
    }
    /**
     * Set password
     * 
     * @return self
     */
    public function setPassword($password)
    {
        $this->password = $password;
    }

    /**
     * Get username is egal email for login
     *
     */ 
    public function getUsername()
    {
        return $this->username;
    }
    public function setUsername($username)
    {
        $this->username = $username;
    }

    /**
     * Get nickname
     *
     * @return  self
     */ 
    public function getNickname()
    {
        return $this->nickname;
    }
    /**
     * Set nickname
     * 
     * @return self
     */
    public function setNickname($nickname)
    {
        $this->nickname = $nickname;
    }

    /**
     * Get array 
     * @return  self
     */ 
    public function getLanguages()
    {
        return $this->languages;
    }

    public function setLanguages($language)
    {
        $this->languages[] = $language;
        return $this;
    }

    /**
     * Get the value of presentation
     *
     * @return  self
     */ 
    public function getPresentation()
    {
        return $this->presentation;
    }

    /**
     * Set the value of presentation
     *
     * @param  $presentation  
     */ 
    public function setPresentation($presentation)
    {
        $this->presentation = $presentation;
    }

    /**
     * Get the value of country
     *
     * @return self
     */ 
    public function getCountry()
    {
        return $this->country;
    }

    /**
     * Set the value of country
     *
     * @param  $country
     */ 
    public function setCountry($country)
    {
        $this->country = $country;
    }

    /**
     * Get city
     *
     * @return  self
     */ 
    public function getCity()
    {
        return $this->city;
    }

    /**
     * Set city
     *
     * @param  $city  
     */ 
    public function setCity($city)
    {
        $this->city = $city;
    }

    /**
     * Get gender ( "m" or "f")
     *
     * @return  self
     */ 
    public function getGender()
    {
        return $this->gender;
    }

    /**
     * Set gender ( "m" or "f")
     *
     * @param  $gender
     */ 
    public function setGender($gender)
    {
        $this->gender = $gender;
    }

    /**
     * Get array 
     * @return  self
     */ 
    public function getInterest()
    {
        return $this->interest;
    }

    /**
     * Set array 
     * @param  $interest Array 
     */ 
    public function setInterest($interest)
    {
        $this->interest[] = $interest;
    }

    /**
     * Get path picture user
     *
     * @return  self
     */ 
    public function getPicture()
    {
        return $this->picture;
    }

    /**
     * Set path picture user
     *
     * @param  $picture 
     */ 
    public function setPicture(Image $picture = null)
    {
        $this->picture = $picture;
    }

    /**
     * Get date subscribe
     *
     * @return  self
     */ 
    public function getFirstConnection()
    {
        return $this->firstConnection;
    }

    /**
     * Set date subscribe
     *
     */ 
    public function setFirstConnection($firstConnection)
    {
        $this->firstConnection = $firstConnection;
    }

    /**
     * Get date LastConnection
     *
     * @return  self
     */ 
    public function getLastConnection()
    {
        return $this->lastConnection;
    }

    /**
     * Set date LastConnection
     */ 
    public function setLastConnection($lastConnection)
    {
        $this->lastConnection = $lastConnection;
    }  

    /**
     * Get locale user give the language of Site
     */ 
    public function getLocale()
    {
        return $this->locale;
    }

    /**
     * Set locale user give the language of Site
     *
     */ 
    public function setLocale($locale)
    {
        $this->locale = $locale;
    }

    /**
     * Get token for confirm registration
     */ 
    public function getTokenRegister()
    {
        return $this->tokenRegister;
    }

    /**
     * Set token for confirm registration
     *
     * @return  self
     */ 
    public function setTokenRegister($tokenRegister)
    {
        $this->tokenRegister = $tokenRegister;

        return $this;
    }
}

Language

<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Table(name="language")
 * @ORM\Entity(repositoryClass="App\Repository\LanguageRepository")
 */
class Language {

    /**
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(name="code", type="string", length=10)
     */
    private $code;

    /**
     * Get the value of id
     */ 
    public function getId()
    {
        return $this->id;
    }

    /**
     * Get the value of code
     */ 
    public function getCode()
    {
        return $this->code;
    }

    /**
     * Set the value of code
     *
     * @return  self
     */ 
    public function setCode($code)
    {
        $this->code = $code;

        return $this;
    }
}

UserLanguage

<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="user_language")
 * @ORM\Entity(repositoryClass="App\Repository\UserLanguageRepository")
 */
class UserLanguage
{

    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="App\Entity\User")
     * @ORM\JoinTable(name="user_language")
     */
    protected $users;

    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="App\Entity\Language")
     * @ORM\JoinTable(name="user_language")
     */
    protected $languages;

    /**
     * @var int
     * @ORM\Column(name="level",type="integer")
     */
    private $level;

    /**
     * Get the value of level
     */ 
    public function getLevel()
    {
        return $this->level;
    }

    /**
     * Set the value of level
     *
     * @return  self
     */ 
    public function setLevel($level)
    {
        $this->level = $level;

        return $this;
    }

    /**
     * Get the value of users
     */ 
    public function getUsers()
    {
        return $this->users;
    }

    /**
     * Set the value of users
     *
     * @return  self
     */ 
    public function setUsers($user)
    {
        $this->users = $user;

        return $this;
    }

    /**
     * Get the value of languages
     */ 
    public function getLanguages()
    {
        return $this->languages;
    }

    /**
     * Set the value of languages
     *
     * @return  self
     */ 
    public function setLanguages($language)
    {
        $this->languages = $language;

        return $this;
    }
}

Bon pour l'instant je peux créer un user sans erreur, mais rien dans les tables user_language, ni dans language ! D'ailleurs dans la table user je vois rien en rapport avec language...

Ben, comme dit l'erreur, c'est pas quand tu créés un user mais quand tu lui ajoutes ou enlèves une langue! Vu que t'as seulement deux méthodes qui prennent une langue en argument... Donc, regarde toutes les occurrences d'utilisation de ces méthodes et trouves celle(s) auxquelles tu files une string au lieu d'un Language!

Je viens de modifier :-D

C'est un ORM que tu as développé? Perso, je trouverais ça plus simple d'avoir :

  • dans Language une proriété users de type array indexé par lvl, genre [1 => $users, 2 => $users, 3 => $users, 4 => $users]
  • dans User une propriété langs du même genre ^^
  • et du coup, une méthode
class User {

    public function addLang(Language $lang, int $lvl, boolean $addSelf) {

        $this->langs[$lvl][] = $lang;

        if ($addSelf) $lang->addUser($this, $lvl, false);
    }

Et le même genre dans Language!

J'utilise Doctrine avec symfony4 comme orm.

Pour la derniere idée, je suis pas chaud, car le level n'est pas propriete de user ni language mais bien de la relation user_language

La relation concerne les données! Pas le diagramme de classes... Faire un copier-coller de ton schéma de DB en tant que diagramme de classe est loin d'être la meilleure solution! En pratique, quand tu as une relation E1(1, 1) <-> (0, n)E2, dans ton diagramme de classe tu as C1 dans laquelle tu trouves une instance de C2 et C2 dans laquelle tu trouves la liste complète des instances de C1 lui correspondant ;-) Dans ton cas, la façon "no brain" de faire serait d'avoir C1 qui contient un tableau avec la langue en key et l'ensemble des données d'association en value et C2 qui contient un tableau avec le user en key et l'ensemble des données d'association en value! Mais vu qu'on a qu'une seule données d'association, on peut adapter la théorie afin d'avoir une pratique plus aisée :-p

Et le pourquoi? Eh bien parce que les DB imposent de stocker dans des fichiers et le langage SQL est optimisé pour ça! Alors qu'en programmation on manipule des références dans la ram, et des structures de données diverses...
Si tu tentes de faire du simili-SQL en PHP ça va simplement prendre 20 fois le temps nécessaire pour des requêtes pas trop compliquées :-s

D'ailleurs, j'ai l'impression que ton instinct a bien compris tout ça ;-) Dans ta classe User_Language tu as nommé tes propriétés au pluriel! Cependant chaque instance de cet objet ne contiendra qu'un couple User/Language! Du coup, pour trouver tous les users associés à un language il faudra parcourir la liste complète des users... Dans tous les cas ce sera beaucoup plus lent que si tu as déjà à disposition cette information dans ton instance de Language ^^

M'enfin bon, de toute façon, si tu utilises un ORM, il faut respecter les contraintes qu'il t'impose et garder à l'esprit qu'au final, le but d'un tel outil n'est clairement pas la rapidité mais plutôt la "delegation of concern"... Quand on a pas envie de s'occuper d'un truc, on accepte volontiers les conséquences :-)

Merci encore pour ces infos, pour la petite histoire, je suis en train de me former, et j'ai mon dernier projet à faire qui impose d'utiliser symfony, twig, orm etc...Mon idée est de refaire un site que j'avais fait avant de connaitre la poo (jaaser.com), avec tout ces nouveaux outils indispensable...Mais j'ai l'impression de meme pas arriver à faire un truc simple, que je faisais facilement en mode procéduriale. Bref De toute façon maitriser tout m'est indispensable.

Bon du coup, tes infos sont super contructives, mais je suis encore plus(+), à ne pas savoir comment faire mon systeme simple qu'un user peut avoir une langue ou plusieurs avaec chacune leur niveau....Et comment gérer tout ça (inscription, modif profil, etc)...

J'imagine que tu as des tutos sous le coude ;-) Construit ta couche ORM de la façon la plus simple possible, d'après ce que disent les tutos ^^

Pour ta DB, un lien (0, n) <-> (0, n) avec un lvl 0 à 3 me parait bien ^^ Avec 0 = native vu que le zéro peut avoir un status particulier dans le code, si besoin : "if (!$lvl)" donc native...

La POO c'est pas compliqué ^^ Renseigne-toi sur les design patterns, le polymorphisme, les notions de méthode d'instance et de méthode de classe, l'UML...

Salut Psylozoff, merci pour ton retour.
La poo oui j'ai bien compris, mais depuis symfony, je me mélange grave les pinceaux ...

A quel niveau?

PS : c'est compréhensible vu que l'idée de Symfony, comme tous les frameworks PHP, est de structurer un langage complètement bordellique pour en faire un outil d'apparence solide... ^^'

Dans le fait de jamais savoir comment faire un truc que je pouvais faire en php sans m'occuper de la machinerie symfony :D

T'as pas un exemple? Un exemple sur lequel tu butes actuellement de préférence ;-p Comme ça je te débloque!

Déjà mes relations entre table marche pas, quand j'enregistre un user, c'est ok, mais la table language et user_language est vide.
De plus je sens que j'ai la maitrise sur rien, je tatonne pour faire le moindre truc dans mon controller.

Tu cherches donc à enregistrer un user auquel sont associées des langues? Tes fichiers ont été autogénérés par Doctrine, c'est bien ça? Je ne connais pas du tout Doctrine mais ça ne doit pas être bien compliqué ;-p

Il est fort possible que, par défaut, l'ORM ne s'occupe pas des Languages si tu manipules un User! En gros, il faudrait que tes Languages existent déjà pour que l'ORM puisse faire les créations dans la table d'association... Mais je ne suis pas sûr de ce que je viens de dire ;-)