diff --git a/README.md b/README.md
index 33e1612..8d97dbd 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,8 @@ Checks if the field under validation falls within the range of `:min` and `:max`
- For string data, the value corresponds to the number of characters.
- For numeric data, the value corresponds to a given integer value.
- For an array, the value corresponds to the count of the array.
+ - For file uploads (`$_FILES` array or WordPress attachment ID), the value corresponds to the file size in kilobytes.
+ e.g. `['avatar' => ['required', 'file', 'between:100,2048']]` (between 100KB and 2MB)
4. **`date`**
Checks if the field under validation is a valid date according to the `strtotime` PHP function.
5. **`digit_between:min,max`**
@@ -185,12 +187,39 @@ Checks if the field under validation has exactly the same size as `:size`.
- For string data, the value corresponds to the number of characters.
- For numeric data, the value corresponds to a given integer value.
- For an array, the value corresponds to the count of the array.
+ - For file uploads (`$_FILES` array or WordPress attachment ID), the value corresponds to the file size in kilobytes.
+ e.g. `['document' => ['required', 'file', 'size:512']]` (exactly 512KB)
23. **`string`**
Checks if the given value is a string.
24. **`uppercase`**
Checks if the string value consists of all uppercase letters.
25. **`url`**
Checks if the value is a valid URL.
+26. **`file`**
+Checks if the field under validation is a valid file upload. Supports standard PHP `$_FILES` arrays and WordPress attachment IDs.
+e.g. `['avatar' => ['required', 'file']]`
+27. **`mimes:type1,type2,...`**
+Checks if the file under validation has one of the allowed MIME types or extensions. Accepts comma-separated values (e.g., `jpg`, `png`, `pdf`).
+e.g. `['document' => ['required', 'file', 'mimes:pdf,doc,docx']]`
+28. **`max_file:size`**
+Checks if the file size (in kilobytes) is less than or equal to the specified maximum size.
+e.g. `['avatar' => ['required', 'file', 'max_file:2048']]` (2MB limit)
+29. **`image`**
+Checks if the file under validation is an image using WordPress allowed image MIME types.
+e.g. `['photo' => ['required', 'file', 'image']]`
+30. **`dimension:constraint=value,...`**
+Checks image dimensions against one or more constraints. Available constraints: `min_width`, `max_width`, `min_height`, `max_height`, `width` (exact), `height` (exact), `ratio` (as fraction `3/2` or float `1.5`).
+e.g. `['avatar' => ['required', 'image', 'dimension:max_width=1920,max_height=1080']]`
+e.g. `['avatar' => ['required', 'image', 'dimension:min_width=100,min_height=100,ratio=16/9']]`
+31. **`extensions:ext1,ext2,...`**
+Checks if the file has one of the specified extensions (based on filename only, no content inspection). Accepts comma-separated extensions without dots.
+e.g. `['document' => ['required', 'file', 'extensions:pdf,doc,docx']]`
+32. **`mimetypes:type1,type2,...`**
+Checks if the file's actual MIME type (verified from file content via `wp_check_filetype_and_ext`) matches one of the specified types. More secure than `mimes` because it inspects file content, not just the filename or browser-supplied type.
+e.g. `['upload' => ['required', 'file', 'mimetypes:image/jpeg,image/png,application/pdf']]`
+33. **`encoding:enc1,enc2,...`**
+Checks if the file's character encoding matches one of the specified encodings. Useful for validating text files (CSV, JSON, XML, etc.). Accepts comma-separated encoding names supported by PHP's `mb_detect_encoding`.
+e.g. `['csv' => ['required', 'file', 'encoding:UTF-8,ASCII']]`
Missing any validation rule that you need? Refer to the [Custom Validation Rule](#custom-validation-rule) section to know how you can create and use custom validation rules in your project alongside the library.
diff --git a/src/Helpers.php b/src/Helpers.php
index 1859266..b624e70 100644
--- a/src/Helpers.php
+++ b/src/Helpers.php
@@ -25,6 +25,16 @@ protected function getValueLength($value)
}
+ protected function getAttachmentSizeBytes(int $attachmentId, string $filePath)
+ {
+ $metadata = wp_get_attachment_metadata($attachmentId);
+ if ($metadata && isset($metadata['filesize'])) {
+ return (int) $metadata['filesize'];
+ }
+
+ return filesize($filePath);
+ }
+
public function setNestedElement(&$data, $keys, $value)
{
$current = &$data;
diff --git a/src/Rule.php b/src/Rule.php
index 44c0ec8..1b2d89e 100644
--- a/src/Rule.php
+++ b/src/Rule.php
@@ -46,7 +46,10 @@ public function getParamKeys()
public function setParameterValues($paramKeys, $paramValues): void
{
- if (count($paramKeys) === count($paramValues)) {
+ if (count($paramKeys) === 1 && count($paramValues) > 1) {
+ // Single-key rules that accept comma-separated values (e.g. mimes:jpg,png,pdf)
+ $this->params = [$paramKeys[0] => implode(',', $paramValues)];
+ } elseif (count($paramKeys) === count($paramValues)) {
$this->params = array_combine($paramKeys, $paramValues);
}
}
diff --git a/src/Rules/BetweenRule.php b/src/Rules/BetweenRule.php
index 67bb79e..108da93 100644
--- a/src/Rules/BetweenRule.php
+++ b/src/Rules/BetweenRule.php
@@ -17,17 +17,28 @@ public function validate($value)
$this->checkRequiredParameter($this->requireParameters);
$min = (int) $this->getParameter('min');
-
$max = (int) $this->getParameter('max');
- $length = $this->getValueLength($value);
+ if (is_array($value) && isset($value['size'])) {
+ $sizeKB = (int) round($value['size'] / 1024);
+ return $sizeKB >= $min && $sizeKB <= $max;
+ }
- if ($length) {
- return $length >= $min && $length <= $max;
+ if (is_numeric($value) && function_exists('get_attached_file')) {
+ $attachmentId = (int) $value;
+ $filePath = get_attached_file($attachmentId);
+ if (! empty($filePath) && file_exists($filePath)) {
+ $sizeKB = (int) round($this->getAttachmentSizeBytes($attachmentId, $filePath) / 1024);
+ return $sizeKB >= $min && $sizeKB <= $max;
+ }
}
- return false;
+ $length = $this->getValueLength($value);
+ if ($length === false) {
+ return false;
+ }
+ return $length >= $min && $length <= $max;
}
public function getParamKeys()
diff --git a/src/Rules/DimensionRule.php b/src/Rules/DimensionRule.php
new file mode 100644
index 0000000..d315e03
--- /dev/null
+++ b/src/Rules/DimensionRule.php
@@ -0,0 +1,146 @@
+checkRequiredParameter($this->requireParameters);
+
+ if ($this->isEmpty($value)) {
+ return false;
+ }
+
+ $constraints = $this->parseConstraints();
+ if (empty($constraints)) {
+ return false;
+ }
+
+ $dimensions = $this->getDimensions($value);
+ if (! $dimensions) {
+ return false;
+ }
+
+ return $this->checkConstraints($dimensions['width'], $dimensions['height'], $constraints);
+ }
+
+ private function parseConstraints(): array
+ {
+ $raw = $this->getParameter('dimensions');
+ if (empty($raw)) {
+ return [];
+ }
+
+ $constraints = [];
+ foreach (explode(',', $raw) as $item) {
+ $parts = explode('=', $item, 2);
+ if (count($parts) === 2) {
+ $constraints[trim($parts[0])] = trim($parts[1]);
+ }
+ }
+
+ return $constraints;
+ }
+
+ private function checkConstraints(int $width, int $height, array $constraints): bool
+ {
+ $checks = [
+ 'width' => static function ($w, $h, $v) { return $w === (int) $v; },
+ 'height' => static function ($w, $h, $v) { return $h === (int) $v; },
+ 'min_width' => static function ($w, $h, $v) { return $w >= (int) $v; },
+ 'max_width' => static function ($w, $h, $v) { return $w <= (int) $v; },
+ 'min_height' => static function ($w, $h, $v) { return $h >= (int) $v; },
+ 'max_height' => static function ($w, $h, $v) { return $h <= (int) $v; },
+ ];
+
+ foreach ($constraints as $key => $val) {
+ if ($key === 'ratio') {
+ if (! $this->checkRatio($width, $height, $val)) {
+ return false;
+ }
+ continue;
+ }
+
+ if (isset($checks[$key]) && ! $checks[$key]($width, $height, $val)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private function checkRatio(int $width, int $height, string $ratio): bool
+ {
+ if ($height === 0) {
+ return false;
+ }
+
+ if (strpos($ratio, '/') !== false) {
+ list($ratioW, $ratioH) = explode('/', $ratio, 2);
+ $expected = (float) $ratioW / (float) $ratioH;
+ } else {
+ $expected = (float) $ratio;
+ }
+
+ return abs(($width / $height) - $expected) < 0.01;
+ }
+
+ private function getDimensions($value)
+ {
+ $filePath = null;
+
+ if (is_numeric($value)) {
+ $attachmentId = (int) $value;
+ $filePath = get_attached_file($attachmentId);
+ if (empty($filePath) || ! file_exists($filePath)) {
+ return null;
+ }
+
+ $metadata = wp_get_attachment_metadata($attachmentId);
+ if ($metadata && isset($metadata['width'], $metadata['height'])) {
+ return [
+ 'width' => (int) $metadata['width'],
+ 'height' => (int) $metadata['height'],
+ ];
+ }
+ }
+
+ if (is_array($value) && isset($value['tmp_name'])) {
+ if (empty($value['tmp_name']) || ! is_uploaded_file($value['tmp_name'])) {
+ return null;
+ }
+ $filePath = $value['tmp_name'];
+ }
+
+ if ($filePath && file_exists($filePath)) {
+ $imageSize = @getimagesize($filePath);
+ if ($imageSize && isset($imageSize[0], $imageSize[1])) {
+ return [
+ 'width' => (int) $imageSize[0],
+ 'height' => (int) $imageSize[1],
+ ];
+ }
+ }
+
+ return null;
+ }
+
+ public function getParamKeys()
+ {
+ return $this->requireParameters;
+ }
+
+ public function message()
+ {
+ return $this->message;
+ }
+}
diff --git a/src/Rules/EncodingRule.php b/src/Rules/EncodingRule.php
new file mode 100644
index 0000000..a441b84
--- /dev/null
+++ b/src/Rules/EncodingRule.php
@@ -0,0 +1,72 @@
+checkRequiredParameter($this->requireParameters);
+
+ if ($this->isEmpty($value)) {
+ return false;
+ }
+
+ $allowedEncodings = array_map('trim', explode(',', $this->getParameter('encoding')));
+
+ $filePath = $this->getFilePath($value);
+ if (! $filePath) {
+ return false;
+ }
+
+ $contents = $this->getFileContentForEncodingDetection($filePath);
+ if ($contents === false) {
+ return false;
+ }
+
+ return mb_detect_encoding($contents, $allowedEncodings, true) !== false;
+ }
+
+ private function getFileContentForEncodingDetection($filePath)
+ {
+ if (function_exists('WP_Filesystem') && WP_Filesystem()) {
+ global $wp_filesystem;
+ $contents = $wp_filesystem->get_contents($filePath);
+ return $contents !== false ? substr($contents, 0, 32768) : false;
+ }
+
+ return file_get_contents($filePath, false, null, 0, 32768);
+ }
+
+ private function getFilePath($value)
+ {
+ if (is_numeric($value)) {
+ $filePath = get_attached_file((int) $value);
+ return (! empty($filePath) && file_exists($filePath)) ? $filePath : null;
+ }
+
+ if (is_array($value) && isset($value['tmp_name']) && is_uploaded_file($value['tmp_name'])) {
+ return $value['tmp_name'];
+ }
+
+ return null;
+ }
+
+ public function getParamKeys()
+ {
+ return $this->requireParameters;
+ }
+
+ public function message()
+ {
+ return $this->message;
+ }
+}
diff --git a/src/Rules/ExtensionsRule.php b/src/Rules/ExtensionsRule.php
new file mode 100644
index 0000000..fffb713
--- /dev/null
+++ b/src/Rules/ExtensionsRule.php
@@ -0,0 +1,61 @@
+checkRequiredParameter($this->requireParameters);
+
+ if ($this->isEmpty($value)) {
+ return false;
+ }
+
+ $allowedExtensions = array_map(function ($e) {
+ return strtolower(trim($e));
+ }, explode(',', $this->getParameter('extensions')));
+
+ $fileName = $this->getFileName($value);
+ if (! $fileName) {
+ return false;
+ }
+
+ $wpFileType = wp_check_filetype($fileName);
+ $extension = strtolower($wpFileType['ext'] ?? '');
+
+ return ! empty($extension) && in_array($extension, $allowedExtensions, true);
+ }
+
+ private function getFileName($value)
+ {
+ if (is_numeric($value)) {
+ $filePath = get_attached_file((int) $value);
+ return (! empty($filePath) && file_exists($filePath)) ? basename($filePath) : null;
+ }
+
+ if (is_array($value) && isset($value['name'])) {
+ return $value['name'];
+ }
+
+ return null;
+ }
+
+ public function getParamKeys()
+ {
+ return $this->requireParameters;
+ }
+
+ public function message()
+ {
+ return $this->message;
+ }
+}
diff --git a/src/Rules/FileRule.php b/src/Rules/FileRule.php
new file mode 100644
index 0000000..bb48653
--- /dev/null
+++ b/src/Rules/FileRule.php
@@ -0,0 +1,49 @@
+isEmpty($value)) {
+ return false;
+ }
+
+ if (is_numeric($value)) {
+ $attachmentId = (int) $value;
+ $filePath = get_attached_file($attachmentId);
+ return ! empty($filePath) && file_exists($filePath);
+ }
+
+ if (is_array($value)) {
+ $requiredKeys = array_flip(['name', 'type', 'tmp_name', 'error', 'size']);
+ if (array_diff_key($requiredKeys, $value)) {
+ return false;
+ }
+
+ if ($value['error'] !== UPLOAD_ERR_OK) {
+ return false;
+ }
+
+ if (empty($value['tmp_name']) || ! is_uploaded_file($value['tmp_name'])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public function message()
+ {
+ return $this->message;
+ }
+}
diff --git a/src/Rules/ImageRule.php b/src/Rules/ImageRule.php
new file mode 100644
index 0000000..8f97e39
--- /dev/null
+++ b/src/Rules/ImageRule.php
@@ -0,0 +1,75 @@
+isEmpty($value)) {
+ return false;
+ }
+
+ $fileInfo = $this->getFileInfo($value);
+ if (! $fileInfo || empty($fileInfo['type'])) {
+ return false;
+ }
+
+ return in_array($fileInfo['type'], $this->getImageMimeTypes(), true);
+ }
+
+ private function getFileInfo($value)
+ {
+ if (is_numeric($value)) {
+ $attachmentId = (int) $value;
+ $filePath = get_attached_file($attachmentId);
+ if (empty($filePath) || ! file_exists($filePath)) {
+ return null;
+ }
+
+ $checked = wp_check_filetype_and_ext($filePath, basename($filePath));
+ return [
+ 'name' => basename($filePath),
+ 'type' => $checked['type'] ?? '',
+ ];
+ }
+
+ if (is_array($value) && isset($value['name'], $value['tmp_name'])) {
+ $checked = wp_check_filetype_and_ext($value['tmp_name'], $value['name']);
+ return [
+ 'name' => $value['name'],
+ 'type' => $checked['type'] ?? '',
+ ];
+ }
+
+ return null;
+ }
+
+ private function getImageMimeTypes(): array
+ {
+ $imageExtensions = wp_get_ext_types()['image'] ?? [];
+ $imageMimes = [];
+ foreach (wp_get_mime_types() as $exts => $mime) {
+ foreach (explode('|', $exts) as $ext) {
+ if (in_array($ext, $imageExtensions, true)) {
+ $imageMimes[] = $mime;
+ break;
+ }
+ }
+ }
+
+ return array_values(array_unique($imageMimes));
+ }
+
+ public function message()
+ {
+ return $this->message;
+ }
+}
+
diff --git a/src/Rules/MaxFileRule.php b/src/Rules/MaxFileRule.php
new file mode 100644
index 0000000..fe19a68
--- /dev/null
+++ b/src/Rules/MaxFileRule.php
@@ -0,0 +1,63 @@
+checkRequiredParameter($this->requireParameters);
+
+ if ($this->isEmpty($value)) {
+ return false;
+ }
+
+ $maxSizeKB = (int) $this->getParameter('max');
+ $maxSizeBytes = $maxSizeKB * 1024; // Convert KB to bytes
+
+ $fileSize = $this->getFileSize($value);
+ if ($fileSize === false) {
+ return false;
+ }
+
+ return $fileSize <= $maxSizeBytes;
+ }
+
+ private function getFileSize($value)
+ {
+ if (is_numeric($value)) {
+ $attachmentId = (int) $value;
+ $filePath = get_attached_file($attachmentId);
+ if (empty($filePath) || !file_exists($filePath)) {
+ return false;
+ }
+
+ return $this->getAttachmentSizeBytes($attachmentId, $filePath);
+ }
+
+ if (is_array($value) && isset($value['size'])) {
+ return (int) $value['size'];
+ }
+
+ return false;
+ }
+
+ public function getParamKeys()
+ {
+ return $this->requireParameters;
+ }
+
+ public function message()
+ {
+ return $this->message;
+ }
+}
+
diff --git a/src/Rules/MimesRule.php b/src/Rules/MimesRule.php
new file mode 100644
index 0000000..3f8d0d8
--- /dev/null
+++ b/src/Rules/MimesRule.php
@@ -0,0 +1,84 @@
+checkRequiredParameter($this->requireParameters);
+
+ if ($this->isEmpty($value)) {
+ return false;
+ }
+
+ $allowedMimes = $this->getParameter('mimes');
+ if (empty($allowedMimes)) {
+ return false;
+ }
+
+ $allowedTypes = array_map('trim', explode(',', $allowedMimes));
+
+ $fileInfo = $this->getFileInfo($value);
+ if (! $fileInfo) {
+ return false;
+ }
+
+ if (in_array($fileInfo['type'], $allowedTypes, true)) {
+ return true;
+ }
+
+ $wpFileType = wp_check_filetype($fileInfo['name']);
+ if ($wpFileType && isset($wpFileType['ext'])) {
+ return in_array(strtolower($wpFileType['ext']), $allowedTypes, true);
+ }
+
+ return false;
+ }
+
+ private function getFileInfo($value)
+ {
+ // Handle attachment ID
+ if (is_numeric($value)) {
+ $attachmentId = (int) $value;
+ $filePath = get_attached_file($attachmentId);
+ if (empty($filePath) || ! file_exists($filePath)) {
+ return null;
+ }
+
+ $wpFileType = wp_check_filetype(basename($filePath));
+ return [
+ 'name' => basename($filePath),
+ 'type' => $wpFileType['type'] ?? '',
+ ];
+ }
+
+ if (is_array($value) && isset($value['name'], $value['tmp_name'])) {
+ $checked = wp_check_filetype_and_ext($value['tmp_name'], $value['name']);
+ return [
+ 'name' => $value['name'],
+ 'type' => $checked['type'] ?? '',
+ ];
+ }
+
+ return null;
+ }
+
+ public function getParamKeys()
+ {
+ return $this->requireParameters;
+ }
+
+ public function message()
+ {
+ return $this->message;
+ }
+}
diff --git a/src/Rules/MimetypesRule.php b/src/Rules/MimetypesRule.php
new file mode 100644
index 0000000..a1a77e9
--- /dev/null
+++ b/src/Rules/MimetypesRule.php
@@ -0,0 +1,63 @@
+checkRequiredParameter($this->requireParameters);
+
+ if ($this->isEmpty($value)) {
+ return false;
+ }
+
+ $allowedMimes = array_map('trim', explode(',', $this->getParameter('mimetypes')));
+
+ $fileInfo = $this->getFileInfo($value);
+ if (! $fileInfo || empty($fileInfo['type'])) {
+ return false;
+ }
+
+ return in_array($fileInfo['type'], $allowedMimes, true);
+ }
+
+ private function getFileInfo($value)
+ {
+ if (is_numeric($value)) {
+ $attachmentId = (int) $value;
+ $filePath = get_attached_file($attachmentId);
+ if (empty($filePath) || ! file_exists($filePath)) {
+ return null;
+ }
+
+ $checked = wp_check_filetype_and_ext($filePath, basename($filePath));
+ return ['type' => $checked['type'] ?? ''];
+ }
+
+ if (is_array($value) && isset($value['name'], $value['tmp_name'])) {
+ $checked = wp_check_filetype_and_ext($value['tmp_name'], $value['name']);
+ return ['type' => $checked['type'] ?? ''];
+ }
+
+ return null;
+ }
+
+ public function getParamKeys()
+ {
+ return $this->requireParameters;
+ }
+
+ public function message()
+ {
+ return $this->message;
+ }
+}
diff --git a/src/Rules/SizeRule.php b/src/Rules/SizeRule.php
index 4f78694..5ca3ea0 100644
--- a/src/Rules/SizeRule.php
+++ b/src/Rules/SizeRule.php
@@ -1,10 +1,12 @@
checkRequiredParameter($this->requireParameters);
- $size = $this->getParameter('size');
+ $size = (int) $this->getParameter('size');
+
+ if (is_array($value) && isset($value['size'])) {
+ return (int) round($value['size'] / 1024) === $size;
+ }
+
+ if (is_numeric($value) && function_exists('get_attached_file')) {
+ $attachmentId = (int) $value;
+ $filePath = get_attached_file($attachmentId);
+ if (! empty($filePath) && file_exists($filePath)) {
+ return (int) round($this->getAttachmentSizeBytes($attachmentId, $filePath) / 1024) === $size;
+ }
+ }
if (is_string($value)) {
- return strlen($value) == $size;
+ return strlen($value) === $size;
}
+
if (is_int($value)) {
return $value === $size;
}
@@ -25,6 +40,7 @@ public function validate($value): bool
if (is_array($value)) {
return count($value) === $size;
}
+
return false;
}