Passed
Pull Request — master (#53)
by Alexander
13:52
created

Config::replaceMarkers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
c 0
b 0
f 0
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Yiisoft\Composer\Config\Config;
4
5
use Yiisoft\Arrays\ArrayHelper;
6
use Yiisoft\Composer\Config\Builder;
7
use Yiisoft\Composer\Config\ContentWriter;
8
use Yiisoft\Composer\Config\Reader\ReaderFactory;
9
use Yiisoft\Composer\Config\Util\Helper;
10
use Yiisoft\Composer\Config\Util\PathHelper;
11
12
/**
13
 * Config class represents output configuration file.
14
 */
15
class Config
16
{
17
    private const BASE_DIR_MARKER = '<<<base-dir>>>';
18
19
    /**
20
     * @var string config name
21
     */
22
    private string $name;
23
24
    /**
25
     * @var array sources - paths to config source files
26
     */
27
    private array $sources = [];
28
29
    /**
30
     * @var array config value
31
     */
32
    protected array $values = [];
33
34
    protected Builder $builder;
35
36
    protected ContentWriter $contentWriter;
37
38
    public function __construct(Builder $builder, string $name)
39
    {
40
        $this->builder = $builder;
41
        $this->name = $name;
42
        $this->contentWriter = new ContentWriter();
43
    }
44
45
    public function clone(Builder $builder): self
46
    {
47
        $config = new Config($builder, $this->name);
48
        $config->sources = $this->sources;
49
        $config->values = $this->values;
50
51
        return $config;
52
    }
53
54
    public function getValues(): array
55
    {
56
        return $this->values;
57
    }
58
59
    public function load(array $paths = []): self
60
    {
61
        $this->sources = $this->loadFiles($paths);
62
63
        return $this;
64
    }
65
66
    private function loadFiles(array $paths): array
67
    {
68
        switch (count($paths)) {
69
            case 0:
70
                return [];
71
            case 1:
72
                return [$this->loadFile(reset($paths))];
73
        }
74
75
        $configs = [];
76
        foreach ($paths as $path) {
77
            $cs = $this->loadFiles($this->glob($path));
78
            foreach ($cs as $config) {
79
                if (!empty($config)) {
80
                    $configs[] = $config;
81
                }
82
            }
83
        }
84
85
        return $configs;
86
    }
87
88
    private function glob(string $path): array
89
    {
90
        if (strpos($path, '*') === false) {
91
            return [$path];
92
        }
93
94
        return glob($path);
0 ignored issues
show
Bug Best Practice introduced by
The expression return glob($path) could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
95
    }
96
97
    /**
98
     * Reads config file.
99
     *
100
     * @param string $path
101
     * @return array configuration read from file
102
     */
103
    protected function loadFile($path): array
104
    {
105
        $reader = ReaderFactory::get($this->builder, $path);
106
107
        return $reader->read($path);
108
    }
109
110
    /**
111
     * Merges given configs and writes at given name.
112
     *
113
     * @return Config
114
     */
115
    public function build(): self
116
    {
117
        $this->values = $this->calcValues($this->sources);
118
119
        return $this;
120
    }
121
122
    public function write(): self
123
    {
124
        $this->writeFile($this->getOutputPath(), $this->values);
125
126
        return $this;
127
    }
128
129
    protected function calcValues(array $sources): array
130
    {
131
        $values = ArrayHelper::merge(...$sources);
132
133
        return $this->substituteOutputDirs($values);
134
    }
135
136
    protected function writeFile(string $path, array $data): void
137
    {
138
        $depth = $this->findDepth();
139
        $baseDir = $depth > 0 ? "dirname(__DIR__, $depth)" : '__DIR__';
140
141
        $envs = $this->envsRequired() ? "\$_ENV = array_merge((array) require __DIR__ . '/envs.php', \$_ENV);" : '';
142
        $constants = $this->constantsRequired() ? $this->builder->getConfig('constants')->buildRequires() : '';
143
        $params = $this->paramsRequired() ? "\$params = (array) require __DIR__ . '/params.php';" : '';
144
        $variables = Helper::exportVar($data);
145
146
        $content = <<<PHP
147
<?php
148
149
\$baseDir = {$baseDir};
150
151
{$envs}
152
153
{$constants}
154
155
{$params}
156
157
return {$variables};
158
PHP;
159
160
        $this->contentWriter->write($path, $this->replaceMarkers($content) . "\n");
161
    }
162
163
    public function envsRequired(): bool
164
    {
165
        return true;
166
    }
167
168
    public function constantsRequired(): bool
169
    {
170
        return true;
171
    }
172
173
    public function paramsRequired(): bool
174
    {
175
        return true;
176
    }
177
178
    private function findDepth(): int
179
    {
180
        $outDir = PathHelper::realpath(dirname($this->getOutputPath()));
181
        $diff = substr($outDir, strlen(PathHelper::realpath($this->getBaseDir())));
182
183
        return substr_count($diff, '/');
184
    }
185
186
    private function replaceMarkers(string $content): string
187
    {
188
        return str_replace(
189
            ["'" . self::BASE_DIR_MARKER, "'?" . self::BASE_DIR_MARKER],
190
            ["\$baseDir . '", "'?' . \$baseDir . '"],
191
            $content
192
        );
193
    }
194
195
    /**
196
     * Substitute output paths in given data array recursively with marker.
197
     *
198
     * @param array $data
199
     * @return array
200
     */
201
    protected function substituteOutputDirs(array $data): array
202
    {
203
        $dir = PathHelper::normalize($this->getBaseDir());
204
205
        return $this->substitutePaths($data, $dir, self::BASE_DIR_MARKER);
206
    }
207
208
    /**
209
     * Substitute all paths in given array recursively with alias if applicable.
210
     *
211
     * @param array $data
212
     * @param string $dir
213
     * @param string $alias
214
     * @return array
215
     */
216
    private function substitutePaths($data, $dir, $alias): array
217
    {
218
        foreach ($data as &$value) {
219
            if (is_string($value)) {
220
                $value = $this->substitutePath($value, $dir, $alias);
221
            } elseif (is_array($value)) {
222
                $value = $this->substitutePaths($value, $dir, $alias);
223
            }
224
        }
225
226
        return $data;
227
    }
228
229
    /**
230
     * Substitute path with alias if applicable.
231
     *
232
     * @param string $path
233
     * @param string $dir
234
     * @param string $alias
235
     * @return string
236
     */
237
    private function substitutePath($path, $dir, $alias): string
238
    {
239
        $end = $dir . '/';
240
        $skippable = 0 === strncmp($path, '?', 1);
241
        if ($skippable) {
242
            $path = substr($path, 1);
243
        }
244
        if ($path === $dir) {
245
            $result = $alias;
246
        } elseif (strpos($path, $end) === 0) {
247
            $result = $alias . substr($path, strlen($end) - 1);
248
        } else {
249
            $result = $path;
250
        }
251
252
        return ($skippable ? '?' : '') . $result;
253
    }
254
255
    private function getBaseDir(): string
256
    {
257
        return $this->builder->getBaseDir();
258
    }
259
260
    protected function getOutputPath(string $name = null): string
261
    {
262
        return $this->builder->getOutputPath($name ?: $this->name);
263
    }
264
}
265