Passed
Push — develop ( a702bc...4ae82e )
by Florian
07:05
created

PathBuilder   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Test Coverage

Coverage 81.25%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 88
c 2
b 0
f 0
dl 0
loc 249
ccs 65
cts 80
cp 0.8125
rs 10
wmc 21

12 Methods

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