Buy

Updating an Entity with New Fields

It's time to get back to work on the article page... because... some of this stuff is still hardcoded! Lame! Like, the author, number of hearts, and this image. There are a few possible images in our project that our dummy articles can point to.

Our mission is clear: create three new fields in Article and use those to make all of this finally dynamic! Let's go!

Open your Article entity. The simplest way to add new fields is just to... add them by hand! It's easy enough to copy an existing property, paste, rename, and configure it. Of course, if you want a getter and setter method, you'll also need to create those.

Generating New Fields into the Entity

Because of that, my favorite way to add fields is to, once again, be lazy, and generate them! Find your terminal and run the same command as before:

php bin/console make:entity

If you pass this the name of an existing entity, it can actually update that class and add new fields. Magic! First, add author, use string as the type. And yea, in the future when we have a "user" system, this field might be a database relation to that table. But for now, use a string. Say no to nullable. Reminder: when you say no to nullable, it means that this field must be set in the database. If you try to save an entity without any data on it, you'll get a huge database exception.

Next, add heartCount, as an integer, and say not null: this should always have a value, even if it's zero. Then, finally, the image. In the database, we'll store only the image filename. And, full disclosure, uploading files is a whole different topic that we'll cover in a different tutorial. In this example, we're going to use a few existing images in the public/ directory. But, both in this situation and in a real-file upload situation, the field on your entity looks the same: imageFilename as a string and nullable yes, because maybe the image is optional when you first start writing an article.

Ok, hit enter and, done! Let's go check out the entity! Great: three new properties on top:

144 lines src/Entity/Article.php
... lines 1 - 9
class Article
{
... lines 12 - 38
/**
* @ORM\Column(type="string", length=255)
*/
private $author;
/**
* @ORM\Column(type="integer")
*/
private $heartCount;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $imageFilename;
... lines 53 - 142
}

And of course, at the bottom, here are their getter and setter methods:

144 lines src/Entity/Article.php
... lines 1 - 9
class Article
{
... lines 12 - 107
public function getAuthor(): ?string
{
return $this->author;
}
public function setAuthor(string $author): self
{
$this->author = $author;
return $this;
}
public function getHeartCount(): ?int
{
return $this->heartCount;
}
public function setHeartCount(int $heartCount): self
{
$this->heartCount = $heartCount;
return $this;
}
public function getImageFilename(): ?string
{
return $this->imageFilename;
}
public function setImageFilename(?string $imageFilename): self
{
$this->imageFilename = $imageFilename;
return $this;
}
}

Now that we have the new fields, don't forget! We need a migration:

php bin/console make:migration

When that finishes, go look at the new file to make sure it doesn't have any surprises: ALTER TABLE article, and then it adds author, heart_count and image_filename:

29 lines src/Migrations/Version20180414171443.php
... lines 1 - 10
class Version20180414171443 extends AbstractMigration
{
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE article ADD author VARCHAR(255) NOT NULL, ADD heart_count INT NOT NULL, ADD image_filename VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE article DROP author, DROP heart_count');
}
}

I love it!

Close that, run back to your terminal, and migrate!

php bin/console doctrine:migrations:migrate

Field Default Value

Next, we need to make sure these new fields are populated on our dummy articles. Open ArticleAdminController.

Oh, but first, remember that, in the Article entity, heartCount is required in the database:

144 lines src/Entity/Article.php
... lines 1 - 9
class Article
{
... lines 12 - 43
/**
* @ORM\Column(type="integer")
*/
private $heartCount;
... lines 48 - 142
}

Actually, to be more clear: nullable=true means that it is allowed to be null in the database. If you don't see nullable, it uses the default value, which is false.

Anyways, this means that heartCount needs a value! But here's a cool idea: once our admin area is fully finished, when an author creates a new article, they shouldn't need to set the heartCount manually. I mean, it's not like we expect the form to have a "heart count" input field on it. Nope, we expect it to automatically default to zero for new articles.

So... how can we give a property a default value in the database? By giving it a default value in PHP: $heartCount = 0:

144 lines src/Entity/Article.php
... lines 1 - 9
class Article
{
... lines 12 - 43
/**
* @ORM\Column(type="integer")
*/
private $heartCount = 0;
... lines 48 - 142
}

Using the new Fields

Ok, back to ArticleAdminController! Add $article->setAuthor() and use the same data we had on the original, hardcoded articles:

