Passed
Pull Request — master (#5)
by Dmitriy
13:22
created

Config::loadFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Yiisoft\Composer\Config\Configs;
4
5
use ReflectionException;
6
use Yiisoft\Composer\Config\Builder;
7
use Yiisoft\Composer\Config\Exceptions\FailedWriteException;
8
use Yiisoft\Composer\Config\Readers\ReaderFactory;
9
use Yiisoft\Composer\Config\Utils\Helper;
10
11
/**
12
 * Config class represents output configuration file.
13
 */
14
class Config
15
{
16
    private const UNIX_DS = '/';
17
    private const BASE_DIR_MARKER = '<<<base-dir>>>';
18
19
    /**
20
     * @var string config name
21
     */
22
    protected $name;
23
24
    /**
25
     * @var array sources - paths to config source files
26
     */
27
    protected $sources = [];
28
29
    /**
30
     * @var array config value
31
     */
32
    protected $values = [];
33
34
    /**
35
     * @var Builder
36
     */
37
    protected $builder;
38
39
    public function __construct(Builder $builder, string $name)
40
    {
41
        $this->builder = $builder;
42
        $this->name = $name;
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 getName(): string
55
    {
56
        return $this->name;
57
    }
58
59
    public function getValues(): array
60
    {
61
        return $this->values;
62
    }
63
64
    public function load(array $paths = []): self
65
    {
66
        $this->sources = $this->loadFiles($paths);
67
68
        return $this;
69
    }
70
71
    protected function loadFiles(array $paths): array
72
    {
73
        switch (count($paths)) {
74
        case 0:
75
            return [];
76
        case 1:
77
            return [$this->loadFile(reset($paths))];
78
        }
79
80
        $configs = [];
81
        foreach ($paths as $path) {
82
            $cs = $this->loadFiles($this->glob($path));
83
            foreach ($cs as $config) {
84
                if (!empty($config)) {
85
                    $configs[] = $config;
86
                }
87
            }
88
        }
89
90
        return $configs;
91
    }
92
93
    protected function glob(string $path): array
94
    {
95
        if (strpos($path, '*') === false) {
96
            return [$path];
97
        }
98
99
        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...
100
    }
101
102
    /**
103
     * Reads config file.
104
     * @param string $path
105
     * @return array configuration read from file
106
     */
107
    protected function loadFile($path): array
108
    {
109
        $reader = ReaderFactory::get($this->builder, $path);
110
111
        return $reader->read($path);
112
    }
113
114
    /**
115
     * Merges given configs and writes at given name.
116
     * @return Config
117
     */
118
    public function build(): self
119
    {
120
        $this->values = $this->calcValues($this->sources);
121
122
        return $this;
123
    }
124
125
    public function write(): self
126
    {
127
        $this->writeFile($this->getOutputPath(), $this->values);
128
129
        return $this;
130
    }
131
132
    protected function calcValues(array $sources): array
133
    {
134
        $values = call_user_func_array([Helper::class, 'mergeConfig'], $sources);
135
        $values = Helper::fixConfig($values);
136
137
        return $this->substituteOutputDirs($values);
138
    }
139
140
    protected function writeFile(string $path, array $data): void
141
    {
142
        $this->writePhpFile($path, $data);
143
    }
144
145
    /**
146
     * Writes complete PHP config file by full path.
147
     * @param string $path
148
     * @param string|array $data
149
     * @param bool $withEnv
150
     * @param bool $withDefines
151
     * @throws FailedWriteException
152
     * @throws ReflectionException
153
     */
154
    protected function writePhpFile(string $path, $data): void
155
    {
156
        $depth = $this->findDepth();
157
        $baseDir = $depth>0 ? "dirname(__DIR__, $depth)" : '__DIR__';
158
        static::putFile($path, $this->replaceMarkers(implode("\n\n", array_filter([
159
            'header'  => '<?php',
160
            'baseDir' => "\$baseDir = $baseDir;",
161
            'BASEDIR' => "defined('COMPOSER_CONFIG_PLUGIN_BASEDIR') or define('COMPOSER_CONFIG_PLUGIN_BASEDIR', \$baseDir);",
162
            'dotenv'  => $this->withEnv() ? "\$_ENV = array_merge((array) require __DIR__ . '/dotenv.php', (array) \$_ENV);" : '',
163
            'defines' => $this->withDefines() ? $this->builder->getConfig('defines')->buildRequires() : '',
164
            'params'  => $this->withParams() ? "\$params = require __DIR__ . '/params.php';" : '',
165
            'content' => is_array($data) ? $this->renderVars($data) : $data,
166
        ]))) . "\n");
167
    }
168
169
    private function withEnv(): bool
170
    {
171
        return !in_array(static::class, [System::class, DotEnv::class], true);
172
    }
173
174
    private function withDefines(): bool
175
    {
176
        return !in_array(static::class, [System::class, DotEnv::class, Defines::class], true);
177
    }
178
179
    private function withParams(): bool
180
    {
181
        return !in_array(static::class, [System::class, DotEnv::class, Defines::class, Params::class], true);
182
    }
183
184
    private function findDepth(): int
185
    {
186
        $outDir = dirname(self::normalizePath($this->getOutputPath()));
187
        $diff = substr($outDir, strlen($this->getBaseDir()));
188
189
        return substr_count($diff, self::UNIX_DS);
190
    }
191
192
    /**
193
     * @param array $vars array to be exported
194
     * @return string
195
     * @throws ReflectionException
196
     */
197
    protected function renderVars(array $vars): string
198
    {
199
        return 'return ' . Helper::exportVar($vars) . ';';
200
    }
201
202
    protected function replaceMarkers(string $content): string
203
    {
204
        $content = str_replace("'" . self::BASE_DIR_MARKER, "\$baseDir . '", $content);
205
206
        return str_replace("'?" . self::BASE_DIR_MARKER, "'?' . \$baseDir . '", $content);
207
    }
208
209
    /**
210
     * Writes file if content changed.
211
     * @param string $path
212
     * @param string $content
213
     * @throws FailedWriteException
214
     */
215
    protected static function putFile($path, $content): void
216
    {
217
        if (file_exists($path) && $content === file_get_contents($path)) {
218
            return;
219
        }
220
        $dirname = dirname($path);
221
        if (!file_exists($dirname) && !mkdir($dirname, 0777, true) && !is_dir($dirname)) {
222
            throw new FailedWriteException(sprintf('Directory "%s" was not created', $dirname));
223
        }
224
        if (false === file_put_contents($path, $content)) {
225
            throw new FailedWriteException("Failed write file $path");
226
        }
227
    }
228
229
    /**
230
     * Substitute output paths in given data array recursively with marker.
231
     * @param array $data
232
     * @return array
233
     */
234
    public function substituteOutputDirs(array $data): array
235
    {
236
        $dir = static::normalizePath($this->getBaseDir());
237
238
        return static::substitutePaths($data, $dir, self::BASE_DIR_MARKER);
239
    }
240
241
    /**
242
     * Normalizes given path with given directory separator.
243
     * Default forced to Unix directory separator for substitutePaths to work properly in Windows.
244
     * @param string $path path to be normalized
245
     * @param string $ds directory separator
246
     * @return string
247
     */
248
    public static function normalizePath($path, $ds = self::UNIX_DS): string
249
    {
250
        return rtrim(strtr($path, '/\\', $ds . $ds), $ds);
251
    }
252
253
    /**
254
     * Substitute all paths in given array recursively with alias if applicable.
255
     * @param array $data
256
     * @param string $dir
257
     * @param string $alias
258
     * @return array
259
     */
260
    public static function substitutePaths($data, $dir, $alias): array
261
    {
262
        foreach ($data as &$value) {
263
            if (is_string($value)) {
264
                $value = static::substitutePath($value, $dir, $alias);
265
            } elseif (is_array($value)) {
266
                $value = static::substitutePaths($value, $dir, $alias);
267
            }
268
        }
269
270
        return $data;
271
    }
272
273
    /**
274
     * Substitute path with alias if applicable.
275
     * @param string $path
276
     * @param string $dir
277
     * @param string $alias
278
     * @return string
279
     */
280
    protected static function substitutePath($path, $dir, $alias): string
281
    {
282
        $end = $dir . self::UNIX_DS;
283
        $skippable = 0 === strncmp($path, '?', 1) ? '?' : '';
284
        if ($skippable) {
285
            $path = substr($path, 1);
286
        }
287
        if ($path === $dir) {
288
            $result = $alias;
289
        } elseif (strpos($path, $end) === 0) {
290
            $result = $alias . substr($path, strlen($end) - 1);
291
        } else {
292
            $result = $path;
293
        }
294
295
        return $skippable . $result;
296
    }
297
298
    public function getBaseDir(): string
299
    {
300
        return dirname(__DIR__, 5);
301
    }
302
303
    public function getOutputPath(string $name = null): string
304
    {
305
        return $this->builder->getOutputPath($name ?: $this->name);
306
    }
307
}
308