Setting up Data Objects

By the end of this lesson, you’ll have a practical understanding of how to prepare the database for storing data objects. Along the way, we’ll be:

  • Discussing the Database Abstraction Layer
  • Defining data objects
  • Altering the Database schema
Follow along using the code available from this GIT repository.
Start - creating_our_template
End - setting_up_data_objects

Doctrine

In this day and age, there is little reason for writing raw database queries. For the slight performance benefit that writing raw statements provides, in general you miss out on:

  • Supporting multiple database vendors with a single code set
  • Easy to read code that is simple to share and maintain
  • A series of helper functions that often save you having to write any extra code simplifying operations
  • Being forced to or automatically creating statements which use best practises. e.g. Automatic variable escaping, DRY (don't repeat yourself) style coding by keeping database operations in a library loaded on the side

Doctrine is a DBAL (DataBase Abstraction Layer) built for PHP and is the default choice with Symfony. Aside from the generic benefits listed above, with Symfony and Doctrine you also get:

  • Easy schema definition through annotations (or configuration files) which are to synchronised with a single command or migration scripts.
  • Automatic data change tracking so you don't have to pay special attention to what the Database changes are
  • Event listeners which allow you to easily run code at certain points in the database event cycle
  • The ability to use multiple connections at the same time or different databases depending on the environment

Configuration

If you have been following the tutorial up to this point (or using the Docker version) the database will already be configured so, this section will just highlight the section of the config file to change if you want to make any changes.

Note that in the example below, the values are enclosed in percentage signs. This is a form of variable substitution that will replace the key (what's written within the percentage signs) with a Symfony parameter as they are defined in the app/config/parameters.yml file.

  1. # app/config/config.yml
  2.  
  3. # Doctrine Configuration
  4. doctrine:
  5.    dbal:
  6.        driver:   pdo_mysql
  7.        host:     "%database_host%"
  8.        port:     "%database_port%"
  9.        dbname:   "%database_name%"
  10.        user:     "%database_user%"
  11.        password: "%database_password%"
Symfony configuration is covered in greater detail in the Ready to go User System tutorial.

Defining the Data

From this point forwards, Doctrine will be referred to as the ORM (Object Relational Mapper) because Doctrine itself also supports NoSQL (Document) driven databases and in terms of the namespaces used it makes sense to know what ORM stands for.

We'll be creating data objects which handle blog posts and comments associated with them as represented by the following diagram.

Database schema diagram

We define the schema as a simple class (called an entity) within the application space in the same way that you would use it in operation.

When using the Symfony Standard Edition, the framework comes configured to use annotations for the definitions; it is these annotations which create the bridge.

  1. <?php
  2. // src/AppBundle/Entity/Post.php
  3.  
  4. namespace AppBundle\Entity;
  5.  
  6. // Autoload the annotations for Data definition
  7. use Doctrine\ORM\Mapping as ORM;
  8. /**
  9. * @ORM\Entity
  10. * @ORM\Table(name="post")
  11. */
  12. class Post
  13. {
  14.    /**
  15.     * @ORM\Column(type="integer")
  16.     * @ORM\Id
  17.     * @ORM\GeneratedValue(strategy="AUTO")
  18.     */
  19.    private $id;
  20.    /**
  21.     * @ORM\Column(type="string", length=155)
  22.     */
  23.    private $title;
  24.    /**
  25.     * @ORM\Column(type="string", length=300)
  26.     */
  27.    private $summary;
  28.    /**
  29.     * @ORM\Column(type="text")
  30.     */
  31.    private $content;
  32.    /**
  33.     * @ORM\Column(type="datetime")
  34.     */
  35.    private $createdAt;
  36.    /**
  37.      * @ORM\OneToMany(targetEntity="Comment", mappedBy="post", orphanRemoval=true)
  38.      */
  39.    protected $comments;
  40.    /**
  41.     * @ORM\Column(type="integer", options={"default" : 0})
  42.     */
  43.    private $accessCounter = 0;
  44. }
  1. <?php
  2. // src/AppBundle/Entity/Post.php
  3.  
  4. namespace AppBundle\Entity;
  5.  
  6. // Autoload the annotations for Data definition
  7. use Doctrine\ORM\Mapping as ORM;
  8. /**
  9.  * @ORM\Entity()
  10.  * @ORM\Table(name="comment")
  11.  */
  12. class Comment
  13. {
  14.     /**
  15.      * @ORM\Column(name="id", type="integer")
  16.      * @ORM\Id
  17.      * @ORM\GeneratedValue(strategy="AUTO")
  18.      */
  19.     private $id;
  20.     /**
  21.      * @ORM\ManyToOne(targetEntity="Post", inversedBy="comments")
  22.      */
  23.     private $post;
  24.     /**
  25.      * @ORM\Column(type="string", length=100)
  26.      */
  27.     private $email;
  28.     /**
  29.      * @ORM\Column(name="body", type="text")
  30.      */
  31.     private $body;
  32.     /**
  33.      * @ORM\Column(type="datetime")
  34.      */
  35.     private $createdAt;
  36. }

Explaining the annotations

The Doctrine annotation mapper is imported with an alias of ORM, this is a standard practise and done because it is possible you could be writing definitions for MongoDB. At that point:

  • The schema is "initialised" above the class definition by using the Entity annotation and defining the table name.
  • Each column has a definition with at least a type given. It is possible to use @ORM\Integer (for example) as a column definition instead Column(type="integer") but with newer versions of PHP, this can cause error due to it being a reserved word. For writing future proof code, I would recommend spending the extra time to write the slightly longer definition.
  • Fields which act as IDs have a seperate definition. In this case (for the id field), another definition is put alongside it to indicate that it is auto-incrementing
  • Any standard extra options (such as length restrictions) relating to the column are also contained within the bracket definition
  • Relationships are also defined using annotations. In this case, we are creating a bi-directional relationship which means both sides are aware of each other. Whilst not entirely necessary to declare the relationship this way, I prefer the flexibility it provides.
  • Within the Post Comments definition, we have set the option of orphanRemoval to true. This means that if we delete the Post, all comments will be automatically removed. In some cases, you may want to do something else with the comments; look up the documentation on cascading or write some logic in to your code that will handle them prior to removal.

Altering the Database

With the data class defined, it is necessary to synchronise with the Database and actual create the tables. Shown below are two commands to do this.

  1. # Perform a check on the schema prior to updating
  2. php bin/console doctrine:schema:validate
  3.  # Perform the actual schema update
  4. php bin/console doctrine:schema:update --force

In the above case, the second command forces the schema change with an extra option on the end of the command. When making schema changes, there is the possibility that existing data will be altered and Doctrine puts a check in place to stop this.

For this tutorial, I have given this as the method for altering the schema because it is simple and quick, make a mental note that it isn't ideal in reality though. Database updates will be covered in greater depth in a later tutorial.

Exercises

  • Read up on the Doctrine annotations here
  • Try to create an entity using the built in generator
    1. php bin/console doctrine:generate:entity

Communication, our greatest tool

Whether you want to connect, have a problem I can help with or simply want to drop a line, I welcome your words!