Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.55% covered (success)
96.55%
28 / 29
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComposerSelfUpdateScopeResolver
96.55% covered (success)
96.55%
28 / 29
75.00% covered (warning)
75.00%
3 / 4
16
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
 isGlobalInstallation
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 getComposerHomeCandidates
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
9.02
 normalizePath
100.00% covered (success)
100.00%
3 / 3
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\SelfUpdate;
21
22use FastForward\DevTools\Environment\EnvironmentInterface;
23use FastForward\DevTools\Path\DevToolsPathResolver;
24use Symfony\Component\Filesystem\Path;
25use Throwable;
26
27use function Safe\realpath;
28
29/**
30 * Detects Composer global DevTools installations from known Composer home paths.
31 */
32  class ComposerSelfUpdateScopeResolver implements SelfUpdateScopeResolverInterface
33{
34    private const string PACKAGE_PATH = 'vendor/fast-forward/dev-tools';
35
36    /**
37     * @param EnvironmentInterface $environment reads Composer home environment values
38     * @param string|null $packagePath the DevTools package path; defaults to the active package root
39     */
40    public function __construct(
41        private EnvironmentInterface $environment,
42        private ?string $packagePath = null,
43    ) {}
44
45    /**
46     * Returns whether DevTools is running from Composer's global installation.
47     */
48    public function isGlobalInstallation(): bool
49    {
50        $packagePath = $this->normalizePath($this->packagePath ?? DevToolsPathResolver::getPackagePath());
51
52        foreach ($this->getComposerHomeCandidates() as $composerHome) {
53            $globalPackagePath = $this->normalizePath(Path::join($composerHome, self::PACKAGE_PATH));
54
55            if ($packagePath === $globalPackagePath || str_starts_with(
56                $packagePath,
57                $globalPackagePath . \DIRECTORY_SEPARATOR
58            )) {
59                return true;
60            }
61        }
62
63        return false;
64    }
65
66    /**
67     * Returns candidate Composer home directories for supported platforms.
68     *
69     * @return list<string>
70     */
71    private function getComposerHomeCandidates(): array
72    {
73        $candidates = [];
74        $composerHome = $this->environment->get('COMPOSER_HOME');
75
76        if (null !== $composerHome && '' !== $composerHome) {
77            $candidates[] = $composerHome;
78        }
79
80        $xdgConfigHome = $this->environment->get('XDG_CONFIG_HOME');
81
82        if (null !== $xdgConfigHome && '' !== $xdgConfigHome) {
83            $candidates[] = Path::join($xdgConfigHome, 'composer');
84        }
85
86        $home = $this->environment->get('HOME');
87
88        if (null !== $home && '' !== $home) {
89            $candidates[] = Path::join($home, '.composer');
90            $candidates[] = Path::join($home, '.config/composer');
91            $candidates[] = Path::join($home, 'Library/Application Support/Composer');
92        }
93
94        $appData = $this->environment->get('APPDATA');
95
96        if (null !== $appData && '' !== $appData) {
97            $candidates[] = Path::join($appData, 'Composer');
98        }
99
100        return array_values(array_unique($candidates));
101    }
102
103    /**
104     * Safely canonicalizes a path, resolving symlinks when available.
105     *
106     * @param string $path
107     */
108    private function normalizePath(string $path): string
109    {
110        try {
111            return Path::canonicalize(realpath($path));
112        } catch (Throwable) {
113            return Path::canonicalize($path);
114        }
115    }
116}