Passed
Push — master ( a4ef1c...1fb27f )
by Florian
12:17 queued 12s
created

PathBuilder   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Test Coverage

Coverage 91.55%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 82
dl 0
loc 216
ccs 65
cts 71
cp 0.9155
rs 10
c 1
b 0
f 0
wmc 18

9 Methods

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