Skip to content

Data Modeling

We are modeling a simple blog system, it includes the following concepts.

  • A Post presents an article post in the blog system.
  • A Comment presents the comments under a specific post.
  • The common Tag can be applied on different posts, which categorizes posts by topic, categories, etc.

You can draft your model relations in your mind or draw it in graph data modeling tools, such as draw.io, etc.

  • Post and comments is a one-to-many relation
  • Post and tag is a many-to-many relation

It is easy to convert the idea to real codes via Doctrine Entity.

An Entity Example

A Doctrine entity is very similar to the JPA Entity class.

#[Entity()]
class Post
{
    #[Id]
    #[GeneratedValue(strategy: "AUTO")
    #[Column(type: "integer")]
    private ?int $id;

    #[Column(type: "string", length: 255)]
    private string $title;

    #[Column(type: "string", length: 255)]
    private string $content;
}

An entity is annotated with Entity attribute, optionally you can setup another Table attribute to configure backed table metadata.

An entity should contain an identifier field, annotated wtih a Id attribute. Together with Id, the GeneratedValue is used to identifier generation before inserting into database tables.

A Column attribute is used to configure the column metadata in the backed table.

Identifier Generation Strategy

The GeneratedValue attribute contains an optional parameter strategy which is used to set the name of identifier generation strategy. Valid values are AUTO, SEQUENCE, IDENTITY, UUID (deprecated), CUSTOM and NONE. If not specified, the default value is AUTO.

Since Doctrine ORM 2.10.0 and Dbal 3.0, the deprecated UUID strategy does not work. We will switch to the CUSTOM strategy and the UuidGenerator form symfony\uid.

Install symfony\uid firstly.

$ composer require symfony/uid

Use the following instead of the legacy UUID strategy.

#[GeneratedValue(strategy: "CUSTOM")]
#[CustomIdGenerator(class: UuidGenerator::class)]
#[Column(type: "uuid", unique: true)]
private ?Uuid $id = null;

Next, let's create Post, Comment and Tag entities.

Creating Entities

Run the following command, and follow the interactive steps to create Post, Comment and Tag one by one.

$ php bin/console make:entity

Finally we got three entities in the src/Entity folder.

Change them as you expected.

// src/Entity/Post.php
#[Entity(repositoryClass: PostRepository::class)]
class Post
{
    #[Id]
    //#[GeneratedValue(strategy: "UUID")
    //#[Column(type: "string", unique: true)]
    #[Column(type: "uuid", unique: true)]
    #[GeneratedValue(strategy: "CUSTOM")]
    #[CustomIdGenerator(class: UuidGenerator::class)]
    private ?Uuid $id = null;

    #[Column(type: "string", length: 255)]
    private string $title;

    #[Column(type: "string", length: 255)]
    private string $content;

    #[Column(name: "created_at", type: "datetime", nullable: true)]
    private DateTime|null $createdAt = null;

    #[Column(name: "published_at", type: "datetime", nullable: true)]
    private DateTime|null $publishedAt = null;

    #[OneToMany(mappedBy: "post", targetEntity: Comment::class, cascade: ['persist', 'merge', "remove"], fetch: 'LAZY', orphanRemoval: true)]
    private Collection $comments;

    #[ManyToMany(targetEntity: Tag::class, mappedBy: "posts", cascade: ['persist', 'merge'], fetch: 'EAGER')]
    private Collection $tags;

    public function __construct()
    {
        $this->createdAt = new DateTime();
        $this->comments = new ArrayCollection();
        $this->tags = new ArrayCollection();
    }
    //other getters and setters
}

// src/Entity/Comment.php
#[Entity(repositoryClass: CommentRepository::class)]
class Comment
{
    #[Id]
    //#[GeneratedValue(strategy: "UUID")]
    #[Column(type: "uuid", unique: true)]
    #[GeneratedValue(strategy: "CUSTOM")]
    #[CustomIdGenerator(class: UuidGenerator::class)]
    private ?Uuid $id = null;

    #[Column(type: "string", length: 255)]
    private string $content;

    #[Column(name: "created_at", type: "datetime", nullable: true)]
    private DateTime|null $createdAt = null;

    #[ManyToOne(targetEntity: "Post", inversedBy: "comments")]
    #[JoinColumn(name: "post_id", referencedColumnName: "id")]
    private Post $post;

    public function __construct()
    {
        $this->createdAt = new DateTime();
    }
    //other getters and setters
}

//src/Entity/Tag.php
#[Entity(repositoryClass: TagRepository::class)]
class Tag
{
    #[Id]
    //#[GeneratedValue(strategy: "UUID")
    //#[Column(type: "string", unique: true)]
    #[Column(type: "uuid", unique: true)]
    #[GeneratedValue(strategy: "CUSTOM")]
    #[CustomIdGenerator(class: UuidGenerator::class)]
    private ?Uuid $id = null;

    #[Column(type: "string", length: 255)]
    private ?string $name;

    #[ManyToMany(targetEntity: Post::class, inversedBy: "tags")]
    private Collection $posts;

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

Note, the one-to-many and many-to-many relations we use a Collection and initialize a ArrayCollection in the constructor to maintain the relations. The mappedBy, targetEntity, cascade, orphanRemoval, and fetch are easy if you have used Hibernate/JPA before. More details, please read the OneToMany and ManyToMany section in the Doctrine attribute reference.

At the same time, it generated three Repository classes for these entities.

// src/Repository/PostRepsoitory.php
class PostRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Post::class);
    }
}

// src/Repository/CommentRepsoitory.php
class CommentRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Comment::class);
    }
}

//src/Repository/TagRepository.php
class TagRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Tag::class);
    }
}

You can use Doctrine migration to maintain database schema in development and production environments.

Doctrine Migration

Run the following command to generate a Migration file.

$ php bin/console make:migration

Finally a Migration file is generated in the /migrations folder, the naming is like Version20211104031420. It is a simple class extended AbstractMigration, there is a up function is used to upgrade to this version and the down function is used to downgrade to the previous version.

There are some commands to apply these migrations scripts on database automatically.

# migrate to the latest
$ php bin/console doctrine:migrations:migrate

# return to prev version
$ php bin/console doctrine:migrations:migrate prev

# migrate to next
$ php bin/console doctrine:migrations:migrate next

# These alias are defined : first, latest, prev, current and next

# certain version fully qualified class name
$ php bin/console doctrine:migrations:migrate FQCN

Doctrine bundle also includes some command to maintain database and schema. eg.

# create and drop database
$ php bin/console doctrine:database:create
$ php bin/console doctrine:database:drop

# schema create, drop, update and validate
$ php bin/console doctrine:schema:create
$ php bin/console doctrine:schema:drop
$ php bin/console doctrine:schema:update
$ php bin/console doctrine:schema:validate

As you see, these commands are used to execute small tasks, esp, performing some administration tasks. You can create your own command.