Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

One of the coolest, but maybe most surprising things about Doctrine, is that it wants you to pretend like the database doesn't exist! Yea, instead of thinking about tables and columns, Doctrine wants us to think about objects and properties.

For example, let's say that we want to save some product data. The way we do that with Doctrine is by creating a Product class with properties that hold the data. Then you instantiate a Product object, set data onto it and politely ask Doctrine to save it for you. We don't have to worry about how Doctrine does that.

But, of course, behind the scenes Doctrine is talking to a database. It will INSERT the data from the Product object into a product table where each property is mapped to a column. This is called an Object Relational Mapper, or ORM.

Later, when we want to get that data back, we don't think about "querying" that table and its columns. Nope, we simply ask Doctrine to find the object that we had earlier. Of course, it will query the table... then recreate the object with the data. But that's not a detail we think about: we ask for the Product object, and it gives it to us. Doctrine handles all of the saving and querying automatically.

Generating the Entity with make:entity

Anyways, when we use an ORM like Doctrine, if we want to save something to the database, we need to create a class that models the thing we want to save, like a Product class. In Doctrine, these classes are given a special name: entities. Though, they're really just normal PHP classes. And while you can create these entity classes by hand, there's a MakerBundle command that makes life much nicer.

Spin over to your terminal and run:

php bin/console make:entity

In this case, we don't have to run symfony console make:entity because this command will not talk to the database: it just generates code. But, if you're ever not sure, using symfony console is always safe.

Okay, we want to create a class to store all of the vinyl mixes in our system. So let's create a new class called VinylMix. Then answer no for broadcasting entity updates: that's an extra feature related to Symfony Turbo.

Ok, here's the important part: it asks which properties we want. We're going to add several. Start with one called title. Next it asks which type this field is. Hit ? to see the full list.

These are Doctrine types... and each one will map to a different column type in your database, depending on which database you're using, like MySQL or Postgres. The basic types are on top like string, text - which can hold more than a string) - boolean, integer and float. Then relationship fields - we'll talk about those in the next tutorial - some special fields, like storing JSON and date fields.

For title, use string, which can hold up to 255 characters. I'll keep the default length... then it asks us if the field can be null in the database. I'll answer no. This means that the column cannot be null. In other words, the column will be required in the database.

And... one field done! Let's add a few more. We need a description, and make this a text type. string maxes out at 255 characters, text can hold a ton more. This time, I'll say yes to making it nullable. So this will be an optional column in the database. Another one down!

For the next property, call it trackCount. It will be an integer and will be not null. Then add genre, as a string, length 255... and also not null so that it's required in the database.

Finally, add a createdAt field so we can know when each vinyl mix was originally created. This time, because the field name ends in "At", the command suggests a datetime_immutable type. Hit "enter" to use that, and also make this not null in the database.

We don't need to add any more properties right now so hit "enter" one more time to exit the command.

Done! What did this do? Well first, I can tell you that this did not talk to or change our database at all. Nope, it simply generated two classes. The first is src/Entity/VinylMix.php. The second is src/Repository/VinylMixRepository.php. Ignore the Repository one for now... we'll talk about its purpose in a few minutes.

... lines 1 - 8
#[ORM\Entity(repositoryClass: VinylMixRepository::class)]
class VinylMix
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column()]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null;
... lines 22 - 31
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
... lines 48 - 95
}

Checking out the Entity Class & Attributes

Go open up the VinylMix.php entity. Say hello to... a... wow, pretty normal, boring PHP class! It generated a private property for each field we added, plus an extra id property. The command also added a getter and setter method for each of these. So... this is basically just a class that holds data... and we can access and set that data via the getter and setter methods

The only thing that makes this class special are the attributes. The ORM\Entity above the class tells Doctrine:

Hey! I want to be able to save objects of this class to the database. This is an entity.

Then, above each property, we use ORM\Column to tell Doctrine that we want to save this property as a column in the table. This also communicates other options like the length of the column and whether or not it should be nullable. nullable: false is the default... so the command only generated nullable: true on the one property that needs it.

The other thing ORM\Column controls is the field type. That's set via this type option. As I mentioned, this doesn't refer directly to a MySQL or Postgres type... its a Doctrine type that will then map to something specific based on our database.

Field Type Guessing

But, interesting: the type option only shows up on the $description field. The reason for that is really cool... and new! Doctrine is smart. It looks at the type on your property and guesses the field type from that. So when you have a string property type, Doctrine assumes that you want that to be its string type. You could write Types::STRING inside ORM\Column... but that would be totally redundant.

We do need it for the description field, however... because we want to use the TEXT type, not the STRING type. But in every other situation, it works. Doctrine guesses the correct type from the ?int property type... and the same thing happens down here for the ?\DateTimeImmutable type.

Table and Column Naming

In addition to controlling things about each column, we can also control the name of the table by adding an ORM\Table above the class with name set to, for example, vinyl_mix. But, surprise! We don't need to do that! Why? Because Doctrine is really good at generating great names. It generates the table name by transforming the class into snake case. So even without ORM\Table, this will be the name of the table. The same applies to properties. $trackCount will map to a track_count column. Doctrine handles all of this for us: we don't need to think about our table or column names at all.

