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