Skip to content

Commit 107dab9

Browse files
committed
feat: add output and interactivity
1 parent ff52c6e commit 107dab9

File tree

11 files changed

+247
-136
lines changed

11 files changed

+247
-136
lines changed

config/persistence.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
44

5+
use Zenstruck\Foundry\Command\LoadStoryCommand;
56
use Zenstruck\Foundry\Persistence\PersistenceManager;
67
use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
78

@@ -17,5 +18,14 @@
1718
tagged_iterator('.foundry.persistence.database_resetter'),
1819
tagged_iterator('.foundry.persistence.schema_resetter'),
1920
])
21+
22+
->set('.zenstruck_foundry.story.load_story-command', LoadStoryCommand::class)
23+
->arg('$databaseResetters', tagged_iterator('.foundry.persistence.database_resetter'))
24+
->arg('$kernel', service('kernel'))
25+
->tag('console.command', [
26+
'command' => 'foundry:load-stories',
27+
'aliases' => ['foundry:load-fixtures'],
28+
'description' => 'Load stories which are marked with #[AsFixture] attribute.',
29+
])
2030
;
2131
};

config/services.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
44

55
use Faker;
6-
use Zenstruck\Foundry\Command\LoadStoryCommand;
76
use Zenstruck\Foundry\Configuration;
87
use Zenstruck\Foundry\FactoryRegistry;
98
use Zenstruck\Foundry\Object\Instantiator;
@@ -38,14 +37,5 @@
3837
service('.zenstruck_foundry.in_memory.repository_registry'),
3938
])
4039
->public()
41-
42-
->set('.zenstruck_foundry.story.load_story-command', LoadStoryCommand::class)
43-
->arg('$databaseResetters', tagged_iterator('.foundry.persistence.database_resetter'))
44-
->arg('$kernel', service('kernel'))
45-
->tag('console.command', [
46-
'command' => 'foundry:load-story',
47-
'aliases' => ['foundry:load-stories'],
48-
'description' => 'Load stories which are marked with #[AsFixture]',
49-
])
5040
;
5141
};

docs/index.rst

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ Foundry
44
Foundry makes creating fixtures data fun again, via an expressive, auto-completable, on-demand fixtures system with
55
Symfony and Doctrine:
66

7-
The factories can be used inside `DoctrineFixturesBundle <https://symfony.com/bundles/DoctrineFixturesBundle/current/index.html>`_
8-
to load fixtures or inside your tests, :ref:`where it has even more features <using-in-your-tests>`.
9-
107
Foundry supports ``doctrine/orm`` (with `doctrine/doctrine-bundle <https://github.com/doctrine/doctrinebundle>`_),
118
``doctrine/mongodb-odm`` (with `doctrine/mongodb-odm-bundle <https://github.com/doctrine/DoctrineMongoDBBundle>`_)
129
or a combination of these.
@@ -1284,52 +1281,6 @@ You can even create associative arrays, with the nice DX provided by Foundry:
12841281
// will create ['prop1' => 'foo', 'prop2' => 'default value 2']
12851282
$array = SomeArrayFactory::createOne(['prop1' => 'foo']);
12861283

1287-
Using with DoctrineFixturesBundle
1288-
---------------------------------
1289-
1290-
Foundry works out of the box with `DoctrineFixturesBundle <https://symfony.com/bundles/DoctrineFixturesBundle/current/index.html>`_.
1291-
You can simply use your factories and stories right within your fixture files:
1292-
1293-
::
1294-
1295-
// src/DataFixtures/AppFixtures.php
1296-
namespace App\DataFixtures;
1297-
1298-
use App\Factory\CategoryFactory;
1299-
use App\Factory\CommentFactory;
1300-
use App\Factory\PostFactory;
1301-
use App\Factory\TagFactory;
1302-
use Doctrine\Bundle\FixturesBundle\Fixture;
1303-
use Doctrine\Persistence\ObjectManager;
1304-
1305-
class AppFixtures extends Fixture
1306-
{
1307-
public function load(ObjectManager $manager)
1308-
{
1309-
// create 10 Category's
1310-
CategoryFactory::createMany(10);
1311-
1312-
// create 20 Tag's
1313-
TagFactory::createMany(20);
1314-
1315-
// create 50 Post's
1316-
PostFactory::createMany(50, function() {
1317-
return [
1318-
// each Post will have a random Category (chosen from those created above)
1319-
'category' => CategoryFactory::random(),
1320-
1321-
// each Post will have between 0 and 6 Tag's (chosen from those created above)
1322-
'tags' => TagFactory::randomRange(0, 6),
1323-
1324-
// each Post will have between 0 and 10 Comment's that are created new
1325-
'comments' => CommentFactory::new()->range(0, 10),
1326-
];
1327-
});
1328-
}
1329-
}
1330-
1331-
Run the ``doctrine:fixtures:load`` as normal to seed your database.
1332-
13331284
Using in your Tests
13341285
-------------------
13351286

@@ -2436,6 +2387,49 @@ You can use the ``#[WithStory]`` attribute to load stories in your tests:
24362387

24372388
If used on the class, the story will be loaded before each test method.
24382389

2390+
Loading stories as fixtures in your database
2391+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2392+
2393+
.. versionadded:: 2.6
2394+
2395+
Command ``foundry:load-stories`` and attribute ``#[AsFixture]`` were added in 2.6.
2396+
2397+
Using command ``bin/console foundry:load-stories``, you can load stories as fixtures in your database.
2398+
This is mainly useful to load fixtures in "dev" mode.
2399+
2400+
Mark with the attribute ``#[AsFixture]`` the stories your want to be loaded by the command:
2401+
2402+
::
2403+
2404+
use Zenstruck\Foundry\Attribute\AsFixture;
2405+
2406+
#[AsFixture(name: 'category')]
2407+
final class CategoryStory extends Story
2408+
{
2409+
// ...
2410+
}
2411+
2412+
``bin/console foundry:load-stories category`` will now load the story ``CategoryStory`` in your database.
2413+
2414+
You can also load stories by group, by using the ``groups`` option:
2415+
2416+
::
2417+
2418+
use Zenstruck\Foundry\Attribute\AsFixture;
2419+
2420+
#[AsFixture(name: 'category', groups: ['all-stories'])]
2421+
final class CategoryStory extends Story {}
2422+
2423+
#[AsFixture(name: 'post', groups: ['all-stories'])]
2424+
final class PostStory extends Story {}
2425+
2426+
``bin/console foundry:load-stories all-stories`` will load both stories ``CategoryStory`` and ``PostStory``.
2427+
2428+
.. note::
2429+
2430+
It is possible to call a story inside another story, by using `OtherStory::load();`. Because the stories are only
2431+
loaded once, it will work regardless of the order of the stories.
2432+
24392433
Static Analysis
24402434
---------------
24412435

src/Command/LoadStoryCommand.php

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
use Symfony\Component\Console\Command\Command;
1515
use Symfony\Component\Console\Exception\InvalidArgumentException;
16+
use Symfony\Component\Console\Exception\LogicException;
1617
use Symfony\Component\Console\Input\InputArgument;
1718
use Symfony\Component\Console\Input\InputInterface;
1819
use Symfony\Component\Console\Input\InputOption;
1920
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Style\SymfonyStyle;
2022
use Symfony\Component\DependencyInjection\ServiceLocator;
2123
use Symfony\Component\HttpKernel\KernelInterface;
2224
use Zenstruck\Foundry\Configuration;
@@ -32,10 +34,10 @@
3234
final class LoadStoryCommand extends Command
3335
{
3436
public function __construct(
35-
/** @var ServiceLocator<Story> */
36-
private readonly ServiceLocator $stories,
37-
/** @var ServiceLocator<list<Story>> */
38-
private readonly ServiceLocator $groupedStories,
37+
/** @var array<string, class-string<Story>> */
38+
private readonly array $stories,
39+
/** @var array<string, array<string, class-string<Story>>> */
40+
private readonly array $groupedStories,
3941
/** @var iterable<BeforeFirstTestResetter> */
4042
private iterable $databaseResetters,
4143
private KernelInterface $kernel,
@@ -54,33 +56,55 @@ protected function configure(): void
5456

5557
protected function execute(InputInterface $input, OutputInterface $output): int
5658
{
59+
if (count($this->stories) === 0) {
60+
throw new LogicException('No story as fixture available: add attribute #[AsFixture] to your story classes before running this command.');
61+
}
62+
63+
$io = new SymfonyStyle($input, $output);
64+
5765
if (!$input->getOption('append')) {
5866
$this->resetDatabase();
5967
}
6068

6169
$stories = [];
6270

6371
if (null === ($name = $input->getArgument('name'))) {
64-
// todo: ask interactively
72+
$storyNames = array_keys($this->stories);
73+
if (count($this->groupedStories) > 0) {
74+
$storyNames[] = '(choose a group of stories...)';
75+
}
76+
$name = $io->choice('Choose a story to load:', $storyNames);
77+
78+
if (!isset($this->stories[$name])) {
79+
$groupsNames = array_keys($this->groupedStories);
80+
$name = $io->choice('Choose a group of stories:', $groupsNames);
81+
}
6582
}
6683

67-
if ($this->stories->has($name)) {
68-
$stories = [$this->stories->get($name)];
84+
if (isset($this->stories[$name])) {
85+
$io->comment("Loading story with name \"{$name}\"...");
86+
$stories = [$name => $this->stories[$name]];
6987
}
7088

71-
if ($this->groupedStories->has($name)) {
72-
$stories = $this->groupedStories->get($name);
89+
if (isset($this->groupedStories[$name])) {
90+
$io->comment("Loading stories group \"{$name}\"...");
91+
$stories = $this->groupedStories[$name];
7392
}
7493

7594
if (!$stories) {
76-
throw new InvalidArgumentException("Fixture with name \"$name\" does not exist.");
95+
throw new InvalidArgumentException("Story with name \"$name\" does not exist.");
7796
}
7897

79-
foreach ($stories as $story) {
80-
// todo add some output
81-
$story::load();
98+
foreach ($stories as $name => $storyClass) {
99+
$storyClass::load();
100+
101+
if ($io->isVerbose()) {
102+
$io->info("Story \"$storyClass\" loaded (name: $name).");
103+
}
82104
}
83105

106+
$io->success('Stories successfully loaded!');
107+
84108
return self::SUCCESS;
85109
}
86110

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Zenstruck\Foundry\DependencyInjection;
4+
5+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
6+
use Symfony\Component\DependencyInjection\ContainerBuilder;
7+
use Symfony\Component\DependencyInjection\Exception\LogicException;
8+
use Symfony\Component\DependencyInjection\Reference;
9+
10+
final class AsFixtureStoryCompilerPass implements CompilerPassInterface
11+
{
12+
public function process(ContainerBuilder $container): void
13+
{
14+
if (!$container->has('.zenstruck_foundry.story.load_story-command')) {
15+
return;
16+
}
17+
18+
/** @var array<string, Reference> $fixtureStories */
19+
$fixtureStories = [];
20+
$groupedFixtureStories = [];
21+
foreach ($container->findTaggedServiceIds('foundry.story.fixture') as $id => $tags) {
22+
if (count($tags) !== 1) {
23+
throw new LogicException('Tag "foundry.story.fixture" must be used only once per service.');
24+
}
25+
26+
$name = $tags[0]['name'];
27+
28+
if (isset($fixtureStories[$name])) {
29+
throw new LogicException(
30+
"Cannot use #[AsFixture] name \"{$name}\" for service \"{$id}\". This name is already used by service \"{$fixtureStories[$name]}\"."
31+
);
32+
}
33+
34+
$storyClass = $container->findDefinition($id)->getClass();
35+
36+
$fixtureStories[$name] = $storyClass;
37+
38+
$groups = $tags[0]['groups'];
39+
if (!$groups) {
40+
continue;
41+
}
42+
43+
foreach ($groups as $group) {
44+
$groupedFixtureStories[$group] ??= [];
45+
$groupedFixtureStories[$group][$name] = $storyClass;
46+
}
47+
}
48+
49+
if ($collisionNames = array_intersect(array_keys($fixtureStories), array_keys($groupedFixtureStories))) {
50+
$collisionNames = implode('", "', $collisionNames);
51+
throw new LogicException(
52+
"Cannot use #[AsFixture] group(s) \"{$collisionNames}\", they collide with fixture names."
53+
);
54+
}
55+
56+
$container->findDefinition('.zenstruck_foundry.story.load_story-command')
57+
->setArgument('$stories', $fixtureStories)
58+
->setArgument('$groupedStories', $groupedFixtureStories);
59+
}
60+
}

src/ZenstruckFoundryBundle.php

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\DependencyInjection\Reference;
2121
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
2222
use Zenstruck\Foundry\Attribute\AsFixture;
23+
use Zenstruck\Foundry\DependencyInjection\AsFixtureStoryCompilerPass;
2324
use Zenstruck\Foundry\InMemory\DependencyInjection\InMemoryCompilerPass;
2425
use Zenstruck\Foundry\InMemory\InMemoryRepository;
2526
use Zenstruck\Foundry\Mongo\MongoResetter;
@@ -237,6 +238,7 @@ public function build(ContainerBuilder $container): void
237238

238239
$container->addCompilerPass($this);
239240
$container->addCompilerPass(new InMemoryCompilerPass());
241+
$container->addCompilerPass(new AsFixtureStoryCompilerPass());
240242
}
241243

242244
public function process(ContainerBuilder $container): void
@@ -248,46 +250,6 @@ public function process(ContainerBuilder $container): void
248250
->addMethodCall('addProvider', [new Reference($id)])
249251
;
250252
}
251-
252-
// todo use proper compiler pass
253-
// fixture stories
254-
/** @var array<string, Reference> $fixtureStories */
255-
$fixtureStories = [];
256-
$groupedFixtureStories = [];
257-
foreach ($container->findTaggedServiceIds('foundry.story.fixture') as $id => $tags) {
258-
if (count($tags) !== 1) {
259-
throw new LogicException('Tag "foundry.story.fixture" must be used only once per service.');
260-
}
261-
262-
$name = $tags[0]['name'];
263-
264-
if (isset($fixtureStories[$name])) {
265-
throw new LogicException("Cannot use #[AsFixture] name \"{$name}\" for service \"{$id}\". This name is already used by service \"{$fixtureStories[$name]}\".");
266-
}
267-
268-
$fixtureStories[$name] = new Reference($id);
269-
270-
$groups = $tags[0]['groups'];
271-
if (!$groups) {
272-
continue;
273-
}
274-
275-
foreach ($groups as $group) {
276-
$groupedFixtureStories[$group] ??= [];
277-
$groupedFixtureStories[$group][] = new Reference($id);
278-
}
279-
}
280-
281-
if ($collisionNames = array_intersect(array_keys($fixtureStories), array_keys($groupedFixtureStories))) {
282-
$collisionNames = implode('", "', $collisionNames);
283-
// todo: better message
284-
throw new LogicException("Cannot use #[AsFixture] group(s) \"{$collisionNames}\" They collide with fixture names.");
285-
}
286-
287-
$container->findDefinition('.zenstruck_foundry.story.load_story-command')
288-
->setArgument('$stories', ServiceLocatorTagPass::register($container, $fixtureStories))
289-
->setArgument('$groupedStories', ServiceLocatorTagPass::register($container, $groupedFixtureStories))
290-
;
291253
}
292254

293255
/**

tests/Fixture/TestKernel.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@
1919
use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory;
2020
use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryAddressRepository;
2121
use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryContactRepository;
22-
use Zenstruck\Foundry\Tests\Fixture\Stories\Fixtures\FixtureInGroupStory;
23-
use Zenstruck\Foundry\Tests\Fixture\Stories\Fixtures\FixtureStory;
24-
use Zenstruck\Foundry\Tests\Fixture\Stories\Fixtures\FixtureUsingAnotherFixtureStory;
2522
use Zenstruck\Foundry\Tests\Fixture\Stories\ServiceStory;
2623

2724
/**
@@ -56,12 +53,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
5653
$c->register(ArrayFactory::class)->setAutowired(true)->setAutoconfigured(true);
5754
$c->register(Object1Factory::class)->setAutowired(true)->setAutoconfigured(true);
5855
$c->register(ServiceStory::class)->setAutowired(true)->setAutoconfigured(true);
59-
6056
$c->register(InMemoryAddressRepository::class)->setAutowired(true)->setAutoconfigured(true);
6157
$c->register(InMemoryContactRepository::class)->setAutowired(true)->setAutoconfigured(true);
62-
63-
$c->register(FixtureStory::class)->setAutowired(true)->setAutoconfigured(true);
64-
$c->register(FixtureInGroupStory::class)->setAutowired(true)->setAutoconfigured(true);
65-
$c->register(FixtureUsingAnotherFixtureStory::class)->setAutowired(true)->setAutoconfigured(true);
6658
}
6759
}

0 commit comments

Comments
 (0)