Doctrine DBAL et ORM

Doctrine-Project est une suite de bibliothèques dont la fonction première est la manipulation de données de base de données.

Doctrine DBAL (DataBase Abstraction Layer) et Doctrine ORM (Object Relational Mapper).

On distingue 2 projets Doctrine :

Qu’est-ce qu’un ORM ?


Entities

Les entités Doctrine se déclarent par des attributs (ou annotations).

L’entité est une classe qui, une fois instanciée, représente une entrée dans la base de données.

Prenons par exemple la classe User. Les conventions de nommage nous dit de stocker l’entité dans un dossier Entity.

Pour la suite, imaginons que les propriétés ont toutes des getters/setters déclarés. Id n’a pas de setter.

namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;

class User
{
    private ?int $id = null;
    private ?string $name = null;
    private ?int $age = null;
    private ?\DateTimeImmutable $createdAt = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }

    public function getAge(): ?int
    {
        return $this->age;
    }
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\Column(type: "integer")]
    private ?int $age = null;

    #[ORM\Column]
    private ?\DateTimeImmutable $createdAt = null;

...
}

En exécutant la requête de création/mise à jour du schéma de la base (dans cet exemple SQLite), on obtient le code suivant :

CREATE TABLE "user"
(
    id         INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    name       VARCHAR(255) NOT NULL,
    age        INTEGER      NOT NULL,
    created_at DATETIME     NOT NULL --(DC2Type:datetime_immutable)
);

Si on souhaite supprimer une table, il suffit de supprimer son entité et de détacher les différents liens avec la table en question.


Types Existants

Une liste des types que l’on peut utiliser en temps que type de colonne :

Types principaux

  • string
  • text
  • boolean
  • integer (or smallint, bigint)
  • float
  • enum

Date/Time Types

  • datetime (datetime_immutable)
  • datetimetz (datetimetz_immutable)
  • date (date_immutable)
  • time (time_immutable)
  • dateinterval

Tableaux/Objets

  • array (or simple_array)
  • json

Other Types

  • ascii_string
  • decimal
  • guid

Relations

Avant de continuer, on va créer une entité Post avec ses getters/setters :

#[ORM\Entity()]
class Post
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(type: Types::TEXT)]
    private ?string $content = null;
}

Un post a un auteur, et un auteur peut publier plusieurs posts. Dans la classe Post on va ajouter une propriété nommée author.

...
#[ORM\ManyToOne(inversedBy: 'posts')]
#[ORM\JoinColumn(nullable: false)]
private ?User $author = null;

public function getAuthor(): ?User
{
    return $this->author;
}

public function setAuthor(?User $author): self
{
    $this->author = $author;
    return $this;
}


Si on récupère un Post, en appelant la méthode getAuthor(), on obtiendra un objet de type User.

Mais on aimerait récupérer tous les Posts d’un utilisateur aussi simplement :

#[ORM\OneToMany(mappedBy: 'author', targetEntity: Post::class, orphanRemoval: true)]
private Collection $posts;

public function __construct()
{
    $this->posts = new ArrayCollection();
}

public function getPosts(): Collection
{
    return $this->posts;
}

public function addPost(Post $post): self
{
    if (!$this->posts->contains($post)) {
        $this->posts->add($post);
        $post->setAuthor($this);
    }

    return $this;
}

On peut également rajouter 2 méthodes pour se faciliter la vie :

public function addPost(Post $post): self
{
    if (!$this->posts->contains($post)) {
        $this->posts->add($post);
        $post->setAuthor($this);
    }
    return $this;
}
public function removePost(Post $post): self
{
    if ($this->posts->removeElement($post)) {
        // set the owning side to null (unless already changed)
        if ($post->getAuthor() === $this) {
            $post->setAuthor(null);
        }
    }
    return $this;
}

En exécutant la mise à jour de la base, les requêtes exécutées sont les suivantes :

