Factory Data Seeding
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 SubscribeI 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.
// ... 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
.
// ... 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()
.
// ... 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.
// ... 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.