Passed
Pull Request — master (#977)
by Maxim
10:15
created

StorageConfig::normalize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Storage\Config;
6
7
use AsyncAws\S3\S3Client as S3AsyncClient;
8
use Aws\Credentials\Credentials;
9
use Aws\S3\S3Client;
10
use League\Flysystem\AsyncAwsS3\AsyncAwsS3Adapter;
11
use League\Flysystem\AsyncAwsS3\PortableVisibilityConverter as AsyncAwsS3PortableVisibilityConverter;
12
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
13
use League\Flysystem\AwsS3V3\PortableVisibilityConverter as AwsS3PortableVisibilityConverter;
14
use League\Flysystem\FilesystemAdapter;
15
use League\Flysystem\Local\LocalFilesystemAdapter;
16
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
17
use Spiral\Config\Exception\InvalidArgumentException;
18
use Spiral\Core\Exception\ConfigException;
19
use Spiral\Core\InjectableConfig;
20
use Spiral\Storage\Storage;
21
use Spiral\Storage\Visibility;
22
23
class StorageConfig extends InjectableConfig
24
{
25
    public const CONFIG = 'storage';
26
27
    private string $default;
28
29
    /** @var array<string, FilesystemAdapter> */
30
    private array $adapters = [];
31
32
    /**
33
     * @var array<string, string>
34
     */
35
    private array $distributions = [];
36
37 9
    public function __construct(array $config = [])
38
    {
39 9
        $config = $this->normalize($config);
40 9
        parent::__construct($config);
41
42 9
        $this->default = $config['default'] ?? '';
43 9
        $this->bootStorages($config);
44
    }
45
46 4
    public function getDefaultBucket(): string
47
    {
48 4
        return $this->default;
49
    }
50
51
    /**
52
     * @return array<string, FilesystemAdapter>
53
     */
54 8
    public function getAdapters(): array
55
    {
56 8
        return $this->adapters;
57
    }
58
59
    /**
60
     * @return array<string, string>
61
     */
62 3
    public function getDistributions(): array
63
    {
64 3
        return $this->distributions;
65
    }
66
67 9
    private function normalize(array $config): array
68
    {
69 9
        $defaults = [
70 9
            'default' => Storage::DEFAULT_STORAGE,
71 9
            'servers' => [],
72 9
            'buckets' => [],
73 9
        ];
74
75 9
        return \array_merge($defaults, $config);
76
    }
77
78 9
    private function bootStorages(array $config): void
79
    {
80 9
        foreach ($config['buckets'] as $name => $bucket) {
81 6
            if (!\is_string($name)) {
82
                throw new InvalidArgumentException(
83
                    \vsprintf('Storage bucket config key must be a string, but %s defined', [
84
                        \get_debug_type($name),
85
                    ])
86
                );
87
            }
88
89 6
            $serverName = $bucket['server'] ?? null;
90 6
            if (!\is_string($serverName)) {
91
                throw new InvalidArgumentException(
92
                    \vsprintf('Storage bucket `%s.server` config key required and must be a string, but %s defined', [
93
                        $name,
94
                        \get_debug_type($serverName),
95
                    ])
96
                );
97
            }
98
99 6
            $server = $config['servers'][$serverName] ?? null;
100 6
            if (!\is_array($server)) {
101
                throw new InvalidArgumentException(
102
                    \vsprintf('Storage bucket `%s` relates to non-existing server `%s`', [
103
                        $name,
104
                        $serverName,
105
                    ])
106
                );
107
            }
108
109 6
            $adapter = $server['adapter'] ?? null;
110 6
            if (!\is_string($adapter)) {
111
                throw new InvalidArgumentException(
112
                    \vsprintf('Storage server `%s.adapter` config key required and must be a string, but %s defined', [
113
                        $serverName,
114
                        \get_debug_type($adapter),
115
                    ])
116
                );
117
            }
118
119 6
            $adapter = $this->createAdapter($serverName, $bucket, $server);
120
121 6
            $this->adapters[$name] = $adapter;
122
123 6
            if (isset($bucket['distribution'])) {
124
                $this->distributions[$name] = $bucket['distribution'];
125
            }
126
        }
127
    }
128
129 6
    private function createAdapter(string $serverName, array $bucket, array $server): FilesystemAdapter
130
    {
131 6
        return match ($server['adapter']) {
132 6
            'local' => $this->createLocalAdapter($serverName, $bucket, $server),
133 6
            's3' => $this->createS3Adapter($serverName, $bucket, $server, false),
134 6
            's3-async' => $this->createS3Adapter($serverName, $bucket, $server, true),
135 6
            default => $this->createCustomAdapter($serverName, $server),
136 6
        };
137
    }
138
139 5
    private function createS3Adapter(string $serverName, array $bucket, array $server, bool $async): FilesystemAdapter
140
    {
141 5
        if (!\class_exists(Credentials::class)) {
142
            throw new ConfigException(
143
                'Can not create AWS credentials while creating "' . $serverName . '" server. '
144
                . 'Perhaps you forgot to install the "league/flysystem-aws-s3-v3" package?'
145
            );
146
        }
147
148 5
        $config = [
149 5
            'version'     => $server['version'] ?? 'latest',
150 5
            'region'      => $bucket['region'] ?? $server['region'] ?? null,
151 5
            'endpoint'    => $server['endpoint'] ?? null,
152 5
            'credentials' => new Credentials(
153 5
                $server['key'] ?? null,
154 5
                $server['secret'] ?? null,
155 5
                $server['token'] ?? null,
156 5
                $server['expires'] ?? null
157 5
            ),
158 5
        ] + ($server['options'] ?? []);
159
160 5
        $name = $bucket['bucket'] ?? $server['bucket'];
161 5
        $visibility = $bucket['visibility'] ?? $server['visibility'] ?? Visibility::VISIBILITY_PUBLIC;
162
163 5
        if ($async) {
164
            if (!\class_exists(AsyncAwsS3Adapter::class)) {
165
                throw new InvalidArgumentException(
166
                    'Can not create async S3 client, please install "league/flysystem-async-aws-s3"'
167
                );
168
            }
169
170
            return new AsyncAwsS3Adapter(
171
                new S3AsyncClient($config),
172
                $name,
173
                $bucket['prefix'] ?? $server['prefix'] ?? '',
174
                new AsyncAwsS3PortableVisibilityConverter(
175
                    $visibility
176
                )
177
            );
178
        }
179
180 5
        if (!\class_exists(AwsS3V3Adapter::class)) {
181
            throw new InvalidArgumentException(
182
                'Can not create S3 client, please install "league/flysystem-aws-s3-v3"'
183
            );
184
        }
185
186 5
        return new AwsS3V3Adapter(
187 5
            new S3Client($config),
188 5
            $name,
189 5
            $bucket['prefix'] ?? $server['prefix'] ?? '',
190 5
            new AwsS3PortableVisibilityConverter(
191 5
                $visibility
192 5
            )
193 5
        );
194
    }
195
196 1
    private function createLocalAdapter(string $serverName, array $bucket, array $server): FilesystemAdapter
197
    {
198 1
        if (!\is_string($server['directory'] ?? null)) {
199
            throw new InvalidArgumentException(
200
                \vsprintf('Storage server `%s.directory` config key required and must be a string, but %s defined', [
201
                    $serverName,
202
                    \get_debug_type($server['directory'] ?? null),
203
                ])
204
            );
205
        }
206
207 1
        $visibility = new PortableVisibilityConverter(
208 1
            $server['visibility']['public']['file'] ?? 0644,
209 1
            $server['visibility']['private']['file'] ?? 0600,
210 1
            $server['visibility']['public']['dir'] ?? 0755,
211 1
            $server['visibility']['private']['dir'] ?? 0700,
212 1
            $bucket['visibility'] ?? $server['visibility']['default'] ?? Visibility::VISIBILITY_PRIVATE
213 1
        );
214
215 1
        $directory = \implode('/', [
216 1
            \rtrim($server['directory'], '/'),
217 1
            \trim($bucket['prefix'] ?? '', '/'),
218 1
        ]);
219
220 1
        return new LocalFilesystemAdapter(\rtrim($directory, '/'), $visibility);
221
    }
222
223
    private function createCustomAdapter(string $serverName, array $server): FilesystemAdapter
224
    {
225
        $adapter = $server['adapter'];
226
        $isFilesystemAdapter = \is_subclass_of($adapter, FilesystemAdapter::class, true);
227
228
        if (!$isFilesystemAdapter) {
229
            throw new InvalidArgumentException(
230
                \vsprintf('Storage server `%s` must be a class string of %s, but `%s` defined', [
231
                    $serverName,
232
                    FilesystemAdapter::class,
233
                    $adapter,
234
                ])
235
            );
236
        }
237
238
        try {
239
            return new $adapter(...\array_values($server['options'] ?? []));
240
        } catch (\Throwable $e) {
241
            $message = 'An error occurred while server `%s` initializing: %s';
242
            throw new InvalidArgumentException(\sprintf($message, $serverName, $e->getMessage()), 0, $e);
243
        }
244
    }
245
}
246