Skip to content

Commit 9b96324

Browse files
[make:stimulus-controller] New Stimulus controller Maker command (#1075)
Co-authored-by: Jesse Rushlow <[email protected]>
1 parent 4028f85 commit 9b96324

File tree

7 files changed

+373
-0
lines changed

7 files changed

+373
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 Symfony\Bundle\MakerBundle\Maker;
13+
14+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
15+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
16+
use Symfony\Bundle\MakerBundle\Generator;
17+
use Symfony\Bundle\MakerBundle\InputConfiguration;
18+
use Symfony\Bundle\MakerBundle\Str;
19+
use Symfony\Component\Console\Command\Command;
20+
use Symfony\Component\Console\Input\InputArgument;
21+
use Symfony\Component\Console\Input\InputInterface;
22+
use Symfony\Component\Console\Question\Question;
23+
use Symfony\WebpackEncoreBundle\WebpackEncoreBundle;
24+
25+
/**
26+
* @author Abdelilah Jabri <[email protected]>
27+
*
28+
* @internal
29+
*/
30+
final class MakeStimulusController extends AbstractMaker
31+
{
32+
public static function getCommandName(): string
33+
{
34+
return 'make:stimulus-controller';
35+
}
36+
37+
public static function getCommandDescription(): string
38+
{
39+
return 'Creates a new Stimulus controller';
40+
}
41+
42+
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
43+
{
44+
$command
45+
->addArgument('name', InputArgument::REQUIRED, 'The name of the Stimulus controller (e.g. <fg=yellow>hello</>)')
46+
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeStimulusController.txt'));
47+
}
48+
49+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
50+
{
51+
$command->addArgument('extension', InputArgument::OPTIONAL);
52+
$command->addArgument('targets', InputArgument::OPTIONAL, '', []);
53+
$command->addArgument('values', InputArgument::OPTIONAL, '', []);
54+
55+
$chosenExtension = $io->choice(
56+
'Language (<fg=yellow>JavaScript</> or <fg=yellow>TypeScript</>)',
57+
[
58+
'js' => 'JavaScript',
59+
'ts' => 'TypeScript',
60+
]
61+
);
62+
63+
$input->setArgument('extension', $chosenExtension);
64+
65+
if ($io->confirm('Do you want to include targets?')) {
66+
$targets = [];
67+
$isFirstTarget = true;
68+
69+
while (true) {
70+
$newTarget = $this->askForNextTarget($io, $targets, $isFirstTarget);
71+
$isFirstTarget = false;
72+
73+
if (null === $newTarget) {
74+
break;
75+
}
76+
77+
$targets[] = $newTarget;
78+
}
79+
80+
$input->setArgument('targets', $targets);
81+
}
82+
83+
if ($io->confirm('Do you want to include values?')) {
84+
$values = [];
85+
$isFirstValue = true;
86+
while (true) {
87+
$newValue = $this->askForNextValue($io, $values, $isFirstValue);
88+
$isFirstValue = false;
89+
90+
if (null === $newValue) {
91+
break;
92+
}
93+
94+
$values[$newValue['name']] = $newValue;
95+
}
96+
97+
$input->setArgument('values', $values);
98+
}
99+
}
100+
101+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
102+
{
103+
$controllerName = Str::asSnakeCase($input->getArgument('name'));
104+
$chosenExtension = $input->getArgument('extension');
105+
$targets = $input->getArgument('targets');
106+
$values = $input->getArgument('values');
107+
108+
$targets = empty($targets) ? $targets : sprintf("['%s']", implode("', '", $targets));
109+
110+
$fileName = sprintf('%s_controller.%s', $controllerName, $chosenExtension);
111+
$filePath = sprintf('assets/controllers/%s', $fileName);
112+
113+
$generator->generateFile(
114+
$filePath,
115+
'stimulus/Controller.tpl.php',
116+
[
117+
'targets' => $targets,
118+
'values' => $values,
119+
]
120+
);
121+
122+
$generator->writeChanges();
123+
124+
$this->writeSuccessMessage($io);
125+
126+
$io->text([
127+
'Next:',
128+
sprintf('- Open <info>%s</info> and add the code you need', $filePath),
129+
'Find the documentation at <fg=yellow>https://github.com/symfony/stimulus-bridge</>',
130+
]);
131+
}
132+
133+
private function askForNextTarget(ConsoleStyle $io, array $targets, bool $isFirstTarget): ?string
134+
{
135+
$questionText = 'New target name (press <return> to stop adding targets)';
136+
137+
if (!$isFirstTarget) {
138+
$questionText = 'Add another target? Enter the target name (or press <return> to stop adding targets)';
139+
}
140+
141+
$targetName = $io->ask($questionText, null, function (?string $name) use ($targets) {
142+
if (\in_array($name, $targets)) {
143+
throw new \InvalidArgumentException(sprintf('The "%s" target already exists.', $name));
144+
}
145+
146+
return $name;
147+
});
148+
149+
return !$targetName ? null : $targetName;
150+
}
151+
152+
private function askForNextValue(ConsoleStyle $io, array $values, bool $isFirstValue): ?array
153+
{
154+
$questionText = 'New value name (press <return> to stop adding values)';
155+
156+
if (!$isFirstValue) {
157+
$questionText = 'Add another value? Enter the value name (or press <return> to stop adding values)';
158+
}
159+
160+
$valueName = $io->ask($questionText, null, function ($name) use ($values) {
161+
if (\array_key_exists($name, $values)) {
162+
throw new \InvalidArgumentException(sprintf('The "%s" value already exists.', $name));
163+
}
164+
165+
return $name;
166+
});
167+
168+
if (!$valueName) {
169+
return null;
170+
}
171+
172+
$defaultType = 'String';
173+
// try to guess the type by the value name prefix/suffix
174+
// convert to snake case for simplicity
175+
$snakeCasedField = Str::asSnakeCase($valueName);
176+
177+
if ('_id' === $suffix = substr($snakeCasedField, -3)) {
178+
$defaultType = 'Number';
179+
} elseif (0 === strpos($snakeCasedField, 'is_')) {
180+
$defaultType = 'Boolean';
181+
} elseif (0 === strpos($snakeCasedField, 'has_')) {
182+
$defaultType = 'Boolean';
183+
}
184+
185+
$type = null;
186+
$types = $this->getValuesTypes();
187+
188+
while (null === $type) {
189+
$question = new Question('Value type (enter <comment>?</comment> to see all types)', $defaultType);
190+
$question->setAutocompleterValues($types);
191+
$type = $io->askQuestion($question);
192+
193+
if ('?' === $type) {
194+
$this->printAvailableTypes($io);
195+
$io->writeln('');
196+
197+
$type = null;
198+
} elseif (!\in_array($type, $types)) {
199+
$this->printAvailableTypes($io);
200+
$io->error(sprintf('Invalid type "%s".', $type));
201+
$io->writeln('');
202+
203+
$type = null;
204+
}
205+
}
206+
207+
return ['name' => $valueName, 'type' => $type];
208+
}
209+
210+
private function printAvailableTypes(ConsoleStyle $io): void
211+
{
212+
foreach ($this->getValuesTypes() as $type) {
213+
$io->writeln(sprintf('<info>%s</info>', $type));
214+
}
215+
}
216+
217+
private function getValuesTypes(): array
218+
{
219+
return [
220+
'Array',
221+
'Boolean',
222+
'Number',
223+
'Object',
224+
'String',
225+
];
226+
}
227+
228+
public function configureDependencies(DependencyBuilder $dependencies): void
229+
{
230+
$dependencies->addClassDependency(
231+
WebpackEncoreBundle::class,
232+
'webpack-encore-bundle'
233+
);
234+
}
235+
}

src/Resources/config/makers.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,9 @@
131131
<argument>%kernel.project_dir%</argument>
132132
<tag name="maker.command" />
133133
</service>
134+
135+
<service id="maker.maker.make_stimulus_controller" class="Symfony\Bundle\MakerBundle\Maker\MakeStimulusController">
136+
<tag name="maker.command" />
137+
</service>
134138
</services>
135139
</container>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The <info>%command.name%</info> command generates new Stimulus Controller.
2+
3+
<info>php %command.full_name% hello</info>
4+
5+
If the argument is missing, the command will ask for the controller name interactively.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
/*
4+
* The following line makes this controller "lazy": it won't be downloaded until needed
5+
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
6+
*/
7+
/* stimulusFetch: 'lazy' */
8+
export default class extends Controller {
9+
<?= $targets ? " static targets = $targets\n" : "" ?>
10+
<?php if ($values) { ?>
11+
static values = {
12+
<?php foreach ($values as $value): ?>
13+
<?= $value['name'] ?>: <?= $value['type'] ?>,
14+
<?php endforeach; ?>
15+
}
16+
<?php } ?>
17+
// ...
18+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 Symfony\Bundle\MakerBundle\Tests\Maker;
13+
14+
use Symfony\Bundle\MakerBundle\Maker\MakeStimulusController;
15+
use Symfony\Bundle\MakerBundle\Test\MakerTestCase;
16+
use Symfony\Bundle\MakerBundle\Test\MakerTestRunner;
17+
18+
class MakeStimulusControllerTest extends MakerTestCase
19+
{
20+
protected function getMakerClass(): string
21+
{
22+
return MakeStimulusController::class;
23+
}
24+
25+
public function getTestDetails(): \Generator
26+
{
27+
yield 'it_generates_stimulus_controller_with_targets' => [$this->createMakerTest()
28+
->run(function (MakerTestRunner $runner) {
29+
$runner->runMaker(
30+
[
31+
'with_targets', // controller name
32+
'js', // controller language
33+
'yes', // add targets
34+
'results', // first target
35+
'messages', // second target
36+
'errors', // third target
37+
'', // empty input to stop adding targets
38+
]);
39+
40+
$generatedFilePath = $runner->getPath('assets/controllers/with_targets_controller.js');
41+
42+
$this->assertFileExists($generatedFilePath);
43+
44+
$generatedFileContents = file_get_contents($generatedFilePath);
45+
$expectedContents = file_get_contents(__DIR__.'/../fixtures/make-stimulus-controller/with_targets.js');
46+
47+
$this->assertSame(
48+
$expectedContents,
49+
$generatedFileContents
50+
);
51+
}),
52+
];
53+
54+
yield 'it_generates_stimulus_controller_without_targets' => [$this->createMakerTest()
55+
->run(function (MakerTestRunner $runner) {
56+
$runner->runMaker(
57+
[
58+
'without_targets', // controller name
59+
'js', // controller language
60+
'no', // do not add targets
61+
]);
62+
63+
$generatedFilePath = $runner->getPath('assets/controllers/without_targets_controller.js');
64+
65+
$this->assertFileExists($generatedFilePath);
66+
67+
$generatedFileContents = file_get_contents($generatedFilePath);
68+
$expectedContents = file_get_contents(__DIR__.'/../fixtures/make-stimulus-controller/without_targets.js');
69+
70+
$this->assertSame(
71+
$expectedContents,
72+
$generatedFileContents
73+
);
74+
}),
75+
];
76+
77+
yield 'it_generates_typescript_stimulus_controller' => [$this->createMakerTest()
78+
->run(function (MakerTestRunner $runner) {
79+
$runner->runMaker(
80+
[
81+
'typescript', // controller name
82+
'ts', // controller language
83+
'no', // do not add targets
84+
]);
85+
86+
$this->assertFileExists($runner->getPath('assets/controllers/typescript_controller.ts'));
87+
}),
88+
];
89+
}
90+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
/*
4+
* The following line makes this controller "lazy": it won't be downloaded until needed
5+
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
6+
*/
7+
/* stimulusFetch: 'lazy' */
8+
export default class extends Controller {
9+
static targets = ['results', 'messages', 'errors']
10+
// ...
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
/*
4+
* The following line makes this controller "lazy": it won't be downloaded until needed
5+
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
6+
*/
7+
/* stimulusFetch: 'lazy' */
8+
export default class extends Controller {
9+
// ...
10+
}

0 commit comments

Comments
 (0)