|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Ehyiah\ApiDocBundle\Command\ComponentGeneration\Tag; |
| 4 | + |
| 5 | +use Ehyiah\ApiDocBundle\Attributes\AsTuiGenerator; |
| 6 | +use Ehyiah\ApiDocBundle\Command\ComponentGeneration\AbstractTuiComponentGenerator; |
| 7 | +use Ehyiah\ApiDocBundle\Enum\ComponentType; |
| 8 | +use Ehyiah\ApiDocBundle\Helper\LoadApiDocConfigHelper; |
| 9 | +use Symfony\Component\Console\Input\InputInterface; |
| 10 | +use Symfony\Component\Console\Output\OutputInterface; |
| 11 | +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; |
| 12 | +use Symfony\Component\HttpKernel\KernelInterface; |
| 13 | +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; |
| 14 | +use Symfony\Component\Tui\Event\CancelEvent; |
| 15 | +use Symfony\Component\Tui\Event\SelectEvent; |
| 16 | +use Symfony\Component\Tui\Event\SettingChangeEvent; |
| 17 | +use Symfony\Component\Tui\Event\SubmitEvent; |
| 18 | +use Symfony\Component\Tui\Tui; |
| 19 | +use Symfony\Component\Tui\Widget\ContainerWidget; |
| 20 | +use Symfony\Component\Tui\Widget\InputWidget; |
| 21 | +use Symfony\Component\Tui\Widget\SelectListWidget; |
| 22 | +use Symfony\Component\Tui\Widget\SettingItem; |
| 23 | +use Symfony\Component\Tui\Widget\SettingsListWidget; |
| 24 | +use Symfony\Component\Tui\Widget\TextWidget; |
| 25 | + |
| 26 | +use function Symfony\Component\String\u; |
| 27 | + |
| 28 | +#[AsTuiGenerator] |
| 29 | +class TagTuiGenerator extends AbstractTuiComponentGenerator |
| 30 | +{ |
| 31 | + private ?OutputInterface $currentOutput = null; |
| 32 | + |
| 33 | + public function __construct( |
| 34 | + private readonly TagTuiManager $manager, |
| 35 | + KernelInterface $kernel, |
| 36 | + ParameterBagInterface $parameterBag, |
| 37 | + PropertyInfoExtractorInterface $propertyInfoExtractor, |
| 38 | + LoadApiDocConfigHelper $apiDocConfigHelper, |
| 39 | + ) { |
| 40 | + parent::__construct($kernel, $parameterBag, $propertyInfoExtractor, $apiDocConfigHelper); |
| 41 | + } |
| 42 | + |
| 43 | + public function getLabel(): string |
| 44 | + { |
| 45 | + return 'Tag'; |
| 46 | + } |
| 47 | + |
| 48 | + public function getDescription(): string |
| 49 | + { |
| 50 | + return 'Generate an OpenAPI tag for grouping operations'; |
| 51 | + } |
| 52 | + |
| 53 | + public function isSupported(): bool |
| 54 | + { |
| 55 | + return true; |
| 56 | + } |
| 57 | + |
| 58 | + public function run(Tui $tui, InputInterface $input, OutputInterface $output, callable $onBack): void |
| 59 | + { |
| 60 | + $this->currentOutput = $output; |
| 61 | + $this->showComponentList($tui, $onBack); |
| 62 | + } |
| 63 | + |
| 64 | + private function showComponentList(Tui $tui, callable $onBack): void |
| 65 | + { |
| 66 | + $existing = $this->manager->getExistingComponents(); |
| 67 | + $choices = []; |
| 68 | + foreach ($existing as $name) { |
| 69 | + $choices[] = ['value' => $name, 'label' => $this->currentOutput->getFormatter()->format(sprintf(' %-20s', $name))]; |
| 70 | + } |
| 71 | + $choices[] = ['value' => '__new__', 'label' => $this->currentOutput->getFormatter()->format(' <fg=green>[+ New]</fg=green>')]; |
| 72 | + $choices[] = ['value' => '__back__', 'label' => $this->currentOutput->getFormatter()->format(' <comment>[← Back]</comment>')]; |
| 73 | + |
| 74 | + $selectWidget = new SelectListWidget($choices, 12); |
| 75 | + |
| 76 | + $tui->clear(); |
| 77 | + $container = new ContainerWidget(); |
| 78 | + $container->expandVertically(true); |
| 79 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format("\n<info>+---------------------------------------------+</info>"))); |
| 80 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format('<info>| Tags |</info>'))); |
| 81 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format("<info>+---------------------------------------------+</info>\n"))); |
| 82 | + $container->add($selectWidget); |
| 83 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format("\n<fg=gray>--------------------------------------------------</fg=gray>"))); |
| 84 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format('<fg=gray> ↑↓ Navigate ↵ Select Esc Back</fg=gray>'))); |
| 85 | + $tui->add($container); |
| 86 | + $tui->setFocus($selectWidget); |
| 87 | + |
| 88 | + $selectListener = function (SelectEvent $event) use ($tui, $selectWidget, $onBack, &$selectListener, &$cancelListener) { |
| 89 | + if ($event->getTarget() !== $selectWidget) { |
| 90 | + return; |
| 91 | + } |
| 92 | + $tui->getEventDispatcher()->removeListener(SelectEvent::class, $selectListener); |
| 93 | + $tui->getEventDispatcher()->removeListener(CancelEvent::class, $cancelListener); |
| 94 | + |
| 95 | + $value = $event->getValue(); |
| 96 | + if ('__back__' === $value) { |
| 97 | + $onBack(); |
| 98 | + |
| 99 | + return; |
| 100 | + } |
| 101 | + |
| 102 | + $state = new TagTuiState(); |
| 103 | + $state->outputDir = $this->manager->getDefaultDumpLocation(); |
| 104 | + if ('__new__' !== $value) { |
| 105 | + $state->name = $value; |
| 106 | + $existing = $this->manager->loadComponentConfig($value); |
| 107 | + if (null !== $existing) { |
| 108 | + $state->name = $existing['name']; |
| 109 | + $state->description = $existing['description']; |
| 110 | + $state->externalDocsUrl = $existing['externalDocsUrl']; |
| 111 | + $state->externalDocsDescription = $existing['externalDocsDescription']; |
| 112 | + } |
| 113 | + $state->loadedFrom = $this->manager->findComponentFile($value); |
| 114 | + } |
| 115 | + $this->showForm($tui, $state, $onBack); |
| 116 | + }; |
| 117 | + |
| 118 | + $cancelListener = static function (CancelEvent $event) use ($tui, $selectWidget, $onBack, &$selectListener, &$cancelListener) { |
| 119 | + if ($event->getTarget() === $selectWidget) { |
| 120 | + $tui->getEventDispatcher()->removeListener(SelectEvent::class, $selectListener); |
| 121 | + $tui->getEventDispatcher()->removeListener(CancelEvent::class, $cancelListener); |
| 122 | + $onBack(); |
| 123 | + } |
| 124 | + }; |
| 125 | + |
| 126 | + $tui->addListener($selectListener); |
| 127 | + $tui->addListener($cancelListener); |
| 128 | + } |
| 129 | + |
| 130 | + private function showForm(Tui $tui, TagTuiState $state, callable $onBack): void |
| 131 | + { |
| 132 | + $textInputCallback = static function (string $currentValue, callable $onDone) { |
| 133 | + $inputWidget = new InputWidget(); |
| 134 | + $inputWidget->setValue($currentValue); |
| 135 | + $inputWidget->setPrompt('Input: '); |
| 136 | + $inputWidget->onSubmit(static function (SubmitEvent $event) use ($onDone) { |
| 137 | + $onDone($event->getValue()); |
| 138 | + }); |
| 139 | + $inputWidget->onCancel(static function (CancelEvent $event) use ($onDone) { |
| 140 | + $onDone(null); |
| 141 | + }); |
| 142 | + |
| 143 | + return $inputWidget; |
| 144 | + }; |
| 145 | + |
| 146 | + $settingItems = []; |
| 147 | + $settingItems[] = new SettingItem('name', 'Name', $state->name, 'Tag name (e.g. Users)', [], $textInputCallback); |
| 148 | + $settingItems[] = new SettingItem('desc', 'Description', $state->description, 'Tag description (supports Markdown)', [], $textInputCallback); |
| 149 | + $settingItems[] = new SettingItem('externalDocsUrl', 'External Docs URL', $state->externalDocsUrl, 'External documentation URL', [], $textInputCallback); |
| 150 | + $settingItems[] = new SettingItem('externalDocsDesc', 'External Docs Description', $state->externalDocsDescription, 'External documentation description', [], $textInputCallback); |
| 151 | + |
| 152 | + $settingItems[] = new SettingItem('format_output', 'Output Format', $state->format_output, 'YAML or PHP', ['yaml', 'php']); |
| 153 | + $settingItems[] = new SettingItem('output', 'Output Directory', $state->outputDir, 'Target directory', [], $textInputCallback); |
| 154 | + $settingItems[] = new SettingItem('action_validate', 'Save', '✓ Confirm', 'Save and return to the list.', ['✓ Confirm']); |
| 155 | + $settingItems[] = new SettingItem('action_cancel', 'Cancel', '← Cancel', 'Return without saving.', ['← Cancel']); |
| 156 | + |
| 157 | + $settingsWidget = new SettingsListWidget($settingItems, 12); |
| 158 | + |
| 159 | + $tui->clear(); |
| 160 | + $container = new ContainerWidget(); |
| 161 | + $container->expandVertically(true); |
| 162 | + $title = $state->name ? "Edit: {$state->name}" : 'New Tag'; |
| 163 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format("\n<info>+---------------------------------------------+</info>"))); |
| 164 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format("<info>| {$title}</info>"))); |
| 165 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format("<info>+---------------------------------------------+</info>\n"))); |
| 166 | + $container->add($settingsWidget); |
| 167 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format("\n<fg=gray>--------------------------------------------------</fg=gray>"))); |
| 168 | + $container->add(new TextWidget($this->currentOutput->getFormatter()->format('<fg=gray> ↵ Save Esc Cancel</fg=gray>'))); |
| 169 | + $tui->add($container); |
| 170 | + $tui->setFocus($settingsWidget); |
| 171 | + |
| 172 | + $changeListener = function (SettingChangeEvent $event) use ($tui, $settingsWidget, $state, $onBack, &$changeListener, &$cancelListener) { |
| 173 | + if ($event->getTarget() !== $settingsWidget) { |
| 174 | + return; |
| 175 | + } |
| 176 | + |
| 177 | + switch ($event->getId()) { |
| 178 | + case 'action_cancel': |
| 179 | + $tui->getEventDispatcher()->removeListener(SettingChangeEvent::class, $changeListener); |
| 180 | + $tui->getEventDispatcher()->removeListener(CancelEvent::class, $cancelListener); |
| 181 | + $this->showComponentList($tui, $onBack); |
| 182 | + break; |
| 183 | + case 'action_validate': |
| 184 | + $state->name = $settingsWidget->getValue('name') ?? ''; |
| 185 | + $state->description = $settingsWidget->getValue('desc') ?? ''; |
| 186 | + $state->externalDocsUrl = $settingsWidget->getValue('externalDocsUrl') ?? ''; |
| 187 | + $state->externalDocsDescription = $settingsWidget->getValue('externalDocsDesc') ?? ''; |
| 188 | + $state->format_output = $settingsWidget->getValue('format_output') ?? 'yaml'; |
| 189 | + $state->outputDir = $settingsWidget->getValue('output') ?? $this->manager->getDefaultDumpLocation(); |
| 190 | + |
| 191 | + $tui->getEventDispatcher()->removeListener(SettingChangeEvent::class, $changeListener); |
| 192 | + $tui->getEventDispatcher()->removeListener(CancelEvent::class, $cancelListener); |
| 193 | + $tui->stop(); |
| 194 | + $this->generateComponent($state); |
| 195 | + break; |
| 196 | + } |
| 197 | + }; |
| 198 | + |
| 199 | + $cancelListener = function (CancelEvent $event) use ($tui, $settingsWidget, $onBack, &$changeListener, &$cancelListener) { |
| 200 | + if ($event->getTarget() === $settingsWidget) { |
| 201 | + $tui->getEventDispatcher()->removeListener(SettingChangeEvent::class, $changeListener); |
| 202 | + $tui->getEventDispatcher()->removeListener(CancelEvent::class, $cancelListener); |
| 203 | + $this->showComponentList($tui, $onBack); |
| 204 | + } |
| 205 | + }; |
| 206 | + |
| 207 | + $tui->addListener($changeListener); |
| 208 | + $tui->addListener($cancelListener); |
| 209 | + } |
| 210 | + |
| 211 | + private function generateComponent(TagTuiState $state): void |
| 212 | + { |
| 213 | + $tag = [ |
| 214 | + 'name' => $state->name, |
| 215 | + ]; |
| 216 | + |
| 217 | + if ('' !== $state->description) { |
| 218 | + $tag['description'] = $state->description; |
| 219 | + } |
| 220 | + |
| 221 | + if ('' !== $state->externalDocsUrl) { |
| 222 | + $externalDocs = ['url' => $state->externalDocsUrl]; |
| 223 | + if ('' !== $state->externalDocsDescription) { |
| 224 | + $externalDocs['description'] = $state->externalDocsDescription; |
| 225 | + } |
| 226 | + $tag['externalDocs'] = $externalDocs; |
| 227 | + } |
| 228 | + |
| 229 | + $array = [ |
| 230 | + 'documentation' => [ |
| 231 | + 'tags' => [ |
| 232 | + $tag, |
| 233 | + ], |
| 234 | + ], |
| 235 | + ]; |
| 236 | + |
| 237 | + $destination = ComponentType::Tags->value; |
| 238 | + |
| 239 | + if (null !== $state->loadedFrom && file_exists($state->loadedFrom)) { |
| 240 | + $dumpLocation = dirname($state->loadedFrom) . '/' . $state->name . '.yaml'; |
| 241 | + // Merge with existing config to preserve manually-added fields |
| 242 | + $existingConfig = \Symfony\Component\Yaml\Yaml::parseFile($state->loadedFrom); |
| 243 | + if (isset($existingConfig['documentation']['tags']) && is_array($existingConfig['documentation']['tags'])) { |
| 244 | + $mergedTags = $existingConfig['documentation']['tags']; |
| 245 | + // Find and update existing tag or add new one |
| 246 | + $found = false; |
| 247 | + foreach ($mergedTags as &$existingTag) { |
| 248 | + if (isset($existingTag['name']) && $existingTag['name'] === $state->name) { |
| 249 | + $existingTag = array_merge($existingTag, $tag); |
| 250 | + $found = true; |
| 251 | + break; |
| 252 | + } |
| 253 | + } |
| 254 | + unset($existingTag); |
| 255 | + if (!$found) { |
| 256 | + $mergedTags[] = $tag; |
| 257 | + } |
| 258 | + $array['documentation']['tags'] = $mergedTags; |
| 259 | + } |
| 260 | + } else { |
| 261 | + $outputDir = u($state->outputDir)->ensureStart('/')->ensureEnd('/'); |
| 262 | + $dumpDirectory = $this->kernel->getProjectDir() . $outputDir . u($destination)->ensureEnd('/'); |
| 263 | + |
| 264 | + if (!is_dir($dumpDirectory)) { |
| 265 | + mkdir($dumpDirectory, 0755, true); |
| 266 | + } |
| 267 | + |
| 268 | + $dumpLocation = $dumpDirectory . $state->name . '.yaml'; |
| 269 | + } |
| 270 | + |
| 271 | + if ('yaml' === $state->format_output) { |
| 272 | + $this->writeYamlFile($array, $dumpLocation, $this->currentOutput); |
| 273 | + } else { |
| 274 | + $dumpLocation = str_replace('.yaml', '.php', $dumpLocation); |
| 275 | + $phpCode = $this->generatePhpBuilderCode($array, $state->name, $destination); |
| 276 | + $this->writePhpFile($phpCode, $dumpLocation, $this->currentOutput); |
| 277 | + } |
| 278 | + |
| 279 | + $this->currentOutput->writeln(sprintf('<info>Tag "%s" generated successfully in %s</info>', $state->name, $dumpLocation)); |
| 280 | + } |
| 281 | +} |
0 commit comments