Objective

Learn the basic concepts of Doctrine ORM, implementing the Data Mapper pattern with php.

Requirements

  • Composer (php package manager)
  • A working lamp setup
  • Understanding the basic object oriented programming and php
  • Understanding the basic database concepts

Difficulty

MEDIUM

Conventions

  • # - requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command
  • $ - requires given linux commands to be executed as a regular non-privileged user

Introduction

The data mapper pattern is an architectural pattern by means of which it's possible to achieve separation between a data persistence layer (in this case a mysql database) and an in-memory data representation (in this case php objects), so that the two layers can be separated and completely unaware of each other, thus respecting separation of concerns.

In this tutorial we will see how to take our first steps with Doctrine, a data mapper pattern implementation that is part of the Symfony php framework, but can also be used on its own.

The Database creation

Before anything else, we should create the database we will use for data persistence. In this tutorial we will represent an user and its posts in a blog:
MariaDB [(none)]> CREATE DATABASE blog;
MariaDB [(none)]> GRANT ALL PRIVILEGES ON blog.* TO 'testuser'@'localhost' IDENTIFIED BY 'testpassword';
MariaDB [(none)]> FLUSH PRIVILEGES;
MariaDB [(none)]> exit;

Install and initialize Doctrine

The next step in our journey will be the installation of Doctrine: we will use composer, the php package and dependency manager. In the root of our project we create the composer.json file, specifying doctrine/orm as a dependency:
{
    "require": {
        "doctrine/orm": "^2.6"
    }
}
Now, to proceed with the installation, while in the same directory, open a terminal and run:
$ composer install
Composer will install Doctrine and all its dependencies inside the vendor directory that it will create. Once Doctrine is installed, we need to initialize it. Save the code below into a file (for this tutorial, we will call it bootstrap.php):
<?php
require_once "vendor/autoload.php";

// Setup Doctrine
$configuration = Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(
    $paths = [__DIR__ . '/entities'],
    $isDevMode = true
);

// Setup connection parameters
$connection_parameters = [
    'dbname' => 'blog';
    'user' => 'testuser',
    'password' => 'testpassword',
    'host' => 'localhost',
    'driver' => 'pdo_mysql'
];

// Get the entity manager
$entity_manager = Doctrine\ORM\EntityManager::create($connection_parameters, $configuration);
First of all we required in Line 2 the composer autoload file autoload.php, which takes care of autoloading the needed libraries.

By calling the createAnnotationMetadataConfiguration static method of the Setup class in Line 5, we started to setup Doctrine. This method takes 5 arguments, but we will provide only the first two, leaving the rest to their defaults, since we are not interest in them at the moment. The First argument in Line 6 is an array of paths where Entity classes are to be found in our project. An entity is a class that represents a row in the database (the in-memory rapresentation we mentioned above): in our example we will use two entities: Author and Post.

The second argument in Line 7 takes a boolean value, and defines whether we are working in "dev" mode or not. This defines the Doctrine behavior about proxy objects and caching: when in "dev" mode, proxy objects will be regenerated on each request and caching will happen in memory, because it is assumed that during development, changes will happen very often. We will set it to true for now.

After that, we must specify the connection parameters in Lines 11-16, in the form of an associative array containing, in order, the database name, database user, database password, the database host, and the driver to use to access the database. It's important to notice that at a lower level, Doctrine uses PDO to interact with the database, and it's designed to be database-agnostic.

Finally we created an instance of the EntityManager object in Line 20, calling the factory method "create" of the EntityManager class, passing the array of connection information we just defined as first parameter, and the Configuration object as the second one. The EntityManager object will give us access to all our entities, and make us able to easily manage their persistence and lifecycle.

Creating our entities

It's time to create our entities. Just like we stated in the configuration, we are going to create an 'entities' directory in the root of our project to store our entities. The first entity we are going to define is Author:
<?php
namespace entities;

