PathBuilder::buildPath()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 40
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 32
c 2
b 0
f 0
nc 4
nop 3
dl 0
loc 40
rs 9.0968
1
<?php
2
3
/**
4
 * Copyright (c) Florian Krämer (https://florian-kraemer.net)
5
 * Licensed under The MIT License
6
 * For full copyright and license information, please see the LICENSE.txt
7
 * Redistributions of files must retain the above copyright notice.
8
 *
9
 * @copyright Copyright (c) Florian Krämer (https://florian-kraemer.net)
10
 * @author    Florian Krämer
11
 * @link      https://github.com/Phauthentic
12
 * @license   https://opensource.org/licenses/MIT MIT License
13
 */
14
15
declare(strict_types=1);
16
17
namespace Phauthentic\Infrastructure\Storage\PathBuilder;
18
19
use DateTime;
20
use DateTimeInterface;
21
use InvalidArgumentException;
22
use Phauthentic\Infrastructure\Storage\FileInterface;
23
use Phauthentic\Infrastructure\Storage\Utility\FilenameSanitizer;
24
use Phauthentic\Infrastructure\Storage\Utility\FilenameSanitizerInterface;
25
use Phauthentic\Infrastructure\Storage\Utility\PathInfo;
26
27
/**
28
 * A path builder is an utility class that generates a path and filename for a
29
 * file storage entity. All the fields from the entity can bed used to create
30
 * a path and file name.
31
 */
