Starship Upgrade: Adding Slug and Timestamp Fields
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNew 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
:
// ... 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:
// ... 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":
// ... 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
.
// ... 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:
// ... 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":
// ... 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'):
// ... 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!