/**
 * @Entity
 * @Table(name="author")
 */
class Author
{
    /**
     * @Id
     * @GeneratedValue
     * @Column(type="smallint")
     */
    private $id;

    /**
     * @Column(type="string")
     */
    private $first_name;

    /**
     * @Column(type="string")
     */
    private $last_name;
}
We defined our first, very simple, entity. We used annotations to give Doctrine the necessary information to handle it. First in Line 5, using, @Entity we are telling Doctrine that the class must be considered an entity, which will be persisted in the author database table. In this case we used the @Table(name="author") annotation in Line 6 to specify this, however in this situation it is redundant, and we could have omitted it completely: it is optional, and if not used, the entity will be persisted in a table named after the unqualified class name.

Each property of the class corresponds to a column in the table, and we must provide information about the table data type. The $id property, for example, represents the primary key of the table: we state this by using the @Id annotation in Line 11.

The value of the id column will be auto-generated, that's why we used the @GeneratedValue annotation in Line 12. It has sense only when associated with @id, and by using it, it's even possible to specify the generation strategy to adopt (if none is specified, it will default to AUTO).

The datatype used for our primary key, will be SMALLINT, which we defined via the @Column(type="smallint") annotation in Line 13. The other two properties are $first_name and $last_name, and they are defined with the same technique. They are of type string: when using mysql, it will be translated to the VARCHAR database data type. For a complete reference about data type associations, you can consult this page.

When using Doctrine the visibility of the properties of an entity class can be either protected or private but not public.

We didn't define getters and setters for the class yet. There is no need to do that manually, since Doctrine can do it for us, and we will see how in a moment, we still have another entity to define, Post:
<?php
namespace entities;

/**
 * @Entity
 * @Table(name="post")
 */
class Post
{
    /**
     * @Id
     * @GeneratedValue
     * @Column(type="smallint")
     */
    private $id;

    /**
     * @Column(type="string")
     */
    private $title;

    /**
     * @Column(type="text")
     */
    private $text;

    /**
     * @Column(type="datetime")
     */
    private $date;
}
We introduced two new data types. The first one is text in Line 23 which will map and convert string data without a maximum length: when using mysql, it will be converted to the LONGTEXT data type. The second one is datetime in Line 28, for our $date property. It will be translated into the same type for mysql, and in an instance of php's DateTime object.

Now we can generate our getters and setters but before we do that we must create the cli-config.php script in the root of our project: it is needed in order to use doctrine from command line:
<?php
use Doctrine\ORM\Tools\Console\ConsoleRunner;
require_once 'bootstrap.php';

return ConsoleRunner::createHelperSet($entity_manager);
Now, open a terminal shell in the root directory of the project, and execute the following linux command:
$ php vendor/bin/doctrine orm:generate-entities .
The command above will generate getters and setters for the found entities, and will place them inside the specified directory. Now, if we take a look at the Author entity we can see that getters and setters have been generated:
<?php
namespace entities;

/**
 * @Entity
 * @Table(name="author")
 */
class Author
{
    /**
     * @Id
     * @GeneratedValue
     * @Column(type="smallint")
     */
    private $id;

    /**
     * @Column(type="string")
     */
    private $first_name;

    /**
     * @Column(type="string")
     */
    private $last_name;

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

    /**
     * Set firstName.
     *
     * @param string $firstName
     *
     * @return Author
     */
    public function setFirstName($firstName)
    {
        $this->first_name = $firstName;

        return $this;
    }

    /**
     * Get firstName.
     *
     * @return string
     */
    public function getFirstName()
    {
        return $this->first_name;
    }

    /**
     * Set lastName.
     *
     * @param string $lastName
     *
     * @return Author
     */
    public function setLastName($lastName)
    {
        $this->last_name = $lastName;

        return $this;
    }

