Passed
Pull Request — master (#407)
by Kirill
11:08 queued 03:58
created

StorageConfig::getTmpDir()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 2
b 1
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * This file is part of Spiral Framework package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Storage\Config;
13
14
use League\Flysystem\FilesystemAdapter;
15
use Spiral\Storage\Config\DTO\FileSystemInfo\FileSystemInfoInterface;
16
use Spiral\Storage\Exception\ConfigException;
17
use Spiral\Storage\Config\DTO\BucketInfo;
18
use Spiral\Storage\Config\DTO\BucketInfoInterface;
19
use Spiral\Storage\Config\DTO\FileSystemInfo;
20
use Spiral\Storage\Exception\StorageException;
21
22
/**
23
 * @psalm-type ConfigServerSection = array {
24
 *      adapter:  class-string<FilesystemAdapter>,
25
 *      options?: array<string, mixed>
26
 * }
27
 *
28
 * @psalm-type ConfigBucketSection = array {
29
 *      server:   string,
30
 *      options?: array<string, mixed>
31
 * }
32
 *
33
 * @psalm-type ConfigArray = array {
34
 *      servers: array<string, ConfigServerSection>,
35
 *      buckets: array<string, ConfigBucketSection>,
36
 *      temp:    string
37
 * }
38
 *
39
 * @psalm-type ConfigInputArray = array {
40
 *      servers?: array<string, ConfigServerSection>|null,
41
 *      buckets?: array<string, ConfigBucketSection>|null,
42
 *      temp?:    string|null
43
 * }
44
 *
45
 * @see FilesystemAdapter
46
 */
47
final class StorageConfig implements ConfigInterface
48
{
49
    /**
50
     * @var string
51
     */
52
    public const CONFIG = 'storage';
53
54
    /**
55
     * @var string
56
     */
57
    private const SERVERS_KEY = 'servers';
58
59
    /**
60
     * @var string
61
     */
62
    private const STORAGES_KEY = 'buckets';
63
64
    /**
65
     * @var string
66
     */
67
    private const TMP_DIR_KEY = 'temp';
68
69
    /**
70
     * @var ConfigArray
0 ignored issues
show
Bug introduced by
The type Spiral\Storage\Config\ConfigArray was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
71
     */
72
    protected $config = [
73
        self::SERVERS_KEY  => [],
74
        self::STORAGES_KEY => [],
75
        self::TMP_DIR_KEY  => '',
76
    ];
77
78
    /**
79
     * Internal list allows to keep once built filesystems info
80
     *
81
     * @var FileSystemInfoInterface[]
82
     */
83
    protected $fileSystemsInfoList = [];
84
85
    /**
86
     * Internal list allows to keep once built buckets info
87
     *
88
     * @var BucketInfoInterface[]
89
     */
90
    protected $bucketsInfoList = [];
91
92
    /**
93
     * @param ConfigInputArray $config
0 ignored issues
show
Bug introduced by
The type Spiral\Storage\Config\ConfigInputArray was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
94
     * @throws ConfigException
95
     */
96
    public function __construct(array $config = [])
97
    {
98
        $this->config = $this->normalize($config);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->normalize($config) of type array is incompatible with the declared type Spiral\Storage\Config\ConfigArray of property $config.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
99
    }
100
101
    /**
102
     * {@inheritDoc}
103
     */
104
    public function getServersKeys(): array
105
    {
106
        return \array_keys($this->config[self::SERVERS_KEY]);
107
    }
108
109
    /**
110
     * {@inheritDoc}
111
     */
112
    public function hasServer(string $key): bool
113
    {
114
        return isset($this->config[self::SERVERS_KEY][$key]);
115
    }
116
117
    /**
118
     * {@inheritDoc}
119
     */
120
    public function getBucketsKeys(): array
121
    {
122
        return \array_keys($this->config[self::STORAGES_KEY]);
123
    }
124
125
    /**
126
     * {@inheritDoc}
127
     */
128
    public function hasBucket(string $key): bool
129
    {
130
        return isset($this->config[self::STORAGES_KEY][$key]);
131
    }
132
133
    /**
134
     * {@inheritDoc}
135
     */
136
    public function getTmpDir(): string
137
    {
138
        return $this->config[self::TMP_DIR_KEY];
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144
    public function buildFileSystemInfo(string $fs, ?bool $force = false): FileSystemInfoInterface
145
    {
146
        if (!$this->hasBucket($fs)) {
147
            throw new ConfigException(
148
                \sprintf('Bucket `%s` was not found', $fs)
149
            );
150
        }
151
152
        if (!$force && array_key_exists($fs, $this->fileSystemsInfoList)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $force of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
153
            return $this->fileSystemsInfoList[$fs];
154
        }
155
156
        $bucketInfo = $this->buildBucketInfo($fs);
157
158
        if (!$this->hasServer($bucketInfo->getServer())) {
159
            throw new ConfigException(
160
                \sprintf(
161
                    'Server `%s` info for filesystem `%s` was not detected',
162
                    $bucketInfo->getServer(),
163
                    $fs
164
                )
165
            );
166
        }
167
168
        $serverInfo = $this->config[self::SERVERS_KEY][$bucketInfo->getServer()];
169
170
        switch ($this->extractServerAdapter($serverInfo)) {
171
            case \League\Flysystem\Local\LocalFilesystemAdapter::class:
172
                $fsInfoDTO = new FileSystemInfo\LocalInfo($fs, $serverInfo);
173
                break;
174
            case \League\Flysystem\AwsS3V3\AwsS3V3Adapter::class:
175
            case \League\Flysystem\AsyncAwsS3\AsyncAwsS3Adapter::class:
176
                $serverInfo[FileSystemInfo\Aws\AwsS3Info::OPTIONS_KEY] = array_merge(
177
                    [
178
                        FileSystemInfo\Aws\AwsS3Info::BUCKET_KEY => $bucketInfo->getOption(
179
                            BucketInfoInterface::BUCKET_KEY
180
                        ),
181
                    ],
182
                    $serverInfo[FileSystemInfo\Aws\AwsS3Info::OPTIONS_KEY]
183
                );
184
185
                $fsInfoDTO = new FileSystemInfo\Aws\AwsS3Info($fs, $serverInfo);
186
                break;
187
            default:
188
                throw new ConfigException(
189
                    \sprintf('Adapter can\'t be identified for filesystem `%s`', $fs)
190
                );
191
        }
192
193
        $this->fileSystemsInfoList[$fs] = $fsInfoDTO;
194
        $bucketInfo->setFileSystemInfo($fsInfoDTO);
195
196
        return $this->fileSystemsInfoList[$fs];
197
    }
198
199
    /**
200
     * {@inheritDoc}
201
     */
202
    public function buildBucketInfo(string $bucketLabel, ?bool $force = false): BucketInfoInterface
203
    {
204
        if (!$this->hasBucket($bucketLabel)) {
205
            throw new StorageException(
206
                \sprintf('Bucket `%s` was not found', $bucketLabel)
207
            );
208
        }
209
210
        if (!$force && array_key_exists($bucketLabel, $this->bucketsInfoList)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $force of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
211
            return $this->bucketsInfoList[$bucketLabel];
212
        }
213
214
        $bucketInfo = $this->config[self::STORAGES_KEY][$bucketLabel];
215
216
        $this->bucketsInfoList[$bucketLabel] = new BucketInfo(
217
            $bucketLabel,
218
            $bucketInfo[BucketInfoInterface::SERVER_KEY],
219
            $bucketInfo
220
        );
221
222
        return $this->bucketsInfoList[$bucketLabel];
223
    }
224
225
    /**
226
     * @param ConfigInputArray $config
227
     * @return ConfigArray
228
     * @throws ConfigException
229
     */
230
    private function normalize(array $config): array
231
    {
232
        $config = $this->normalizeServers($config);
233
        $config = $this->normalizeBuckets($config);
234
        $config = $this->normalizeTemporaryDirectory($config);
235
236
        return $config;
237
    }
238
239
    /**
240
     * @param ConfigInputArray $config
241
     * @return ConfigArray
242
     * @throws ConfigException
243
     * @psalm-suppress DocblockTypeContradiction
244
     * @psalm-suppress MoreSpecificReturnType
245
     * @psalm-suppress LessSpecificReturnStatement
246
     */
247
    private function normalizeServers(array $config): array
248
    {
249
        if (!isset($config[self::SERVERS_KEY])) {
250
            $config[self::SERVERS_KEY] = [];
251
252
            return $config;
253
        }
254
255
        $servers = $config[self::SERVERS_KEY];
256
257
        if (!\is_array($servers)) {
258
            $message = 'Storage servers list must be an array, but `%s` defined';
259
            throw new ConfigException(\sprintf($message, $this->valueToString($servers)), 0x01);
260
        }
261
262
        foreach ($servers as $name => $server) {
263
            if (!\is_string($name)) {
264
                $message = 'Storage server name must be a string, but %s defined';
265
                $message = \sprintf($message, $this->valueToString($name));
266
267
                throw new ConfigException($message, 0x02);
268
            }
269
270
            if (!\is_array($server)) {
271
                $message = 'Storage server `%s` config must be an array, but %s defined';
272
                $message = \sprintf($message, $name, $this->valueToString($server));
273
274
                throw new ConfigException($message, 0x03);
275
            }
276
277
            $adapter = $server['adapter'] ?? null;
278
279
            if (!\is_string($adapter) || !\is_subclass_of($adapter, FilesystemAdapter::class, true)) {
280
                $message = 'Storage server `%s` adapter must be a class ' .
281
                    'string that implements %s interface, but %s defined'
282
                ;
283
284
                $message = \sprintf($message, $name, FilesystemAdapter::class, $this->valueToString($adapter));
285
286
                throw new ConfigException($message, 0x04);
287
            }
288
289
            if (isset($server['options']) && !\is_array($server['options'])) {
290
                $message = 'Storage server `%s` options must be an array, but %s defined';
291
                $message = \sprintf($message, $name, $this->valueToString($server['options']));
292
293
                throw new ConfigException($message, 0x05);
294
            }
295
        }
296
297
        return $config;
298
    }
299
300
    /**
301
     * @param ConfigInputArray $config
302
     * @return ConfigArray
303
     * @throws ConfigException
304
     * @psalm-suppress DocblockTypeContradiction
305
     * @psalm-suppress MoreSpecificReturnType
306
     * @psalm-suppress LessSpecificReturnStatement
307
     */
308
    private function normalizeBuckets(array $config): array
309
    {
310
        if (!isset($config[self::STORAGES_KEY])) {
311
            $config[self::STORAGES_KEY] = [];
312
313
            return $config;
314
        }
315
316
        $buckets = $config[self::STORAGES_KEY];
317
318
        if (!\is_array($buckets)) {
319
            $message = 'Storage buckets list must be an array, but `%s` defined';
320
            throw new ConfigException(\sprintf($message, $this->valueToString($buckets)), 0x06);
321
        }
322
323
        foreach ($buckets as $name => $bucket) {
324
            if (!\is_string($name)) {
325
                $message = 'Storage bucket name must be a string, but %s defined';
326
                $message = \sprintf($message, $this->valueToString($name));
327
328
                throw new ConfigException($message, 0x07);
329
            }
330
331
            if (!\is_array($bucket)) {
332
                $message = 'Storage bucket `%s` config must be an array, but %s defined';
333
                $message = \sprintf($message, $name, $this->valueToString($bucket));
334
335
                throw new ConfigException($message, 0x08);
336
            }
337
338
            $server = $bucket['server'] ?? null;
339
340
            if (!\is_string($server)) {
341
                $message = 'Storage server of bucket `%s` must be a string, but %s defined';
342
                $message = \sprintf($message, $name, $this->valueToString($server));
343
344
                throw new ConfigException($message, 0x09);
345
            }
346
347
            if (!isset($config[self::SERVERS_KEY][$server])) {
348
                $variants = \implode(', ', \array_keys($config[self::SERVERS_KEY] ?? []));
349
                $message = 'Storage server `%s` of bucket `%s` has not been defined, one of [%s] required';
350
                $message = \sprintf($message, $server, $name, $variants);
351
352
                throw new ConfigException($message, 0x0A);
353
            }
354
355
            if (isset($bucket['options']) && !\is_array($bucket['options'])) {
356
                $message = 'Storage bucket `%s` options must be an array, but %s defined';
357
                $message = \sprintf($message, $name, $this->valueToString($bucket['options']));
358
359
                throw new ConfigException($message, 0x0B);
360
            }
361
        }
362
363
        return $config;
364
    }
365
366
    /**
367
     * @param ConfigInputArray $config
368
     * @return ConfigArray
369
     * @throws ConfigException
370
     * @psalm-suppress DocblockTypeContradiction
371
     * @psalm-suppress MoreSpecificReturnType
372
     * @psalm-suppress LessSpecificReturnStatement
373
     */
374
    private function normalizeTemporaryDirectory(array $config): array
375
    {
376
        if (!isset($config[self::TMP_DIR_KEY])) {
377
            $config[self::TMP_DIR_KEY] = \sys_get_temp_dir();
378
379
            return $config;
380
        }
381
382
        $directory = $config[self::TMP_DIR_KEY];
383
384
        if (!\is_string($directory)) {
385
            $message = 'Storage temporary directory must be a string, but `%s` defined';
386
            throw new ConfigException(\sprintf($message, $this->valueToString($directory)), 0x0C);
387
        }
388
389
        if (!\is_dir($directory)) {
390
            $message = 'Storage temporary directory `%s` must be a valid directory';
391
            throw new ConfigException(\sprintf($message, $directory), 0x0D);
392
        }
393
394
        if (!\is_writable($directory)) {
395
            $message = 'Storage temporary directory `%s` must be writable';
396
            throw new ConfigException(\sprintf($message, $directory), 0x0E);
397
        }
398
399
        return $config;
400
    }
401
402
    /**
403
     * @param mixed $value
404
     * @return string
405
     */
406
    private function valueToString($value): string
407
    {
408
        $suffix = \is_scalar($value) ? "($value)" : (string)$value;
409
410
        return \get_debug_type($value) . $suffix;
411
    }
412
413
    /**
414
     * Extract server adapter class from server description
415
     *
416
     * @param array $serverInfo
417
     *
418
     * @return string|null
419
     */
420
    private function extractServerAdapter(array $serverInfo): ?string
421
    {
422
        return $serverInfo[FileSystemInfoInterface::ADAPTER_KEY] ?? null;
423
    }
424
}
425