Buy Access to Course
05.

Factory Data Seeding

|

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

I have a confession: I've been making us do way too much work!

To seed the database, we instantiate the entity, grab the EntityManager, then persist and flush it. There's nothing wrong with this, but Foundry is about to make our life a lot easier.

Generating the Factory

At your terminal, run:

php bin/console make:factory

This command comes from Foundry. I'll select to generate all the factories.

The idea is that you'll create a factory for each entity that you want to create dummy data for, either in a test or for your normal fixtures. We only need LockDownFactory, but that's fine.

Spin over and look at src/Factory/LockDownFactory.php. I'm not going to talk too much about these factory classes: we already cover them in our Doctrine tutorial. But this class will make it easy to create LockDown objects, even setting createdAt to a random DateTime, reason to some random text, and status randomly to one of the valid statuses, by default.

72 lines | src/Factory/LockDownFactory.php
// ... lines 1 - 30
final class LockDownFactory extends ModelFactory
{
// ... lines 33 - 47
protected function getDefaults(): array
{
return [
'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
'reason' => self::faker()->text(),
'status' => self::faker()->randomElement(LockDownStatus::cases()),
];
}
// ... lines 56 - 70
}

Using the Factory in a Test

Using this in a test is a delight. Say LockDownFactory::createOne(). Here, we can pass an array of any field that we want to explicitly set. The only thing we care about is that this LockDown has an ACTIVE status. So, set status to LockDownStatus::ACTIVE.

// ... lines 1 - 6
use App\Factory\LockDownFactory;
// ... lines 8 - 13
class LockDownRepositoryTest extends KernelTestCase
{
// ... lines 16 - 24
public function testIsInLockDownReturnsTrueIfMostRecentLockDownIsActive()
{
// ... lines 27 - 28
LockDownFactory::createOne([
'status' => LockDownStatus::ACTIVE,
]);
// ... lines 32 - 33
}
// ... lines 35 - 39
}

That's it! We don't need to create this LockDown and we don't need the EntityManager. That one call takes care of everything.

Watch, when we run the test:

symfony php vendor/bin/phpunit tests/Integration/Repository/LockDownRepositoryTest.php

It passes! I love that.

Foundry Proxy Objects

By the way, the LockDownRepository method returns the new LockDown object... which can often be handy. But it's actually wrapped in a special proxy object. So if we run the test now, you can see it's a proxy... and the LockDown is hiding inside.

Why does Foundry do that? Well, if you go and find their documentation, they have a whole section about using this library inside of tests. One spot talks about the object proxy. The proxy allows you to call all the normal methods on your entity plus several additional methods, like ->save(), ->remove() or even ->repository() to get another proxy object that wraps the repository.

So it looks and acts like your normal object, but with some extra methods. That's not important for us right now, I just wanted you to be aware of it. If you do need the real entity object, you can call ->object() to get it.

// ... lines 1 - 24
public function testIsInLockDownReturnsTrueIfMostRecentLockDownIsActive()
{
// ... lines 27 - 31
dd($lockDown->assertNotPersisted());
// ... lines 33 - 34
}
// ... lines 36 - 42

Adding More Objects

Anyway, now that adding data is so simple, we can quickly make our test more robust. To see if we can trick my query, call createMany()... to create 5 LockDown objects with LockDownStatus::ENDED.

To make sure our query looks only at the newest LockDown, for the active one, set its createdAt to -1 day. And for the ENDED, set these to something older.

// ... lines 1 - 24
public function testIsInLockDownReturnsTrueIfMostRecentLockDownIsActive()
{
// ... lines 27 - 28
LockDownFactory::createOne([
'createdAt' => new \DateTimeImmutable('-1 day'),
'status' => LockDownStatus::ACTIVE,
]);
LockDownFactory::createMany(5, [
'createdAt' => new \DateTimeImmutable('-2 day'),
'status' => LockDownStatus::ENDED,
]);
// ... lines 37 - 38
}
// ... lines 40 - 46