61 lines src/Controller/ArticleAdminController.php
... lines 1 - 10
class ArticleAdminController extends AbstractController
{
... lines 13 - 15
public function new(EntityManagerInterface $em)
{
... lines 18 - 40
// publish most articles
if (rand(1, 10) > 2) {
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100))));
}
$article->setAuthor('Mike Ferengi')
... lines 47 - 58
}
}

Then, ->setHeartCount() and give this a random number between, how about, 5 and 100:

61 lines src/Controller/ArticleAdminController.php
... lines 1 - 10
class ArticleAdminController extends AbstractController
{
... lines 13 - 15
public function new(EntityManagerInterface $em)
{
... lines 18 - 45
$article->setAuthor('Mike Ferengi')
->setHeartCount(rand(5, 100))
... lines 48 - 58
}
}

And finally, ->setImageFilename(). The file we've been using is called asteroid.jpeg. Keep using that:

61 lines src/Controller/ArticleAdminController.php
... lines 1 - 10
class ArticleAdminController extends AbstractController
{
... lines 13 - 15
public function new(EntityManagerInterface $em)
{
... lines 18 - 45
$article->setAuthor('Mike Ferengi')
->setHeartCount(rand(5, 100))
->setImageFilename('asteroid.jpeg')
;
... lines 50 - 58
}
}

Excelente! Because we already have a bunch of records in the database where these fields are blank, just to keep things simple, let's delete the table entirely and start fresh. Do that with:

php bin/console doctrine:query:sql "TRUNCATE TABLE article"

If you check out the page now and refresh... cool, it's empty. Now, go to /admin/article/new and... refresh a few times. Awesome! Check out the homepage!

We have articles... but actually... this author is still hardcoded in the template. Easy fix!

Updating the Templates

Open up homepage.html.twig. Let's first change the... where is it... ah, yes! The author's name: use {{ article.author }}:

58 lines templates/article/homepage.html.twig
... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<!-- Article List -->
<div class="col-sm-12 col-md-8">
... lines 10 - 18
<!-- Supporting Articles -->
{% for article in articles %}
<div class="article-container my-1">
<a href="{{ path('article_show', {slug: article.slug}) }}">
... line 24
<div class="article-title d-inline-block pl-3 align-middle">
... lines 26 - 27
<span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('images/alien-profile.png') }}"> {{ article.author }} </span>
... line 29
</div>
</a>
</div>
{% endfor %}
</div>
... lines 35 - 54
</div>
</div>
{% endblock %}

Then, in show.html.twig, change the article's heart count - here it is - to {{ article.heartCount }}. And also update the author, just like before:

86 lines templates/article/show.html.twig
... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
<div class="row">
<div class="col-sm-12">
... line 13
<div class="show-article-title-container d-inline-block pl-3 align-middle">
... lines 15 - 16
<span class="align-left article-details"><img class="article-author-img rounded-circle" src="{{ asset('images/alien-profile.png') }}"> {{ article.author }} </span>
... lines 18 - 20
<span class="pl-2 article-details">
<span class="js-like-article-count">{{ article.heartCount }}</span>
... line 23
</span>
</div>
</div>
</div>
... lines 28 - 73
</div>
</div>
</div>
</div>
{% endblock %}
... lines 80 - 86

If you try the homepage now, ok, it looks exactly the same, but we know that these author names are now dynamic. If you click into an article.. yea! We have 88 hearts - that's definitely coming from the database.

Updating the Image Path

The last piece that's still hardcoded is this image. Go back to homepage.html.twig. The image path uses asset('images/asteroid.jpeg'):

58 lines templates/article/homepage.html.twig
... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<!-- Article List -->
<div class="col-sm-12 col-md-8">
... lines 10 - 18
<!-- Supporting Articles -->
{% for article in articles %}
<div class="article-container my-1">
<a href="{{ path('article_show', {slug: article.slug}) }}">
<img class="article-img" src="{{ asset('images/asteroid.jpeg') }}">
... lines 25 - 30
</a>
</div>
{% endfor %}
</div>
... lines 35 - 54
</div>
</div>
{% endblock %}

So... this is a little bit tricky, because only part of this - the asteroid.jpeg part - is stored in the database. One solution would be to use Twig's concatenation operator, which is ~, then article.imageFilename:

