PathBuilder::setVariantPathTemplate()   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
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 10
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
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
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 $config Configuration options.
72
     */
73 7
    public function __construct(array $config = [])
74
    {
75 7
        $this->config = $config + $this->defaultConfig;
76
77 7
        if (!$this->config['filenameSanitizer'] instanceof FilenameSanitizerInterface) {
78 7
            $this->filenameSanitizer = new FilenameSanitizer();
79
        }
80 7
    }
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 $options Options
131
     * @return string
132
     */
133 6
    public function path(FileInterface $file, array $options = []): string
134
    {
135 6
        return $this->buildPath($file, null, $options);
136
    }
137
138
    /**
139
     * @inheritDoc
140
     */
141 2
    public function pathForVariant(FileInterface $file, string $variant, array $options = []): string
142
    {
143 2
        return $this->buildPath($file, $variant, $options);
144
    }
145
146
    /**
147
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file
148
     * @param array $options Options
149
     * @return string
150
     */
151 7
    protected function filename(FileInterface $file, array $options = []): string
152
    {
153 7
        $config = $options + $this->config;
154
155 7
        $pathInfo = PathInfo::for($file->filename());
156 7
        $filename = $pathInfo->filename();
157
158 7
        if ($config['sanitizeFilename'] === true) {
159 7
            $filename = $this->filenameSanitizer->sanitize($pathInfo->filename());
160
        }
161
162 7
        if ($config['beautifyFilename'] === true) {
163
            $filename = $this->filenameSanitizer->beautify($pathInfo->filename());
164
        }
165
166 7
        return $filename;
167
    }
168
169
    /**
170
     * Creates a semi-random path based on a string.
171
     *
172
     * Makes it possible to overload this functionality.
173
     *
174
     * @param string $string Input string
175
     * @param int $level Depth of the path to generate.
176
     * @param string $method Hash method, crc32 or sha1.
177
     * @throws \InvalidArgumentException
178
     * @return string
179
     */
180 7
    protected function randomPath($string, $level = 3, $method = 'sha1'): string
181
    {
182 7
        if ($method === 'sha1') {
183 7
            return $this->randomPathSha1($string, $level);
184
        }
185
186
        if (is_callable($method)) {
187
            return $method($string, $level);
188
        }
189
190
        throw new InvalidArgumentException(sprintf(
191
            'BasepathBuilder::randomPath() invalid hash `%s` method provided!',
192
            $method
193
        ));
194
    }
195
196
    /**
197
     * Creates a semi-random path based on a string.
198
     *
199
     * Makes it possible to overload this functionality.
200
     *
201
     * @param string $string Input string
202
     * @param int $level Depth of the path to generate.
203
     * @return string
204
     */
205 7
    protected function randomPathSha1(string $string, int $level): string
206
    {
207 7
        $result = sha1($string);
208 7
        $randomString = '';
209 7
        $counter = 0;
210 7
        for ($i = 1; $i <= $level; $i++) {
211 7
            $counter += 2;
212 7
            $randomString .= substr($result, $counter, 2) . DIRECTORY_SEPARATOR;
213
        }
214
215 7
        return substr($randomString, 0, -1);
216
    }
217
218
    /**
219
     * Override this methods if you want or need another object
220
     *
221
     * @return \DateTimeInterface
222
     */
223 6
    protected function getDateObject(): DateTimeInterface
224
    {
225 6
        return new DateTime();
226
    }
227
228
    /**
229
     * @inheritDoc
230
     */
231 7
    protected function buildPath(FileInterface $file, ?string $variant, array $options = []): string
232
    {
233 7
        $config = $options + $this->config;
234 7
        $ds = $this->config['directorySeparator'];
235 7
        $filename = $this->filename($file, $options);
236 7
        $hashedVariant = substr(hash('sha1', (string)$variant), 0, 6);
237 7
        $template = $variant ? $config['variantPathTemplate'] : $config['pathTemplate'];
238 7
        $dateTime = $this->getDateObject();
239 7
        $randomPathLevels = (int)$config['randomPathLevels'] ?: 3;
240
241
        $placeholders = [
242 7
            '{ds}' => $ds,
243 7
            '{model}' => $file->model(),
244 7
            '{collection}' => $file->collection(),
245 7
            '{id}' => $file->uuid(),
246 7
            '{randomPath}' => $this->randomPath($file->uuid(), $randomPathLevels),
247 7
            '{modelId}' => $file->modelId(),
248 7
            '{strippedId}' => str_replace('-', '', $file->uuid()),
249 7
            '{extension}' => $file->extension(),
250 7
            '{mimeType}' => $file->mimeType(),
251 7
            '{filename}' => $filename,
252 7
            '{hashedFilename}' => sha1($filename),
253 7
            '{variant}' => $variant,
254 7
            '{hashedVariant}' => $hashedVariant,
255 7
            '{year}' => $dateTime->format($config['dateFormat']['year']),
256 7
            '{month}' => $dateTime->format($config['dateFormat']['month']),
257 7
            '{day}' => $dateTime->format($config['dateFormat']['day']),
258 7
            '{hour}' => $dateTime->format($config['dateFormat']['hour']),
259 7
            '{minute}' => $dateTime->format($config['dateFormat']['minute']),
260 7
            '{date}' => $dateTime->format($config['dateFormat']['custom']),
261
        ];
262
263 7
        $result = $this->parseTemplate($placeholders, $template, $ds);
264
265 7
        $pathInfo = PathInfo::for($result);
266 7
        if (!$pathInfo->hasExtension() && substr($result, -1) === '.') {
267
            return substr($result, 0, -1);
268
        }
269
270 7
        return $result;
271
    }
272
273
    /**
274
     * Parses the path string template
275
     *
276
     * @param array $placeholders Assoc array of placeholder to value
277
     * @param string $template Template string
278
     * @param string $separator Directory Separator
279
     * @return string
280
     */
281 7
    protected function parseTemplate(
282
        array $placeholders,
283
        string $template,
284
        string $separator
285
    ): string {
286 7
        $result = str_replace(
287 7
            array_keys($placeholders),
288 7
            array_values($placeholders),
289
            $template
290
        );
291
292
        // Remove double or more separators caused by empty template vars
293 7
        return (string)preg_replace('/(\\\{2,})|(\/{2,})/', $separator, $result);
294
    }
295
}
296