    /**
     * Get lastName.
     *
     * @return string
     */
    public function getLastName()
    {
        return $this->last_name;
    }
}
The same has happened for the Post entity:
<?php
namespace entities;

/**
 * @Entity
 * @Table(name="post")
 */
class Post
{
    /**
     * @Id
     * @GeneratedValue
     * @Column(type="smallint")
     */
    private $id;

    /**
     * @Column(type="string")
     */
    private $title;

    /**
     * @Column(type="text")
     */
    private $text;

    /**
     * @Column(type="datetime")
     */
    private $date;

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

    /**
     * Set title.
     *
     * @param string $title
     *
     * @return Post
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Get title.
     *
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Set text.
     *
     * @param string $text
     *
     * @return Post
     */
    public function setText($text)
    {
        $this->text = $text;

        return $this;
    }

    /**
     * Get text.
     *
     * @return string
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Set date.
     *
     * @param \DateTime $date
     *
     * @return Post
     */
    public function setDate($date)
    {
        $this->date = $date;

        return $this;
    }

    /**
     * Get date.
     *
     * @return \DateTime
     */
    public function getDate()
    {
        return $this->date;
    }
}

Defining the relationship between the entities

For our example, we want to define a bidirectional one to many relationship between our entities, where bidirectional means that each entity holds a reference to the other. The relationship between an Author and its posts, is many-to-one (an author can write many posts and many posts can belong to one author). Using Doctrine, defining such an association is very simple:
<?php

/**
 * @Entity
 * @Table(name="author")
 */
class Author
{
    [...]

    /**
     * One author can write many post
     * @OneToMany(targetEntity="Post", mappedBy="author", cascade={"all"})
     * @var Doctrine\Common\Collection\ArrayCollection
     */
    private $posts;

    [...]
}

// Post.php
/**
 * @Entity
 * @Table(name="post")
 */
class Post
{
    [...]

    /**
     * Many posts belong to one author
     * @ManyToOne(targetEntity="Author", inversedBy="posts")
     * @JoinColumn(name="author_id", referencedColumnName="id", nullable=false)
     * @var \entities\Author
     */
    private $author;

    [...]
}
We added one new property in each entity. In Author, it's $posts in Line 16, and in the Post entity, $author in Line 36. What kind of data type will those variables hold? The first one, $posts will be an instance of Doctrine's ArrayColletion object: it is a special class used to better manage collection of entities.

The second one, $author, in Post.php, will be an instance of the Author entity, representing the author of the post: as said before, each entity holds a reference to the other.

Similarly to what we did for the other properties, we defined the relationship by using annotations. In our case, since we are dealing with a bidirectional one-to-many relation, we used the @OneToMany annotation in Line 13, in the Author entity, and @ManyToOne in Line 32 in Post.

In both cases, with TargetEntity we defined what Entity the property points to. For example in the case of the Author's $posts property, the target entity is Post. As you can see, we used respectively the inversedBy and mappedBy annotations. These annotations are used to tell Doctrine what property, in the other side of the relationship, refers to the object: inversedBy must be used in the side which owns the FOREIGN KEY, (in this case the Post entity).

As you can see, in Author, we used mappedBy, specifying that in the target entity Post, the corresponding property is $author. We also introduced a new parameter, cascade, setting it to "all". This means that by persisting or removing the entity from the database, all its posts will also be influenced: for example, deleting a user will also cause the deletion of all its posts. Is what we define via ON DELETE CASCADE in SQL code.

Vice versa, in the Post entity, which holds the FOREIGN KEY in the database, we used inversedBy, telling Doctrine that in the target entity Author, the property which refers to the object is posts. We have also used the @JoinColumn annotation in Line 33, specifying the columns involved in the SQL JOIN, setting the foreign key as not nullable (NOT NULL).

Once the relationship between the two entities is defined, we must update the methods needed to manage the added properties. Again we just run:
$ php vendor/bin/doctrine orm:generate-entities .

Generate the database schema

