Entity Class

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Doctrine is an ORM: an object relational mapper. That's a fancy way of saying that, for each table in the database, we will have a corresponding class in PHP. And for each column on that table, there will be a property in that class. When you query for a row in a table, Doctrine will give you an object with that row's data set on the properties.

So if you want to create a database table in Doctrine, the way you do that is actually by creating the class it will map to. These "classes" that map to tables have a special name: entity classes.

make:entity

You can create an entity class by hand - there's nothing very special about them. But... come on! There's a much easier way. Find your terminal and run one of my favorite bin/console commands:

php bin/console make:entity

You can also run:

symfony console make:entity

It doesn't matter in this case, because this command won't talk directly to the database, and so, it doesn't need the environment variables. This command just generates code.

Now remember: the whole point of our site is for witches and wizards to ask questions and then post answers. So the very first thing we need is a Question entity. So, a question table in the database.

Enter Question. The command immediately starts asking what properties we want, which really, also means what columns we want in the table. Let's add a few. Call the first one name - that will be the short name or "title" of the question like "Reversing a Spell". The command then asks what "type" we want. Doctrine has its own type system: enter "?" to see the full list.

These aren't MySQL column types, but each maps to one. For example, a string maps to a VARCHAR in MySQL. And there are a bunch more.

In our case, choose string: that's good for any text 255 characters or less. For the column length, I'll use the default 255. The next question says:

Can this field be nullable in the database?

Say "No". This means that the column will be required in the database.

And... congrats! We just added our first field! Let's add a few more. Call the next slug: this will be the URL-safe version of the name that shows up in the URL. It will also be a string, let's set its length to 100 and, once again, I'll say "no" to "nullable".

For the actual body of the question, let's call it question. This will be long, so we can't use the string type. Instead, use text... and make this also required in the database.

Add one more field: askedAt. This will be a date field - kind of like a "published at" field. For the type, ooh! It recommends datetime - that is exactly what we want! Hit enter and this time say "yes" to nullable. The idea is that users will be able to start writing a question and save it to the database with a null askedAt because it's not finished. When the user is finished and ready to post it, then we would set that value.

And... we're done! Hit enter one more time to exit make:entity.

Hello Entity Class

So... what did this do? Well, first, I'll tell you that this made absolutely no changes to our database: we do not have a question database table yet.

If you scroll back up to the top of the command, you can see that it created 2 files: src/Entity/Question.php and src/Repository/QuestionRepository.php.

For now, completely ignore the repository class: it's not important yet and we'll talk about it later.

This entity class, however, is super important. Go open it up: src/Entity/Question.php.

... lines 1 - 2
namespace App\Entity;
use App\Repository\QuestionRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=QuestionRepository::class)
*/
class Question
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="string", length=100)
*/
private $slug;
/**
* @ORM\Column(type="text")
*/
private $question;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $askedAt;
... lines 39 - 91
}

As we talked about, we're going to have one class per table.

The first thing I want you to notice is that... there's nothing special about this class. It's a boring, normal class... it doesn't even extend a base class! It has several private properties and, to access those, the command generated getter and setter methods, like getName(), getSlug() and setSlug().

... lines 1 - 10
class Question
{
... lines 13 - 39
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 getSlug(): ?string
{
return $this->slug;
}
public function setSlug(string $slug): self
{
$this->slug = $slug;
return $this;
}
public function getQuestion(): ?string
{
return $this->question;
}
public function setQuestion(string $question): self
{
$this->question = $question;
return $this;
}
public function getAskedAt(): ?\DateTimeInterface
{
return $this->askedAt;
}
public function setAskedAt(?\DateTimeInterface $askedAt): self
{
$this->askedAt = $askedAt;
return $this;
}
}

It's just about the most boring class you'll ever see.

But of course, if Doctrine is going to map this class and its properties to a database table, it's needs to know a few things. For example, it needs to know that the name property should map to a name column and that its type is a string.

... lines 1 - 10
class Question
{
... lines 13 - 19
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
... lines 24 - 91
}

The way that Doctrine does this is by reading annotations. Well, you can also use XML, but I love annotations.

For example, the @ORM\Entity() above the class is what actually tells Doctrine:

... lines 1 - 7
/**
* @ORM\Entity(repositoryClass=QuestionRepository::class)
*/
class Question
{
... lines 13 - 91
}

Hey! This is not just a normal, boring PHP class. This is an "entity": a class that I want to be able to store in the database.

Then, the @ORM\Column() above the properties is how Doctrine knows which properties should be stored in the table and their types.

Annotations Reference

Now, there are a bunch of different annotations and options you can use to configure Doctrine. Most are pretty simple - but let me show you an awesome reference. Search for doctrine annotations reference to find a really nice spot on their site where you can see a list of all the different possible annotations and their options.

For example, @Column() tells you all the different options that you can pass to it, like a name option. So if you wanted to control the name of the slug column - instead of letting Doctrine determine it automatically - you would do that by adding a name option.

One of the other annotations that you'll see here is @Table. Oh, and notice, in the docs, the annotation will be called @Table. But inside Symfony, we always use @ORM\Table. Those are both referring to the same thing.

Anyways, if you wanted to control the name of the table, we could say @ORM\Table() and pass it name="" with some cool table name.

But I won't bother doing that because Doctrine will guess a good table name from the class. Oh, and by the way, the auto-completion that you're seeing on the annotations comes from the "PHP Annotations" plugin in PhpStorm.

Ok: status check! The make:entity command helped us create this new entity class, but it did not talk to the database. There is still no question table.

How do we create the table? By generating a migration. Doctrine's migrations system is amazing. It will even allow us to perform table changes with basically zero work. Let's find out how next.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.8", // 1.8.2
        "doctrine/doctrine-bundle": "^2.1", // 2.1.0
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.7", // v2.7.3
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "knplabs/knp-time-bundle": "^1.11", // v1.12.0
        "sensio/framework-extra-bundle": "^5.5", // v5.6.1
        "sentry/sentry-symfony": "^3.4", // 3.5.2
        "stof/doctrine-extensions-bundle": "^1.4", // v1.4.0
        "symfony/asset": "5.1.*", // v5.1.2
        "symfony/console": "5.1.*", // v5.1.2
        "symfony/dotenv": "5.1.*", // v5.1.2
        "symfony/flex": "^1.3.1", // v1.9.0
        "symfony/framework-bundle": "5.1.*", // v5.1.2
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/stopwatch": "5.1.*", // v5.1.2
        "symfony/twig-bundle": "5.1.*", // v5.1.2
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.1.*", // v5.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.0.4
        "twig/twig": "^2.12|^3.0" // v3.0.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.3.1
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.20.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.1.0
    }
}