In the previous video we implemented a basic form of database fixtures by way of a Symfony Console Command. This is cool for a couple of reasons.

Firstly, it proves that our entities behave as expected. We also covered a few little 'tricks' to pull data from our images such as the width and height, and the filename without the extension as a slug.

Secondly, it taught us a bunch of ways in which we can spruce up our console command output. We looked at the progress bar, and the Table helper, along with the SymfonyStyle for making our text output look good.

Next up, we need to add in the concept of Category. We saw this in our array-backed implementation, but since switching to a database-backed approach, we've yet to 'migrate' this piece over.

If we try to do this using our basic Console Command 'fixtures' then we're going to hit on a few problems. Chief amongst these problems is that we have no (easy) way to determine what image should go into which category.

Now, the truth is that even when using fixtures we aren't going to have an easy way to determine this. We will have to do it the old fashioned way. By which I mean: we must specify everything explicitly.

As mentioned in the previous video, fixtures are a solved problem. This means we can stand on the shoulders of giants and leverage all their hard work and efforts, saving ourselves a bunch of time (and bugs), and keeping our focus on the real task.

We're going to make use of [Doctrine Fixtures Bundle][1]. Other alternatives exist, but as we're buying into the Doctrine ecosystem, it makes sense (from my point of view) to use the related projects as they are most likely to offer us the easiest ride.

Adding Doctrine Fixtures Bundle

Adding Doctrine Fixtures Bundle to our Symfony project is very straightforward. It's a case of:

composer require --dev doctrine/doctrine-fixtures-bundle

And then:

<?php

// /app/AppKernel.php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            // * snip *
        ];

        if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
            $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
            $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
            $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
            $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();

            // new line here
            $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
        }

        return $bundles;
    }

    // * snip*

Jolly hockey sticks, we are good to go.

Creating Fixtures For Wallpapers

The truth is we've just done the easy bit.

Now comes the typing. Oh, the typing.

Well, fortunately for you, the copy / pasting. But for me, oh, the typing!

Let's start by looking at one Wallpaper fixture entry:

<?php

// /src/AppBundle/DataFixtures/ORM/LoadWallpaperData.php

namespace AppBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use AppBundle\Entity\Wallpaper;

class LoadWallpaperFixtures implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $wallpaper = (new Wallpaper())
            ->setFilename('abstract-background-pink.jpg')
            ->setSlug('abstract-background-pink')
            ->setWidth(1920)
            ->setHeight(1080)
        ;

        $manager->persist($wallpaper);
        $manager->flush();
    }
}

It's very similar to what we did in our console command. Only this time it's a lot more manual.

There very well may be an incredibly easy / smart way to do this that is eluding me, but as it is eluding me, I don't know of it and therefore can't share it with you. Boo.

Running this fixture is simple enough. The Doctrine Fixtures Bundle comes with a command to do just this:

php bin/console doctrine:fixtures:load

Careful, database will be purged. Do you want to continue y/N ?y

  > purging database
  > loading AppBundle\DataFixtures\ORM\LoadWallpaperFixtures

And as easy as that, we have one entry in our wallpaper table.

It feels like a step backwards in a way. Before, everything was easy. Now everything is manual.

It all comes down to categories.

We must relate this entity to a Category entity. As mentioned we could do some fancy regex for this, but it would only work in a really specific context (i.e. mine), and I want to share this with you - where you likely have different images with names not following a set convention.

Creating And Relating Category

Before we can go much further we are really starting to need that Category entity. Let's generate one:

php bin/console doctrine:generate:entity


  Welcome to the Doctrine2 entity generator



This command helps you generate Doctrine2 entities.

First, you need to give the entity name you want to generate.
You must use the shortcut notation like AcmeBlogBundle:Post.

The Entity shortcut name: AppBundle:Category

Determine the format to use for the mapping information.

Configuration format (yml, xml, php, or annotation) [annotation]:

Instead of starting with a blank entity, you can add some fields now.
Note that the primary key will be added automatically (named id).

