Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Agree to Terms Database Field

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

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

Login Subscribe

If you compare our old registration form and our new one, we're missing one annoying, piece: the "Agree to terms" checkbox, which, if you're like me, is just one of my favorite things in the world - right behind a fine wine or day at the beach.

Legally speaking, this field is important. So let's code it up correctly.

Adding the "Agreed Terms" Persisted Date Field

A few years ago, we might have added this as a simple unmapped checkbox field with some validation to make sure it was checked. But these days, to be compliant, we need to save the date the terms were agreed to.

Let's start by adding a new property for that! Find your terminal and run:

php bin/console make:entity

Update the User class and add a new field called agreedTermsAt. This will be a datetime field and it cannot be nullable in the database: we need this to always be set. Hit enter to finish.

... lines 1 - 19
class User implements UserInterface
... lines 22 - 68
* @ORM\Column(type="datetime")
private $agreedTermsAt;
... lines 73 - 270

Adding the Checkbox Field

Before we worry about the migration, let's think about the form. What we want is very simple: a checkbox. Call it, how about, agreeTerms. Notice: this creates a familiar problem: the form field is called agreeTerms but the property on User is agreedTermsAt. We are going to need more setup to get this working.

... lines 1 - 13
class UserRegistrationFormType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
... lines 19 - 33
->add('agreeTerms', CheckboxType::class)
... lines 37 - 43

But first, Google for "Symfony form types" and click the "Form Type Reference" page. Let's see if we can find a checkbox field - ah: CheckboxType. Interesting: it says that this field type should be used for a field that has a boolean value. If the box is checked, the form system will set the value to true. If the box is unchecked, the value will be set to false. That makes sense! That's the whole point of a checkbox!

Back on the form, set the type to CheckboxType::class.

... lines 1 - 15
public function buildForm(FormBuilderInterface $builder, array $options)
... lines 19 - 33
->add('agreeTerms', CheckboxType::class)
... lines 37 - 45

Nice start! Before I forget, find your terminal and make the migration:

php bin/console make:migration

As usual, go to the migrations directory, open that file and... yep! It adds the one field. Run it with:

php bin/console doctrine:migrations:migrate

Oh no! Things are not happy. We have existing users in the database! When we suddenly create a new field that is NOT NULL, MySQL has a hard time figuring out what datetime value to use for the existing user rows!

Migrating Existing User Data

Our migration needs to be smarter. First: when a migration fails, Doctrine does not record it as having been executed. That makes sense. And because there is only one statement in this migration, we know that it completely failed, and we can try it again as soon as we fix it. In other words, the agreed_terms_at column was not added.

If a migration has multiple statements, it's possible that the first few queries were successful, and then one failed. When that happens, I usually delete the migration file entirely, fully drop the database, then re-migrate to get back to a "clean" migration state. But also, some database engines like PostgreSQL are smart enough to rollback the first changes, if a later change fails. In other words, those database engines avoid the problem of partially-executed-migrations.

Anyways, to fix the migration, change the NOT NULL part to DEFAULT NULL temporarily. Then add another statement: $this->addSql('UPDATE user SET agreed_terms_at = NOW()');.

... lines 1 - 10
final class Version20181016183947 extends AbstractMigration
public function up(Schema $schema) : void
... lines 15 - 17
$this->addSql('ALTER TABLE user ADD agreed_terms_at DATETIME DEFAULT NULL');
$this->addSql('UPDATE user SET agreed_terms_at = NOW()');
... lines 21 - 28

Great! First, let's run just this migration

php bin/console doctrine:migrations:migrate

This time... it works! To finish the change, make one more migration:

php bin/console make:migration

... lines 1 - 10
final class Version20181016184244 extends AbstractMigration
public function up(Schema $schema) : void
// 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 user CHANGE agreed_terms_at agreed_terms_at DATETIME NOT NULL');
... lines 20 - 27

Go check it out! Perfect! This gives us the last piece we need: changing the column back to NOT NULL, which will work because each existing user now has a real value for this field. Oh, but, for legal purposes, on a real site - it may not be proper to automatically set the agreed_terms_at for existing users. Yep, you've gotta check with a lawyer on that kind of stuff.

But from a database migration standpoint, this should fix everything! Run the last migration:

php bin/console doctrine:migrations:migrate

Excellent! Next: we have a CheckboxType field on the form... which is good at setting true/false values. And, we have an agreedTermsAt DateTime field on the User class. Somehow, those need to work together!

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.6
        "symfony/console": "^4.0", // v4.1.6
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.6
        "symfony/framework-bundle": "^4.0", // v4.1.6
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.6
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.6
        "symfony/validator": "^4.0", // v4.1.6
        "symfony/web-server-bundle": "^4.0", // v4.1.6
        "symfony/yaml": "^4.0", // v4.1.6
        "twig/extensions": "^1.5" // v1.5.2
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
        "symfony/dotenv": "^4.0", // v4.1.6
        "symfony/maker-bundle": "^1.0", // v1.8.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.6