{# templates/article/homepage.html.twig #}

{# ... #}
    <img class="article-img" src="{{ asset('images/'~article.imageFilename) }}">
{# ... #}

You don't see the ~ much in Twig, but it works like a . in PHP.

That's fine, but a nicer way would be to create a new method that does this for us. Open Article and, at the bottom, create a new public function getImagePath():

149 lines src/Entity/Article.php
... lines 1 - 9
class Article
{
... lines 12 - 143
public function getImagePath()
{
... line 146
}
}

Inside, return images/ and then $this->getImageFilename():

149 lines src/Entity/Article.php
... lines 1 - 9
class Article
{
... lines 12 - 143
public function getImagePath()
{
return 'images/'.$this->getImageFilename();
}
}

Thanks to this, in the template, we only need to say article.imagePath:

58 lines templates/article/homepage.html.twig
... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<!-- Article List -->
<div class="col-sm-12 col-md-8">
... lines 10 - 18
<!-- Supporting Articles -->
{% for article in articles %}
<div class="article-container my-1">
<a href="{{ path('article_show', {slug: article.slug}) }}">
<img class="article-img" src="{{ asset(article.imagePath) }}">
... lines 25 - 30
</a>
</div>
{% endfor %}
</div>
... lines 35 - 54
</div>
</div>
{% endblock %}

And yea, imagePath is totally not a real property on Article! But thanks to the kung fu powers of Twig, this works fine.

Oh, and side note: notice that there is not an opening slash on these paths:

149 lines src/Entity/Article.php
... lines 1 - 9
class Article
{
... lines 12 - 143
public function getImagePath()
{
return 'images/'.$this->getImageFilename();
}
}

As a reminder, you do not need to include the opening / when using the asset() function: Symfony will add it there automatically.

Ok, try it out - refresh! It still works! And now that we've centralized that method, in show.html.twig, it's super easy to make the same change: article.imagePath:

86 lines templates/article/show.html.twig
... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
<div class="row">
<div class="col-sm-12">
<img class="show-article-img" src="{{ asset(article.imagePath) }}">
... lines 14 - 25
</div>
</div>
... lines 28 - 73
</div>
</div>
</div>
</div>
{% endblock %}
... lines 80 - 86

Awesome. And when you click on the show page, it works too.

Next! Now that the heart count is stored in the database, let's make our AJAX endpoint that "likes" an article actually work correctly. Right now, it does nothing, and returns a random number. We can do better.

Leave a comment!

  • 2018-05-08 weaverryan

    Great answer! :)

  • 2018-05-08 boedah

    Hi Petru Lebada,

    just wanted to add some thoughts:

    * Doctrine FAQs recommend setting default values using properties: https://www.doctrine-projec...
    * Doctrine will not go back to the database after persisting a new object to get the default values
    * if you rely on your DB to set default values you cannot create easier, faster tests for your entities (without using the DB)
    * there could be problems when default values for e.g. date types are different per DBMS (when your live DBMS is MySQL but your tests run on SQLite for example)
    * double check on Doctrine Forms overwriting your default values

    Because of these reasons we almost always stick to solely setting defaults in the entities themselves.

    EDIT: as this is about adding fields to entities, you might have problems with existing rows for a new column which should be not null and but has no default value...

  • 2018-05-08 weaverryan

    Hey Petru Lebada!

    Ah, very good catch :). So, it depends. Personally, setting it in PHP is much more important to me: I want my PHP object to have the value that will eventually be stored in the database. To use your example, if you ONLY have the "default": 1, then your object might have a null value for this field in PHP, but suddenly it changes to 1 in the database. That's strange to me.

    However, I think the options={"default": 1} can be a useful "extra" thing you can add... if you want to (I don't use this, but I can see why some people do). With this config (in addition to defaulting the value in PHP), you make your database a bit more flexible. For example, if you ever need to manually hack a new row manually via an INSERT query, this default value will be reflected. So, it's up to you - but I don't think options= is a better practice, it's just an extra practice, which can be good, depending on how much you care about the underlying database :).

    Cheers!

  • 2018-05-07 Petru Lebada

    Hello,

    Just a minor confusion... you said here, that we should give the property a value so we can have a default value for the column, but i found a topic on SO saying that @ORM\Column(type="bigint", options={"default": 1}) is a better practice . Do you have any thoughts on which one is better and why? Should i do both?