Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
59 / 59
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
DocsCommand
100.00% covered (success)
100.00%
59 / 59
100.00% covered (success)
100.00%
3 / 3
6
100.00% covered (success)
100.00%
1 / 1
 configure
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 createPhpDocumentorConfig
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of fast-forward/dev-tools.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/dev-tools
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\DevTools\Command;
20
21use Symfony\Component\Console\Input\InputInterface;
22use Symfony\Component\Console\Input\InputOption;
23use Symfony\Component\Console\Output\OutputInterface;
24use Symfony\Component\Filesystem\Path;
25use Symfony\Component\Process\Process;
26
27use RuntimeException;
28
29use function Safe\file_get_contents;
30use function Safe\file_put_contents;
31use function array_map;
32use function implode;
33use function is_string;
34use function ltrim;
35use function Safe\mkdir;
36use function Safe\realpath;
37use function sprintf;
38use function strtr;
39
40/**
41 * Handles the generation of API documentation for the project.
42 * This class MUST NOT be extended and SHALL utilize phpDocumentor to accomplish its task.
43 */
44final class DocsCommand extends AbstractCommand
45{
46    /**
47     * Configures the command instance.
48     *
49     * The method MUST set up the name and description. It MAY accept an optional `--target` option
50     * pointing to an alternative configuration target path.
51     *
52     * @return void
53     */
54    protected function configure(): void
55    {
56        $this
57            ->setName('docs')
58            ->setDescription('Generates API documentation.')
59            ->setHelp('This command generates API documentation using phpDocumentor.')
60            ->addOption(
61                name: 'target',
62                shortcut: 't',
63                mode: InputOption::VALUE_OPTIONAL,
64                description: 'Path to the output directory for the generated HTML documentation.',
65                default: 'public/docs',
66            )
67            ->addOption(
68                name: 'source',
69                shortcut: 's',
70                mode: InputOption::VALUE_OPTIONAL,
71                description: 'Path to the source directory for the generated HTML documentation.',
72                default: 'docs',
73            );
74    }
75
76    /**
77     * Executes the generation of the documentation files.
78     *
79     * This method MUST compile arguments based on PSR-4 namespaces to feed into phpDocumentor.
80     * It SHOULD provide feedback on generation progress, and SHALL return `self::SUCCESS` on success.
81     *
82     * @param InputInterface $input the input details for the command
83     * @param OutputInterface $output the output mechanism for logging
84     *
85     * @return int the final execution status code
86     */
87    protected function execute(InputInterface $input, OutputInterface $output): int
88    {
89        $output->writeln('<info>Generating API documentation...</info>');
90
91        $source = $this->getAbsolutePath($input->getOption('source'));
92
93        if (!$this->filesystem->exists($source)) {
94            $output->writeln(sprintf('<error>Source directory not found: %s</error>', $source));
95
96            return self::FAILURE;
97        }
98
99        $target = $this->getAbsolutePath($input->getOption('target'));
100
101        $htmlConfig = $this->createPhpDocumentorConfig(
102            source: $source,
103            target: $target,
104            template: 'default',
105        );
106
107        $command = new Process([
108            $this->getAbsolutePath('vendor/bin/phpdoc'),
109            '--config',
110            $htmlConfig,
111        ]);
112
113        return parent::runProcess($command, $output);
114    }
115
116    /**
117     * Creates a temporary phpDocumentor configuration for the current project.
118     *
119     * @param string $source the source directory for the generated documentation
120     * @param string $target the output directory for the generated documentation
121     * @param string $template the phpDocumentor template name or path
122     *
123     * @return string the absolute path to the generated configuration
124     */
125    private function createPhpDocumentorConfig(string $source, string $target, string $template): string
126    {
127        $workingDirectory = $this->getCurrentWorkingDirectory();
128
129        $templateFile = $this->getAbsolutePath('resources/phpdocumentor.xml');
130
131        $configDirectory = $this->getAbsolutePath('tmp/cache/phpdoc');
132        $configFile = $configDirectory . '/phpdocumentor.xml';
133    
134        if (! $this->filesystem->exists($configDirectory)) {
135            $this->filesystem->mkdir($configDirectory);
136        }
137
138        $psr4Namespaces = $this->getPsr4Namespaces();
139        $paths = implode("\n", array_map(
140            fn (string $path): string => sprintf('<path>%s</path>', ltrim(str_replace($workingDirectory, '', $path), '/')),
141            $psr4Namespaces,
142        ));
143
144        $guidePath = Path::makeRelative($source, $workingDirectory);
145
146        $defaultPackageName = array_key_first($psr4Namespaces) ?: '';
147        $templateContents = $this->filesystem->readFile($templateFile);
148        
149        $this->filesystem->dumpFile($configFile, strtr($templateContents, [
150            '%%TITLE%%' => $this->getProjectDescription(),
151            '%%TEMPLATE%%' => $template,
152            '%%TARGET%%' => $target,
153            '%%WORKING_DIRECTORY%%' => $workingDirectory,
154            '%%PATHS%%' => $paths,
155            '%%GUIDE_PATH%%' => $guidePath,
156            '%%DEFAULT_PACKAGE_NAME%%' => $defaultPackageName,
157        ]));
158
159        return $configFile;
160    }
161}