Passed
Push — develop ( 93d2fc...db50bf )
by Florian
02:31
created

PathBuilder::parseTemplate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 13
ccs 5
cts 5
cp 1
crap 1
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
        'sanitizer' => 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 = array_merge($this->defaultConfig, $config);
76
77 7
        if (!$this->config['sanitizer'] instanceof FilenameSanitizerInterface) {
78 7
            $this->filenameSanitizer = new FilenameSanitizer();
79
        }
80 7
    }
81
82
    /**
83
     * @param string $template Template string
84
     * @return self
85
     */
86
    public function setPathTemplate(string $template): self
87
    {
88
        $this->config['pathTemplate'] = $template;
89
90
        return $this;
91
    }
92
93
    /**
94
     * @param string $template Template string
95
     * @return self
96
     */
97
    public function setVariantPathTemplate(string $template): self
98
    {
99
        $this->config['variantPathTemplate'] = $template;
100
101
        return $this;
102
    }
103
104
    /**
105
     * @param string $format Date format
106
     * @return self
107
     */
108
    public function setCustomDateFormat(string $format): self
109
    {
110
        $this->config['dateFormat']['custom'] = $format;
111
112
        return $this;
113
    }
114
115
    /**
116
     * Builds the path under which the data gets stored in the storage adapter.
117
     *
118
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file
119
     * @param array $options Options
120
     * @return string
121
     */
122 6
    public function path(FileInterface $file, array $options = []): string
123
    {
124 6
        return $this->buildPath($file, null, $options);
125
    }
126
127
    /**
128
     * @inheritDoc
129
     */
130 2
    public function pathForVariant(FileInterface $file, string $variant, array $options = []): string
131
    {
132 2
        return $this->buildPath($file, $variant, $options);
133
    }
134
135
    /**
136
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file
137
     * @param array $options Options
138
     * @return string
139
     */
140 7
    protected function filename(FileInterface $file, array $options = []): string
141
    {
142 7
        $config = array_merge($this->config, $options);
143
144 7
        $pathInfo = PathInfo::for($file->filename());
145 7
        $filename = $pathInfo->filename();
146
147 7
        if ($config['sanitizeFilename'] === true) {
148 7
            $filename = $this->filenameSanitizer->sanitize($pathInfo->filename());
149
        }
150
151 7
        if ($config['beautifyFilename'] === true) {
152
            $filename = $this->filenameSanitizer->beautify($pathInfo->filename());
153
        }
154
155 7
        return $filename;
156
    }
157
158
    /**
159
     * Creates a semi-random path based on a string.
160
     *
161
     * Makes it possible to overload this functionality.
162
     *
163
     * @param string $string Input string
164
     * @param int $level Depth of the path to generate.
165
     * @param string $method Hash method, crc32 or sha1.
166
     * @throws \InvalidArgumentException
167
     * @return string
168
     */
169 7
    protected function randomPath($string, $level = 3, $method = 'sha1'): string
170
    {
171 7
        if ($method === 'sha1') {
172 7
            return $this->randomPathSha1($string, $level);
173
        }
174
175
        if (is_callable($method)) {
176
            return $method($string, $level);
177
        }
178
179
        throw new InvalidArgumentException(sprintf(
180
            'BasepathBuilder::randomPath() invalid hash `%s` method provided!',
181
            $method
182
        ));
183
    }
184
185
    /**
186
     * Creates a semi-random path based on a string.
187
     *
188
     * Makes it possible to overload this functionality.
189
     *
190
     * @param string $string Input string
191
     * @param int $level Depth of the path to generate.
192
     * @return string
193
     */
194 7
    protected function randomPathSha1(string $string, int $level): string
195
    {
196 7
        $result = sha1($string);
197 7
        $randomString = '';
198 7
        $counter = 0;
199 7
        for ($i = 1; $i <= $level; $i++) {
200 7
            $counter += 2;
201 7
            $randomString .= substr($result, $counter, 2) . DIRECTORY_SEPARATOR;
202
        }
203
204 7
        return substr($randomString, 0, -1);
205
    }
206
207
    /**
208
     * Override this methods if you want or need another object
209
     *
210
     * @return \DateTimeInterface
211
     */
212 6
    protected function getDateObject(): DateTimeInterface
213
    {
214 6
        return new DateTime();
215
    }
216
217
    /**
218
     * @inheritDoc
219
     */
220 7
    protected function buildPath(FileInterface $file, ?string $variant, array $options = []): string
221
    {
222 7
        $config = array_merge($this->config, $options);
223 7
        $ds = $this->config['directorySeparator'];
224 7
        $filename = $this->filename($file, $options);
225 7
        $hashedVariant = substr(hash('sha1', (string)$variant), 0, 6);
226 7
        $template = $variant ? $config['variantPathTemplate'] : $config['pathTemplate'];
227 7
        $dateTime = $this->getDateObject();
228 7
        $randomPathLevels = empty($config['randomPathLevels']) ? (int)$config['randomPathLevels'] : 3;
229
230
        $placeholders = [
231 7
            '{ds}' => $ds,
232 7
            '{model}' => $file->model(),
233 7
            '{collection}' => $file->collection(),
234 7
            '{id}' => $file->uuid(),
235 7
            '{randomPath}' => $this->randomPath($file->uuid(), $randomPathLevels),
236 7
            '{modelId}' => $file->modelId(),
237 7
            '{strippedId}' => str_replace('-', '', $file->uuid()),
238 7
            '{extension}' => $file->extension(),
239 7
            '{mimeType}' => $file->mimeType(),
240 7
            '{filename}' => $filename,
241 7
            '{hashedFilename}' => sha1($filename),
242 7
            '{variant}' => $variant,
243 7
            '{hashedVariant}' => $hashedVariant,
244 7
            '{year}' => $dateTime->format($config['dateFormat']['year']),
245 7
            '{month}' => $dateTime->format($config['dateFormat']['month']),
246 7
            '{day}' => $dateTime->format($config['dateFormat']['day']),
247 7
            '{hour}' => $dateTime->format($config['dateFormat']['hour']),
248 7
            '{minute}' => $dateTime->format($config['dateFormat']['minute']),
249 7
            '{date}' => $dateTime->format($config['dateFormat']['custom']),
250
        ];
251
252 7
        $result = $this->parseTemplate($placeholders, $template, $ds);
253
254 7
        $pathInfo = PathInfo::for($result);
255 7
        if (!$pathInfo->hasExtension() && substr($result, -1) === '.') {
256
            return substr($result, 0, -1);
257
        }
258
259 7
        return $result;
260
    }
261
262
    /**
263
     * Parses the path string template
264
     *
265
     * @param array $placeholders Assoc array of placeholder to value
266
     * @param string $template Template string
267
     * @param string $separator Directory Separator
268
     * @return string
269
     */
270 7
    protected function parseTemplate(
271
        array $placeholders,
272
        string $template,
273
        string $separator
274
    ): string {
275 7
        $result = str_replace(
276 7
            array_keys($placeholders),
277 7
            array_values($placeholders),
278
            $template
279
        );
280
281
        // Remove double or more separators caused by empty template vars
282 7
        return  preg_replace('/(\\\{2,})|(\/{2,})/', $separator, $result);
283
    }
284
}
285