Issues (88)

src/Rules/File.php (5 issues)

1
<?php
2
3
/**
4
 * This file is part of Dimtrovich/Validation.
5
 *
6
 * (c) 2023 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace Dimtrovich\Validation\Rules;
13
14
use BlitzPHP\Traits\Conditionable;
15
use BlitzPHP\Traits\Macroable;
16
use BlitzPHP\Utilities\Helpers;
17
use BlitzPHP\Utilities\Iterable\Arr;
18
use BlitzPHP\Utilities\String\Text;
19
use BlitzPHP\Utilities\Support\Invader;
20
use Dimtrovich\Validation\Traits\FileTrait;
21
use Dimtrovich\Validation\Validator;
22
use InvalidArgumentException;
23
24
class File extends AbstractRule
25
{
26
    use Macroable;
0 ignored issues
show
The trait BlitzPHP\Traits\Macroable requires the property $name which is not provided by Dimtrovich\Validation\Rules\File.
Loading history...
27
    use FileTrait;
28
    use Conditionable;
29
30
    /**
31
     * The MIME types that the given file should match. This array may also contain file extensions.
32
     */
33
    protected array $allowedMimetypes = [];
34
35
    /**
36
     * The extensions that the given file should match.
37
     */
38
    protected array $allowedExtensions = [];
39
40
    /**
41
     * The minimum size in kilobytes that the file can be.
42
     */
43
    protected ?int $minimumFileSize = null;
44
45
    /**
46
     * The maximum size in kilobytes that the file can be.
47
     */
48
    protected ?int $maximumFileSize = null;
49
50
    /**
51
     * An array of custom rules that will be merged into the validation rules.
52
     */
53
    protected array $customRules = [];
54
55
    /**
56
     * The callback that will generate the "default" version of the file rule.
57
     *
58
     * @var array|callable|string|null
59
     */
60
    public static $defaultCallback;
61
62
    /**
63
     * Set the default callback to be used for determining the file default rules.
64
     *
65
     * If no arguments are passed, the default file rule configuration will be returned.
66
     *
67
     * @param callable|static|null $callback
68
     *
69
     * @return static|null
70
     */
71
    public static function defaults($callback = null)
72
    {
73
        if (null === $callback) {
74
            return static::default();
75
        }
76
77
        if (! is_callable($callback) && ! $callback instanceof static) {
78
            throw new InvalidArgumentException('The given callback should be callable or an instance of ' . static::class);
79
        }
80
81
        static::$defaultCallback = $callback;
82
    }
83
84
    /**
85
     * Get the default configuration of the file rule.
86
     */
87
    public static function default(): static
88
    {
89
        $file = is_callable(static::$defaultCallback)
90
            ? call_user_func(static::$defaultCallback)
0 ignored issues
show
It seems like static::defaultCallback can also be of type null; however, parameter $callback of call_user_func() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

90
            ? call_user_func(/** @scrutinizer ignore-type */ static::$defaultCallback)
Loading history...
91 2
            : static::$defaultCallback;
92
93 2
        return $file instanceof static ? $file : new static();
94
    }
95
96
    /**
97
     * Limit the uploaded file to only image types.
98
     */
99
    public static function image(): ImageFile
100
    {
101 2
        return new ImageFile();
102
    }
103
104
    /**
105
     * Limit the uploaded file to the given MIME types or file extensions.
106
     *
107
     * @param array<int, string>|string $mimetypes
108
     */
109
    public static function types(array|string $mimetypes): static
110
    {
111 2
        return Helpers::tap(new static(), fn ($file) => $file->allowedMimetypes = (array) $mimetypes);
112
    }
113
114
    /**
115
     * Limit the uploaded file to the given file extensions.
116
     *
117
     * @param array<int, string>|string $extensions
118
     */
119
    public function extensions(array|string $extensions): static
120
    {
121 2
        $this->allowedExtensions = (array) $extensions;
122
123 2
        return $this;
124
    }
125
126
    /**
127
     * Indicate that the uploaded file should be exactly a certain size in kilobytes.
128
     */
129
    public function size(int|string $size): static
130
    {
131 2
        $this->minimumFileSize = $this->toKilobytes($size);
132 2
        $this->maximumFileSize = $this->minimumFileSize;
133
134 2
        return $this;
135
    }
136
137
    /**
138
     * Indicate that the uploaded file should be between a minimum and maximum size in kilobytes.
139
     */
140
    public function between(int|string $minSize, int|string $maxSize): static
141
    {
142 2
        $this->minimumFileSize = $this->toKilobytes($minSize);
143 2
        $this->maximumFileSize = $this->toKilobytes($maxSize);
144
145 2
        return $this;
146
    }
147
148
    /**
149
     * Indicate that the uploaded file should be no less than the given number of kilobytes.
150
     */
151
    public function min(int|string $size): static
