Config::build()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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