Let's see if our query is robust enough to still behave correctly.

symfony php vendor/bin/phpunit tests/Integration/Repository/LockDownRepositoryTest.php

It is!

But... actually... management has some extra tricky rules around a lockdown. Copy this test, paste it, and rename it to testIsInLockdownReturnsFalseIfTheMostRecentIsNotActive.

// ... lines 1 - 40
public function testIsInLockDownReturnsFalseIfMostRecentIsNotActive()
{
// ... lines 43 - 52
}
// ... lines 54 - 60

To explain management's weird rule, let me tweak the data. Make the first LockDown ENDED... then the next, older 5 status ACTIVE. Finally, assertFalse() at the bottom.

// ... lines 1 - 40
public function testIsInLockDownReturnsFalseIfMostRecentIsNotActive()
{
self::bootKernel();
LockDownFactory::createOne([
'createdAt' => new \DateTimeImmutable('-1 day'),
'status' => LockDownStatus::ENDED,
]);
LockDownFactory::createMany(5, [
'createdAt' => new \DateTimeImmutable('-2 days'),
'status' => LockDownStatus::ACTIVE,
]);
$this->assertFalse($this->getLockDownRepository()->isInLockDown());
}
// ... lines 56 - 62

That... might look confusing... and it kind of is. According to management, when determining if we're in lockdown, we should ONLY look at the MOST recent LockDown status. If there are older active lockdowns... those, apparently, don't matter.

Not surprisingly, when we try the tests:

symfony php vendor/bin/phpunit tests/Integration/Repository/LockDownRepositoryTest.php

This one fails. But, look on the bright side: that test was super-fast to write! And now we can go into LockDownRepository to fix things. I'll fast-forward through some changes that fetch the most recent LockDown regardless of its status.

If we don't find any lockdowns, return false. Else, I'll add an assert() to help my editor... then return true if the status does not equal LockDownStatus::ENDED.

43 lines | src/Repository/LockDownRepository.php
// ... lines 1 - 17
class LockDownRepository extends ServiceEntityRepository
{
// ... lines 20 - 24
public function isInLockDown(): bool
{
// find the most recent lock down
$lockDown = $this->createQueryBuilder('lock_down')
->orderBy('lock_down.createdAt', 'DESC')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
if (!$lockDown) {
return false;
}
assert($lockDown instanceof LockDown);
return $lockDown->getStatus() !== LockDownStatus::ENDED;
}
}

And now:

symfony php vendor/bin/phpunit tests/Integration/Repository/LockDownRepositoryTest.php

We're green!

Using the LockDown Feature

We've been living in our terminal so long that I think we should celebrate by using this on our site. In the fixtures, I've added an active LockDown by default.

Head over to MainController... and autowire LockdownRepository $lockdownRepository. Then throw a new variable in the template called isLockedDown set to $lockdownRepository->isInLockdown().

41 lines | src/Controller/MainController.php
// ... lines 1 - 6
use App\Repository\LockDownRepository;
// ... lines 8 - 13
class MainController extends AbstractController
{
// ... line 16
public function index(GithubService $github, DinosaurRepository $repository, LockDownRepository $lockDownRepository): Response
{
// ... lines 19 - 24
return $this->render('main/index.html.twig', [
// ... line 26
'isLockedDown' => $lockDownRepository->isInLockDown(),
]);
}
// ... lines 30 - 39
}

Finally, in the template - templates/main/index.html.twig - I already have a _lockdownAlert.html.twig template. If, isLockedDown, include that.

56 lines | templates/main/index.html.twig
// ... lines 1 - 2
{% block body %}
{% if isLockedDown %}
{{ include('main/_lockDownAlert.html.twig') }}
{% endif %}
// ... lines 7 - 54
{% endblock %}

Moment of truth. Refresh. Run for your life! We are in lockdown!

Next: we need a way to turn a lockdown off. Because, if I click this, it... does nothing! To help with this new task, we're going to use an integration test on a different class: on one of our normal services.