Passed
Pull Request — master (#69)
by Sergei
08:59 queued 06:36
created

Aliases::joinPathAndSubpath()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 2
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Aliases;
6
7
use InvalidArgumentException;
8
9
use function array_key_exists;
10
use function is_array;
11
use function is_string;
12
use function strlen;
13
14
final class Aliases
15
{
16
    /**
17
     * @var array
18
     * @psalm-var array<string, string|array<string, string>>
19
     */
20
    private array $aliases = [];
21
22
    /**
23
     * @psalm-param array<string, string> $config
24
     *
25
     * @throws InvalidArgumentException if $path is an invalid alias.
26
     *
27
     * @see set()
28
     * @see get()
29
     */
30 36
    public function __construct(array $config = [])
31
    {
32 36
        foreach ($config as $alias => $path) {
33 29
            $this->set($alias, $path);
34
        }
35
    }
36
37
    /**
38
     * Registers a path alias.
39
     *
40
     * A path alias is a short name representing a long path (a file path, a URL, etc.)
41
     *
42
     * For example, `@vendor` may store path to `vendor` directory.
43
     *
44
     * A path alias must start with the character '@' so that it can be easily differentiated
45
     * from non-alias paths.
46
     *
47
     * Note that this method does not check if the given path exists or not. All it does is
48
     * to associate the alias with the path.
49
     *
50
     * @param string $alias The alias name (e.g. "@vendor"). It must start with a '@' character.
51
     * It may contain the forward slash '/' which serves as boundary character when performing
52
     * alias translation by {@see get()}.
53
     * @param string $path The path corresponding to the alias. This can be:
54
     *
55
     * - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`);
56
     * - a URL (e.g. `http://www.yiiframework.com`);
57
     * - a path alias (e.g. `@vendor/yiisoft`), it will be resolved on {@see get()} call.
58
     *
59
     * @see get()
60
     */
61 33
    public function set(string $alias, string $path): void
62
    {
63 33
        if (!$this->isAlias($alias)) {
64 1
            $alias = '@' . $alias;
65
        }
66 33
        $pos = strpos($alias, '/');
67
        /** @psalm-var string $root */
68 33
        $root = $pos === false ? $alias : substr($alias, 0, $pos);
69
70 33
        if (!array_key_exists($root, $this->aliases)) {
71 33
            if ($pos === false) {
72 32
                $this->aliases[$root] = $path;
73
            } else {
74 33
                $this->aliases[$root] = [$alias => $path];
75
            }
76 4
        } elseif (is_string($this->aliases[$root])) {
77 4
            if ($pos === false) {
78 1
                $this->aliases[$root] = $path;
79
            } else {
80 4
                $this->aliases[$root] = [
81 4
                    $alias => $path,
82 4
                    $root => $this->aliases[$root],
83 4
                ];
84
            }
85
        } else {
86 2
            $this->aliases[$root][$alias] = $path;
87 2
            krsort($this->aliases[$root]);
88
        }
89
    }
90
91
    /**
92
     * Remove alias.
93
     *
94
     * @param string $alias Alias to be removed.
95
     */
96 2
    public function remove(string $alias): void
97
    {
98 2
        if (!$this->isAlias($alias)) {
99 1
            $alias = '@' . $alias;
100
        }
101 2
        $pos = strpos($alias, '/');
102 2
        $root = $pos === false ? $alias : substr($alias, 0, $pos);
103
104 2
        if (array_key_exists($root, $this->aliases)) {
105 2
            if (is_array($this->aliases[$root])) {
106 1
                unset($this->aliases[$root][$alias]);
107 1
            } elseif ($pos === false) {
108 1
                unset($this->aliases[$root]);
109
            }
110
        }
111
    }
112
113
    /**
114
     * Translates a path alias into an actual path.
115
     *
116
     * The translation is done according to the following procedure:
117
     *
118
     * 1. If the given alias does not start with '@', it is returned back without change;
119
     * 2. Otherwise, look for the longest registered alias that matches the beginning part
120
     *    of the given alias. If it exists, replace the matching part of the given alias with
121
     *    the corresponding registered path.
122
     * 3. Throw an exception if path alias cannot be resolved.
123
     *
124
     * For example, if '@vendor' is registered as the alias to the vendor directory,
125
     * say '/path/to/vendor'. The alias '@vendor/yiisoft' would then be translated into '/path/to/vendor/yiisoft'.
126
     *
127
     * If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config'
128
     * would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path.
129
     * This is because the longest alias takes precedence.
130
     *
131
     * However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced
132
     * instead of '@foo/bar', because '/' serves as the boundary character.
133
     *
134
     * Note, this method does not check if the returned path exists or not.
135
     *
136
     * @param string $alias The alias to be translated.
137
     *
138
     * @throws InvalidArgumentException If the root alias is not previously registered.
139
     *
140
     * @return string The path corresponding to the alias.
141
     *
142
     * @see setAlias()
143
     */
144 34
    public function get(string $alias): string
145
    {
146 34
        if (!$this->isAlias($alias)) {
147 6
            return $alias;
148
        }
149
150 32
        $foundAlias = $this->findAlias($alias);
151
152 32
        if ($foundAlias === null) {
153 2
            throw new InvalidArgumentException("Invalid path alias: $alias");
154
        }
155
156 30
        $foundSubAlias = $this->findAlias($foundAlias);
157 30
        if ($foundSubAlias === null) {
158 29
            return $foundAlias;
159
        }
160
161 3
        return $this->get($foundSubAlias);
162
    }
163
164
    /**
165
     * Bulk translates path aliases into actual paths.
166
     *
167
     * @param string[] $aliases Aliases to be translated.
168
     *
169
     * @throws InvalidArgumentException If the root alias was not previously registered.
170
     *
171
     * @return string[] The paths corresponding to the aliases.
172
     */
173 2
    public function getArray(array $aliases): array
174
    {
175 2
        return array_map(
176 2
            fn (string $alias) => $this->get($alias),
177 2
            $aliases,
178 2
        );
179
    }
180
181
    /**
182
     * Returns all path aliases translated into an actual paths.
183
     *
184
     * @return array Actual paths indexed by alias name.
185
     */
186 3
    public function getAll(): array
187
    {
188 3
        $result = [];
189 3
        foreach ($this->aliases as $name => $path) {
190 2
            if (is_array($path)) {
191 1
                foreach ($path as $innerName => $innerPath) {
192 1
                    $result[$innerName] = $innerPath;
193
                }
194
            } else {
195 2
                $result[$name] = $this->get($path);
196
            }
197
        }
198
199 3
        return $result;
200
    }
201
202 32
    private function findAlias(string $alias): ?string
203
    {
204 32
        $pos = strpos($alias, '/');
205 32
        $root = $pos === false ? $alias : substr($alias, 0, $pos);
206
207 32
        if (array_key_exists($root, $this->aliases)) {
208 30
            if (is_string($this->aliases[$root])) {
209 27
                return $pos === false
210 10
                    ? $this->aliases[$root]
211 27
                    : $this->joinPathAndSubpath($this->aliases[$root], substr($alias, $pos));
212
            }
213
214 3
            foreach ($this->aliases[$root] as $name => $path) {
215 3
                if (strpos($alias . '/', $name . '/') === 0) {
216 3
                    return $this->joinPathAndSubpath($path, substr($alias, strlen($name)));
217
                }
218
            }
219
        }
220
221 31
        return null;
222
    }
223
224 22
    private function joinPathAndSubpath(string $path, string $subpath): string
225
    {
226 22
        if ($path === '/' || $path === '\\') {
227 2
            $subpath = ltrim($subpath, '\\/');
228
        } else {
229 20
            $path = rtrim($path, '\\/');
230
        }
231 22
        return $path . $subpath;
232
    }
233
234 35
    private function isAlias(string $alias): bool
235
    {
236 35
        return !strncmp($alias, '@', 1);
237
    }
238
}
239