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