At this point, we've run make:entity and it generated an entity class for us. Yay! But... we don't actually have a vinyl_mix table in our database yet. How do we create one? With the magic of database migrations. That's next.

Leave a comment!

12
Login or Register to join the conversation
Michail Avatar

Why are the non-nullable properties (e.g. title) type-hinted as possibly having a null value and initialised as null?

2 Reply

Hey Michail!

GREAT question. That was a design decision in MakerBundle and the goal was to be "friendly" over "strict" (definitely a subjective decision). So, if make bundle instead generate things like private string $title, then if the user called $obj->getTitle() before setting it, they would get the "not initialized" error from PHP. So, to make the entities behave identically to the "pre property types world", we made them nullable and default to null. It's very possible that, as a community, we will eventually desire to be more strict... or we might even add an option in MakerBundle. But, that's the explanation :).

Cheers!

2 Reply
BYT Avatar

Can you point me to tutorial about setup of symfony to use yaml format instead of php for the entity classes.

Reply

Hey BYT,

We always use PHP attributes (or annotations on old tutorials) for configuring entities, so I can't point you to any tutorial but you can see examples in the Symfony docs. For example, you can see how to configure an association here (just click on the YAML option) https://symfony.com/doc/current/doctrine/associations.html#mapping-the-manytoone-relationship
And you may want to read how to configure your entities when using YAML https://symfony.com/doc/current/reference/configuration/doctrine.html#custom-mapping-entities-in-a-bundle

Cheers!

Reply
BYT Avatar
BYT Avatar BYT | posted 6 months ago | edited

Creating tables and columns one by one will take very long. I have hundreds of tables and thousands of columns listed in Excel. I can export table/columns/relationship info to a CSV or any format. Is there a way to generate all of the Entity classes from this existing file? I'm not talking about Down from an existing database because the exiting database has a different way. I can list schema in files, a file or any format.

Reply

Hey BYT,

I don't think so... unless you will find a bundle or a library on GitHub that may help you with this. Doctrine only allows you to import mapping information from an existing database - for this you can leverage doctrine:mapping:import command.

If it's not something you need - I think you can create your own Symfony command and read the CSV file to import all the data from that, i.e. create entity files based on the schema in that file.

Cheers!

Reply
t5810 Avatar
t5810 Avatar t5810 | Victor | posted 4 months ago | edited

Hi

I want to be sure that I understand this right. I want to start new project, and I already have the database, with about 200 tables in it.

If I run "symfony console doctrine:mapping:import" command, an entity for each table will be created for me? Is my assumption correct?

Thanks.

Reply
Victor Avatar Victor | SFCASTS | t5810 | posted 4 months ago | edited

Hey @t5810 ,

Yes, that's how it suppose to be used, it creates entities with the corresponding mapping. But on practice, it depends on how well you have your legacy DB configured. But you can easily try it and see how it works for you ;)

Cheers!

1 Reply
Dmitriy-M Avatar
Dmitriy-M Avatar Dmitriy-M | posted 6 months ago

Hello. I want to create a CRM system in which there will be users with their own sets of Task entities.
It is very convenient when the ID of each entity is represented by an integer and the numbers for each individual user go sequentially and inextricably.
In simple words, you need an ID with auto-increment, then separately for each user. But how to do it right? It should be fast and have good scalability.

Reply

Hey Dmitriy,

But Doctrine already does this for you. You just need to generate an entity via MakerBundle, or make sure your User entity has this mapping config:

User
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;
}

Every time you will create a new entity - it will generate a sequential and inextricable ID for it. But if you delete a user from the DB - it won't be sequential anymore I suppose, and nothing much you can do with it.

I hope this helps!

Cheers!

Reply
Wei Avatar
Wei Avatar Wei | posted 7 months ago | edited

Hi, is there a way to create the entity in a specific folder? (e.g src/CustomFolder/Entity/...)

Reply

Hey Wei,

Unfortunately, no. The Maker bundle follows Symfony's best practice when all your entities are in the src/Entity/ folder. This is also done for the simplicity of this command. The easiest way would be to generate an entity in that folder and then move it to another folder if needed.

Well, technically, you can put your entity in a subfolder of the src/Entity/ with the following syntax:

bin/console make:entity Subfolder\\YourEntityName

But if you're looking for a way to create it outside of the default src/Entity/ - nope, you would need to move it manually there.

I hope this helps!

Cheers!

2 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.12", // 2.12.3
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.2", // v6.2.6
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.2
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.2
        "symfony/framework-bundle": "6.1.*", // v6.1.2
        "symfony/http-client": "6.1.*", // v6.1.2
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.1.*", // v6.1.0
        "symfony/runtime": "6.1.*", // v6.1.1
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/ux-turbo": "^2.0", // v2.3.0
        "symfony/webpack-encore-bundle": "^1.13", // v1.15.1
        "symfony/yaml": "6.1.*", // v6.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.1.*", // v6.1.0
        "symfony/maker-bundle": "^1.41", // v1.44.0
        "symfony/stopwatch": "6.1.*", // v6.1.0
        "symfony/web-profiler-bundle": "6.1.*", // v6.1.2
        "zenstruck/foundry": "^1.21" // v1.21.0
    }
}
userVoice