CREATE TABLE post
(
    id        INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    author_id INTEGER      NOT NULL,
    title     VARCHAR(255) NOT NULL,
    content   CLOB         NOT NULL,
    CONSTRAINT FK_5A8A6C8DF675F31B
        FOREIGN KEY (author_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE
);

CREATE INDEX IDX_5A8A6C8DF675F31B ON post (author_id);

En continuant sur cet exemple, on dira qu’un Post peut être liké par plusieurs utilisateurs, qui eux-même peuvent liker plusieurs posts.

Entité User

#[ORM\ManyToMany(targetEntity: Post::class, mappedBy: 'likers')]
private Collection $likes;

public function __construct()
{
    $this->posts = new ArrayCollection();
    $this->likes = new ArrayCollection();
}

public function getLikes(): Collection
{
    return $this->likes;
}

public function addLike(Post $post): self
{
    if (!$this->likes->contains($post)) {
        $this->likes->add(post);
        $post->addLiker($this);
    }

    return $this;
}

public function removeLike(Post $post): self
{
    if ($this->likes->removeElement($post)) {
        $post->removeLiker($this);
    }

    return $this;
}

Entité Post

#[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'likes')]
private Collection $likers;

public function __construct()
{
    $this->likers = new ArrayCollection();
}

public function getLikers(): Collection
{
    return $this->likers;
}

public function addLiker(User $user): self
{
    if (!$this->likers->contains($user)) {
        $this->likers->add($user);
    }

    return $this;
}

public function removeLiker(User $user): self
{
    $this->likers->removeElement($user);

    return $this;
}

CREATE TABLE post_user
(
    post_id INTEGER NOT NULL,
    user_id INTEGER NOT NULL,
    PRIMARY KEY (post_id, user_id),
    CONSTRAINT FK_44C6B1424B89032C
        FOREIGN KEY (post_id)
            REFERENCES post (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE,
    CONSTRAINT FK_44C6B142A76ED395
        FOREIGN KEY (user_id)
            REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
);

CREATE INDEX IDX_44C6B1424B89032C ON post_user (post_id);
CREATE INDEX IDX_44C6B142A76ED395 ON post_user (user_id);


Exemple :

$user = new User();
$user->setName("Henri");
$user->setAge(33);
$user->setCreatedAt(new \DateTime());

$post = new Post();
$post->setTitle("Cours de Symfony")
		 ->setContent("Some awesome content is coming")
     ->setAuthor($user);

$users = $post->getLikers();

Manipulation d’entités

La manipulation des entités se fait via l’entityManager Doctrine\ORM\EntityManagerInterface.

Cet objet a 4 méthodes indispensables (et pleins d’autres dont on ne parlera pas) :

  • persist($entity)
  • remove($entity)
  • flush()
  • getRepository($entityClassName)

Chargement des données

Un Repository est une classe liée directement au modèle. Dans les conventions de code, elle se nomme NomdeLEntite Repository et se trouve dans un dossier Repository.

#[ORM\Entity(repositoryClass: UserRepository::class)]

Le Repository doit hériter de la classe : Doctrine\ORM\EntityRepository

namespace App\Repository;

use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository
{
}

Une fois le lien fait entre entité et repository, on peut récupérer le repository de cette manière :

$repository = $entityManager->getRepository(User::class);

La classe parente est équipée de 4 méthodes pratiques :

$repository->findAll(); //renvoie toute la table
$repository->find($id); //récupère l'entité par son id
$repository->findBy(["field"=>"value"]); 
//récupère des entités par un ou plusieurs champs.
$repository->findOneBy(["field"=>"value"]);

Par exemple :

$me = $repository->findOneBy(["id"=>1]);

Pour plus de souplesse, on peut définir nos propres méthodes dans le Repository et utiliser le Query Builder - Dans les cas où les fonctions de base ne suffisent pas.

public function findByAuthorAge(int $age)
{
    return $this->createQueryBuilder('p')
        ->join("p.author", "a")
        ->where("a.age = :age")
        ->setParameter("age", $age)
        ->getQuery()
        ->getResult();
}

Editer

Pour modifier une entité existante, il faut d’abord la récupérer puis la modifier en utilisant les setters :

$repository = $entityManager->getRepository(User::class);
$me = $repository->find(1);
$me->setAge(32);

Ajouter

Lors de la création d’un objet de type Entity, cet objet n’est pas reconnu par Doctrine. La méthode de liaison est la méthode**persist()**

$user = new User();
$user->setName("Henri");
$user->setAge(31);
$user->setCreatedAt(new \DateTime());
$entityManager->persist($user);
.

Supprimer

Pour supprimer une entité, il faut la récupérer puis dire à l’entity manager de la supprimer.

$repository = $entityManager->getRepository(User::class);
$me = $repository->find(1);

$entityManager->remove($me);

Mettre à jour la base de données

Une fois les changements effectués sur les entités, il faut appliquer ses modifications, en appelant la méthode :

$entityManager->flush();