Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.59% covered (success)
96.59%
85 / 88
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DocsCommand
96.59% covered (success)
96.59%
85 / 88
75.00% covered (warning)
75.00%
3 / 4
12
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%
33 / 33
100.00% covered (success)
100.00%
1 / 1
1
 execute
92.11% covered (success)
92.11%
35 / 38
0.00% covered (danger)
0.00%
0 / 1
8.03
 createPhpDocumentorConfig
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 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 Twig\Environment;
27use FastForward\DevTools\Filesystem\FilesystemInterface;
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;
38
39use function Safe\getcwd;
40
41/**
42 * Generates the package API documentation through phpDocumentor.
43 *
44 * The command prepares a temporary phpDocumentor configuration from the
45 * current package metadata, then delegates execution to the shared process
46 * queue so logging and grouped output stay consistent with the rest of the
47 * command surface.
48 */
49#[AsCommand(
50    name: 'reports:docs',
51    description: 'Generates API documentation.',
52    aliases: ['reports:phpdoc', 'phpdoc', 'phpDocumentor', 'docs'],
53)]
54 class DocsCommand extends Command
55{
56    use HasCacheOption;
57    use HasJsonOption;
58    use LogsCommandResults;
59
60    /**
61     * Creates a new DocsCommand instance.
62     *
63     * @param ProcessBuilderInterface $processBuilder the process builder for executing phpDocumentor
64     * @param ProcessQueueInterface $processQueue the process queue for managing execution
65     * @param Environment $renderer
66     * @param FilesystemInterface $filesystem the filesystem for handling file operations
67     * @param ComposerJsonInterface $composer the composer.json handler for accessing project metadata
68     * @param LoggerInterface $logger the output-aware logger
69     */
70    public function __construct(
71        private  ProcessBuilderInterface $processBuilder,
72        private  ProcessQueueInterface $processQueue,
73        private  Environment $renderer,
74        private  FilesystemInterface $filesystem,
75        private  ComposerJsonInterface $composer,
76        private  LoggerInterface $logger,
77    ) {
78        parent::__construct();
79    }
80
81    /**
82     * Configures the command options used to generate API documentation.
83     */
84    protected function configure(): void
85    {
86        $this->setHelp('This command generates API documentation using phpDocumentor.');
87        $this
88            ->addJsonOption()
89            ->addCacheOption('Whether to enable phpDocumentor caching.')
90            ->addCacheDirOption(
91                description: 'Path to the cache directory for phpDocumentor.',
92                default: ManagedWorkspace::getCacheDirectory(ManagedWorkspace::PHPDOC),
93            )
94            ->addOption(
95                name: 'progress',
96                mode: InputOption::VALUE_NONE,
97                description: 'Whether to enable progress output from phpDocumentor.',
98            )
99            ->addOption(
100                name: 'target',
101                shortcut: 't',
102                mode: InputOption::VALUE_OPTIONAL,
103                description: 'Path to the output directory for the generated HTML documentation.',
104                default: ManagedWorkspace::getOutputDirectory(),
105            )
106            ->addOption(
107                name: 'source',
108                shortcut: 's',
109                mode: InputOption::VALUE_OPTIONAL,
110                description: 'Path to the source directory for the generated HTML documentation.',
111                default: 'docs',
112            )
113            ->addOption(
114                name: 'template',
115                mode: InputOption::VALUE_OPTIONAL,
116                description: 'Path to the template directory for the generated HTML documentation.',
117                default: 'vendor/fast-forward/phpdoc-bootstrap-template',
118            );
119    }
120
121    /**
122     * Generates the HTML API documentation for the configured source tree.
123     *
124     * @param InputInterface $input the input details for the command
125     * @param OutputInterface $output the output mechanism for logging
126     *
127     * @return int the final execution status code
128     */
129    protected function execute(InputInterface $input, OutputInterface $output): int
130    {
131        $jsonOutput = $this->isJsonOutput($input);
132        $processOutput = $jsonOutput ? new BufferedOutput() : $output;
133        $progress = ! $jsonOutput && (bool) $input->getOption('progress');
134        $cacheEnabled = $this->isCacheEnabled($input);
135
136        $source = $this->filesystem->getAbsolutePath($input->getOption('source'));
137        $target = $this->filesystem->getAbsolutePath($input->getOption('target'));
138        $cacheDir = $this->filesystem->getAbsolutePath($input->getOption('cache-dir'));
139
140        $this->logger->info('Generating API documentation...', [
141            'input' => $input,
142        ]);
143
144        if (! $this->filesystem->exists($source)) {
145            return $this->failure('Source directory not found: {source}', $input, [
146                'source' => $source,
147            ]);
148        }
149
150        $config = $this->createPhpDocumentorConfig(
151            source: $source,
152            target: $target,
153            template: $input->getOption('template'),
154            cacheDir: $cacheEnabled ? $cacheDir : sys_get_temp_dir(),
155        );
156
157        $processBuilder = $this->processBuilder
158            ->withArgument('--config', $config)
159            ->withArgument('--ansi')
160            ->withArgument('--markers', 'TODO,FIXME,BUG,HACK');
161
162        if ($cacheEnabled) {
163            $processBuilder = $processBuilder->withArgument('--cache-folder', $cacheDir);
164        }
165
166        if (! $progress) {
167            $processBuilder = $processBuilder->withArgument('--no-progress');
168        }
169
170        $phpdoc = $processBuilder->build('vendor/bin/phpdoc');
171
172        $this->processQueue->add(process: $phpdoc, label: 'Generating API Docs with phpDocumentor');
173
174        $result = $this->processQueue->run($processOutput);
175
176        if (self::SUCCESS === $result) {
177            return $this->success('API documentation generated successfully.', $input, [
178                'output' => $processOutput,
179            ]);
180        }
181
182        return $this->failure('API documentation generation failed.', $input, [
183            'output' => $processOutput,
184        ]);
185    }
186
187    /**
188     * Creates a temporary phpDocumentor configuration for the current project.
189     *
190     * @param string $source the source directory for the generated documentation
191     * @param string $target the output directory for the generated documentation
192     * @param string $template the phpDocumentor template name or path
193     * @param string $cacheDir the cache directory for phpDocumentor
194     *
195     * @return string the absolute path to the generated configuration
196     */
197    private function createPhpDocumentorConfig(
198        string $source,
199        string $target,
200        string $template,
201        string $cacheDir
202    ): string {
203        $workingDirectory = getcwd();
204        $autoload = $this->composer->getAutoload('psr-4');
205        $guidePath = $this->filesystem->makePathRelative($source);
206        $defaultPackageName = array_key_first($autoload) ?: '';
207
208        $content = $this->renderer->render('phpdocumentor.xml', [
209            'title' => $this->composer->getName(),
210            'template' => $template,
211            'target' => $target,
212            'cacheDir' => $cacheDir,
213            'workingDirectory' => $workingDirectory,
214            'paths' => $autoload,
215            'guidePath' => $guidePath,
216            'defaultPackageName' => rtrim($defaultPackageName, '\\'),
217        ]);
218
219        $this->filesystem->dumpFile(filename: 'phpdocumentor.xml', content: $content, path: $cacheDir);
220
221        return $this->filesystem->getAbsolutePath('phpdocumentor.xml', $cacheDir);
222    }
223}