The Answer Entity

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.

Oh hey there friends! Welcome back to part 2 of our Doctrine in Symfony series... you wonderful database nerds you.

Last time we mastered the basics, but good stuff! Creating an entity, migrations, fixtures, saving, querying and making the perfect omelette... I think. This time, we're going to do some mega study on Doctrine relations.

Project Setup

So let's get our project rocking. To avoid foreign key constraints in your brain while watching the tutorial, I recommend downloading the course code from this page and coding along with me. After unzipping the file, you'll find a start/ directory with all the fancy files that you see here. Check out the README.md file for all the fun details on how to get this project running.

The last step will be to find a terminal, move into the project and run:

symfony serve -d

I'm using the Symfony binary to start a local web server. Let's go see our site. Spin over to your browser and head to https://127.0.0.1:8000.

Oh, hey there Cauldron Overflow! This is a site where the budding industry of witches and wizards can come to ask questions... after - sometimes - prematurely shipping their spells to production... and turning their clients into small adorable frogs. It could be worse.

The questions on the homepage are coming from the database... we rock! We built a Question entity in the first tutorial. But if you click into a question... yea. These answers? These are totally hard-coded. Time to change that.

Making the Answer Entity

I want you to, for now, forget about any potential relationship between questions and answers. It's really simple: our site has answers! And so, if we want to store those answers in the database, we need an Answer entity.

At your terminal, let's generate one. Run:

symfony console make:entity

Now, as a reminder, symfony console is just a fancy way of saying php bin/console. I'm using the Docker & Symfony web server integration. That's where the Symfony web server reads your docker-compose.yaml file and exposes environment variables to the services inside of it. We talked about that in the first Symfony 5 tutorial. By using symfony console - instead of running bin/console directly - my commands will be able to talk to my Docker services... which for me is just a database. That's not needed for this command, but it will be for others.

Anyways, run this and create a new entity called Answer. Let's give this a few basic properties like content which will store the answer itself. Set this to a text type: the string type maxes out at 255 characters. Say "no" to nullable: that will make this column required in the database.

Let's also add a username property, which will be a string. Eventually, in the security tutorial, we'll change this to be a relationship to a User entity. Use the 255 length and make it not nullable.

Oh, and one more: a votes property that's an integer so that people can up vote and down vote this answer. Make this not nullable and... done! Hit enter one more time to finish.

... lines 1 - 2
namespace App\Entity;
use App\Repository\AnswerRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=AnswerRepository::class)
*/
class Answer
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="text")
*/
private $content;
/**
* @ORM\Column(type="string", length=255)
*/
private $username;
/**
* @ORM\Column(type="integer")
*/
private $votes;
public function getId(): ?int
{
return $this->id;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
public function getVotes(): ?int
{
return $this->votes;
}
public function setVotes(int $votes): self
{
$this->votes = $votes;
return $this;
}
}

Timestampable and Default votes Value

Before we generate the migration, go open up that class: src/Entity/Answer.php. So far... there's nothing special here! It looks pretty much like our other entity. Oh, but if you're using PHP 8, then the command may have generated PHP 8 attributes instead of annotations. That's great! They work exactly the same and you should use attributes if you can.

At the top of the class, add use TimestampableEntity. We talked about that in the last tutorial: it adds nice createdAt and updatedAt properties that will be set automatically.

... lines 1 - 6
use Gedmo\Timestampable\Traits\TimestampableEntity;
... lines 8 - 11
class Answer
{
use TimestampableEntity;
... lines 15 - 77
}

Oh, and one other thing: default the votes to zero. I made this column not nullable in the database. Thanks to this = 0, if we do not set the votes on a new answer, instead of getting a database error about null not being allowed, the Answer will save with votes = 0.

... lines 1 - 11
class Answer
{
... lines 14 - 32
/**
* @ORM\Column(type="integer")
*/
private $votes = 0;
... lines 37 - 77
}

Making the Migration

Now let's generate the migration. Find your terminal and run:

symfony console make:migration

As a reminder, this command is smart: it looks at all of your entities and your actual database structure, and generates the SQL needed to make them match. Go check out that new file... it's in the migrations/ directory. And... perfect! CREATE TABLE answer... and then it adds all of the columns.

... lines 1 - 2
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210902130926 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE answer (id INT AUTO_INCREMENT NOT NULL, content LONGTEXT NOT NULL, username VARCHAR(255) NOT NULL, votes INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE answer');
}
}

Run the migration with:

symfony console doctrine:migrations:migrate

All good! Our database now has a question table and an answer table. Next, let's relate them.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.3
        "doctrine/doctrine-bundle": "^2.1", // 2.4.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.9.5
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "sensio/framework-extra-bundle": "^6.0", // v6.1.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.7
        "symfony/flex": "^1.3.1", // v1.15.1
        "symfony/framework-bundle": "5.3.*", // v5.3.7
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.33.0
        "symfony/var-dumper": "5.3.*", // v5.3.7
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.5
        "zenstruck/foundry": "^1.1" // v1.13.1
    }
}