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