@@ -17,6 +17,16 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule
17
17
{
18
18
use Conditionable, Macroable;
19
19
20
+ /**
21
+ * Binary units flag used for size validation.
22
+ */
23
+ public const BINARY = 'binary ' ;
24
+
25
+ /**
26
+ * International units flag used for size validation.
27
+ */
28
+ public const INTERNATIONAL = 'international ' ;
29
+
20
30
/**
21
31
* The MIME types that the given file should match. This array may also contain file extensions.
22
32
*
@@ -45,6 +55,11 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule
45
55
*/
46
56
protected $ maximumFileSize = null ;
47
57
58
+ /**
59
+ * The units used for size validation.
60
+ */
61
+ protected ?string $ units = null ;
62
+
48
63
/**
49
64
* An array of custom rules that will be merged into the validation rules.
50
65
*
@@ -150,83 +165,134 @@ public function extensions($extensions)
150
165
return $ this ;
151
166
}
152
167
168
+ /**
169
+ * Set the default units for size validation to binary.
170
+ */
171
+ public static function binary (): static
172
+ {
173
+ return tap (new static , static fn ($ file ) => $ file ->units = self ::BINARY );
174
+ }
175
+
176
+ /**
177
+ * Set the default units for size validation to international.
178
+ */
179
+ public static function international (): static
180
+ {
181
+ return tap (new static , static fn ($ file ) => $ file ->units = self ::INTERNATIONAL );
182
+ }
183
+
153
184
/**
154
185
* Indicate that the uploaded file should be exactly a certain size in kilobytes.
155
- *
156
- * @param string|int $size
157
- * @return $this
158
186
*/
159
- public function size ($ size)
187
+ public function size (string | int $ size, ? string $ units = null ): static
160
188
{
161
- $ this ->minimumFileSize = $ this ->toKilobytes ($ size );
189
+ $ this ->minimumFileSize = $ this ->toKilobytes ($ size, $ this -> units ( $ units ) );
162
190
$ this ->maximumFileSize = $ this ->minimumFileSize ;
163
191
164
192
return $ this ;
165
193
}
166
194
167
195
/**
168
196
* Indicate that the uploaded file should be between a minimum and maximum size in kilobytes.
169
- *
170
- * @param string|int $minSize
171
- * @param string|int $maxSize
172
- * @return $this
173
197
*/
174
- public function between ($ minSize , $ maxSize)
198
+ public function between (string | int $ minSize , string | int $ maxSize, ? string $ units = null ): static
175
199
{
176
- $ this ->minimumFileSize = $ this ->toKilobytes ($ minSize );
177
- $ this ->maximumFileSize = $ this ->toKilobytes ($ maxSize );
200
+ $ this ->minimumFileSize = $ this ->toKilobytes ($ minSize, $ this -> units ( $ units ) );
201
+ $ this ->maximumFileSize = $ this ->toKilobytes ($ maxSize, $ this -> units ( $ units ) );
178
202
179
203
return $ this ;
180
204
}
181
205
182
206
/**
183
207
* Indicate that the uploaded file should be no less than the given number of kilobytes.
184
- *
185
- * @param string|int $size
186
- * @return $this
187
208
*/
188
- public function min ($ size)
209
+ public function min (string | int $ size, ? string $ units = null ): static
189
210
{
190
- $ this ->minimumFileSize = $ this ->toKilobytes ($ size );
211
+ $ this ->minimumFileSize = $ this ->toKilobytes ($ size, $ this -> units ( $ units ) );
191
212
192
213
return $ this ;
193
214
}
194
215
195
216
/**
196
217
* Indicate that the uploaded file should be no more than the given number of kilobytes.
197
- *
198
- * @param string|int $size
199
- * @return $this
200
218
*/
201
- public function max ($ size)
219
+ public function max (string | int $ size, ? string $ units = null ): static
202
220
{
203
- $ this ->maximumFileSize = $ this ->toKilobytes ($ size );
221
+ $ this ->maximumFileSize = $ this ->toKilobytes ($ size, $ this -> units ( $ units ) );
204
222
205
223
return $ this ;
206
224
}
207
225
226
+ protected function units (?string $ units = null ): string
227
+ {
228
+ return $ units ?? $ this ->units ?? static ::INTERNATIONAL ;
229
+ }
230
+
231
+ /**
232
+ * Converts whole numbers to integers for exact arithmetic while keeping
233
+ * fractional numbers as floats; also provides overflow protection by
234
+ * falling back to float arithmetic for values too large for integer range.
235
+ */
236
+ protected function prepareValueForPrecision (float $ value ): float |int
237
+ {
238
+ return $ value > PHP_INT_MAX || $ value < PHP_INT_MIN || ((float ) (int ) $ value ) !== $ value
239
+ ? $ value
240
+ : (int ) $ value ;
241
+ }
242
+
208
243
/**
209
244
* Convert a potentially human-friendly file size to kilobytes.
210
- *
211
- * @param string|int $size
212
- * @return mixed
213
245
*/
214
- protected function toKilobytes ($ size)
246
+ protected function toKilobytes (string | int $ size, string $ units ): float | int
215
247
{
216
248
if (! is_string ($ size )) {
217
249
return $ size ;
218
250
}
219
251
220
252
$ size = strtolower (trim ($ size ));
221
253
222
- $ value = floatval ($ size );
254
+ $ value = (float ) $ size ;
255
+
256
+ return $ units === self ::BINARY
257
+ ? $ this ->toBinaryKilobytes ($ size , $ value )
258
+ : $ this ->toInternationalKilobytes ($ size , $ value );
259
+ }
260
+
261
+ /**
262
+ * Convert a human-friendly file size to kilobytes using the International System.
263
+ */
264
+ protected function toInternationalKilobytes (string $ size , float $ value ): float |int
265
+ {
266
+ $ value = $ this ->prepareValueForPrecision ($ value );
223
267
224
268
return round (match (true ) {
225
- Str::endsWith ($ size , 'kb ' ) => $ value * 1 ,
269
+ is_numeric ($ size ) => $ value ,
270
+ Str::endsWith ($ size , 'kb ' ) => $ value ,
226
271
Str::endsWith ($ size , 'mb ' ) => $ value * 1_000 ,
227
272
Str::endsWith ($ size , 'gb ' ) => $ value * 1_000_000 ,
228
273
Str::endsWith ($ size , 'tb ' ) => $ value * 1_000_000_000 ,
229
- default => throw new InvalidArgumentException ('Invalid file size suffix. ' ),
274
+ default => throw new InvalidArgumentException (
275
+ 'Invalid file size suffix. Valid suffixes are: KB, MB, GB, TB (case insensitive). '
276
+ ),
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Convert a human-friendly file size to kilobytes using the Binary System.
282
+ */
283
+ protected function toBinaryKilobytes (string $ size , float $ value ): float |int
284
+ {
285
+ $ value = $ this ->prepareValueForPrecision ($ value );
286
+
287
+ return round (match (true ) {
288
+ is_numeric ($ size ) => $ value ,
289
+ Str::endsWith ($ size , 'kb ' ) => $ value ,
290
+ Str::endsWith ($ size , 'mb ' ) => $ value * 1_024 ,
291
+ Str::endsWith ($ size , 'gb ' ) => $ value * 1_048_576 ,
292
+ Str::endsWith ($ size , 'tb ' ) => $ value * 1_073_741_824 ,
293
+ default => throw new InvalidArgumentException (
294
+ 'Invalid file size suffix. Valid suffixes are: KB, MB, GB, TB (case insensitive). '
295
+ ),
230
296
});
231
297
}
232
298
@@ -283,14 +349,18 @@ protected function buildValidationRules()
283
349
$ rules [] = 'extensions: ' .implode (', ' , array_map (strtolower (...), $ this ->allowedExtensions ));
284
350
}
285
351
286
- $ rules [] = match (true ) {
287
- is_null ( $ this ->minimumFileSize ) && is_null ( $ this ->maximumFileSize ) => null ,
288
- is_null ( $ this ->maximumFileSize ) => "min: {$ this ->minimumFileSize }" ,
289
- is_null ( $ this ->minimumFileSize ) => "max: {$ this ->maximumFileSize }" ,
290
- $ this ->minimumFileSize !== $ this ->maximumFileSize => "between : {$ this ->minimumFileSize } , { $ this -> maximumFileSize }" ,
291
- default => "size : {$ this ->minimumFileSize }" ,
352
+ $ rule = match (true ) {
353
+ $ this ->minimumFileSize === null && $ this ->maximumFileSize === null => null ,
354
+ $ this ->maximumFileSize === null => "min: {$ this ->minimumFileSize }" ,
355
+ $ this ->minimumFileSize === null => "max: {$ this ->maximumFileSize }" ,
356
+ $ this ->minimumFileSize === $ this ->maximumFileSize => "size : {$ this ->minimumFileSize }" ,
357
+ default => "between : {$ this ->minimumFileSize } , { $ this -> maximumFileSize }" ,
292
358
};
293
359
360
+ if ($ rule ) {
361
+ $ rules [] = $ rule ;
362
+ }
363
+
294
364
return array_merge (array_filter ($ rules ), $ this ->customRules );
295
365
}
296
366
0 commit comments