152
    {
153 2
        $this->minimumFileSize = $this->toKilobytes($size);
154
155 2
        return $this;
156
    }
157
158
    /**
159
     * Indicate that the uploaded file should be no more than the given number of kilobytes.
160
     */
161
    public function max(int|string $size)
162
    {
163 2
        $this->maximumFileSize = $this->toKilobytes($size);
164
165 2
        return $this;
166
    }
167
168
    /**
169
     * Convert a potentially human-friendly file size to kilobytes.
170
     *
171
     * @return float|int
172
     */
173
    protected function toKilobytes(int|string $size)
174
    {
175
        if (! is_string($size)) {
176 2
            return $size;
177
        }
178
179 2
        $value = (float) $size;
180
181
        return round(match (true) {
182
            Text::endsWith($size, 'kb') => $value * 1,
0 ignored issues
show
'kb' of type string is incompatible with the type iterable expected by parameter $needles of BlitzPHP\Utilities\String\Text::endsWith(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

182
            Text::endsWith($size, /** @scrutinizer ignore-type */ 'kb') => $value * 1,
Loading history...
183
            Text::endsWith($size, 'mb') => $value * 1000,
184
            Text::endsWith($size, 'gb') => $value * 1000000,
185
            Text::endsWith($size, 'tb') => $value * 1000000000,
186
            default                     => throw new InvalidArgumentException('Invalid file size suffix.'),
187 2
        });
188
    }
189
190
    /**
191
     * Specify additional validation rules that should be merged with the default rules during validation.
192
     */
193
    public function rules(array|string $rules): static
194
    {
195 2
        $this->customRules = array_merge($this->customRules, Arr::wrap($rules));
196
197 2
        return $this;
198
    }
199
200
    /**
201
     * {@inheritDoc}
202
     */
203
    public function check($value): bool
204
    {
205 2
        $attribute = $this->getAttribute()->getKey();
206
207
        $validator = Validator::make(
208
            [$attribute => $value],
209
            [$attribute => $this->buildValidationRules()],
210
            $this->validation->getMessages(),
0 ignored issues
show
The method getMessages() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

210
            $this->validation->/** @scrutinizer ignore-call */ 
211
                               getMessages(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
211
            Invader::make($this->validation)->aliases
0 ignored issues
show
It seems like $this->validation can also be of type null; however, parameter $obj of BlitzPHP\Utilities\Support\Invader::make() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

211
            Invader::make(/** @scrutinizer ignore-type */ $this->validation)->aliases
Loading history...
212 2
        );
213
214
        if ($validator->fails()) {
215
            foreach ($validator->errors()->toArray() as $error) {
216
                foreach ($error as $rule => $message) {
217 2
                    $this->validation->errors()->add($attribute, $rule, $message);
218
                }
219
            }
220
221 2
            return false;
222
        }
223
224 2
        return true;
225
    }
226
227
    /**
228
     * Build the array of underlying validation rules based on the current state.
229
     */
230
    protected function buildValidationRules(): array
231
    {
232 2
        $_this = $this;
233
234
        $rules = [
235
            fn ($value) => $_this->isValidFileInstance($value),
236 2
        ];
237
238 2
        $rules = array_merge($rules, $this->buildMimetypes());
239
240
        if (! empty($this->allowedExtensions)) {
241 2
            $rules[] = 'ext:' . implode(',', array_map('strtolower', $this->allowedExtensions));
242
        }
243
244
        $rules[] = match (true) {
245
            null === $this->minimumFileSize && null === $this->maximumFileSize => null,
246
            null === $this->maximumFileSize                                    => "min:{$this->minimumFileSize}",
247
            null === $this->minimumFileSize                                    => "max:{$this->maximumFileSize}",
248
            $this->minimumFileSize !== $this->maximumFileSize                  => "between:{$this->minimumFileSize},{$this->maximumFileSize}",
249
            default                                                            => "size:{$this->minimumFileSize}",
250
        };
251
252 2
        return array_merge(array_filter($rules), $this->customRules);
253
    }
254
255
    /**
256
     * Separate the given mimetypes from extensions and return an array of correct rules to validate against.
257
     */
258
    protected function buildMimetypes(): array
259
    {
260
        if (count($this->allowedMimetypes) === 0) {
261 2
            return [];
262
        }
263
264 2
        $rules = [];
265
266
        $mimetypes = array_filter(
267
            $this->allowedMimetypes,
268
            fn ($type) => str_contains($type, '/')
269 2
        );
270
271 2
        $mimes = array_diff($this->allowedMimetypes, $mimetypes);
272
273
        if (count($mimetypes) > 0) {
274 2
            $rules[] = 'mimetypes:' . implode(',', $mimetypes);
275
        }
276
277
        if (count($mimes) > 0) {
278 2
            $rules[] = 'mimes:' . implode(',', $mimes);
279
        }
280
281 2
        return $rules;
282
    }
283
}
284