Login to bookmark this video
Buy Access to Course
10.

Starship Upgrade: Adding Slug and Timestamp Fields

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

New requirements have come down from the admirals at Starfleet HQ. Instead of seeing the id in the URL, like /starship/1, they want to see a human-readable name, like /starship/enterprise. This is called a "slug". To accomplish this, we need to add a new field to our Starship entity.

Adding Fields to an Existing Entity

We could add this by hand, super-easily: add the property, getter, setter, and the #[ORM\Column] attribute. Or we can cheat! Run:

symfony console make:entity Starship

Instead of creating a new entity, this time we'll add fields to an existing entity. Add slug, type string, length 255. "Should it be nullable?" - no, but choose yes for now. Let's add 2 more handy fields: updatedAt, datetime_immutable, nullable? yes, temporarily, and createdAt, datetime_immutable, nullable? yes.

Hit Enter to exit the command. Before creating the migration, go check the Starship entity: src/Entity/Starship.php:

155 lines | src/Entity/Starship.php
// ... lines 1 - 8
class Starship
{
// ... lines 11 - 30
#[ORM\Column(length: 255, nullable: true)]
private ?string $slug = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
// ... lines 39 - 153
}

Cool! And we can even remove length: 255 for $slug. That's the default.

First Migration

New field, check! But does the new column exist in the database? Nope! That's a job for a migration.

At your terminal, create one with:

symfony console make:migration

Open up the new migration file. I love this. Remember that migrations are generated by comparing the database to the entity class. Doctrine sees the new fields in the class, does not see the corresponding columns in the table, generates the SQL to fix that and nestles it safely in the up() method:

39 lines | migrations/Version20241201203154.php
// ... lines 1 - 12
final class Version20241201203154 extends AbstractMigration
{
// ... lines 15 - 19
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE starship ADD slug VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE starship ADD created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('ALTER TABLE starship ADD updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN starship.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN starship.updated_at IS \'(DC2Type:datetime_immutable)\'');
}
// ... lines 29 - 37
}

It's optional, but let's add a description: "Add slug and timestamps to starship":

39 lines | migrations/Version20241201203154.php
// ... lines 1 - 12
final class Version20241201203154 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add slug and timestamps to starship';
}
// ... lines 19 - 37
}

In your terminal, run the migration:

symfony console doctrine:migrations:migrate

Success! The new columns were added. Prove it by running:

symfony console doctrine:query:sql 'SELECT name, slug, updated_at, created_at FROM starship'

Yup! The columns are there, but still empty. Eventually, we'll set up Doctrine to automatically set these fields for us. But first, these columns should all be required in the database, known as nullable: false in Doctrine.

Making Fields Required

Open up Starship. Above $slug, remove nullable: true. This now means nullable: false: that's the default value. In other words, this tells Doctrine the column should be required in the database.

Also set unique: true to make this a unique column. For $updatedAt and $createdAt, also remove nullable: true.

155 lines | src/Entity/Starship.php
// ... lines 1 - 8
class Starship
{
// ... lines 11 - 30
#[ORM\Column(unique: true)]
private ?string $slug = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column]
private ?\DateTimeImmutable $updatedAt = null;
// ... lines 39 - 153
}

Second Migration

Once again, we've made changes to our entity that aren't reflected in the database. Migration time! Run:

symfony console make:migration

Open the new migration. Cool! In the up() method, it alters the three columns to make them NOT NULL and creates a unique index on the slug column:

39 lines | migrations/Version20241201203519.php
// ... lines 1 - 12
final class Version20241201203519 extends AbstractMigration
{
// ... lines 15 - 19
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE starship ALTER slug SET NOT NULL');
$this->addSql('ALTER TABLE starship ALTER created_at SET NOT NULL');
$this->addSql('ALTER TABLE starship ALTER updated_at SET NOT NULL');
$this->addSql('CREATE UNIQUE INDEX UNIQ_C414E64A989D9B62 ON starship (slug)');
}
// ... lines 28 - 37
}

Add a description: "Make slug and timestamps not nullable":

39 lines | migrations/Version20241201203519.php
// ... lines 1 - 12
final class Version20241201203519 extends AbstractMigration
{
public function getDescription(): string
{
return 'Make slug and timestamps not nullable';
}
// ... lines 19 - 37
}

In the terminal, run it!

symfony console doctrine:migrations:migrate

Error! These fields can't be set to NOT NULL because they contain null values. Doh! This is a tricky situation where we need to do things in 3 steps: add the new columns, give them each a value, and then make them NOT NULL.

Editing the Migration with Custom SQL

Open the last migration again. Most of the time, Doctrine does all the work for us. But we can add our own SQL to a migration.

In the up() method, before the generated SQL, write $this->addSql('UPDATE starship SET slug = id, updated_at = arrived_at, created_at = arrived_at'):

41 lines | migrations/Version20241201203519.php
// ... lines 1 - 12
final class Version20241201203519 extends AbstractMigration
{
// ... lines 15 - 19
public function up(Schema $schema): void
{
$this->addSql('UPDATE starship SET slug = id, created_at = arrived_at, updated_at = arrived_at');
// ... lines 23 - 28
}
// ... lines 30 - 39
}

Let's unpack this. We're updating the starship table, setting slug equal to id. Why? Because id is unique and not null - exactly what we need for the slug. We're also setting updated_at and created_at equal to arrived_at. We know arrived_at is also a timestamp and not null.

Back in the terminal, run the migrations again:

symfony console doctrine:migrations:migrate

It worked! Run the query again to see the data:

symfony console doctrine:query:sql 'SELECT name, slug, updated_at, created_at FROM starship'

Look at that! Three new fields filled in with data.

Reloading the Fixtures

We have a problem now though. Dang! Reload the fixtures:

symfony console doctrine:fixtures:load

Explosion! There's nothing in our fixtures that sets these three required fields.

We could update our StarshipFactory to set default values for these fields... but I want to show a different way: a "doctrine extension" package that can set these automatically. It's the best, and it's next!