Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.13% covered (warning)
81.13%
43 / 53
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProjectCapabilitiesResolver
81.13% covered (warning)
81.13%
43 / 53
57.14% covered (warning)
57.14%
4 / 7
31.90
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
 resolve
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 resolveDefaultPackageName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 resolveApiDirectories
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 resolveRelativeApiDirectory
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 resolveHasPhpSourceFiles
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
7.05
 normalizeAutoloadPaths
42.86% covered (danger)
42.86%
6 / 14
0.00% covered (danger)
0.00%
0 / 1
19.94
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\Project;
21
22use FastForward\DevTools\Composer\Json\ComposerJsonInterface;
23use FastForward\DevTools\Filesystem\FilesystemInterface;
24
25use function array_key_first;
26use function array_values;
27use function is_dir;
28use function is_file;
29use function rtrim;
30use function str_ends_with;
31use function strtolower;
32
33/**
34 * Resolves which repository surfaces are available to documentation, testing, and wiki tooling.
35 */
36  class ProjectCapabilitiesResolver implements ProjectCapabilitiesResolverInterface
37{
38    /**
39     * @var list<string> Composer autoload sections that MAY expose API source directories
40     */
41    private const array API_AUTOLOAD_TYPES = ['psr-4', 'psr-0', 'classmap'];
42
43    /**
44     * Creates a capability resolver backed by Composer autoload metadata and filesystem checks.
45     *
46     * @param ComposerJsonInterface $composer the composer.json accessor for autoload metadata
47     * @param FilesystemInterface $filesystem the filesystem used to resolve project-relative paths
48     */
49    public function __construct(
50        private ComposerJsonInterface $composer,
51        private FilesystemInterface $filesystem,
52    ) {}
53
54    /**
55     * Resolves which documentation, testing, and wiki surfaces are available for the current repository.
56     *
57     * @param string $testsPath the project-relative tests directory to inspect
58     * @param string $guideDirectory the project-relative guide directory to inspect
59     * @param string $wikiTarget the project-relative wiki output target to inspect
60     */
61    public function resolve(
62        string $testsPath = ProjectCapabilitiesResolverInterface::DEFAULT_TESTS_PATH,
63        string $guideDirectory = ProjectCapabilitiesResolverInterface::DEFAULT_GUIDE_DIRECTORY,
64        string $wikiTarget = ProjectCapabilitiesResolverInterface::DEFAULT_WIKI_TARGET,
65    ): ProjectCapabilities {
66        $psr4Autoload = $this->composer->getAutoload('psr-4');
67        $apiDirectories = $this->resolveApiDirectories();
68
69        return new ProjectCapabilities(
70            $apiDirectories,
71            $this->resolveDefaultPackageName($psr4Autoload),
72            $this->filesystem->exists($guideDirectory),
73            $this->filesystem->exists($testsPath),
74            $this->filesystem->exists($wikiTarget),
75            $this->resolveHasPhpSourceFiles($apiDirectories),
76        );
77    }
78
79    /**
80     * Resolves the default API package name from the first PSR-4 namespace entry when available.
81     *
82     * @param array<string, mixed> $psr4Autoload the PSR-4 autoload map from composer.json
83     */
84    private function resolveDefaultPackageName(array $psr4Autoload): ?string
85    {
86        $defaultPackageName = array_key_first($psr4Autoload);
87
88        if (! \is_string($defaultPackageName) || '' === $defaultPackageName) {
89            return null;
90        }
91
92        return rtrim($defaultPackageName, '\\');
93    }
94
95    /**
96     * Resolves project-relative API directories exposed by Composer autoload configuration.
97     *
98     * @return list<string>
99     */
100    private function resolveApiDirectories(): array
101    {
102        $directories = [];
103
104        foreach (self::API_AUTOLOAD_TYPES as $autoloadType) {
105            foreach ($this->normalizeAutoloadPaths($this->composer->getAutoload($autoloadType)) as $path) {
106                $relativePath = $this->resolveRelativeApiDirectory($path);
107
108                if (null === $relativePath) {
109                    continue;
110                }
111
112                $directories[$relativePath] = $relativePath;
113            }
114        }
115
116        return array_values($directories);
117    }
118
119    /**
120     * Resolves a Composer autoload path into a project-relative API directory when it exists.
121     *
122     * @param string $path the Composer autoload path candidate
123     */
124    private function resolveRelativeApiDirectory(string $path): ?string
125    {
126        $absolutePath = $this->filesystem->getAbsolutePath($path);
127
128        if (! \is_string($absolutePath)) {
129            return null;
130        }
131
132        if (! is_dir($absolutePath)) {
133            return null;
134        }
135
136        return $this->filesystem->makePathRelative($absolutePath);
137    }
138
139    /**
140     * Resolves whether Composer autoload metadata exposes testable PHP source for the repository.
141     *
142     * @param list<string> $apiDirectories the resolved API directories exposed by Composer autoload metadata
143     */
144    private function resolveHasPhpSourceFiles(array $apiDirectories): bool
145    {
146        if ([] !== $apiDirectories) {
147            return true;
148        }
149
150        foreach (self::API_AUTOLOAD_TYPES as $autoloadType) {
151            foreach ($this->normalizeAutoloadPaths($this->composer->getAutoload($autoloadType)) as $path) {
152                $absolutePath = $this->filesystem->getAbsolutePath($path);
153
154                if (! \is_string($absolutePath)) {
155                    continue;
156                }
157
158                if (is_file($absolutePath) && str_ends_with(strtolower($absolutePath), '.php')) {
159                    return true;
160                }
161            }
162        }
163
164        return false;
165    }
166
167    /**
168     * Flattens Composer autoload path definitions into a normalized list of non-empty paths.
169     *
170     * @param array<string, mixed> $autoload the Composer autoload section to normalize
171     *
172     * @return list<string>
173     */
174    private function normalizeAutoloadPaths(array $autoload): array
175    {
176        $paths = [];
177
178        foreach ($autoload as $path) {
179            if (\is_string($path) && '' !== $path) {
180                $paths[] = $path;
181
182                continue;
183            }
184
185            if (! \is_array($path)) {
186                continue;
187            }
188
189            foreach ($path as $nestedPath) {
190                if (! \is_string($nestedPath)) {
191                    continue;
192                }
193
194                if ('' === $nestedPath) {
195                    continue;
196                }
197
198                $paths[] = $nestedPath;
199            }
200        }
201
202        return $paths;
203    }
204}