Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.88% covered (warning)
65.88%
56 / 85
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikiCommand
65.88% covered (warning)
65.88%
56 / 85
40.00% covered (danger)
40.00%
2 / 5
19.71
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
1
 execute
76.47% covered (warning)
76.47%
26 / 34
0.00% covered (danger)
0.00%
0 / 1
7.64
 initializeWikiSubmodule
33.33% covered (danger)
33.33%
10 / 30
0.00% covered (danger)
0.00%
0 / 1
5.67
 getGitRepositoryUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Console\Command;
21
22use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
23use Composer\Command\BaseCommand;
24use FastForward\DevTools\Composer\Json\ComposerJsonInterface;
25use FastForward\DevTools\Console\Input\HasJsonOption;
26use FastForward\DevTools\Filesystem\FilesystemInterface;
27use FastForward\DevTools\Git\GitClientInterface;
28use FastForward\DevTools\Process\ProcessBuilderInterface;
29use FastForward\DevTools\Process\ProcessQueueInterface;
30use FastForward\DevTools\Path\ManagedWorkspace;
31use Psr\Log\LoggerInterface;
32use Symfony\Component\Console\Attribute\AsCommand;
33use Symfony\Component\Console\Input\InputInterface;
34use Symfony\Component\Console\Input\InputOption;
35use Symfony\Component\Console\Output\BufferedOutput;
36use Symfony\Component\Console\Output\OutputInterface;
37use Symfony\Component\Filesystem\Path;
38
39use function Safe\getcwd;
40
41/**
42 * Handles the generation of API documentation for the project.
43 * This class MUST NOT be extended and SHALL utilize phpDocumentor to accomplish its task.
44 */
45#[AsCommand(
46    name: 'wiki',
47    description: 'Generates API documentation in Markdown format.',
48    help: 'This command generates API documentation in Markdown format using phpDocumentor. '
49    . 'It accepts an optional `--target` option to specify the output directory and `--init` to initialize the wiki submodule.'
50)]
51 class WikiCommand extends BaseCommand implements LoggerAwareCommandInterface
52{
53    use HasJsonOption;
54    use LogsCommandResults;
55
56    /**
57     * Creates a new WikiCommand instance.
58     *
59     * @param ComposerJsonInterface $composer the composer.json accessor
60     * @param ProcessBuilderInterface $processBuilder
61     * @param ProcessQueueInterface $processQueue
62     * @param FilesystemInterface $filesystem the filesystem used to inspect the wiki target
63     * @param GitClientInterface $gitClient
64     * @param LoggerInterface $logger the output-aware logger
65     */
66    public function __construct(
67        private  ProcessBuilderInterface $processBuilder,
68        private  ProcessQueueInterface $processQueue,
69        private  ComposerJsonInterface $composer,
70        private  FilesystemInterface $filesystem,
71        private  GitClientInterface $gitClient,
72        private  LoggerInterface $logger,
73    ) {
74        return parent::__construct();
75    }
76
77    /**
78     * Configures the command instance.
79     *
80     * The method MUST set up the name and description. It MAY accept an optional `--target` option
81     * pointing to an alternative configuration target path.
82     *
83     * @return void
84     */
85    protected function configure(): void
86    {
87        $this->addJsonOption()
88            ->addOption(
89                name: 'target',
90                shortcut: 't',
91                mode: InputOption::VALUE_OPTIONAL,
92                description: 'Path to the output directory for the generated Markdown documentation.',
93                default: '.github/wiki'
94            )
95            ->addOption(
96                name: 'cache-dir',
97                mode: InputOption::VALUE_OPTIONAL,
98                description: 'Path to the cache directory for phpDocumentor.',
99                default: ManagedWorkspace::getCacheDirectory(ManagedWorkspace::PHPDOC)
100            )
101            ->addOption(
102                name: 'init',
103                mode: InputOption::VALUE_NONE,
104                description: 'Initialize the configured wiki target as a Git submodule.',
105            );
106    }
107
108    /**
109     * Executes the generation of the documentation files in Markdown format.
110     *
111     * This method MUST compile arguments based on PSR-4 namespaces to feed into phpDocumentor.
112     * It SHOULD provide feedback on generation progress, and SHALL return `self::SUCCESS` on success.
113     *
114     * @param InputInterface $input the input details for the command
115     * @param OutputInterface $output the output mechanism for logging
116     *
117     * @return int the final execution status code
118     */
119    protected function execute(InputInterface $input, OutputInterface $output): int
120    {
121        $jsonOutput = $this->isJsonOutput($input);
122        $processOutput = $jsonOutput ? new BufferedOutput() : $output;
123        $target = (string) $input->getOption('target');
124
125        if ($input->getOption('init')) {
126            return $this->initializeWikiSubmodule($input, $target, $processOutput);
127        }
128
129        if (! $jsonOutput) {
130            $this->logger->info('Generating wiki documentation...', [
131                'input' => $input,
132            ]);
133        }
134
135        $processBuilder = $this->processBuilder
136            ->withArgument('--visibility', 'public,protected')
137            ->withArgument('--template', 'vendor/saggre/phpdocumentor-markdown/themes/markdown')
138            ->withArgument('--title', $this->composer->getDescription())
139            ->withArgument('--target', $target)
140            ->withArgument('--cache-folder', $input->getOption('cache-dir'));
141
142        $psr4Namespaces = $this->composer->getAutoload('psr-4');
143
144        foreach ($psr4Namespaces as $path) {
145            $processBuilder = $processBuilder->withArgument('--directory', $path);
146        }
147
148        if ($defaultPackageName = array_key_first($psr4Namespaces)) {
149            $processBuilder = $processBuilder->withArgument('--defaultpackagename', $defaultPackageName);
150        }
151
152        $this->processQueue->add($processBuilder->build('vendor/bin/phpdoc'));
153
154        $result = $this->processQueue->run($processOutput);
155
156        if (self::SUCCESS === $result) {
157            return $this->success('Wiki documentation generated successfully.', $input, [
158                'output' => $processOutput,
159            ]);
160        }
161
162        return $this->failure(
163            'Wiki documentation generation failed.',
164            $input,
165            [
166                'output' => $processOutput,
167            ],
168            (string) $input->getOption('target'),
169        );
170    }
171
172    /**
173     * Adds the repository wiki as a Git submodule when the target path is missing.
174     *
175     * @param string $target the configured wiki target path
176     * @param OutputInterface $output the output used for process feedback
177     * @param InputInterface $input
178     *
179     * @return int the command status code
180     */
181    private function initializeWikiSubmodule(InputInterface $input, string $target, OutputInterface $output): int
182    {
183        $wikiSubmodulePath = (string) $this->filesystem->getAbsolutePath($target);
184
185        if ($this->filesystem->exists($wikiSubmodulePath)) {
186            return $this->success(
187                'Wiki submodule already exists at {wiki_submodule_path}.',
188                $input,
189                [
190                    'input' => $input,
191                    'wiki_submodule_path' => $wikiSubmodulePath,
192                ],
193            );
194        }
195
196        $repositoryUrl = $this->getGitRepositoryUrl();
197        $wikiRepoUrl = str_replace('.git', '.wiki.git', $repositoryUrl);
198
199        $this->processQueue->add(
200            $this->processBuilder
201                ->withArgument('submodule')
202                ->withArgument('add')
203                ->withArgument($wikiRepoUrl)
204                ->withArgument(Path::makeRelative($wikiSubmodulePath, getcwd()))
205                ->build('git')
206        );
207
208        $result = $this->processQueue->run($output);
209
210        if (self::SUCCESS === $result) {
211            return $this->success('Wiki submodule initialized successfully.', $input, [
212                'wiki_submodule_path' => $wikiSubmodulePath,
213                'wiki_repository_url' => $wikiRepoUrl,
214            ]);
215        }
216
217        return $this->failure('Wiki submodule initialization failed.', $input, [
218            'wiki_submodule_path' => $wikiSubmodulePath,
219            'wiki_repository_url' => $wikiRepoUrl,
220        ], $target);
221    }
222
223    /**
224     * Resolves the current repository remote origin URL.
225     *
226     * @return string the Git remote origin URL
227     */
228    private function getGitRepositoryUrl(): string
229    {
230        return $this->gitClient->getConfig('remote.origin.url', getcwd());
231    }
232}