Available types: array, simple_array, json_array, object,
boolean, integer, smallint, bigint, string, text, datetime, datetimetz,
date, time, decimal, float, binary, blob, guid.

New field name (press <return> to stop adding fields): name
Field type [string]:
Field length [255]:
Is nullable [false]:
Unique [false]: true

New field name (press <return> to stop adding fields):


  Entity generation


  created ./src/AppBundle/Entity/Category.php
> Generating entity class src/AppBundle/Entity/Category.php: OK!
> Generating repository class src/AppBundle/Repository/CategoryRepository.php: OK!


  Everything is OK! Now get to work :).

Cool. In next to no time we've created both the Category entity and the associated Repository class for storing any custom queries we need.

Our Category entity is super simple. It's an object with an id and name. The name should be unique. No point having two categories that represent the same thing.

Again, this entity will not have an associated database table just yet.

Whether you choose to create a migration now, or a migration after you've added in the relation is up to you. I'm going to create a migration now, and another after I've added in the association. Breaking things down into easily digestible chunks works for me, but go with what you prefer.

php bin/console doctrine:migrations:diff

Generated new migration class to "/path/to/my/project/wallpaper/app/DoctrineMigrations/Version20170527140100.php" from schema differences.

<?php

namespace Application\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;

/**
 * Auto-generated Migration: Please modify to your needs!
 */
class Version20170527140100 extends AbstractMigration
{
    /**
     * @param Schema $schema
     */
    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('CREATE TABLE category (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_64C19C15E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
    }

    /**
     * @param Schema $schema
     */
    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('DROP TABLE category');
    }
}

I'm going to apply this Doctrine migration now also:

php bin/console doctrine:migrations:migrate

                    Application Migrations


WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)y
Migrating up to 20170527140100 from 20170527101133

  ++ migrating 20170527140100

     -> CREATE TABLE category (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_64C19C15E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.09s)

  ------------------------

  ++ finished in 0.09s
  ++ 1 migrations executed
  ++ 1 sql queries

Ok, cool. Checking the DB at this point shows three tables:

  • category
  • migration_versions
  • wallpaper

The wallpaper table contains one entry. My fixture entry.

The migration_versions table contains two entries. The entry for creating my wallpaper table, and the entry for creating my category table.

Let's now relate Wallpaper to Category.

For this I'm going with the theory that Many Wallpapers are in One Category. This sounds like [a Many-to-One relationship][2].

Now, between me and you, I constantly have to refer back to the Doctrine docs whenever I create a relationship. It's just info my brain will not keep around. I don't feel bad about this, after all, I need to keep plenty of room free for old Simpson's quotes and Star Trek trivia.

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Wallpaper
 *
 * @ORM\Table(name="wallpaper")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\WallpaperRepository")
 */
class Wallpaper
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * Many Wallpapers are in One Category.
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Category")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;

    // * snip *

    /**
     * @return Category|null
     */
    public function getCategory()
    {
        return $this->category;
    }

    /**
     * @param Category $category
     * @return Wallpaper
     */
    public function setCategory(Category $category = null)
    {
        $this->category = $category;

        return $this;
    }

When we generate a migration for this change, the outcome should be the creation of a new column - category_id - inside our wallpaper table. For each row / entry / wallpaper, we can associate directly to one Category.

We will go with this, and see how we get on. If we need to change it later that's no big deal.

Let's check generate our next migration now:

php bin/console doctrine:migrations:diff

Generated new migration class to "/path/to/my/project/wallpaper/app/DoctrineMigrations/Version20170527141111.php" from schema differences.

<?php

namespace Application\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;

/**
 * Auto-generated Migration: Please modify to your needs!
 */
