Programmation PHP/Symfony/Doctrine

Un livre de Wikilivres.
Sauter à la navigation Sauter à la recherche


Installation[modifier | modifier le wikicode]

Doctrine est l'ORM par défaut de Symfony 3. Il utilise PDO. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la chaine de responsabilité.

Installation en SF4[1] :

composer require symfony/orm-pack
composer require symfony/maker-bundle --dev

Renseigner l'accès au SGBD dans le .env :

DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"

Ensuite la base de données doit être crée avec :

php bin/console doctrine:database:create
À faire...
link={{{link}}}

Différences avec :

  • composer require doctrine/orm
  • composer require doctrine/doctrine-bundle


Entity[modifier | modifier le wikicode]

Une entité est une classe PHP associée à une table de la base de données. Elle est composée d'un attribut par colonne, et de leurs getters et setters respectifs. Pour en générer une :

php bin/console generate:doctrine:entity

Cette association est définie par des annotations Doctrine. Voici par exemple plusieurs types d'attributs :

/*
* @ORM\Entity()
* @ORM\Table(name="word")
*/
class Word
{
    /*
    * @ORM\Id
    * @ORM\Column(name="id", type="integer", nullable=false)
    * @ORM\GeneratedValue(strategy="IDENTITY")
    */
    private $id;

    /*
    * @ORM\Column(name="pronunciation", type="string", nullable=true)
    */
    private $pronunciation;

    /**
     * @var Language
     *
     * @ORM\ManyToOne(targetEntity="Language", inversedBy="words")
     * @ORM\JoinColumn(name="language_id", referencedColumnName="id")
     */
    protected $language;

    /**
     * @var ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="Homophon", mappedBy="word", cascade={"persist", "remove"})
     */
    private $homophons;


    public function setPronunciation($p)
    {
        $this->pronunciation = $p;

        return $this;
    }

    public function getPronunciation()
    {
        return $this->pronunciation;
    }

    public function setLanguage($l)
    {
        $this->language = $l;

        return $this;
    }

    public function getLanguage()
    {
        return $this->language;
    }
}

On voit ici que la table "word" possède trois champs : "id" (clé primaire), "pronunciation" (chaine de caractère) et "language_id" (clé étrangère vers la table "language"). Doctrine stockera automatiquement l'id de la table "language" dans la troisième colonne quand on associera une entité "Language" à une "Word" avec $word->setLanguage($language).

Le quatrième attribut permet juste de récupérer les enregistrements de la table "homophon" ayant une clé étrangère pointant vers "word".

Les opérations en cascade peuvent utiliser deux des trois méthodes applicables aux entités par l'EntityManager :

  • persist() : SQL INSERT.
  • remove() : SQL DELETE.
  • flush() : SQL UPDATE.

De même, les types des attributs peuvent être quelque peu différents du SGBD[2].

Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware[3].

Attention !

Logo Dans le cas de jointure vers une entité d'un autre espace de nom (par exemple une table d'une autre base), il faut indiquer son namespace complet dans l'annotation Doctrine (car elle ne tient pas compte des "use").


Repository[modifier | modifier le wikicode]

On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de Doctrine\ORM\EntityRepository. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés NomDeLEntitéRepository.

SQL[modifier | modifier le wikicode]

Pour exécuter du SQL natif dans Symfony sans Doctrine, il faut créer un service de connexion, par exemple qui appelle PDO en utilisant les identifiants du .env, puis l'injecter dans les repos (dans chaque constructeur ou par une classe mère commune) :

return $this->connection->fetchAll($sql);

Depuis un repository Doctrine, tout ceci est déjà fait et les deux techniques sont disponibles :

1. Par l'attribut entity manager (em, ou _em pour les anciennes versions) hérité de la classe mère (le "use" permettra ici d'appeler des constantes pour paramétrer le résultat) :

use Doctrine\DBAL\Connection;
...
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();

return $statement;

2. En injectant le service de connexion dans le constructeur ('@database_connection') :

use Doctrine\DBAL\Connection;
...
return $this->dbalConnection->fetchAll($sql);

DQL[modifier | modifier le wikicode]

Méthodes magiques[modifier | modifier le wikicode]

Doctrine peut ensuite générer des requêtes SQL à partir du nom d'une méthode PHP appelée mais non écrite dans les repository (car ils en héritent). Ex :

  • $repo->findById($id) engendre automatiquement un SELECT * WHERE id = $id dans la table associée au repo.
  • $repo->findBy(['lastname' => $lastname, 'firstname' => $firstname]) engendre automatiquement un SELECT * WHERE lastname = $lastname AND firstname = $firstname.
  • $repo->findOneById($id) engendre automatiquement un SELECT * WHERE id = $id LIMIT 1.
  • $repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname]) engendre automatiquement un SELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1.

Les méthodes des repos font appel createQueryBuilder() :

public function findAllWithCalculus()
{
    return $this->createQueryBuilder('mon_entité')
        ->where('id < 3')
        ->getQuery()
        ->getResult()
    ;
}

Pour éviter le SELECT * dans cet exemple, on peut utiliser la méthode select().

Attention !

Logo Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "findById" pour le mock du repository. Il vaut donc mieux utiliser findBy().


Jointures[modifier | modifier le wikicode]

Quand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :

use Doctrine\ORM\Query\Expr\Join;
...
    ->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')

Résultats[modifier | modifier le wikicode]

Doctrine renvoie des objets avec leurs méthodes (get pas set) avec getResult, ou un tableau avec getArrayResult, ou 2D avec getScalar.

  • getResult() renvoie un objet ArrayCollection, pour rechercher dedans : ->contains().

Patrons à copier-coller[modifier | modifier le wikicode]

À faire...
link={{{link}}}


  • Connexion à chaque SGBD Doctrine : MSSQL + GUI Linux, MariaDB, Webdis, MySQL
  • Fonctions injectées avec $qb->expr()
  • transactional()


Migrations[modifier | modifier le wikicode]

Pour modifier la base de données avec une commande, par exemple pour ajouter une colonne à une table, il existe une bibliothèque qui s'installe comme suit :

composer require doctrine/doctrine-migrations-bundle

Ensuite, on peut créer un squelette de "migration" :

php bin/console doctrine:migrations:generate

Cette classe comporte une méthode "up()" qui réalise la modification en SQL ou DQL, et une "down()" censée faire l'inverse à des fins de rollback. De plus, on ne peut pas lancer deux fois de suite le "up()" sans un "down()" entre les deux.

On les exécute selon le paramètre, avec la partie variable du nom du fichier de la classe (timestamp) :

php bin/console doctrine:migrations:execute --up 20170321095644'

Critique[modifier | modifier le wikicode]

  1. Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures).
  2. Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le lazy loading est contourné par au choix :
    1. Avant l'appel null, un ObjetJoint->get().
    2. Dans l'entité, un @ManyToOne(…, fetch="EAGER").
    3. Dans le repository, un $this->queryBuilder->addSelect().
  3. Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
  4. Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
  5. Attention au ->setMaxResult() en cas de jointure car il ne conserve que le nombre d'enregistrements de la première table.
  6. L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
  7. Pas de hints.
  8. Bug des UNION ALL quand on joint deux entités non liées dans le repo.

Références[modifier | modifier le wikicode]