32
class PathBuilder implements PathBuilderInterface
33
{
34
    /**
35
     * Default settings.
36
     *
37
     * @var array<string, mixed>
38
     */
39
    protected array $defaultConfig = [
40
        'directorySeparator' => DIRECTORY_SEPARATOR,
41
        'randomPath' => 'sha1',
42
        'randomPathLevels' => 3,
43
        'sanitizeFilename' => true,
44
        'beautifyFilename' => false,
45
        'filenameSanitizer' => null,
46
        'pathTemplate' => '{model}{ds}{randomPath}{ds}{strippedId}{ds}{filename}.{extension}',
47
        'variantPathTemplate' => '{model}{ds}{randomPath}{ds}{strippedId}{ds}{filename}.{hashedVariant}.{extension}',
48
        'dateFormat' => [
49
            'year' => 'Y',
50
            'month' => 'm',
51
            'day' => 'd',
52
            'hour' => 'H',
53
            'minute' => 'i',
54
            'custom' => 'Y-m-d'
55
        ]
56
    ];
57
58
    /**
59
     * @var array<string, mixed>
60
     */
61
    protected array $config = [];
62
63
    /**
64
     * @var \Phauthentic\Infrastructure\Storage\Utility\FilenameSanitizerInterface
65
     */
66
    protected FilenameSanitizerInterface $filenameSanitizer;
67
68
    /**
69
     * Constructor
70
     *
71
     * @param array<string, mixed> $config Configuration options.
72
     */
73
    public function __construct(array $config = [])
74
    {
75
        $this->config = $config + $this->defaultConfig;
76
77
        if (!$this->config['filenameSanitizer'] instanceof FilenameSanitizerInterface) {
78
            $this->filenameSanitizer = new FilenameSanitizer();
79
        }
80
    }
81
82
    /**
83
     * @param \Phauthentic\Infrastructure\Storage\Utility\FilenameSanitizerInterface $sanitizer
84
     * @return self
85
     */
86
    public function setFilenameSanitizer(FilenameSanitizerInterface $sanitizer): self
87
    {
88
        $this->filenameSanitizer = $sanitizer;
89
90
        return $this;
91
    }
92
93
    /**
94
     * @param string $template Template string
95
     * @return self
96
     */
97
    public function setPathTemplate(string $template): self
98
    {
99
        $this->config['pathTemplate'] = $template;
100
101
        return $this;
102
    }
103
104
    /**
105
     * @param string $template Template string
106
     * @return self
107
     */
108
    public function setVariantPathTemplate(string $template): self
109
    {
110
        $this->config['variantPathTemplate'] = $template;
111
112
        return $this;
113
    }
114
115
    /**
116
     * @param string $format Date format
117
     * @return self
118
     */
119
    public function setCustomDateFormat(string $format): self
120
    {
121
        $this->config['dateFormat']['custom'] = $format;
122
123
        return $this;
124
    }
125
126
    /**
127
     * Builds the path under which the data gets stored in the storage adapter.
128
     *
129
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file
130
     * @param array<string, mixed> $options Options
131
     * @return string
132
     */
133
    public function path(FileInterface $file, array $options = []): string
134
    {
135
        return $this->buildPath($file, null, $options);
136
    }
137
138
    /**
139
     * @inheritDoc
140
     * @param array<string, mixed> $options
141
     */
142
    public function pathForVariant(FileInterface $file, string $variant, array $options = []): string
143
    {
144
        return $this->buildPath($file, $variant, $options);
145
    }
146
147
    /**
148
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file
149
     * @param array<string, mixed> $options Options
150
     * @return string
151
     */
152
    protected function filename(FileInterface $file, array $options = []): string
153
    {
154
        $config = $options + $this->config;
155
156
        $pathInfo = PathInfo::for($file->filename());
157
        $filename = $pathInfo->filename();
158
159
        if ($config['sanitizeFilename'] === true) {
160
            $filename = $this->filenameSanitizer->sanitize($pathInfo->filename());
161
        }
162
163
        if ($config['beautifyFilename'] === true) {
164
            $filename = $this->filenameSanitizer->beautify($pathInfo->filename());
165
        }
166
167
        return $filename;
168
    }
169
170
    /**
171
     * Creates a semi-random path based on a string.
172
     *
173
     * Makes it possible to overload this functionality.
174
     *
175
     * @param string $string Input string
176
     * @param int $level Depth of the path to generate.
177
     * @param string $method Hash method, crc32 or sha1.
178
     * @throws \InvalidArgumentException
179
     * @return string
180
     */
181
    protected function randomPath($string, $level = 3, $method = 'sha1'): string
182
    {
183
        if ($method === 'sha1') {
184
            return $this->randomPathSha1($string, $level);
185
        }
186
187
        if (is_callable($method)) {
188
            return $method($string, $level);
189
        }
190
191
        throw new InvalidArgumentException(sprintf(
192
            'BasepathBuilder::randomPath() invalid hash `%s` method provided!',
193
            $method
194
        ));
195
    }
196
197
    /**
198
     * Creates a semi-random path based on a string.
199
     *
200
     * Makes it possible to overload this functionality.
201
     *
202
     * @param string $string Input string
203
     * @param int $level Depth of the path to generate.
204
     * @return string
205
     */
206
    protected function randomPathSha1(string $string, int $level): string
207
    {
208
        $result = sha1($string);
209
        $randomString = '';
210
        $counter = 0;
211
        for ($i = 1; $i <= $level; $i++) {
212
            $counter += 2;
213
            $randomString .= substr($result, $counter, 2) . DIRECTORY_SEPARATOR;
214
        }
215
216
        return substr($randomString, 0, -1);
217
    }
218
219
    /**
220
     * Override this methods if you want or need another object
221
     *
222
     * @return \DateTimeInterface
223
     */
224
    protected function getDateObject(): DateTimeInterface
225
    {
226
        return new DateTime();
227
    }
228
229
    /**
230
     * @inheritDoc
231
     * @param array<string, mixed> $options
232
     */
233
    protected function buildPath(FileInterface $file, ?string $variant, array $options = []): string
234
    {
235
        $config = $options + $this->config;
236
        $ds = $this->config['directorySeparator'];
237
        $filename = $this->filename($file, $options);
238
        $hashedVariant = substr(hash('sha1', (string)$variant), 0, 6);
239
        $template = $variant ? $config['variantPathTemplate'] : $config['pathTemplate'];
240
        $dateTime = $this->getDateObject();
241
        $randomPathLevels = (int)$config['randomPathLevels'] ?: 3;
242
243
        $placeholders = [
244
            '{ds}' => $ds,
245
            '{model}' => $file->model(),
246
            '{collection}' => $file->collection(),
247
            '{id}' => $file->uuid(),
248
            '{randomPath}' => $this->randomPath($file->uuid(), $randomPathLevels),
249
            '{modelId}' => $file->modelId(),
250
            '{strippedId}' => str_replace('-', '', $file->uuid()),
251
            '{extension}' => $file->extension(),
252
            '{mimeType}' => $file->mimeType(),
253
            '{filename}' => $filename,
254
            '{hashedFilename}' => sha1($filename),
255
            '{variant}' => $variant,
256
            '{hashedVariant}' => $hashedVariant,
257
            '{year}' => $dateTime->format($config['dateFormat']['year']),
258
            '{month}' => $dateTime->format($config['dateFormat']['month']),
259
            '{day}' => $dateTime->format($config['dateFormat']['day']),
260
            '{hour}' => $dateTime->format($config['dateFormat']['hour']),
261
            '{minute}' => $dateTime->format($config['dateFormat']['minute']),
262
            '{date}' => $dateTime->format($config['dateFormat']['custom']),
263
        ];
264
265
        $result = $this->parseTemplate($placeholders, $template, $ds);
266
267
        $pathInfo = PathInfo::for($result);
268
        if (!$pathInfo->hasExtension() && substr($result, -1) === '.') {
269
            return substr($result, 0, -1);
270
        }
271
272
        return $result;
273
    }
274
275
    /**
276
     * Parses the path string template
277
     *
278
     * @param array<string, string|null> $placeholders Assoc array of placeholder to value
279
     * @param string $template Template string
280
     * @param string $separator Directory Separator
281
     * @return string
282
     */
283
    protected function parseTemplate(
284
        array $placeholders,
285
        string $template,
286
        string $separator
287
    ): string {
288
        $keys = array_keys($placeholders);
289
        $values = array_map(function ($value) {
290
            return $value ?? '';
291
        }, array_values($placeholders));
292
        $result = str_replace(
293
            $keys,
294
            $values,
295
            $template
296
        );
297
298
        // Remove double or more separators caused by empty template vars
299
        return (string)preg_replace('/(\\\{2,})|(\/{2,})/', $separator, $result);
300
    }
301
}
302