Skip to content

Commit c26b7e1

Browse files
authored
Merge pull request #11 from tito10047/make-action
Add support for generating Action class and template in maker command
2 parents e23f945 + b754eae commit c26b7e1

5 files changed

Lines changed: 189 additions & 1 deletion

File tree

README.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,24 @@
1111

1212
A Symfony bundle that implements the **Single Directory Component (SDC)** methodology for Symfony UX. It bridges the gap between **AssetMapper** and **Twig Components** by providing a fully automated, convention-over-configuration workflow.
1313

14+
## Real-world Usage & Developer Experience
15+
16+
This bundle is actively used in production. Here are some real-world examples:
17+
- [formalitka.mostka.sk](https://formalitka.mostka.sk/)
18+
- [mostka.sk](https://mostka.sk/)
19+
- [ycon.cc](https://ycon.cc)
20+
21+
### Developer Evaluation
22+
Working with UX SDC provides an excellent developer experience. A recommended project structure organizes the code into distinct functional areas:
23+
- **UI**: Generic interface elements (e.g., `Button`, `Spinner`, `Tabs`).
24+
- **Layout**: Structural page elements (e.g., `TopBar`, `Footer`, `FlashMessage`).
25+
- **Component**: Reusable feature blocks.
26+
- **Page**: Complete page components (e.g., `Homepage`, `AboutUs`).
27+
28+
A significant advantage of this architecture is the ability to place Symfony controllers directly within the page-level SDC component directory (e.g., `HomepageAction.php`). The controller merely handles routing and renders the base layout, while all business and presentation logic remains encapsulated in isolated SDC components.
29+
30+
Because the code is highly granular and strictly structured, AI tools work exceptionally well within this architecture, easily generating robust and creative design implementations.
31+
1432
## The Concept
1533

1634
This bundle is inspired by the architectural challenges discussed in **["A Better Architecture for Your Symfony UX Twig Components"](https://hugo.alliau.me/blog/posts/a-better-architecture-for-your-symfony-ux-twig-components)** by **Hugo Alliaume**.
@@ -214,7 +232,54 @@ This will create:
214232
- `src/Component/UI/Alert/Alert.php` (PHP logic)
215233
- `src/Component/UI/Alert/Alert.html.twig` (Twig template)
216234
- `src/Component/UI/Alert/Alert.css` (CSS styles)
217-
- (Optional) `src/Component/Alert/Alert_controller.js` (Stimulus controller)
235+
- (Optional) `src/Component/UI/Alert/Alert_controller.js` (Stimulus controller)
236+
237+
The maker supports options and interactive mode:
238+
- `--stimulus` to force generating a Stimulus controller (non-interactive mode will not create it unless explicitly set)
239+
- `--action` to generate a minimal controller action and a wrapper Twig template for the component
240+
241+
Example with an action:
242+
243+
```bash
244+
php bin/console make:sdc-component Page:Homepage --action
245+
```
246+
247+
This will additionally create:
248+
- `src/Component/Page/Homepage/HomepageAction.php` (Symfony controller)
249+
- `src/Component/Page/Homepage/HomepageAction.html.twig` (page template rendering the component)
250+
251+
Generated files contents:
252+
253+
```php
254+
// src/Component/Page/Homepage/HomepageAction.php
255+
namespace App\Component\Page\Homepage;
256+
257+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
258+
use Symfony\Component\Routing\Attribute\Route;
259+
260+
class HomepageAction extends AbstractController
261+
{
262+
#[Route('/en', name: 'app.homepage')]
263+
public function index(): \Symfony\Component\HttpFoundation\Response
264+
{
265+
return $this->render('Page/Homepage/HomepageAction.html.twig');
266+
}
267+
}
268+
```
269+
270+
```twig
271+
{# src/Component/Page/Homepage/HomepageAction.html.twig #}
272+
{% extends 'layout.html.twig' %}
273+
274+
{% block content %}
275+
<twig:Page:Homepage:Homepage />
276+
{% endblock %}
277+
```
278+
279+
In interactive mode, you will be asked:
280+
- for the component name (supports `:` or `/` separators, e.g. `UI:Alert` or `UI/Alert`)
281+
- whether to generate a Stimulus controller
282+
- whether to generate an Action class and template
218283

219284
---
220285

src/Maker/MakeSdcComponent.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4848
$command
4949
->addArgument('name', InputArgument::OPTIONAL, 'The name of the component (e.g. <alternate>Alert</alternate>)')
5050
->addOption('stimulus', null, InputOption::VALUE_NONE, 'Whether to generate a Stimulus controller')
51+
->addOption('action', null, InputOption::VALUE_NONE, 'Whether to generate an action class and template')
5152
->setHelp(<<<EOF
5253
The <info>make:sdc-component</info> command generates a new Single Directory Component (SDC).
5354
@@ -56,6 +57,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
5657
- A Twig template
5758
- A CSS file
5859
- (Optional) A Stimulus controller
60+
- (Optional) An Action class and template
5961
6062
Example:
6163
<info>php bin/console make:sdc-component UI:Alert</info>
@@ -80,6 +82,15 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
8082
$withStimulus = $io->confirm('Do you want to generate a Stimulus controller?', true);
8183
}
8284

85+
if ($input->getOption('action')) {
86+
$withAction = true;
87+
} elseif (!$input->isInteractive()) {
88+
// In non-interactive mode, do NOT generate Action unless explicitly requested
89+
$withAction = false;
90+
} else {
91+
$withAction = $io->confirm('Do you want to generate an Action class and template?', false);
92+
}
93+
8394
$name = str_replace(['/', ':'], '\\', $name);
8495
$parts = explode('\\', $name);
8596
$componentName = Str::asClassName(array_pop($parts));
@@ -131,6 +142,27 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
131142
);
132143
}
133144

145+
if ($withAction) {
146+
$fullTwigComponentName = ($subPath ? str_replace('/', ':', $subPath) . ':' : '') . $componentName . ':' . $componentName;
147+
148+
$generator->generateClass(
149+
$fullNamespace . '\\' . $componentName . '\\' . $componentName . 'Action',
150+
__DIR__.'/../../templates/sdc/Action.tpl.php',
151+
[
152+
'component_name' => $componentName,
153+
'sub_path' => $subPath,
154+
]
155+
);
156+
157+
$generator->generateFile(
158+
$directory . '/' . $componentName . '/' . $componentName . 'Action.html.twig',
159+
__DIR__.'/../../templates/sdc/ActionTemplate.tpl.php',
160+
[
161+
'full_twig_component_name' => $fullTwigComponentName,
162+
]
163+
);
164+
}
165+
134166
$generator->writeChanges();
135167

136168
$this->writeSuccessMessage($io);

templates/sdc/Action.tpl.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the UX SDC Bundle
5+
*
6+
* (c) Jozef Môstka <https://github.com/tito10047/ux-sdc>
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+
?>
13+
<?= "<?php\n" ?>
14+
15+
namespace <?= $namespace ?>;
16+
17+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
18+
use Symfony\Component\Routing\Attribute\Route;
19+
20+
class <?= $class_name ?> extends AbstractController
21+
{
22+
#[Route('/en', name: 'app.<?= strtolower($component_name) ?>')]
23+
public function index(): \Symfony\Component\HttpFoundation\Response
24+
{
25+
return $this->render('<?= ($sub_path ? $sub_path.'/' : '').$component_name ?>/<?= $component_name ?>Action.html.twig');
26+
}
27+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{% extends 'layout.html.twig' %}
2+
3+
{% block content %}
4+
<twig:<?= $full_twig_component_name ?> />
5+
{% endblock %}

tests/Integration/Maker/MakeSdcComponentTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ protected function setUp(): void
2626
parent::setUp();
2727
$this->removeDir(self::getContainer()->getParameter('ux_sdc.ux_components_dir') . '/UI');
2828
$this->removeDir(self::getContainer()->getParameter('ux_sdc.ux_components_dir') . '/Alert');
29+
$this->removeDir(self::getContainer()->getParameter('ux_sdc.ux_components_dir') . '/Page');
2930
}
3031

3132
protected function tearDown(): void
3233
{
3334
parent::tearDown();
3435
$this->removeDir(self::getContainer()->getParameter('ux_sdc.ux_components_dir') . '/UI');
3536
$this->removeDir(self::getContainer()->getParameter('ux_sdc.ux_components_dir') . '/Alert');
37+
$this->removeDir(self::getContainer()->getParameter('ux_sdc.ux_components_dir') . '/Page');
3638
}
3739

3840
private function removeDir(string $dir): void
@@ -87,6 +89,63 @@ public function testMakeSdcComponent(): void
8789
$this->assertStringContainsString('tests/Integration/Fixtures/Component/UI/Alert/Alert.php', $display);
8890
}
8991

92+
public function testMakeSdcComponentWithAction(): void
93+
{
94+
self::bootKernel();
95+
$application = new Application(self::$kernel);
96+
97+
$command = $application->find('make:sdc-component');
98+
$tester = new CommandTester($command);
99+
100+
// UI:Homepage s voľbou --action
101+
$tester->execute([
102+
'name' => 'Page:Homepage',
103+
'--action' => true,
104+
], [
105+
'interactive' => false,
106+
]);
107+
108+
$tester->assertCommandIsSuccessful();
109+
110+
$baseDir = self::getContainer()->getParameter('ux_sdc.ux_components_dir');
111+
$this->assertFileExists($baseDir . '/Page/Homepage/Homepage.php');
112+
$this->assertFileExists($baseDir . '/Page/Homepage/HomepageAction.php');
113+
$this->assertFileExists($baseDir . '/Page/Homepage/HomepageAction.html.twig');
114+
115+
$actionPhpContent = file_get_contents($baseDir . '/Page/Homepage/HomepageAction.php');
116+
$this->assertStringContainsString('namespace Tito10047\UX\Sdc\Tests\Integration\Fixtures\Component\Page\Homepage;', $actionPhpContent);
117+
$this->assertStringContainsString('class HomepageAction extends AbstractController', $actionPhpContent);
118+
$this->assertStringContainsString('#[Route(\'/en\', name: \'app.homepage\')]', $actionPhpContent);
119+
$this->assertStringContainsString('return $this->render(\'Page/Homepage/HomepageAction.html.twig\');', $actionPhpContent);
120+
121+
$actionTwigContent = file_get_contents($baseDir . '/Page/Homepage/HomepageAction.html.twig');
122+
$this->assertStringContainsString('{% extends \'layout.html.twig\' %}', $actionTwigContent);
123+
$this->assertStringContainsString('<twig:Page:Homepage:Homepage />', $actionTwigContent);
124+
}
125+
126+
public function testMakeSdcComponentInteractiveWithAction(): void
127+
{
128+
self::bootKernel();
129+
$application = new Application(self::$kernel);
130+
131+
$command = $application->find('make:sdc-component');
132+
$tester = new CommandTester($command);
133+
134+
$tester->setInputs([
135+
'Page:Homepage', // Component name
136+
'n', // Stimulus?
137+
'y', // Action?
138+
]);
139+
140+
$tester->execute([]);
141+
142+
$tester->assertCommandIsSuccessful();
143+
144+
$baseDir = self::getContainer()->getParameter('ux_sdc.ux_components_dir');
145+
$this->assertFileExists($baseDir . '/Page/Homepage/HomepageAction.php');
146+
$this->assertFileExists($baseDir . '/Page/Homepage/HomepageAction.html.twig');
147+
}
148+
90149
public function testMakeSdcComponentWithColonSeparator(): void
91150
{
92151
self::bootKernel();

0 commit comments

Comments
 (0)