In our example, we have enough data to be able to generate our database schema. Again, Doctrine can help us, by automatically generate it based on our annotations. All we need to do, is run the following linux command:
$ php vendor/bin/doctrine orm:schema-tool:update --force
If all goes well, the database tables will be generated, let's verify it:
MariaDB [(none)]> DESCRIBE blog.author;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | smallint(6)  | NO   | PRI | NULL    | auto_increment |
| first_name | varchar(255) | NO   |     | NULL    |                |
| last_name  | varchar(255) | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

MariaDB [(none)]> DESCRIBE blog.post;
+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| id        | smallint(6)  | NO   | PRI | NULL    | auto_increment |
| author_id | smallint(6)  | NO   | MUL | NULL    |                |
| title     | varchar(255) | NO   |     | NULL    |                |
| text      | longtext     | NO   |     | NULL    |                |
| date      | datetime     | NO   |     | NULL    |                |
+-----------+--------------+------+-----+---------+----------------+
As expected the tables corresponding to our Entity have been generated, and reflects the annotations we specified. The SQL code used to generate them it's respectively:
MariaDB [(none)]> Show CREATE TABLE blog.author;
    Table: author
Create Table: CREATE TABLE `author` (
  `id` smallint(6) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `last_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

MariaDB [(none)]> Show CREATE TABLE blog.post;
    Table: post
Create Table: CREATE TABLE `post` (
  `id` smallint(6) NOT NULL AUTO_INCREMENT,
  `author_id` smallint(6) NOT NULL,
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `text` longtext COLLATE utf8_unicode_ci NOT NULL,
  `date` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `IDX_5A8A6C8DF675F31B` (`author_id`),
  CONSTRAINT `FK_5A8A6C8DF675F31B` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

Using the entity manager

Now it's time show how to use the entity manager:
<?php
require "bootstrap.php";
require "entities/Author.php";
require "entities/Post.php";

// Create and persist a new Author
$author = (new entities\Author())
    ->setFirstName("John")
    ->setLastName("Smith");

$entity_manager->persist($author);

// Create a new post
$post = (new entities\Post())
    ->setTitle("Hello Wold")
    ->setText("This is a test post")
    ->setAuthor($author)
    ->setDate(new DateTime());

// Add the post the to list of the Author posts. Since we used cascade={"all"}, we
// don't need to persist the post separately: it will be persisted when persisting
// the Author
$author->addPost($post);

// Finally flush and execute the database transaction
$entity_manager->flush();
Executing this code we created an Author and its first post, then we added the post to the Author's post collection and finally we persisted them to the database. With the persist() method we are telling Doctrine to manage the entity, while the actual database transaction happens only when calling flush(). If we now take a look at the author and post table, we can see that a new record exists in both:
MariaDB [(none)]> SELECT * FROM blog.author;
+----+------------+-----------+
| id | first_name | last_name |
+----+------------+-----------+
|  1 | John       | Smith     |
+----+------------+-----------+

MariaDB [(none)]> SELECT * FROM blog.post;
+----+-----------+------------+---------------------+---------------------+
| id | author_id | title      | text                | date                |
+----+-----------+------------+---------------------+---------------------+
|  1 |         1 | Hello Wold | This is a test post | 2018-04-17 08:37:44 |
+----+-----------+------------+---------------------+---------------------+
We also can use the entity manager to retrieve an existing entity, for example:
// Retrieve the author by its last name
$author = $entity_manager->getRepository('entities\Author')->findOneBy(['last_name' => 'Smith']);

Conclusions

The aim of this tutorial was to introduce you to the data mapper pattern in php using Doctrine: we saw how to configure and obtain an entity manager, how to define two basic entities and define a common relationship between them via annotations.

Doctrine is a very powerful library: you can use the project documentation to begin mastering it, hopefully this could be a minimal starting point.
Please refer all article related questions to:

You may also be interested in: