Skip to content

Commit e831276

Browse files
committed
Fix #184: add make:page-data Maker command to scaffold PageData types
New MakePageData command generates an AbstractPageData subclass with #[ApiResource] and ORM column properties from --properties=name:type options. Skeleton template at skeleton/page_data/PageData.tpl.php. Command output includes a nuxt.config cwa.pageData snippet with property names and a CwaFixtureBuilder scaffold stub with ->pageData() and ->pageDataPosition() hints.
1 parent e4693b4 commit e831276

4 files changed

Lines changed: 358 additions & 0 deletions

File tree

src/Maker/MakePageData.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Silverback API Components Bundle Project
5+
*
6+
* (c) Daniel West <daniel@silverback.is>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Silverback\ApiComponentsBundle\Maker;
13+
14+
use Silverback\ApiComponentsBundle\Entity\Core\AbstractPageData;
15+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
16+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
17+
use Symfony\Bundle\MakerBundle\Generator;
18+
use Symfony\Bundle\MakerBundle\InputConfiguration;
19+
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
20+
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
21+
use Symfony\Component\Console\Command\Command;
22+
use Symfony\Component\Console\Input\InputArgument;
23+
use Symfony\Component\Console\Input\InputInterface;
24+
use Symfony\Component\Console\Input\InputOption;
25+
26+
final class MakePageData extends AbstractMaker
27+
{
28+
public static function getCommandName(): string
29+
{
30+
return 'make:page-data';
31+
}
32+
33+
public static function getCommandDescription(): string
34+
{
35+
return 'Create a new CWA PageData entity';
36+
}
37+
38+
public function configureCommand(Command $command, InputConfiguration $inputConf): void
39+
{
40+
$command
41+
->addArgument('name', InputArgument::OPTIONAL, 'The class name for the page data entity (e.g. <fg=yellow>ConferenceData</>)')
42+
->addOption('properties', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Property definitions in <comment>name:type</comment> format (e.g. <comment>headline:?string</comment>)', []);
43+
}
44+
45+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
46+
{
47+
$rawProperties = $input->getOption('properties');
48+
$properties = [];
49+
foreach ($rawProperties as $raw) {
50+
[$propName, $propType] = explode(':', $raw, 2);
51+
$properties[] = [
52+
'name' => $propName,
53+
'type' => $propType,
54+
'nullable' => str_starts_with($propType, '?'),
55+
];
56+
}
57+
58+
$classNameDetails = $generator->createClassNameDetails(
59+
$input->getArgument('name'),
60+
'Entity\\PageData\\'
61+
);
62+
63+
$useStatements = new UseStatementGenerator([
64+
'ApiPlatform\\Metadata\\ApiResource',
65+
['Doctrine\\ORM\\Mapping' => 'ORM'],
66+
AbstractPageData::class,
67+
]);
68+
69+
$generator->generateClass(
70+
$classNameDetails->getFullName(),
71+
__DIR__ . '/../Resources/skeleton/page_data/PageData.tpl.php',
72+
[
73+
'use_statements' => $useStatements,
74+
'properties' => $properties,
75+
]
76+
);
77+
78+
$generator->writeChanges();
79+
80+
$this->writeSuccessMessage($io);
81+
82+
$shortName = $classNameDetails->getShortName();
83+
$propertyNames = array_column($properties, 'name');
84+
85+
$io->text([
86+
'Next: run <comment>php bin/console make:migration</comment> to generate the database migration.',
87+
'<fg=yellow>Always review the generated migration</> before running it.',
88+
'',
89+
'Add the following to your <comment>nuxt.config.ts</comment> under <comment>cwa.pageData</comment>:',
90+
'',
91+
' ' . $shortName . ': {',
92+
' properties: [' . implode(', ', array_map(static fn ($p) => "'" . $p . "'", $propertyNames)) . '],',
93+
' },',
94+
'',
95+
'Fixture scaffold stub:',
96+
'',
97+
' $cwa->pageData(new ' . $shortName . '(), template: \'my-template\');',
98+
]);
99+
100+
if (!empty($propertyNames)) {
101+
$io->text([
102+
'',
103+
'To use a property as a dynamic page data slot in a template group:',
104+
]);
105+
foreach ($propertyNames as $propName) {
106+
$io->text(' ->pageDataPosition(' . $shortName . '::class, \'' . $propName . '\')');
107+
}
108+
}
109+
}
110+
111+
public function configureDependencies(DependencyBuilder $dependencies): void
112+
{
113+
}
114+
}

src/Resources/config/services_maker.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Silverback\ApiComponentsBundle\Resources\config;
1313

1414
use Silverback\ApiComponentsBundle\Maker\MakeApiComponent;
15+
use Silverback\ApiComponentsBundle\Maker\MakePageData;
1516
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
1617

1718
return static function (ContainerConfigurator $container): void {
@@ -22,4 +23,10 @@
2223
->public();
2324

2425
$services->alias(MakeApiComponent::class, 'silverback.api_components.maker.make_api_component');
26+
27+
$services->set('silverback.api_components.maker.make_page_data', MakePageData::class)
28+
->tag('maker.command')
29+
->public();
30+
31+
$services->alias(MakePageData::class, 'silverback.api_components.maker.make_page_data');
2532
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php echo "<?php\n"; ?>
2+
3+
namespace <?php echo $namespace; ?>;
4+
5+
<?php echo $use_statements; ?>
6+
#[ApiResource]
7+
#[ORM\Entity]
8+
class <?php echo $class_name; ?> extends AbstractPageData
9+
{
10+
<?php foreach ($properties as $prop) { ?>
11+
#[ORM\Column(nullable: <?php echo $prop['nullable'] ? 'true' : 'false'; ?>)]
12+
public <?php echo $prop['type']; ?> $<?php echo $prop['name']; ?>;
13+
14+
<?php } ?>
15+
}

tests/Maker/MakePageDataTest.php

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Silverback API Components Bundle Project
5+
*
6+
* (c) Daniel West <daniel@silverback.is>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Silverback\ApiComponentsBundle\Tests\Maker;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Silverback\ApiComponentsBundle\Maker\MakePageData;
16+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
17+
use Symfony\Bundle\MakerBundle\Generator;
18+
use Symfony\Bundle\MakerBundle\InputConfiguration;
19+
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
20+
use Symfony\Component\Console\Command\Command;
21+
use Symfony\Component\Console\Input\ArrayInput;
22+
use Symfony\Component\Console\Output\BufferedOutput;
23+
24+
class MakePageDataTest extends TestCase
25+
{
26+
private function makeMaker(): MakePageData
27+
{
28+
return new MakePageData();
29+
}
30+
31+
private function configuredCommand(): Command
32+
{
33+
$maker = $this->makeMaker();
34+
$command = new Command('make:page-data');
35+
$maker->configureCommand($command, new InputConfiguration());
36+
37+
return $command;
38+
}
39+
40+
private function boundInput(array $params): ArrayInput
41+
{
42+
$command = $this->configuredCommand();
43+
$input = new ArrayInput($params, $command->getDefinition());
44+
$input->setInteractive(false);
45+
46+
return $input;
47+
}
48+
49+
private function makeIo(BufferedOutput $output): ConsoleStyle
50+
{
51+
return new ConsoleStyle(new ArrayInput([]), $output);
52+
}
53+
54+
private function makeGenerator(string $expectedClass, array &$capturedVars): Generator
55+
{
56+
$generator = $this->createMock(Generator::class);
57+
58+
$generator->method('createClassNameDetails')
59+
->willReturn(new ClassNameDetails($expectedClass, 'Entity\\PageData\\'));
60+
61+
$generator->expects($this->once())
62+
->method('generateClass')
63+
->willReturnCallback(static function (string $className, string $template, array $vars) use (&$capturedVars): string {
64+
$capturedVars = $vars;
65+
66+
return 'src/Entity/PageData/' . basename($className) . '.php';
67+
});
68+
69+
$generator->expects($this->once())->method('writeChanges');
70+
71+
return $generator;
72+
}
73+
74+
public function test_command_name(): void
75+
{
76+
$this->assertSame('make:page-data', MakePageData::getCommandName());
77+
}
78+
79+
public function test_command_description(): void
80+
{
81+
$this->assertNotEmpty(MakePageData::getCommandDescription());
82+
}
83+
84+
public function test_generates_minimal_page_data_with_no_properties(): void
85+
{
86+
$vars = [];
87+
$generator = $this->makeGenerator('App\\Entity\\PageData\\ConferenceData', $vars);
88+
$input = $this->boundInput(['name' => 'ConferenceData']);
89+
$output = new BufferedOutput();
90+
91+
$this->makeMaker()->generate($input, $this->makeIo($output), $generator);
92+
93+
$this->assertSame([], $vars['properties']);
94+
}
95+
96+
public function test_generates_with_nullable_string_property(): void
97+
{
98+
$vars = [];
99+
$generator = $this->makeGenerator('App\\Entity\\PageData\\ConferenceData', $vars);
100+
$input = $this->boundInput(['name' => 'ConferenceData', '--properties' => ['headline:?string']]);
101+
$output = new BufferedOutput();
102+
103+
$this->makeMaker()->generate($input, $this->makeIo($output), $generator);
104+
105+
$this->assertCount(1, $vars['properties']);
106+
$this->assertSame('headline', $vars['properties'][0]['name']);
107+
$this->assertSame('?string', $vars['properties'][0]['type']);
108+
$this->assertTrue($vars['properties'][0]['nullable']);
109+
}
110+
111+
public function test_generates_with_non_nullable_property(): void
112+
{
113+
$vars = [];
114+
$generator = $this->makeGenerator('App\\Entity\\PageData\\ConferenceData', $vars);
115+
$input = $this->boundInput(['name' => 'ConferenceData', '--properties' => ['title:string']]);
116+
$output = new BufferedOutput();
117+
118+
$this->makeMaker()->generate($input, $this->makeIo($output), $generator);
119+
120+
$this->assertCount(1, $vars['properties']);
121+
$this->assertSame('title', $vars['properties'][0]['name']);
122+
$this->assertSame('string', $vars['properties'][0]['type']);
123+
$this->assertFalse($vars['properties'][0]['nullable']);
124+
}
125+
126+
public function test_generates_with_multiple_properties(): void
127+
{
128+
$vars = [];
129+
$generator = $this->makeGenerator('App\\Entity\\PageData\\ConferenceData', $vars);
130+
$input = $this->boundInput(['name' => 'ConferenceData', '--properties' => ['headline:?string', 'body:?string']]);
131+
$output = new BufferedOutput();
132+
133+
$this->makeMaker()->generate($input, $this->makeIo($output), $generator);
134+
135+
$this->assertCount(2, $vars['properties']);
136+
$this->assertSame('headline', $vars['properties'][0]['name']);
137+
$this->assertSame('body', $vars['properties'][1]['name']);
138+
}
139+
140+
public function test_stdout_contains_nuxt_config_properties_block(): void
141+
{
142+
$vars = [];
143+
$generator = $this->makeGenerator('App\\Entity\\PageData\\ConferenceData', $vars);
144+
$input = $this->boundInput(['name' => 'ConferenceData', '--properties' => ['headline:?string', 'body:?string']]);
145+
$output = new BufferedOutput();
146+
147+
$this->makeMaker()->generate($input, $this->makeIo($output), $generator);
148+
149+
$text = $output->fetch();
150+
$this->assertStringContainsString('nuxt.config', $text);
151+
$this->assertStringContainsString('cwa.pageData', $text);
152+
$this->assertStringContainsString('headline', $text);
153+
$this->assertStringContainsString('body', $text);
154+
$this->assertStringContainsString('properties', $text);
155+
}
156+
157+
public function test_stdout_contains_fixture_stub(): void
158+
{
159+
$vars = [];
160+
$generator = $this->makeGenerator('App\\Entity\\PageData\\ConferenceData', $vars);
161+
$input = $this->boundInput(['name' => 'ConferenceData', '--properties' => ['headline:?string']]);
162+
$output = new BufferedOutput();
163+
164+
$this->makeMaker()->generate($input, $this->makeIo($output), $generator);
165+
166+
$text = $output->fetch();
167+
$this->assertStringContainsString('->pageData(', $text);
168+
$this->assertStringContainsString('->pageDataPosition(', $text);
169+
$this->assertStringContainsString('ConferenceData', $text);
170+
$this->assertStringContainsString('headline', $text);
171+
}
172+
173+
public function test_stdout_does_not_contain_page_data_position_when_no_properties(): void
174+
{
175+
$vars = [];
176+
$generator = $this->makeGenerator('App\\Entity\\PageData\\ConferenceData', $vars);
177+
$input = $this->boundInput(['name' => 'ConferenceData']);
178+
$output = new BufferedOutput();
179+
180+
$this->makeMaker()->generate($input, $this->makeIo($output), $generator);
181+
182+
$text = $output->fetch();
183+
$this->assertStringContainsString('->pageData(', $text);
184+
$this->assertStringNotContainsString('->pageDataPosition(', $text);
185+
}
186+
187+
public function test_output_includes_migration_reminder(): void
188+
{
189+
$vars = [];
190+
$generator = $this->makeGenerator('App\\Entity\\PageData\\ConferenceData', $vars);
191+
$input = $this->boundInput(['name' => 'ConferenceData']);
192+
$output = new BufferedOutput();
193+
194+
$this->makeMaker()->generate($input, $this->makeIo($output), $generator);
195+
196+
$text = $output->fetch();
197+
$this->assertStringContainsString('make:migration', $text);
198+
}
199+
200+
public function test_template_path_points_to_page_data_skeleton(): void
201+
{
202+
$capturedTemplate = null;
203+
$generator = $this->createMock(Generator::class);
204+
$generator->method('createClassNameDetails')
205+
->willReturn(new ClassNameDetails('App\\Entity\\PageData\\ConferenceData', 'Entity\\PageData\\'));
206+
$generator->expects($this->once())
207+
->method('generateClass')
208+
->willReturnCallback(static function (string $className, string $template, array $vars) use (&$capturedTemplate): string {
209+
$capturedTemplate = $template;
210+
211+
return 'src/Entity/PageData/ConferenceData.php';
212+
});
213+
$generator->method('writeChanges');
214+
215+
$input = $this->boundInput(['name' => 'ConferenceData']);
216+
$output = new BufferedOutput();
217+
$this->makeMaker()->generate($input, $this->makeIo($output), $generator);
218+
219+
$this->assertStringContainsString('page_data', $capturedTemplate);
220+
$this->assertStringContainsString('PageData.tpl.php', $capturedTemplate);
221+
}
222+
}

0 commit comments

Comments
 (0)