class Version20170527141111 extends AbstractMigration
{
    /**
     * @param Schema $schema
     */
    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 wallpaper ADD category_id INT DEFAULT NULL');
        $this->addSql('ALTER TABLE wallpaper ADD CONSTRAINT FK_D592642C12469DE2 FOREIGN KEY (category_id) REFERENCES category (id)');
        $this->addSql('CREATE INDEX IDX_D592642C12469DE2 ON wallpaper (category_id)');
    }

    /**
     * @param Schema $schema
     */
    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 wallpaper DROP FOREIGN KEY FK_D592642C12469DE2');
        $this->addSql('DROP INDEX IDX_D592642C12469DE2 ON wallpaper');
        $this->addSql('ALTER TABLE wallpaper DROP category_id');
    }
}

Three changes here.

To the wallpaper table we add that new category_id column. It can be null.

Next, a foreign key tells MySQL that this category_id column will reference / point to the id column inside the category table.

Lastly, an index is created on the category_id column to speed up lookups.

Likewise, if we need to rollback, these three changes are reverted.

Nice. Applying this change is as ever, very easy:

php bin/console doctrine:migrations:migrate

                    Application Migrations


WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)y
Migrating up to 20170527141111 from 20170527140100

  ++ migrating 20170527141111

     -> ALTER TABLE wallpaper ADD category_id INT DEFAULT NULL
     -> ALTER TABLE wallpaper ADD CONSTRAINT FK_D592642C12469DE2 FOREIGN KEY (category_id) REFERENCES category (id)
     -> CREATE INDEX IDX_D592642C12469DE2 ON wallpaper (category_id)

  ++ migrated (0.13s)

  ------------------------

  ++ finished in 0.13s
  ++ 1 migrations executed
  ++ 3 sql queries

Checking our database now should show the new entry, taking the total to 3 entries in the migration_versions table.

In the wallpaper table we see the new category_id column, and for our single entry / wallpaper we have a value of null here.

There should have been no changes in the category table.


Code For This Course

Get the code for this course.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 Introduction and Site Demo 02:14
2 Setup and a Basic Wallpaper Gallery 08:43
3 Pagination 08:24
4 Adding a Detail View 04:47
5 Creating a Home Page 11:14
6 Creating our Wallpaper Entity 07:50
7 Wallpaper Setup Command - Part 1 - Symfony Commands As a Service 05:56
8 Wallpaper Setup Command - Part 2 - Injection Is Easy 08:53
9 Wallpaper Setup Command - Part 3 - Doing It With Style 05:37
10 Doctrine Fixtures - Part 1 - Setup and Category Entity Creation 08:52
11 Doctrine Fixtures - Part 2 - Relating Wallpapers with Categories 05:56
12 EasyAdminBundle - Setup and Category Configuration 06:02
13 EasyAdminBundle - Wallpaper Setup and List View 07:46
14 EasyAdminBundle - Starting with Wallpaper Uploads 05:57
15 Testing with PhpSpec to Guide Our Implementation 03:39
16 Using PhpSpec to Test our FileMover 05:34
17 Symfony Dependency Testing with PhpSpec 08:47
18 Defensive Counter Measures 06:32
19 No Tests - Part 1 - Uploading Files in EasyAdminBundle 11:02
20 No Tests - Part 2 - Uploading Files in EasyAdminBundle 07:05
21 Don't Mock What You Don't Own 09:36
22 You've Got To Take The Power Back 07:36
23 Making Symfony Work For Us 08:56
24 Testing The Wallpaper File Path Helper 15:11
25 Finally, It Works! 14:56
26 Why I Prefer Not To Use Twig 16:50
27 Fixing The Fixtures 11:20
28 Untested Updates 14:30
29 Untested Updates Part Two - Now We Can Actually Update 06:33
30 Adding an Image Preview On Edit 12:31
31 Delete Should Remove The Wallpaper Image File 11:02
32 Getting Started Testing Wallpaper Updates 10:02
33 Doing The Little Before The Big 08:13
34 Tested Image Preview... Sort Of :) 07:36
35 Finishing Up With a Tested Wallpaper Update 10:44
36 Test Driven Wallpaper Delete - Part 1 11:11
37 Test Driven Wallpaper Delete - Part 2 11:57
38 EasyAdminBundle Login Form Tutorial 08:01