diff --git a/.travis.yml b/.travis.yml index a5aa754..5610f7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,13 @@ language: php php: - - 5.5 - - 5.6 - 7.0 + - 7.1 + - 7.2 before_script: - - composer self-update - - composer install --dev + - composer install + - ./vendor/bin/phpcs -n --standard=PSR2 src/ tests/ -script: phpunit --coverage-text \ No newline at end of file +script: + - phpunit --coverage-text diff --git a/README.md b/README.md index 0dc77ee..07e14e4 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,18 @@ Rakit Validation - PHP Standalone Validation Library PHP Standalone library for validating data. Inspired by `Illuminate\Validation` Laravel. +## Features + +* API like Laravel validation. +* Array validation. +* `$_FILES` validation with multiple file support. +* Custom attribute aliases. +* Custom validation messages. +* Custom rule. + ## Requirements -* PHP 5.5 or higher +* PHP 7.0 or higher * Composer for installation ## Quick Start @@ -258,52 +267,202 @@ $validation_a = $validator->make($dataset_a, [ $validation_a->validate(); ``` +## Translation + +Translation is different with custom messages. +Translation may needed when you use custom message for rule `in`, `not_in`, `mimes`, and `uploaded_file`. + +For example if you use rule `in:1,2,3` we will set invalid message like "The Attribute only allows '1', '2', or '3'" +where part "'1', '2', or '3'" is comes from ":allowed_values" tag. +So if you have custom Indonesian message ":attribute hanya memperbolehkan :allowed_values", +we will set invalid message like "Attribute hanya memperbolehkan '1', '2', or '3'" which is the "or" word is not part of Indonesian language. + +So, to solve this problem, we can use translation like this: + +```php +// Set translation for words 'and' and 'or'. +$validator->setTranslations([ + 'and' => 'dan', + 'or' => 'atau' +]); + +// Set custom message for 'in' rule +$validator->setMessage('in', ":attribute hanya memperbolehkan :allowed_values"); + +// Validate +$validation = $validator->validate($inputs, [ + 'nomor' => 'in:1,2,3' +]); + +$message = $validation->errors()->first('nomor'); // "Nomor hanya memperbolehkan '1', '2', atau '3'" +``` + +> Actually, our built-in rules only use words 'and' and 'or' that you may need to translates. + +## Working with Error Message + +Errors messages are collected in `Rakit\Validation\ErrorBag` object that you can get it using `errors()` method. + +```php +$validation = $validator->validate($inputs, $rules); + +$errors = $validation->errors(); // << ErrorBag +``` + +Now you can use methods below to retrieves errors messages: + +#### `all(string $format = ':message')` + +Get all messages as flatten array. + +Examples: + +```php +$messages = $errors->all(); +// [ +// 'Email is not valid email', +// 'Password minimum 6 character', +// 'Password must contains capital letters' +// ] + +$messages = $errors->all('
  • :message
  • '); +// [ +// '
  • Email is not valid email
  • ', +// '
  • Password minimum 6 character
  • ', +// '
  • Password must contains capital letters
  • ' +// ] +``` + +#### `firstOfAll(string $format = ':message', bool $dotNotation = false)` + +Get only first message from all existing keys. + +Examples: + +```php +$messages = $errors->firstOfAll(); +// [ +// 'email' => Email is not valid email', +// 'password' => 'Password minimum 6 character', +// ] + +$messages = $errors->firstOfAll('
  • :message
  • '); +// [ +// 'email' => '
  • Email is not valid email
  • ', +// 'password' => '
  • Password minimum 6 character
  • ', +// ] +``` + +Argument `$dotNotation` is for array validation. +If it is `false` it will return original array structure, if it `true` it will return flatten array with dot notation keys. + +For example: + +```php +$messages = $errors->firstOfAll(':message', false); +// [ +// 'contacts' => [ +// 1 => [ +// 'email' => 'Email is not valid email', +// 'phone' => 'Phone is not valid phone number' +// ], +// ], +// ] + +$messages = $errors->firstOfAll(':message', true); +// [ +// 'contacts.1.email' => 'Email is not valid email', +// 'contacts.1.phone' => 'Email is not valid phone number', +// ] +``` + +#### `first(string $key)` + +Get first message from given key. It will return `string` if key has any error message, or `null` if key has no errors. + +For example: + +```php +if ($emailError = $errors->first('email')) { + echo $emailError; +} +``` + +#### `toArray()` + +Get all messages grouped by it's keys. + +For example: + +```php +$messages = $errors->toArray(); +// [ +// 'email' => [ +// 'Email is not valid email' +// ], +// 'password' => [ +// 'Password minimum 6 character', +// 'Password must contains capital letters' +// ] +// ] +``` + +#### `count()` + +Get count messages. + +#### `has(string $key)` + +Check if given key has an error. It returns `bool` if a key has an error, and otherwise. + + +## Getting Validated, Valid, and Invalid Data + +For example you have validation like this: + +```php +$validation = $validator->validate([ + 'title' => 'Lorem Ipsum', + 'body' => 'Lorem ipsum dolor sit amet ...', + 'published' => null, + 'something' => '-invalid-' +], [ + 'title' => 'required', + 'body' => 'required', + 'published' => 'default:1|required|in:0,1', + 'something' => 'required|numeric' +]); +``` + +You can get validated data, valid data, or invalid data using methods in example below: + +```php +$validatedData = $validation->getValidatedData(); +// [ +// 'title' => 'Lorem Ipsum', +// 'body' => 'Lorem ipsum dolor sit amet ...', +// 'published' => '1' // notice this +// 'something' => '-invalid-' +// ] + +$validData = $validation->getValidData(); +// [ +// 'title' => 'Lorem Ipsum', +// 'body' => 'Lorem ipsum dolor sit amet ...', +// 'published' => '1' +// ] + +$invalidData = $validation->getInvalidData(); +// [ +// 'something' => '-invalid-' +// ] +``` + ## Available Rules -Below is list of all available validation rules - -* [required](#rule-required) -* [required_if](#rule-required_if) -* [required_unless](#rule-required_unless) -* [required_with](#rule-required_with) -* [required_without](#rule-required_without) -* [required_with_all](#rule-required_with_all) -* [required_without_all](#rule-required_without_all) -* [uploaded_file](#rule-uploaded_file) -* [default/defaults](#rule-default) -* [email](#rule-email) -* [uppercase](#rule-uppercase) -* [lowercase](#rule-lowercase) -* [json](#rule-json) -* [alpha](#rule-alpha) -* [numeric](#rule-numeric) -* [alpha_num](#rule-alpha_num) -* [alpha_dash](#rule-alpha_dash) -* [in](#rule-in) -* [not_in](#rule-not_in) -* [min](#rule-min) -* [max](#rule-max) -* [between](#rule-between) -* [digits](#rule-digits) -* [digits_between](#rule-digits_between) -* [url](#rule-url) -* [integer](#rule-integer) -* [ip](#rule-ip) -* [ipv4](#rule-ipv4) -* [ipv6](#rule-ipv6) -* [array](#rule-array) -* [same](#rule-same) -* [regex](#rule-regex) -* [date](#rule-date) -* [accepted](#rule-accepted) -* [present](#rule-present) -* [different](#rule-different) -* [after](#after) -* [before](#before) -* [callback](#callback) - - -#### required +> Click to show details. + +
    required The field under this validation must be present and not 'empty'. @@ -322,43 +481,50 @@ Here are some examples: For uploaded file, `$_FILES['key']['error']` must not `UPLOAD_ERR_NO_FILE`. - -#### required_if:another_field,value_1,value_2,... +
    + +
    required_if:another_field,value_1,value_2,... The field under this rule must be present and not empty if the anotherfield field is equal to any value. For example `required_if:something,1,yes,on` will be required if `something` value is one of `1`, `'1'`, `'yes'`, or `'on'`. - -#### required_unless:another_field,value_1,value_2,... +
    + +
    required_unless:another_field,value_1,value_2,... The field under validation must be present and not empty unless the anotherfield field is equal to any value. - -#### required_with:field_1,field_2,... +
    + +
    required_with:field_1,field_2,... The field under validation must be present and not empty only if any of the other specified fields are present. - -#### required_without:field_1,field_2,... +
    + +
    required_without:field_1,field_2,... The field under validation must be present and not empty only when any of the other specified fields are not present. - -#### required_with_all:field_1,field_2,... +
    + +
    required_with_all:field_1,field_2,... The field under validation must be present and not empty only if all of the other specified fields are present. - -#### required_without_all:field_1,field_2,... +
    + +
    required_without_all:field_1,field_2,... The field under validation must be present and not empty only when all of the other specified fields are not present. - -#### uploaded_file:min_size,max_size,file_type_a,file_type_b,... +
    + +
    uploaded_file:min_size,max_size,extension_a,extension_b,... -This rule will validate `$_FILES` data, but not for multiple uploaded files. -Field under this rule must be following rules below to be valid: +This rule will validate data from `$_FILES`. +Field under this rule must be follows rules below to be valid: * `$_FILES['key']['error']` must be `UPLOAD_ERR_OK` or `UPLOAD_ERR_NO_FILE`. For `UPLOAD_ERR_NO_FILE` you can validate it with `required` rule. * If min size is given, uploaded file size **MUST NOT** be lower than min size. @@ -372,8 +538,66 @@ Here are some example definitions and explanations: * `uploaded_file:0,1M`: uploaded file size must be between 0 - 1 MB, but uploaded file is optional. * `required|uploaded_file:0,1M,png,jpeg`: uploaded file size must be between 0 - 1MB and mime types must be `image/jpeg` or `image/png`. - -#### default/defaults +Optionally, if you want to have separate error message between size and type validation. +You can use `mimes` rule to validate file types, and `min`, `max`, or `between` to validate it's size. + +For multiple file upload, PHP will give you undesirable array `$_FILES` structure ([here](http://php.net/manual/en/features.file-upload.multiple.php#53240) is the topic). So we make `uploaded_file` rule to automatically resolve your `$_FILES` value to be well-organized array structure. That means, you cannot only use `min`, `max`, `between`, or `mimes` rules to validate multiple file upload. You should put `uploaded_file` just to resolve it's value and make sure that value is correct uploaded file value. + +For example if you have input files like this: + +```html + + + +``` + +You can simply validate it like this: + +```php +$validation = $validator->validate($_FILES, [ + 'photos.*' => 'uploaded_file:0,2M,jpeg,png' +]); + +// or + +$validation = $validator->validate($_FILES, [ + 'photos.*' => 'uploaded_file|max:2M|mimes:jpeg,png' +]); +``` + +Or if you have input files like this: + +```html + + +``` + +You can validate it like this: + +```php +$validation = $validator->validate($_FILES, [ + 'images.*' => 'uploaded_file|max:2M|mimes:jpeg,png', +]); + +// or + +$validation = $validator->validate($_FILES, [ + 'images.profile' => 'uploaded_file|max:2M|mimes:jpeg,png', + 'images.cover' => 'uploaded_file|max:5M|mimes:jpeg,png', +]); +``` + +Now when you use `getValidData()` or `getInvalidData()` you will get well array structure just like single file upload. + +
    + +
    mimes:extension_a,extension_b,... + +The `$_FILES` item under validation must have a MIME type corresponding to one of the listed extensions. + +
    + +
    default/defaults This is special rule that doesn't validate anything. It just set default value to your attribute if that attribute is empty or not present. @@ -393,48 +617,57 @@ $validation->passes(); // true Validation passes because we sets default value for `enabled` and `published` to `1` and `0` which is valid. - -#### email +
    + +
    email The field under this validation must be valid email address. - -#### uppercase +
    + +
    uppercase The field under this validation must be valid uppercase. - -#### lowercase +
    + +
    lowercase The field under this validation must be valid lowercase. - -#### json +
    + +
    json The field under this validation must be valid JSON string. - -#### alpha +
    + +
    alpha The field under this rule must be entirely alphabetic characters. - -#### numeric +
    + +
    numeric The field under this rule must be numeric. - -#### alpha_num +
    + +
    alpha_num The field under this rule must be entirely alpha-numeric characters. - -#### alpha_dash +
    + +
    alpha_dash The field under this rule may have alpha-numeric characters, as well as dashes and underscores. - -#### in:value_1,value_2,... +
    + +
    in:value_1,value_2,... The field under this rule must be included in the given list of values. @@ -454,44 +687,84 @@ $validation = $validator->validate($data, [ Then 'enabled' value should be boolean `true`, or int `1`. - -#### not_in:value_1,value_2,... +
    + +
    not_in:value_1,value_2,... The field under this rule must not be included in the given list of values. This rule also using `in_array`. You can enable strict checking by invoking validator and call `strict()` like example in rule `in` above. - -#### min:number +
    + +
    min:number The field under this rule must have a size greater or equal than the given number. For string data, value corresponds to the number of characters. For numeric data, value corresponds to a given integer value. For an array, size corresponds to the count of the array. - -#### max:number +You can also validate uploaded file using this rule to validate minimum size of uploaded file. +For example: + +```php +$validation = $validator->validate([ + 'photo' => $_FILES['photo'] +], [ + 'photo' => 'required|min:1M' +]); +``` + +
    + +
    max:number The field under this rule must have a size lower or equal than the given number. Value size calculated in same way like `min` rule. - -#### between:min,max +You can also validate uploaded file using this rule to validate maximum size of uploaded file. +For example: + +```php +$validation = $validator->validate([ + 'photo' => $_FILES['photo'] +], [ + 'photo' => 'required|max:2M' +]); +``` + +
    + +
    between:min,max The field under this rule must have a size between min and max params. Value size calculated in same way like `min` and `max` rule. - -#### digits:value +You can also validate uploaded file using this rule to validate size of uploaded file. +For example: + +```php +$validation = $validator->validate([ + 'photo' => $_FILES['photo'] +], [ + 'photo' => 'required|between:1M,2M' +]); +``` + +
    + +
    digits:value The field under validation must be numeric and must have an exact length of `value`. - -#### digits_between:min,max +
    + +
    digits_between:min,max The field under validation must have a length between the given `min` and `max`. - -#### url +
    + +
    url The field under this rule must be valid url format. By default it check common URL scheme format like `any_scheme://...`. @@ -514,62 +787,74 @@ $validation = $validator->validate($inputs, [ > For common URL scheme and mailto, we combine `FILTER_VALIDATE_URL` to validate URL format and `preg_match` to validate it's scheme. Except for JDBC URL, currently it just check a valid JDBC scheme. - -#### integer -The field under this rule must be integer. +
    + +
    integer +The field under t rule must be integer. + +
    - -#### ip +
    ip The field under this rule must be valid ipv4 or ipv6. - -#### ipv4 +
    + +
    ipv4 The field under this rule must be valid ipv4. - -#### ipv6 +
    + +
    ipv6 The field under this rule must be valid ipv6. - -#### array +
    + +
    array The field under this rule must be array. - -#### same:another_field +
    + +
    same:another_field The field value under this rule must be same with `another_field` value. - -#### regex:/your-regex/ +
    + +
    regex:/your-regex/ The field under this rule must be match with given regex. - -#### date:format +
    + +
    date:format The field under this rule must be valid date format. Parameter `format` is optional, default format is `Y-m-d`. - -#### accepted +
    + +
    accepted The field under this rule must be one of `'on'`, `'yes'`, `'1'`, `'true'`, or `true`. - -#### present +
    + +
    present The field under this rule must be exists, whatever the value is. - -#### different:another_field +
    + +
    different:another_field Opposite of `same`. The field value under this rule must be different with `another_field` value. - -#### after:tomorrow +
    + +
    after:tomorrow Anything that can be parsed by `strtotime` can be passed as a parameter to this rule. Valid examples include : - after:next week @@ -577,13 +862,15 @@ Anything that can be parsed by `strtotime` can be passed as a parameter to this - after:2016 - after:2016-12-31 09:56:02 - -#### before:yesterday +
    + +
    before:yesterday This also works the same way as the [after rule](#after). Pass anything that can be parsed by `strtotime` - -#### callback +
    + +
    callback You can use this rule to define your own validation rule. This rule can't be registered using string pipe. @@ -626,7 +913,10 @@ $validation = $validator->validate($_POST, [ > Note: `Rakit\Validation\Rules\Callback` instance is binded into your Closure. So you can access rule properties and methods using `$this`. -## Register/Modify Rule +
    + + +## Register/Override Rule Another way to use custom validation rule is to create a class extending `Rakit\Validation\Rule`. Then register it using `setValidator` or `addValidator`. @@ -653,7 +943,7 @@ class UniqueRule extends Rule $this->pdo = $pdo; } - public function check($value) + public function check($value): bool { // make sure required parameters exists $this->requireParameters(['table', 'column']); @@ -765,45 +1055,82 @@ $validation = $validator->validate($_POST, [ ]); ``` -## Getting Validated, Valid, and Invalid Data +#### Implicit Rule -For example you have validation like this: +Implicit rule is a rule that if it's invalid, then next rules will be ignored. For example if attribute didn't pass `required*` rules, mostly it's next rules will also be invalids. So to prevent our next rules messages to get collected, we make `required*` rules to be implicit. + +To make your custom rule implicit, you can make `$implicit` property value to be `true`. For example: ```php -$validation = $validator->validate([ - 'title' => 'Lorem Ipsum', - 'body' => 'Lorem ipsum dolor sit amet ...', - 'published' => null, - 'something' => '-invalid-' -], [ - 'title' => 'required', - 'body' => 'required', - 'published' => 'default:1|required|in:0,1', - 'something' => 'required|numeric' -]); -``` +getValidatedData(); -// [ -// 'title' => 'Lorem Ipsum', -// 'body' => 'Lorem ipsum dolor sit amet ...', -// 'published' => '1' // notice this -// 'something' => '-invalid-' -// ] +getValidData(); -// [ -// 'title' => 'Lorem Ipsum', -// 'body' => 'Lorem ipsum dolor sit amet ...', -// 'published' => '1' -// ] +use Rakit\Validation\Rule; +use Rakit\Validation\Rules\Interfaces\ModifyValue; -$invalidData = $validation->getInvalidData(); -// [ -// 'something' => '-invalid-' -// ] +class YourCustomRule extends Rule implements ModifyValue +{ + ... + + public function modifyValue($value) + { + // Do something with $value + + return $value; + } + + ... +} ``` +#### Before Validation Hook + +You may want to do some preparation before validation running. For example our `uploaded_file` rule will resolves attribute value that come from `$_FILES` (undesirable) array structure to be well-organized array structure, so we can validate multiple file upload just like validating other data. + +To do this, you should implements `Rakit\Validation\Rules\Interfaces\BeforeValidate` and create method `beforeValidate()` to your custom rule class. + +For example: + +```php +getAttribute(); // Rakit\Validation\Attribute instance + $validation = $this->validation; // Rakit\Validation\Validation instance + + // Do something with $attribute and $validation + // For example change attribute value + $validation->setValue($attribute->getKey(), "your custom value"); + } + + ... +} +``` diff --git a/composer.json b/composer.json index c316baa..d61350f 100644 --- a/composer.json +++ b/composer.json @@ -13,11 +13,23 @@ "Rakit\\Validation\\": "src" } }, + "autoload-dev": { + "psr-4": { + "Rakit\\Validation\\Tests\\": ["tests", "tests/Fixtures"] + } + }, "require": { - "php": ">=5.5.0", + "php": ">=7.0", "ext-mbstring": "*" }, "require-dev": { - "phpunit/phpunit": "4.*" + "phpunit/phpunit": "^6.5", + "squizlabs/php_codesniffer": "^3" + }, + "scripts": { + "test": [ + "phpunit", + "phpcs" + ] } -} \ No newline at end of file +} diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..0858d1d --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,17 @@ + + + Rakit validation coding standard + + + + + + + + + + + + src + tests + \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 61e0272..3390a42 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,22 @@ - + + - + ./tests - - ./src + ./src - \ No newline at end of file + diff --git a/src/Attribute.php b/src/Attribute.php index 4d89162..56c06e0 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -5,65 +5,126 @@ class Attribute { + /** @var array */ protected $rules = []; + /** @var string */ protected $key; + /** @var string|null */ protected $alias; + /** @var Rakit\Validation\Validation */ protected $validation; + /** @var bool */ protected $required = false; + /** @var Rakit\Validation\Validation|null */ protected $primaryAttribute = null; + /** @var array */ protected $otherAttributes = []; + /** @var array */ protected $keyIndexes = []; - public function __construct(Validation $validation, $key, $alias = null, array $rules = array()) - { + /** + * Constructor + * + * @param Rakit\Validation\Validation $validation + * @param string $key + * @param string|null $alias + * @param array $rules + * @return void + */ + public function __construct( + Validation $validation, + string $key, + $alias = null, + array $rules = [] + ) { $this->validation = $validation; $this->alias = $alias; $this->key = $key; - foreach($rules as $rule) { + foreach ($rules as $rule) { $this->addRule($rule); } } + /** + * Set the primary attribute + * + * @param Rakit\Validation\Attribute $primaryAttribute + * @return void + */ public function setPrimaryAttribute(Attribute $primaryAttribute) { $this->primaryAttribute = $primaryAttribute; } + /** + * Set key indexes + * + * @param array $keyIndexes + * @return void + */ public function setKeyIndexes(array $keyIndexes) { $this->keyIndexes = $keyIndexes; } + /** + * Get primary attributes + * + * @return Rakit\Validation\Attribute|null + */ public function getPrimaryAttribute() { return $this->primaryAttribute; } + /** + * Set other attributes + * + * @param array $otherAttributes + * @return void + */ public function setOtherAttributes(array $otherAttributes) { $this->otherAttributes = []; - foreach($otherAttributes as $otherAttribute) { + foreach ($otherAttributes as $otherAttribute) { $this->addOtherAttribute($otherAttribute); } } + /** + * Add other attributes + * + * @param Rakit\Validation\Attribute $otherAttribute + * @return void + */ public function addOtherAttribute(Attribute $otherAttribute) { $this->otherAttributes[] = $otherAttribute; } - public function getOtherAttributes() + /** + * Get other attributes + * + * @return array + */ + public function getOtherAttributes(): array { return $this->otherAttributes; } + /** + * Add rule + * + * @param Rakit\Validation\Rule $rule + * @return void + */ public function addRule(Rule $rule) { $rule->setAttribute($this); @@ -71,42 +132,86 @@ public function addRule(Rule $rule) $this->rules[$rule->getKey()] = $rule; } - public function getRule($ruleKey) + /** + * Get rule + * + * @param string $ruleKey + * @return void + */ + public function getRule(string $ruleKey) { return $this->hasRule($ruleKey)? $this->rules[$ruleKey] : null; } - public function getRules() + /** + * Get rules + * + * @return array + */ + public function getRules(): array { return $this->rules; } - public function hasRule($ruleKey) + /** + * Check the $ruleKey has in the rule + * + * @param string $ruleKey + * @return bool + */ + public function hasRule(string $ruleKey): bool { return isset($this->rules[$ruleKey]); } - public function setRequired($required) + /** + * Set required + * + * @param boolean $required + * @return void + */ + public function setRequired(bool $required) { $this->required = $required; } - public function isRequired() + /** + * Set rule is required + * + * @return boolean + */ + public function isRequired(): bool { - return $this->required === true; + return $this->required; } - public function getKey() + /** + * Get key + * + * @return string + */ + public function getKey(): string { return $this->key; } - public function getKeyIndexes() + /** + * Get key indexes + * + * @return array + */ + public function getKeyIndexes(): array { return $this->keyIndexes; } - public function getValue($key = null) + /** + * Get value + * + * @param string|null $key + * @return void + */ + public function getValue(string $key = null) { if ($key && $this->isArrayAttribute()) { $key = $this->resolveSiblingKey($key); @@ -119,17 +224,33 @@ public function getValue($key = null) return $this->validation->getValue($key); } - public function isArrayAttribute() + /** + * Get that is array attribute + * + * @return boolean + */ + public function isArrayAttribute(): bool { return count($this->getKeyIndexes()) > 0; } - public function isUsingDotNotation() + /** + * Check this attribute is using dot notation + * + * @return boolean + */ + public function isUsingDotNotation(): bool { return strpos($this->getKey(), '.') !== false; } - public function resolveSiblingKey($key) + /** + * Resolve sibling key + * + * @param string $key + * @return string + */ + public function resolveSiblingKey(string $key): string { $indexes = $this->getKeyIndexes(); $keys = explode("*", $key); @@ -141,6 +262,11 @@ public function resolveSiblingKey($key) return call_user_func_array('sprintf', $args); } + /** + * Get humanize key + * + * @return string + */ public function getHumanizedKey() { $primaryAttribute = $this->getPrimaryAttribute(); @@ -149,7 +275,7 @@ public function getHumanizedKey() // Resolve key from array validation if ($primaryAttribute) { $split = explode('.', $key); - $key = implode(' ', array_map(function($word) { + $key = implode(' ', array_map(function ($word) { if (is_numeric($word)) { $word = $word + 1; } @@ -160,14 +286,24 @@ public function getHumanizedKey() return ucfirst($key); } - public function setAlias($alias) + /** + * Set alias + * + * @param string $alias + * @return void + */ + public function setAlias(string $alias) { $this->alias = $alias; } + /** + * Get alias + * + * @return string|null + */ public function getAlias() { return $this->alias; } - } diff --git a/src/ErrorBag.php b/src/ErrorBag.php index 9ceecda..2836d4a 100644 --- a/src/ErrorBag.php +++ b/src/ErrorBag.php @@ -5,14 +5,29 @@ class ErrorBag { + /** @var array */ protected $messages = []; + /** + * Constructor + * + * @param array $messages + * @return void + */ public function __construct(array $messages = []) { $this->messages = $messages; } - public function add($key, $rule, $message) + /** + * Add message for given key and rule + * + * @param string $key + * @param string $rule + * @param string $message + * @return void + */ + public function add(string $key, string $rule, string $message) { if (!isset($this->messages[$key])) { $this->messages[$key] = []; @@ -21,12 +36,23 @@ public function add($key, $rule, $message) $this->messages[$key][$rule] = $message; } - public function count() + /** + * Get messages count + * + * @return int + */ + public function count(): int { return count($this->all()); } - public function has($key) + /** + * Check given key is existed + * + * @param string $key + * @return bool + */ + public function has(string $key): bool { list($key, $ruleName) = $this->parsekey($key); if ($this->isWildcardKey($key)) { @@ -38,12 +64,18 @@ public function has($key) if (!$ruleName) { return !empty($messages); } else { - return !empty($messages) AND isset($messages[$ruleName]); + return !empty($messages) and isset($messages[$ruleName]); } } } - public function first($key) + /** + * Get the first value of array + * + * @param string $key + * @return mixed + */ + public function first(string $key) { list($key, $ruleName) = $this->parsekey($key); if ($this->isWildcardKey($key)) { @@ -65,21 +97,30 @@ public function first($key) } } - public function get($key, $format = ':message') + /** + * Get messages from given key, can be use custom format + * + * @param string $key + * @param string $format + * @return array + */ + public function get(string $key, string $format = ':message'): array { list($key, $ruleName) = $this->parsekey($key); $results = []; if ($this->isWildcardKey($key)) { $messages = $this->filterMessagesForWildcardKey($key, $ruleName); - foreach($messages as $explicitKey => $keyMessages) { + foreach ($messages as $explicitKey => $keyMessages) { foreach ($keyMessages as $rule => $message) { $results[$explicitKey][$rule] = $this->formatMessage($message, $format); } } } else { $keyMessages = isset($this->messages[$key])? $this->messages[$key] : []; - foreach($keyMessages as $rule => $message) { - if ($ruleName AND $ruleName != $rule) continue; + foreach ($keyMessages as $rule => $message) { + if ($ruleName and $ruleName != $rule) { + continue; + } $results[$rule] = $this->formatMessage($message, $format); } } @@ -87,34 +128,62 @@ public function get($key, $format = ':message') return $results; } - public function all($format = ':message') + /** + * Get all messages + * + * @param string $format + * @return array + */ + public function all(string $format = ':message'): array { $messages = $this->messages; $results = []; - foreach($messages as $key => $keyMessages) { - foreach($keyMessages as $message) { + foreach ($messages as $key => $keyMessages) { + foreach ($keyMessages as $message) { $results[] = $this->formatMessage($message, $format); } } return $results; } - public function firstOfAll($format = ':message') + /** + * Get the first message from existing keys + * + * @param string $format + * @param boolean $dotNotation + * @return array + */ + public function firstOfAll(string $format = ':message', bool $dotNotation = false): array { $messages = $this->messages; $results = []; - foreach($messages as $key => $keyMessages) { - $results[] = $this->formatMessage(array_shift($messages[$key]), $format); + foreach ($messages as $key => $keyMessages) { + if ($dotNotation) { + $results[$key] = $this->formatMessage(array_shift($messages[$key]), $format); + } else { + Helper::arraySet($results, $key, $this->formatMessage(array_shift($messages[$key]), $format)); + } } return $results; } - public function toArray() + /** + * Get plain array messages + * + * @return array + */ + public function toArray(): array { return $this->messages; } - protected function parseKey($key) + /** + * Parse $key to get the array of $key and $ruleName + * + * @param string $key + * @return array + */ + protected function parseKey(string $key): array { $expl = explode(':', $key, 2); $key = $expl[0]; @@ -122,12 +191,25 @@ protected function parseKey($key) return [$key, $ruleName]; } - protected function isWildcardKey($key) + /** + * Check the $key is wildcard + * + * @param mixed $key + * @return bool + */ + protected function isWildcardKey(string $key): bool { return false !== strpos($key, '*'); } - protected function filterMessagesForWildcardKey($key, $ruleName = null) + /** + * Filter messages with wildcard key + * + * @param string $key + * @param mixed $ruleName + * @return array + */ + protected function filterMessagesForWildcardKey(string $key, $ruleName = null): array { $messages = $this->messages; $pattern = preg_quote($key, '#'); @@ -140,8 +222,10 @@ protected function filterMessagesForWildcardKey($key, $ruleName = null) continue; } - foreach($keyMessages as $rule => $message) { - if ($ruleName AND $rule != $ruleName) continue; + foreach ($keyMessages as $rule => $message) { + if ($ruleName and $rule != $ruleName) { + continue; + } $filteredMessages[$k][$rule] = $message; } } @@ -149,9 +233,15 @@ protected function filterMessagesForWildcardKey($key, $ruleName = null) return $filteredMessages; } - protected function formatMessage($message, $format) + /** + * Get formatted message + * + * @param string $message + * @param string $format + * @return string + */ + protected function formatMessage(string $message, string $format): string { return str_replace(':message', $message, $format); } - } diff --git a/src/Helper.php b/src/Helper.php index 0596152..816f020 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -9,11 +9,11 @@ class Helper * Determine if a given string matches a given pattern. * Adapted from: https://github.com/illuminate/support/blob/v5.3.23/Str.php#L119 * - * @param string $pattern - * @param string $value + * @param string $pattern + * @param string $value * @return bool */ - public static function strIs($pattern, $value) + public static function strIs(string $pattern, string $value): bool { if ($pattern == $value) { return true; @@ -33,11 +33,11 @@ public static function strIs($pattern, $value) * Check if an item or items exist in an array using "dot" notation. * Adapted from: https://github.com/illuminate/support/blob/v5.3.23/Arr.php#L81 * - * @param array $array - * @param string|array $keys + * @param array $array + * @param string $key * @return bool */ - public static function arrayHas(array $array, $key) + public static function arrayHas(array $array, string $key): bool { if (array_key_exists($key, $array)) { return true; @@ -59,11 +59,11 @@ public static function arrayHas(array $array, $key) * Adapted from: https://github.com/illuminate/support/blob/v5.3.23/Arr.php#L246 * * @param array $array - * @param string $key - * @param mixed $default + * @param string $key + * @param mixed $default * @return mixed */ - public static function arrayGet(array $array, $key, $default = null) + public static function arrayGet(array $array, string $key, $default = null) { if (is_null($key)) { return $array; @@ -88,11 +88,11 @@ public static function arrayGet(array $array, $key, $default = null) * Flatten a multi-dimensional associative array with dots. * Adapted from: https://github.com/illuminate/support/blob/v5.3.23/Arr.php#L81 * - * @param array $array - * @param string $prepend + * @param array $array + * @param string $prepend * @return array */ - public static function arrayDot(array $array, $prepend = '') + public static function arrayDot(array $array, string $prepend = ''): array { $results = []; @@ -111,13 +111,13 @@ public static function arrayDot(array $array, $prepend = '') * Set an item on an array or object using dot notation. * Adapted from: https://github.com/illuminate/support/blob/v5.3.23/helpers.php#L437 * - * @param mixed $target - * @param string|array $key - * @param mixed $value - * @param bool $overwrite + * @param mixed $target + * @param string|array $key + * @param mixed $value + * @param bool $overwrite * @return mixed */ - public static function arraySet(&$target, $key, $value, $overwrite = true) + public static function arraySet(&$target, $key, $value, $overwrite = true): array { $segments = is_array($key) ? $key : explode('.', $key); @@ -158,15 +158,14 @@ public static function arraySet(&$target, $key, $value, $overwrite = true) return $target; } - /** * Unset an item on an array or object using dot notation. * - * @param mixed $target - * @param string|array $key + * @param mixed $target + * @param string|array $key * @return mixed */ - public static function arrayUnset(&$target, $key) + public static function arrayUnset(&$target, $key): array { if (!is_array($target)) { return $target; @@ -195,7 +194,7 @@ public static function arrayUnset(&$target, $key) * @param string $delimiter * @return string */ - public static function snakeCase($value, $delimiter = '_') + public static function snakeCase(string $value, string $delimiter = '_'): string { if (! ctype_lower($value)) { $value = preg_replace('/\s+/u', '', ucwords($value)); @@ -205,4 +204,48 @@ public static function snakeCase($value, $delimiter = '_') return $value; } + /** + * Join string[] to string with given $separator and $lastSeparator. + * + * @param array $pieces + * @param string $separator + * @param string|null $lastSeparator + * @return string + */ + public static function join(array $pieces, string $separator, string $lastSeparator = null): string + { + if (is_null($lastSeparator)) { + $lastSeparator = $separator; + } + + $last = array_pop($pieces); + + switch (count($pieces)) { + case 0: + return $last ?: ''; + case 1: + return $pieces[0] . $lastSeparator . $last; + default: + return implode($separator, $pieces) . $lastSeparator . $last; + } + } + + /** + * Wrap string[] by given $prefix and $suffix + * + * @param array $strings + * @param string $prefix + * @param string|null $suffix + * @return array + */ + public static function wraps(array $strings, string $prefix, string $suffix = null): array + { + if (is_null($suffix)) { + $suffix = $prefix; + } + + return array_map(function ($str) use ($prefix, $suffix) { + return $prefix . $str . $suffix; + }, $strings); + } } diff --git a/src/MimeTypeGuesser.php b/src/MimeTypeGuesser.php index 17d292a..3efe69e 100644 --- a/src/MimeTypeGuesser.php +++ b/src/MimeTypeGuesser.php @@ -5,6 +5,7 @@ class MimeTypeGuesser { + /** @var array */ protected $mimeTypes = [ 'application/andrew-inset' => 'ez', 'application/applixware' => 'aw', @@ -778,15 +779,26 @@ class MimeTypeGuesser 'x-conference/x-cooltalk' => 'ice' ]; - public function getExtension($mimeType) + /** + * Get extension by mime type + * + * @param string $mimeType + * @return string|null + */ + public function getExtension(string $mimeType) { return isset($this->mimeTypes[$mimeType])? $this->mimeTypes[$mimeType] : null; } - public function getMimeType($extension) + /** + * Get mime type by extension + * + * @param string $extension + * @return string|null + */ + public function getMimeType(string $extension) { $key = array_search($extension, $this->mimeTypes); return $key ?: null; } - } diff --git a/src/Rule.php b/src/Rule.php index 9025f25..a2674ab 100644 --- a/src/Rule.php +++ b/src/Rule.php @@ -6,107 +6,227 @@ abstract class Rule { + /** @var string */ protected $key; + /** @var Rakit\Validation\Attribute|null */ protected $attribute; + /** @var Rakit\Validation\Validation|null */ protected $validation; + /** @var bool */ protected $implicit = false; + /** @var array */ protected $params = []; - protected $fillable_params = []; + /** @var array */ + protected $paramsTexts = []; + /** @var array */ + protected $fillableParams = []; + + /** @var string */ protected $message = "The :attribute is invalid"; - abstract public function check($value); + abstract public function check($value): bool; + /** + * Set Validation class instance + * + * @param Rakit\Validation\Validation $validation + * @return void + */ public function setValidation(Validation $validation) { $this->validation = $validation; } - public function setKey($key) + /** + * Set key + * + * @param string $key + * @return void + */ + public function setKey(string $key) { $this->key = $key; } + /** + * Get key + * + * @return string + */ public function getKey() { return $this->key ?: get_class($this); } - public function setAttribute($attribute) + /** + * Set attribute + * + * @param Rakit\Validation\Attribute $attribute + * @return void + */ + public function setAttribute(Attribute $attribute) { $this->attribute = $attribute; } + /** + * Get attribute + * + * @return Rakit\Validation\Attribute|null + */ public function getAttribute() { - return $this->attribute ?: get_class($this); + return $this->attribute; } - public function getParameters() + /** + * Get parameters + * + * @return array + */ + public function getParameters(): array { return $this->params; } - public function setParameters(array $params) + /** + * Set params + * + * @param array $params + * @return Rakit\Validation\Rule + */ + public function setParameters(array $params): Rule { $this->params = array_merge($this->params, $params); return $this; } - public function setParameter($key, $value) + /** + * Set parameters + * + * @param string $key + * @param mixed $value + * @return Rakit\Validation\Rule + */ + public function setParameter(string $key, $value): Rule { $this->params[$key] = $value; return $this; } - public function fillParameters(array $params) + /** + * Fill $params to $this->params + * + * @param array $params + * @return Rakit\Validation\Rule + */ + public function fillParameters(array $params): Rule { - foreach($this->fillable_params as $key) { - if (empty($params)) break; + foreach ($this->fillableParams as $key) { + if (empty($params)) { + break; + } $this->params[$key] = array_shift($params); } return $this; } - public function parameter($key) + /** + * Get parameter from given $key, return null if it not exists + * + * @param string $key + * @return mixed + */ + public function parameter(string $key) { return isset($this->params[$key])? $this->params[$key] : null; } - public function isImplicit() + /** + * Set parameter text that can be displayed in error message using ':param_key' + * + * @param string $key + * @param string $text + * @return void + */ + public function setParameterText(string $key, string $text) + { + $this->paramsTexts[$key] = $text; + } + + /** + * Get $paramsTexts + * + * @return array + */ + public function getParametersTexts(): array { - return $this->implicit === true; + return $this->paramsTexts; } - public function message($message) + /** + * Check whether this rule is implicit + * + * @return boolean + */ + public function isImplicit(): bool + { + return $this->implicit; + } + + /** + * Just alias of setMessage + * + * @param string $message + * @return Rakit\Validation\Rule + */ + public function message(string $message): Rule { return $this->setMessage($message); } - public function setMessage($message) + /** + * Set message + * + * @param string $message + * @return Rakit\Validation\Rule + */ + public function setMessage(string $message): Rule { $this->message = $message; return $this; - } + } - public function getMessage() + /** + * Get message + * + * @return string + */ + public function getMessage(): string { return $this->message; } + /** + * Check given $params must be exists + * + * @param array $params + * @return void + * @throws Rakit\Validation\MissingRequiredParameterException + */ protected function requireParameters(array $params) { - foreach($params as $param) { + foreach ($params as $param) { if (!isset($this->params[$param])) { $rule = $this->getKey(); throw new MissingRequiredParameterException("Missing required parameter '{$param}' on rule '{$rule}'"); } } } - -} \ No newline at end of file +} diff --git a/src/Rules/Accepted.php b/src/Rules/Accepted.php index fb9f27b..6f4655b 100644 --- a/src/Rules/Accepted.php +++ b/src/Rules/Accepted.php @@ -6,14 +6,21 @@ class Accepted extends Rule { + /** @var bool */ protected $implicit = true; + /** @var string */ protected $message = "The :attribute must be accepted"; - public function check($value) + /** + * Check the $value is accepted + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $acceptables = ['yes', 'on', '1', 1, true, 'true']; return in_array($value, $acceptables, true); } - } diff --git a/src/Rules/After.php b/src/Rules/After.php index 8fdf8d1..cb49db6 100644 --- a/src/Rules/After.php +++ b/src/Rules/After.php @@ -7,18 +7,27 @@ class After extends Rule { - use DateUtils; + use Traits\DateUtilsTrait; + /** @var string */ protected $message = "The :attribute must be a date after :time."; - protected $fillable_params = ['time']; - - public function check($value) + /** @var array */ + protected $fillableParams = ['time']; + + /** + * Check the value is valid + * + * @param mixed $value + * @return bool + * @throws Exception + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); + $this->requireParameters($this->fillableParams); $time = $this->parameter('time'); - if (!$this->isValidDate($value)){ + if (!$this->isValidDate($value)) { throw $this->throwException($value); } diff --git a/src/Rules/Alpha.php b/src/Rules/Alpha.php index 225fe5d..376fbb6 100644 --- a/src/Rules/Alpha.php +++ b/src/Rules/Alpha.php @@ -7,11 +7,17 @@ class Alpha extends Rule { + /** @var string */ protected $message = "The :attribute only allows alphabet characters"; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { return is_string($value) && preg_match('/^[\pL\pM]+$/u', $value); } - } diff --git a/src/Rules/AlphaDash.php b/src/Rules/AlphaDash.php index 11c34dd..7813f5b 100644 --- a/src/Rules/AlphaDash.php +++ b/src/Rules/AlphaDash.php @@ -7,9 +7,16 @@ class AlphaDash extends Rule { + /** @var string */ protected $message = "The :attribute only allows a-z, 0-9, _ and -"; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { if (! is_string($value) && ! is_numeric($value)) { return false; @@ -17,5 +24,4 @@ public function check($value) return preg_match('/^[\pL\pM\pN_-]+$/u', $value) > 0; } - } diff --git a/src/Rules/AlphaNum.php b/src/Rules/AlphaNum.php index fba0002..7fb2b3b 100644 --- a/src/Rules/AlphaNum.php +++ b/src/Rules/AlphaNum.php @@ -7,9 +7,16 @@ class AlphaNum extends Rule { + /** @var string */ protected $message = "The :attribute only allows alphabet and numeric"; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { if (! is_string($value) && ! is_numeric($value)) { return false; @@ -17,5 +24,4 @@ public function check($value) return preg_match('/^[\pL\pM\pN]+$/u', $value) > 0; } - } diff --git a/src/Rules/Before.php b/src/Rules/Before.php index b35b09e..22cdfa5 100644 --- a/src/Rules/Before.php +++ b/src/Rules/Before.php @@ -6,18 +6,27 @@ class Before extends Rule { - use DateUtils; + use Traits\DateUtilsTrait; + /** @var string */ protected $message = "The :attribute must be a date before :time."; - protected $fillable_params = ['time']; - - public function check($value) + /** @var array */ + protected $fillableParams = ['time']; + + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + * @throws Exception + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); + $this->requireParameters($this->fillableParams); $time = $this->parameter('time'); - if (!$this->isValidDate($value)){ + if (!$this->isValidDate($value)) { throw $this->throwException($value); } diff --git a/src/Rules/Between.php b/src/Rules/Between.php index fa4df9c..fa7de36 100644 --- a/src/Rules/Between.php +++ b/src/Rules/Between.php @@ -6,27 +6,33 @@ class Between extends Rule { + use Traits\SizeTrait; + /** @var string */ protected $message = "The :attribute must be between :min and :max"; - protected $fillable_params = ['min', 'max']; + /** @var array */ + protected $fillableParams = ['min', 'max']; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); - - $min = (int) $this->parameter('min'); - $max = (int) $this->parameter('max'); - - if (is_int($value) || is_float($value)) { - return $value >= $min AND $value <= $max; - } elseif(is_string($value)) { - return mb_strlen($value, 'UTF-8') >= $min AND mb_strlen($value, 'UTF-8') <= $max; - } elseif(is_array($value)) { - return count($value) >= $min AND count($value) <= $max; - } else { + $this->requireParameters($this->fillableParams); + + $min = $this->getBytesSize($this->parameter('min')); + $max = $this->getBytesSize($this->parameter('max')); + + $valueSize = $this->getValueSize($value); + + if (!is_numeric($valueSize)) { return false; } - } + return ($valueSize >= $min && $valueSize <= $max); + } } diff --git a/src/Rules/Callback.php b/src/Rules/Callback.php index d8e9991..bb18959 100644 --- a/src/Rules/Callback.php +++ b/src/Rules/Callback.php @@ -9,18 +9,33 @@ class Callback extends Rule { + /** @var string */ protected $message = "The :attribute is not valid"; - protected $fillable_params = ['callback']; + /** @var array */ + protected $fillableParams = ['callback']; - public function setCallback(Closure $callback) + /** + * Set the Callback closure + * + * @param Closure $callback + * @return self + */ + public function setCallback(Closure $callback): Rule { return $this->setParameter('callback', $callback); } - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + * @throws Exception + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); + $this->requireParameters($this->fillableParams); $callback = $this->parameter('callback'); if (false === $callback instanceof Closure) { @@ -34,11 +49,10 @@ public function check($value) if (is_string($invalidMessage)) { $this->setMessage($invalidMessage); return false; - } elseif(false === $invalidMessage) { + } elseif (false === $invalidMessage) { return false; } return true; } - } diff --git a/src/Rules/Date.php b/src/Rules/Date.php index 73f4be5..37c5b03 100644 --- a/src/Rules/Date.php +++ b/src/Rules/Date.php @@ -7,20 +7,28 @@ class Date extends Rule { + /** @var string */ protected $message = "The :attribute is not valid date format"; - protected $fillable_params = ['format']; + /** @var array */ + protected $fillableParams = ['format']; + /** @var array */ protected $params = [ 'format' => 'Y-m-d' ]; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); + $this->requireParameters($this->fillableParams); $format = $this->parameter('format'); return date_create_from_format($format, $value) !== false; } - } diff --git a/src/Rules/DateUtils.php b/src/Rules/DateUtils.php deleted file mode 100644 index 659fa64..0000000 --- a/src/Rules/DateUtils.php +++ /dev/null @@ -1,26 +0,0 @@ -requireParameters($this->fillable_params); - + $this->requireParameters($this->fillableParams); + $default = $this->parameter('default'); - return $default; + return true; + } + + /** + * {@inheritDoc} + */ + public function modifyValue($value) + { + return $this->isEmptyValue($value) ? $this->parameter('default') : $value; } + /** + * Check $value is empty value + * + * @param mixed $value + * @return boolean + */ + protected function isEmptyValue($value): bool + { + $requiredValidator = new Required; + return false === $requiredValidator->check($value, []); + } } diff --git a/src/Rules/Different.php b/src/Rules/Different.php index fdab36c..3577318 100644 --- a/src/Rules/Different.php +++ b/src/Rules/Different.php @@ -7,18 +7,25 @@ class Different extends Rule { + /** @var string */ protected $message = "The :attribute must be different with :field"; - protected $fillable_params = ['field']; + /** @var array */ + protected $fillableParams = ['field']; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); + $this->requireParameters($this->fillableParams); $field = $this->parameter('field'); - $another_value = $this->validation->getValue($field); + $anotherValue = $this->validation->getValue($field); - return $value != $another_value; + return $value != $anotherValue; } - } diff --git a/src/Rules/Digits.php b/src/Rules/Digits.php index d578021..87a5f65 100644 --- a/src/Rules/Digits.php +++ b/src/Rules/Digits.php @@ -7,18 +7,25 @@ class Digits extends Rule { + /** @var string */ protected $message = "The :attribute must be numeric and must have an exact length of :length"; - protected $fillable_params = ['length']; + /** @var array */ + protected $fillableParams = ['length']; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); + $this->requireParameters($this->fillableParams); $length = (int) $this->parameter('length'); return ! preg_match('/[^0-9]/', $value) && strlen((string) $value) == $length; } - } diff --git a/src/Rules/DigitsBetween.php b/src/Rules/DigitsBetween.php index 4209fc6..539c09d 100644 --- a/src/Rules/DigitsBetween.php +++ b/src/Rules/DigitsBetween.php @@ -7,13 +7,21 @@ class DigitsBetween extends Rule { + /** @var string */ protected $message = "The :attribute must have a length between the given :min and :max"; - protected $fillable_params = ['min', 'max']; + /** @var array */ + protected $fillableParams = ['min', 'max']; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); + $this->requireParameters($this->fillableParams); $min = (int) $this->parameter('min'); $max = (int) $this->parameter('max'); @@ -23,5 +31,4 @@ public function check($value) return ! preg_match('/[^0-9]/', $value) && $length >= $min && $length <= $max; } - } diff --git a/src/Rules/Email.php b/src/Rules/Email.php index 7d83694..a9bd236 100644 --- a/src/Rules/Email.php +++ b/src/Rules/Email.php @@ -7,11 +7,17 @@ class Email extends Rule { + /** @var string */ protected $message = "The :attribute is not valid email"; - public function check($value) + /** + * Check $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { return filter_var($value, FILTER_VALIDATE_EMAIL) !== false; } - } diff --git a/src/Rules/FileTrait.php b/src/Rules/FileTrait.php deleted file mode 100644 index d9bc3c5..0000000 --- a/src/Rules/FileTrait.php +++ /dev/null @@ -1,74 +0,0 @@ -isValueFromUploadedFiles($value) AND is_uploaded_file($value['tmp_name']); - } - - protected function getBytes($size) - { - if (is_int($size)) { - return $size; - } - - if (!is_string($size)) { - throw new InvalidArgumentException("Size must be string or integer Bytes", 1); - } - - if (!preg_match("/^(?((\d+)?\.)?\d+)(?(B|K|M|G|T|P)B?)?$/i", $size, $match)) { - throw new InvalidArgumentException("Size is not valid format", 1); - } - - $number = (float)$match['number']; - $format = isset($match['format']) ? $match['format'] : ''; - - switch (strtoupper($format)) { - case "KB": - case "K": - return $number * 1024; - - case "MB": - case "M": - return $number * pow(1024, 2); - - case "GB": - case "G": - return $number * pow(1024, 3); - - case "TB": - case "T": - return $number * pow(1024, 4); - - case "PB": - case "P": - return $number * pow(1024, 5); - - default: - return $number; - } - } -} diff --git a/src/Rules/In.php b/src/Rules/In.php index 3845edb..1971ed5 100644 --- a/src/Rules/In.php +++ b/src/Rules/In.php @@ -2,35 +2,60 @@ namespace Rakit\Validation\Rules; +use Rakit\Validation\Helper; use Rakit\Validation\Rule; class In extends Rule { - protected $message = "The :attribute is not allowing :value"; + /** @var string */ + protected $message = "The :attribute only allows :allowed_values"; + /** @var bool */ protected $strict = false; - public function fillParameters(array $params) + /** + * Given $params and assign the $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { - if (count($params) == 1 AND is_array($params[0])) { + if (count($params) == 1 && is_array($params[0])) { $params = $params[0]; } $this->params['allowed_values'] = $params; return $this; } - public function strict($strict = true) + /** + * Set strict value + * + * @param bool $strict + * @return void + */ + public function strict(bool $strict = true) { $this->strict = $strict; } - public function check($value) + /** + * Check $value is existed + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $this->requireParameters(['allowed_values']); - $allowed_values = $this->parameter('allowed_values'); - return in_array($value, $allowed_values, $this->strict); - } + $allowedValues = $this->parameter('allowed_values'); + + $or = $this->validation ? $this->validation->getTranslation('or') : 'or'; + $allowedValuesText = Helper::join(Helper::wraps($allowedValues, "'"), ', ', ", {$or} "); + $this->setParameterText('allowed_values', $allowedValuesText); + return in_array($value, $allowedValues, $this->strict); + } } diff --git a/src/Rules/Integer.php b/src/Rules/Integer.php index 04ffd7c..47aa702 100644 --- a/src/Rules/Integer.php +++ b/src/Rules/Integer.php @@ -7,11 +7,17 @@ class Integer extends Rule { + /** @var string */ protected $message = "The :attribute must be integer"; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { return filter_var($value, FILTER_VALIDATE_INT) !== false; } - } diff --git a/src/Rules/Interfaces/BeforeValidate.php b/src/Rules/Interfaces/BeforeValidate.php new file mode 100644 index 0000000..e3c058f --- /dev/null +++ b/src/Rules/Interfaces/BeforeValidate.php @@ -0,0 +1,13 @@ +requireParameters($this->fillable_params); - - $max = (int) $this->parameter('max'); - if (is_int($value)) { - return $value <= $max; - } elseif(is_string($value)) { - return mb_strlen($value, 'UTF-8') <= $max; - } elseif(is_array($value)) { - return count($value) <= $max; - } else { + $this->requireParameters($this->fillableParams); + + $max = $this->getBytesSize($this->parameter('max')); + $valueSize = $this->getValueSize($value); + + if (!is_numeric($valueSize)) { return false; } - } + return $valueSize <= $max; + } } diff --git a/src/Rules/Mimes.php b/src/Rules/Mimes.php new file mode 100644 index 0000000..9abb5a8 --- /dev/null +++ b/src/Rules/Mimes.php @@ -0,0 +1,95 @@ +params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule + { + $this->allowTypes($params); + return $this; + } + + /** + * Given $types and assign $this->params + * + * @param mixed $types + * @return self + */ + public function allowTypes($types): Rule + { + if (is_string($types)) { + $types = explode('|', $types); + } + + $this->params['allowed_types'] = $types; + + return $this; + } + + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool + { + $allowedTypes = $this->parameter('allowed_types'); + + if ($allowedTypes) { + $or = $this->validation ? $this->validation->getTranslation('or') : 'or'; + $this->setParameterText('allowed_types', Helper::join(Helper::wraps($allowedTypes, "'"), ', ', ", {$or} ")); + } + + // below is Required rule job + if (!$this->isValueFromUploadedFiles($value) or $value['error'] == UPLOAD_ERR_NO_FILE) { + return true; + } + + if (!$this->isUploadedFile($value)) { + return false; + } + + // just make sure there is no error + if ($value['error']) { + return false; + } + + if (!empty($allowedTypes)) { + $guesser = new MimeTypeGuesser; + $ext = $guesser->getExtension($value['type']); + unset($guesser); + + if (!in_array($ext, $allowedTypes)) { + return false; + } + } + + return true; + } +} diff --git a/src/Rules/Min.php b/src/Rules/Min.php index bee43eb..555f8d9 100644 --- a/src/Rules/Min.php +++ b/src/Rules/Min.php @@ -6,25 +6,31 @@ class Min extends Rule { + use Traits\SizeTrait; + /** @var string */ protected $message = "The :attribute minimum is :min"; - protected $fillable_params = ['min']; + /** @var array */ + protected $fillableParams = ['min']; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); - - $min = (int) $this->parameter('min'); - if (is_int($value)) { - return $value >= $min; - } elseif(is_string($value)) { - return mb_strlen($value, 'UTF-8') >= $min; - } elseif(is_array($value)) { - return count($value) >= $min; - } else { + $this->requireParameters($this->fillableParams); + + $min = $this->getBytesSize($this->parameter('min')); + $valueSize = $this->getValueSize($value); + + if (!is_numeric($valueSize)) { return false; } - } + return $valueSize >= $min; + } } diff --git a/src/Rules/NotIn.php b/src/Rules/NotIn.php index 8abbb24..f70275a 100644 --- a/src/Rules/NotIn.php +++ b/src/Rules/NotIn.php @@ -2,34 +2,60 @@ namespace Rakit\Validation\Rules; +use Rakit\Validation\Helper; use Rakit\Validation\Rule; class NotIn extends Rule { - protected $message = "The :attribute is not allowing :value"; + /** @var string */ + protected $message = "The :attribute is not allowing :disallowed_values"; + /** @var bool */ protected $strict = false; - public function fillParameters(array $params) + /** + * Given $params and assign the $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { - if (count($params) == 1 AND is_array($params[0])) { + if (count($params) == 1 and is_array($params[0])) { $params = $params[0]; } $this->params['disallowed_values'] = $params; return $this; } + /** + * Set strict value + * + * @param bool $strict + * @return void + */ public function strict($strict = true) { $this->strict = $strict; } - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $this->requireParameters(['disallowed_values']); - $disallowed_values = (array) $this->parameter('disallowed_values'); - return !in_array($value, $disallowed_values, $this->strict); - } + $disallowedValues = (array) $this->parameter('disallowed_values'); + + $and = $this->validation ? $this->validation->getTranslation('and') : 'and'; + $disallowedValuesText = Helper::join(Helper::wraps($disallowedValues, "'"), ', ', ", {$and} "); + $this->setParameterText('disallowed_values', $disallowedValuesText); + + return !in_array($value, $disallowedValues, $this->strict); + } } diff --git a/src/Rules/Numeric.php b/src/Rules/Numeric.php index f312fb4..ddbf6bd 100644 --- a/src/Rules/Numeric.php +++ b/src/Rules/Numeric.php @@ -7,11 +7,17 @@ class Numeric extends Rule { + /** @var string */ protected $message = "The :attribute must be numeric"; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { return is_numeric($value); } - } diff --git a/src/Rules/Present.php b/src/Rules/Present.php index 4bc6d24..9094475 100644 --- a/src/Rules/Present.php +++ b/src/Rules/Present.php @@ -6,13 +6,20 @@ class Present extends Rule { + /** @var bool */ protected $implicit = true; + /** @var string */ protected $message = "The :attribute must be present"; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { return $this->validation->hasValue($this->attribute->getKey()); } - } diff --git a/src/Rules/Regex.php b/src/Rules/Regex.php index ab68eeb..470944c 100644 --- a/src/Rules/Regex.php +++ b/src/Rules/Regex.php @@ -7,15 +7,22 @@ class Regex extends Rule { + /** @var string */ protected $message = "The :attribute is not valid format"; - protected $fillable_params = ['regex']; + /** @var array */ + protected $fillableParams = ['regex']; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); + $this->requireParameters($this->fillableParams); $regex = $this->parameter('regex'); return preg_match($regex, $value) > 0; } - } diff --git a/src/Rules/Required.php b/src/Rules/Required.php index 48aa1e8..e7c17b2 100644 --- a/src/Rules/Required.php +++ b/src/Rules/Required.php @@ -6,30 +6,46 @@ class Required extends Rule { - use FileTrait; + use Traits\FileTrait; + /** @var bool */ protected $implicit = true; + /** @var string */ protected $message = "The :attribute is required"; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $this->setAttributeAsRequired(); - if ($this->attribute AND $this->attribute->hasRule('uploaded_file')) { - return $this->isValueFromUploadedFiles($value) AND $value['error'] != UPLOAD_ERR_NO_FILE; + if ($this->attribute and $this->attribute->hasRule('uploaded_file')) { + return $this->isValueFromUploadedFiles($value) and $value['error'] != UPLOAD_ERR_NO_FILE; } - if (is_string($value)) return mb_strlen(trim($value), 'UTF-8') > 0; - if (is_array($value)) return count($value) > 0; + if (is_string($value)) { + return mb_strlen(trim($value), 'UTF-8') > 0; + } + if (is_array($value)) { + return count($value) > 0; + } return !is_null($value); } + /** + * Set attribute is required if $this->attribute is true + * + * @return void + */ protected function setAttributeAsRequired() { if ($this->attribute) { $this->attribute->setRequired(true); } } - } diff --git a/src/Rules/RequiredIf.php b/src/Rules/RequiredIf.php index 586f9df..fb4eac4 100644 --- a/src/Rules/RequiredIf.php +++ b/src/Rules/RequiredIf.php @@ -6,18 +6,32 @@ class RequiredIf extends Required { + /** @var bool */ protected $implicit = true; + /** @var string */ protected $message = "The :attribute is required"; - public function fillParameters(array $params) + /** + * Given $params and assign the $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { $this->params['field'] = array_shift($params); $this->params['values'] = $params; return $this; } - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $this->requireParameters(['field', 'values']); @@ -26,14 +40,13 @@ public function check($value) $anotherValue = $this->getAttribute()->getValue($anotherAttribute); $validator = $this->validation->getValidator(); - $required_validator = $validator('required'); + $requiredValidator = $validator('required'); if (in_array($anotherValue, $definedValues)) { $this->setAttributeAsRequired(); - return $required_validator->check($value, []); + return $requiredValidator->check($value, []); } return true; } - } diff --git a/src/Rules/RequiredUnless.php b/src/Rules/RequiredUnless.php index ef4cbec..dc1e3a1 100644 --- a/src/Rules/RequiredUnless.php +++ b/src/Rules/RequiredUnless.php @@ -6,18 +6,32 @@ class RequiredUnless extends Required { + /** @var bool */ protected $implicit = true; + /** @var string */ protected $message = "The :attribute is required"; - public function fillParameters(array $params) + /** + * Given $params and assign the $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { $this->params['field'] = array_shift($params); $this->params['values'] = $params; return $this; } - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $this->requireParameters(['field', 'values']); @@ -26,14 +40,13 @@ public function check($value) $anotherValue = $this->getAttribute()->getValue($anotherAttribute); $validator = $this->validation->getValidator(); - $required_validator = $validator('required'); + $requiredValidator = $validator('required'); if (!in_array($anotherValue, $definedValues)) { $this->setAttributeAsRequired(); - return $required_validator->check($value, []); + return $requiredValidator->check($value, []); } return true; } - } diff --git a/src/Rules/RequiredWith.php b/src/Rules/RequiredWith.php index 78fd5ad..5fd809c 100644 --- a/src/Rules/RequiredWith.php +++ b/src/Rules/RequiredWith.php @@ -6,31 +6,44 @@ class RequiredWith extends Required { + /** @var bool */ protected $implicit = true; + /** @var string */ protected $message = "The :attribute is required"; - public function fillParameters(array $params) + /** + * Given $params and assign $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { $this->params['fields'] = $params; return $this; } - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $this->requireParameters(['fields']); $fields = $this->parameter('fields'); $validator = $this->validation->getValidator(); - $required_validator = $validator('required'); + $requiredValidator = $validator('required'); - foreach($fields as $field) { + foreach ($fields as $field) { if ($this->validation->hasValue($field)) { $this->setAttributeAsRequired(); - return $required_validator->check($value, []); + return $requiredValidator->check($value, []); } } return true; } - } diff --git a/src/Rules/RequiredWithAll.php b/src/Rules/RequiredWithAll.php index c323311..ba7ad59 100644 --- a/src/Rules/RequiredWithAll.php +++ b/src/Rules/RequiredWithAll.php @@ -6,31 +6,44 @@ class RequiredWithAll extends Required { + /** @var bool */ protected $implicit = true; + /** @var string */ protected $message = "The :attribute is required"; - public function fillParameters(array $params) + /** + * Given $params and assign $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { $this->params['fields'] = $params; return $this; } - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $this->requireParameters(['fields']); $fields = $this->parameter('fields'); $validator = $this->validation->getValidator(); - $required_validator = $validator('required'); + $requiredValidator = $validator('required'); - foreach($fields as $field) { + foreach ($fields as $field) { if (!$this->validation->hasValue($field)) { return true; } } $this->setAttributeAsRequired(); - return $required_validator->check($value, []); + return $requiredValidator->check($value, []); } - } diff --git a/src/Rules/RequiredWithout.php b/src/Rules/RequiredWithout.php index 17015c8..074fc0d 100644 --- a/src/Rules/RequiredWithout.php +++ b/src/Rules/RequiredWithout.php @@ -6,31 +6,44 @@ class RequiredWithout extends Required { + /** @var bool */ protected $implicit = true; + /** @var string */ protected $message = "The :attribute is required"; - public function fillParameters(array $params) + /** + * Given $params and assign $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { $this->params['fields'] = $params; return $this; } - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $this->requireParameters(['fields']); $fields = $this->parameter('fields'); $validator = $this->validation->getValidator(); - $required_validator = $validator('required'); + $requiredValidator = $validator('required'); - foreach($fields as $field) { + foreach ($fields as $field) { if (!$this->validation->hasValue($field)) { $this->setAttributeAsRequired(); - return $required_validator->check($value, []); + return $requiredValidator->check($value, []); } } return true; } - } diff --git a/src/Rules/RequiredWithoutAll.php b/src/Rules/RequiredWithoutAll.php index 1154f96..53bac2c 100644 --- a/src/Rules/RequiredWithoutAll.php +++ b/src/Rules/RequiredWithoutAll.php @@ -6,31 +6,44 @@ class RequiredWithoutAll extends Required { + /** @var bool */ protected $implicit = true; + /** @var string */ protected $message = "The :attribute is required"; - public function fillParameters(array $params) + /** + * Given $params and assign $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { $this->params['fields'] = $params; return $this; } - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $this->requireParameters(['fields']); $fields = $this->parameter('fields'); $validator = $this->validation->getValidator(); - $required_validator = $validator('required'); + $requiredValidator = $validator('required'); - foreach($fields as $field) { + foreach ($fields as $field) { if ($this->validation->hasValue($field)) { return true; } } $this->setAttributeAsRequired(); - return $required_validator->check($value, []); + return $requiredValidator->check($value, []); } - } diff --git a/src/Rules/Same.php b/src/Rules/Same.php index 508071b..b127b55 100644 --- a/src/Rules/Same.php +++ b/src/Rules/Same.php @@ -7,18 +7,25 @@ class Same extends Rule { + /** @var string */ protected $message = "The :attribute must be same with :field"; - protected $fillable_params = ['field']; + /** @var array */ + protected $fillableParams = ['field']; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { - $this->requireParameters($this->fillable_params); + $this->requireParameters($this->fillableParams); $field = $this->parameter('field'); $anotherValue = $this->getAttribute()->getValue($field); return $value == $anotherValue; } - } diff --git a/src/Rules/Traits/DateUtilsTrait.php b/src/Rules/Traits/DateUtilsTrait.php new file mode 100644 index 0000000..af33d71 --- /dev/null +++ b/src/Rules/Traits/DateUtilsTrait.php @@ -0,0 +1,43 @@ +isValueFromUploadedFiles($value) && is_uploaded_file($value['tmp_name']); + } + + /** + * Resolve uploaded file value + * + * @param mixed $value + * @return array|null + */ + public function resolveUploadedFileValue($value) + { + if (!$this->isValueFromUploadedFiles($value)) { + return null; + } + + // Here $value should be an array: + // [ + // 'name' => string|array, + // 'type' => string|array, + // 'size' => int|array, + // 'tmp_name' => string|array, + // 'error' => string|array, + // ] + + // Flatten $value to it's array dot format, + // so our array must be something like: + // ['name' => string, 'type' => string, 'size' => int, ...] + // or for multiple values: + // ['name.0' => string, 'name.1' => string, 'type.0' => string, 'type.1' => string, ...] + // or for nested array: + // ['name.foo.bar' => string, 'name.foo.baz' => string, 'type.foo.bar' => string, 'type.foo.baz' => string, ...] + $arrayDots = Helper::arrayDot($value); + + $results = []; + foreach ($arrayDots as $key => $val) { + // Move first key to last key + // name.foo.bar -> foo.bar.name + $splits = explode(".", $key); + $firstKey = array_shift($splits); + $key = count($splits) ? implode(".", $splits) . ".{$firstKey}" : $firstKey; + + Helper::arraySet($results, $key, $val); + } + return $results; + } +} diff --git a/src/Rules/Traits/SizeTrait.php b/src/Rules/Traits/SizeTrait.php new file mode 100644 index 0000000..8d4fab9 --- /dev/null +++ b/src/Rules/Traits/SizeTrait.php @@ -0,0 +1,102 @@ +isUploadedFileValue($value)) { + return (float) $value['size']; + } elseif (is_array($value)) { + return (float) count($value); + } else { + return false; + } + } + + /** + * Given $size and get the bytes + * + * @param string|int $size + * @return float + * @throws InvalidArgumentException + */ + protected function getBytesSize($size) + { + if (is_numeric($size)) { + return (float) $size; + } + + if (!is_string($size)) { + throw new InvalidArgumentException("Size must be string or numeric Bytes", 1); + } + + if (!preg_match("/^(?((\d+)?\.)?\d+)(?(B|K|M|G|T|P)B?)?$/i", $size, $match)) { + throw new InvalidArgumentException("Size is not valid format", 1); + } + + $number = (float) $match['number']; + $format = isset($match['format']) ? $match['format'] : ''; + + switch (strtoupper($format)) { + case "KB": + case "K": + return $number * 1024; + + case "MB": + case "M": + return $number * pow(1024, 2); + + case "GB": + case "G": + return $number * pow(1024, 3); + + case "TB": + case "T": + return $number * pow(1024, 4); + + case "PB": + case "P": + return $number * pow(1024, 5); + + default: + return $number; + } + } + + /** + * Check whether value is from $_FILES + * + * @param mixed $value + * @return bool + */ + public function isUploadedFileValue($value): bool + { + if (!is_array($value)) { + return false; + } + + $keys = ['name', 'type', 'tmp_name', 'size', 'error']; + foreach ($keys as $key) { + if (!array_key_exists($key, $value)) { + return false; + } + } + + return true; + } +} diff --git a/src/Rules/TypeArray.php b/src/Rules/TypeArray.php index d706df9..311e5cc 100644 --- a/src/Rules/TypeArray.php +++ b/src/Rules/TypeArray.php @@ -7,11 +7,17 @@ class TypeArray extends Rule { + /** @var string */ protected $message = "The :attribute must be array"; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { return is_array($value); } - } diff --git a/src/Rules/UploadedFile.php b/src/Rules/UploadedFile.php index 7b016f5..ce5830e 100644 --- a/src/Rules/UploadedFile.php +++ b/src/Rules/UploadedFile.php @@ -2,20 +2,34 @@ namespace Rakit\Validation\Rules; -use Rakit\Validation\Rule; +use Rakit\Validation\Helper; use Rakit\Validation\MimeTypeGuesser; +use Rakit\Validation\Rule; +use Rakit\Validation\Rules\Interfaces\BeforeValidate; -class UploadedFile extends Rule +class UploadedFile extends Rule implements BeforeValidate { - use FileTrait; + use Traits\FileTrait, Traits\SizeTrait; - protected $message = "The :attribute is not valid"; + /** @var string */ + protected $message = "The :attribute is not valid uploaded file"; + /** @var string|int */ protected $maxSize = null; + + /** @var string|int */ protected $minSize = null; + + /** @var array */ protected $allowedTypes = []; - public function fillParameters(array $params) + /** + * Given $params and assign $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { $this->minSize(array_shift($params)); $this->maxSize(array_shift($params)); @@ -24,19 +38,38 @@ public function fillParameters(array $params) return $this; } - public function maxSize($size) + /** + * Given $size and set the max size + * + * @param string|int $size + * @return self + */ + public function maxSize($size): Rule { $this->params['max_size'] = $size; return $this; } - public function minSize($size) + /** + * Given $size and set the min size + * + * @param string|int $size + * @return self + */ + public function minSize($size): Rule { $this->params['min_size'] = $size; return $this; } - public function sizeBetween($min, $max) + /** + * Given $min and $max then set the range size + * + * @param string|int $min + * @param string|int $max + * @return self + */ + public function sizeBetween($min, $max): Rule { $this->minSize($min); $this->maxSize($max); @@ -44,7 +77,13 @@ public function sizeBetween($min, $max) return $this; } - public function fileTypes($types) + /** + * Given $types and assign $this->params + * + * @param mixed $types + * @return self + */ + public function fileTypes($types): Rule { if (is_string($types)) { $types = explode('|', $types); @@ -55,14 +94,52 @@ public function fileTypes($types) return $this; } - public function check($value) - { + /** + * {@inheritDoc} + */ + public function beforeValidate() + { + $attribute = $this->getAttribute(); + + // We only resolve uploaded file value + // from complex attribute such as 'files.photo', 'images.*', 'images.foo.bar', etc. + if (!$attribute->isUsingDotNotation()) { + return; + } + + $keys = explode(".", $attribute->getKey()); + $firstKey = array_shift($keys); + $firstKeyValue = $this->validation->getValue($firstKey); + + $resolvedValue = $this->resolveUploadedFileValue($firstKeyValue); + + // Return original value if $value can't be resolved as uploaded file value + if (!$resolvedValue) { + return; + } + + $this->validation->setValue($firstKey, $resolvedValue); + } + + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool + { $minSize = $this->parameter('min_size'); $maxSize = $this->parameter('max_size'); $allowedTypes = $this->parameter('allowed_types'); + if ($allowedTypes) { + $or = $this->validation ? $this->validation->getTranslation('or') : 'or'; + $this->setParameterText('allowed_types', Helper::join(Helper::wraps($allowedTypes, "'"), ', ', ", {$or} ")); + } + // below is Required rule job - if (!$this->isValueFromUploadedFiles($value) OR $value['error'] == UPLOAD_ERR_NO_FILE) { + if (!$this->isValueFromUploadedFiles($value) or $value['error'] == UPLOAD_ERR_NO_FILE) { return true; } @@ -71,18 +148,22 @@ public function check($value) } // just make sure there is no error - if ($value['error']) return false; + if ($value['error']) { + return false; + } if ($minSize) { - $bytesMinSize = $this->getBytes($minSize); + $bytesMinSize = $this->getBytesSize($minSize); if ($value['size'] < $bytesMinSize) { + $this->setMessage('The :attribute file is too small, minimum size is :min_size'); return false; } } if ($maxSize) { - $bytesMaxSize = $this->getBytes($maxSize); + $bytesMaxSize = $this->getBytesSize($maxSize); if ($value['size'] > $bytesMaxSize) { + $this->setMessage('The :attribute file is too large, maximum size is :max_size'); return false; } } @@ -93,6 +174,7 @@ public function check($value) unset($guesser); if (!in_array($ext, $allowedTypes)) { + $this->setMessage('The :attribute file type must be :allowed_types'); return false; } } diff --git a/src/Rules/Uppercase.php b/src/Rules/Uppercase.php index 641e681..11300dd 100644 --- a/src/Rules/Uppercase.php +++ b/src/Rules/Uppercase.php @@ -7,11 +7,17 @@ class Uppercase extends Rule { + /** @var string */ protected $message = "The :attribute must be uppercase"; - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { - return mb_strtoupper($value, mb_detect_encoding($value)) === $value; + return mb_strtoupper($value, mb_detect_encoding($value)) === $value; } - } diff --git a/src/Rules/Url.php b/src/Rules/Url.php index c1549e8..3ed8583 100644 --- a/src/Rules/Url.php +++ b/src/Rules/Url.php @@ -7,23 +7,42 @@ class Url extends Rule { + /** @var string */ protected $message = "The :attribute is not valid url"; - public function fillParameters(array $params) + /** + * Given $params and assign $this->params + * + * @param array $params + * @return self + */ + public function fillParameters(array $params): Rule { - if (count($params) == 1 AND is_array($params[0])) { + if (count($params) == 1 and is_array($params[0])) { $params = $params[0]; } return $this->forScheme($params); } - public function forScheme($schemes) + /** + * Given $schemes and assign $this->params + * + * @param array $schemes + * @return self + */ + public function forScheme($schemes): Rule { $this->params['schemes'] = (array) $schemes; return $this; } - public function check($value) + /** + * Check the $value is valid + * + * @param mixed $value + * @return bool + */ + public function check($value): bool { $schemes = $this->parameter('schemes'); @@ -36,7 +55,7 @@ public function check($value) if ($this->{$method}($value)) { return true; } - } elseif($this->validateCommonScheme($value, $scheme)) { + } elseif ($this->validateCommonScheme($value, $scheme)) { return true; } } @@ -45,12 +64,25 @@ public function check($value) } } - public function validateBasic($value) + /** + * Validate $value is valid URL format + * + * @param mixed $value + * @return bool + */ + public function validateBasic($value): bool { return filter_var($value, FILTER_VALIDATE_URL) !== false; } - public function validateCommonScheme($value, $scheme = null) + /** + * Validate $value is correct $scheme format + * + * @param mixed $value + * @param null $scheme + * @return bool + */ + public function validateCommonScheme($value, $scheme = null): bool { if (!$scheme) { return $this->validateBasic($value) && (bool) preg_match("/^\w+:\/\//i", $value); @@ -59,14 +91,25 @@ public function validateCommonScheme($value, $scheme = null) } } - public function validateMailtoScheme($value) + /** + * Validate the $value is mailto scheme format + * + * @param mixed $value + * @return bool + */ + public function validateMailtoScheme($value): bool { return $this->validateBasic($value) && preg_match("/^mailto:/", $value); } - public function validateJdbcScheme($value) + /** + * Validate the $value is jdbc scheme format + * + * @param mixed $value + * @return bool + */ + public function validateJdbcScheme($value): bool { return (bool) preg_match("/^jdbc:\w+:\/\//", $value); } - } diff --git a/src/Traits/MessagesTrait.php b/src/Traits/MessagesTrait.php new file mode 100644 index 0000000..14ed28e --- /dev/null +++ b/src/Traits/MessagesTrait.php @@ -0,0 +1,54 @@ +messages[$key] = $message; + } + + /** + * Given $messages and set multiple messages + * + * @param array $messages + * @return void + */ + public function setMessages(array $messages) + { + $this->messages = array_merge($this->messages, $messages); + } + + /** + * Given message from given $key + * + * @param string $key + * @return string + */ + public function getMessage(string $key): string + { + return array_key_exists($key, $this->messages) ? $this->messages[$key] : $key; + } + + /** + * Get all $messages + * + * @return array + */ + public function getMessages(): array + { + return $this->messages; + } +} diff --git a/src/Traits/TranslationsTrait.php b/src/Traits/TranslationsTrait.php new file mode 100644 index 0000000..db935e7 --- /dev/null +++ b/src/Traits/TranslationsTrait.php @@ -0,0 +1,54 @@ +translations[$key] = $translation; + } + + /** + * Given $translations and set multiple translations + * + * @param array $translations + * @return void + */ + public function setTranslations(array $translations) + { + $this->translations = array_merge($this->translations, $translations); + } + + /** + * Given translation from given $key + * + * @param string $key + * @return string + */ + public function getTranslation(string $key): string + { + return array_key_exists($key, $this->translations) ? $this->translations[$key] : $key; + } + + /** + * Get all $translations + * + * @return array + */ + public function getTranslations(): array + { + return $this->translations; + } +} diff --git a/src/Validation.php b/src/Validation.php index 51cb4f5..601ec69 100644 --- a/src/Validation.php +++ b/src/Validation.php @@ -2,70 +2,131 @@ namespace Rakit\Validation; -use Rakit\Validation\Rules\Required; use Closure; -use Rakit\Validation\Rules\Defaults; +use Rakit\Validation\Rules\Interfaces\BeforeValidate; +use Rakit\Validation\Rules\Interfaces\ModifyValue; +use Rakit\Validation\Rules\Required; class Validation { + use Traits\TranslationsTrait, Traits\MessagesTrait; + /** @var mixed */ protected $validator; + /** @var array */ protected $inputs = []; + /** @var array */ protected $attributes = []; - protected $messages = []; - + /** @var array */ protected $aliases = []; + /** @var string */ protected $messageSeparator = ':'; + /** @var array */ protected $validData = []; + + /** @var array */ protected $invalidData = []; - public function __construct(Validator $validator, array $inputs, array $rules, array $messages = array()) - { + /** + * Constructor + * + * @param Rakit\Validation\Validator $validator + * @param array $inputs + * @param array $rules + * @param array $messages + * @return void + */ + public function __construct( + Validator $validator, + array $inputs, + array $rules, + array $messages = [] + ) { $this->validator = $validator; $this->inputs = $this->resolveInputAttributes($inputs); $this->messages = $messages; $this->errors = new ErrorBag; - foreach($rules as $attributeKey => $rules) { + foreach ($rules as $attributeKey => $rules) { $this->addAttribute($attributeKey, $rules); } } - public function addAttribute($attributeKey, $rules) + /** + * Add attribute rules + * + * @param string $attributeKey + * @param string|array $rules + * @return void + */ + public function addAttribute(string $attributeKey, $rules) { - $resolved_rules = $this->resolveRules($rules); - $attribute = new Attribute($this, $attributeKey, $this->getAlias($attributeKey), $resolved_rules); + $resolvedRules = $this->resolveRules($rules); + $attribute = new Attribute($this, $attributeKey, $this->getAlias($attributeKey), $resolvedRules); $this->attributes[$attributeKey] = $attribute; } - public function getAttribute($attributeKey) + /** + * Get attribute by key + * + * @param string $attributeKey + * @return null|Rakit\Validation\Attribute + */ + public function getAttribute(string $attributeKey) { return isset($this->attributes[$attributeKey])? $this->attributes[$attributeKey] : null; } - public function validate(array $inputs = array()) + /** + * Run validation + * + * @param array $inputs + * @return void + */ + public function validate(array $inputs = []) { $this->errors = new ErrorBag; // reset error bag $this->inputs = array_merge($this->inputs, $this->resolveInputAttributes($inputs)); - foreach($this->attributes as $attributeKey => $attribute) { + + // Before validation hooks + foreach ($this->attributes as $attributeKey => $attribute) { + foreach ($attribute->getRules() as $rule) { + if ($rule instanceof BeforeValidate) { + $rule->beforeValidate(); + } + } + } + + foreach ($this->attributes as $attributeKey => $attribute) { $this->validateAttribute($attribute); } } - public function errors() + /** + * Get ErrorBag instance + * + * @return Rakit\Validation\ErrorBag + */ + public function errors(): ErrorBag { return $this->errors; } + /** + * Validate attribute + * + * @param Rakit\Validation\Attribute $attribute + * @return void + */ protected function validateAttribute(Attribute $attribute) { if ($this->isArrayAttribute($attribute)) { $attributes = $this->parseArrayAttribute($attribute); - foreach($attributes as $i => $attr) { + foreach ($attributes as $i => $attr) { $this->validateAttribute($attr); } return; @@ -78,18 +139,17 @@ protected function validateAttribute(Attribute $attribute) $isEmptyValue = $this->isEmptyValue($value); $isValid = true; - foreach($rules as $ruleValidator) { + foreach ($rules as $ruleValidator) { $ruleValidator->setAttribute($attribute); - if ($isEmptyValue && $ruleValidator instanceof Defaults) { - $value = $ruleValidator->check(null); + if ($ruleValidator instanceof ModifyValue) { + $value = $ruleValidator->modifyValue($value); $isEmptyValue = $this->isEmptyValue($value); - continue; } $valid = $ruleValidator->check($value); - if ($isEmptyValue AND $this->ruleIsOptional($attribute, $ruleValidator)) { + if ($isEmptyValue and $this->ruleIsOptional($attribute, $ruleValidator)) { continue; } @@ -109,13 +169,25 @@ protected function validateAttribute(Attribute $attribute) } } - protected function isArrayAttribute(Attribute $attribute) + /** + * Check whether given $attribute is array attribute + * + * @param Rakit\Validation\Attribute $attribute + * @return bool + */ + protected function isArrayAttribute(Attribute $attribute): bool { $key = $attribute->getKey(); return strpos($key, '*') !== false; } - protected function parseArrayAttribute(Attribute $attribute) + /** + * Parse array attribute into it's child attributes + * + * @param Rakit\Validation\Attribute $attribute + * @return array + */ + protected function parseArrayAttribute(Attribute $attribute): array { $attributeKey = $attribute->getKey(); $data = Helper::arrayDot($this->initializeAttributeOnData($attributeKey)); @@ -123,7 +195,8 @@ protected function parseArrayAttribute(Attribute $attribute) $pattern = str_replace('\*', '([^\.]+)', preg_quote($attributeKey)); $data = array_merge($data, $this->extractValuesForWildcards( - $data, $attributeKey + $data, + $attributeKey )); $attributes = []; @@ -154,7 +227,7 @@ protected function parseArrayAttribute(Attribute $attribute) * @param string $attribute * @return array */ - protected function initializeAttributeOnData($attributeKey) + protected function initializeAttributeOnData(string $attributeKey): array { $explicitPath = $this->getLeadingExplicitAttributePath($attributeKey); @@ -177,7 +250,7 @@ protected function initializeAttributeOnData($attributeKey) * @param string $attributeKey * @return array */ - public function extractValuesForWildcards($data, $attributeKey) + public function extractValuesForWildcards(array $data, string $attributeKey): array { $keys = []; @@ -211,7 +284,7 @@ public function extractValuesForWildcards($data, $attributeKey) * @param string $attributeKey * @return string */ - protected function getLeadingExplicitAttributePath($attributeKey) + protected function getLeadingExplicitAttributePath(string $attributeKey): string { return rtrim(explode('*', $attributeKey)[0], '.') ?: null; } @@ -225,7 +298,7 @@ protected function getLeadingExplicitAttributePath($attributeKey) * @param string $attributeKey * @return array */ - protected function extractDataFromPath($attributeKey) + protected function extractDataFromPath(string $attributeKey): array { $results = []; @@ -238,6 +311,14 @@ protected function extractDataFromPath($attributeKey) return $results; } + /** + * Add error to the $this->errors + * + * @param Rakit\Validation\Attribute $attribute + * @param mixed $value + * @param Rakit\Validation\Rule $ruleValidator + * @return void + */ protected function addError(Attribute $attribute, $value, Rule $ruleValidator) { $ruleName = $ruleValidator->getKey(); @@ -246,42 +327,69 @@ protected function addError(Attribute $attribute, $value, Rule $ruleValidator) $this->errors->add($attribute->getKey(), $ruleName, $message); } - protected function isEmptyValue($value) + /** + * Check $value is empty value + * + * @param mixed $value + * @return boolean + */ + protected function isEmptyValue($value): bool { $requiredValidator = new Required; return false === $requiredValidator->check($value, []); } - protected function ruleIsOptional(Attribute $attribute, Rule $rule) + /** + * Check the rule is optional + * + * @param Rakit\Validation\Attribute $attribute + * @param Rakit\Validation\Rule $rule + * @return bool + */ + protected function ruleIsOptional(Attribute $attribute, Rule $rule): bool { - return false === $attribute->isRequired() AND - false === $rule->isImplicit() AND + return false === $attribute->isRequired() and + false === $rule->isImplicit() and false === $rule instanceof Required; } - protected function resolveAttributeName(Attribute $attribute) + /** + * Resolve attribute name + * + * @param Rakit\Validation\Attribute $attribute + * @return string + */ + protected function resolveAttributeName(Attribute $attribute): string { $primaryAttribute = $attribute->getPrimaryAttribute(); if (isset($this->aliases[$attribute->getKey()])) { return $this->aliases[$attribute->getKey()]; - } elseif($primaryAttribute AND isset($this->aliases[$primaryAttribute->getKey()])) { + } elseif ($primaryAttribute and isset($this->aliases[$primaryAttribute->getKey()])) { return $this->aliases[$primaryAttribute->getKey()]; - } elseif ($this->validator->getUseHumanizedKeys()) { + } elseif ($this->validator->isUsingHumanizedKey()) { return $attribute->getHumanizedKey(); } else { return $attribute->getKey(); } } - protected function resolveMessage(Attribute $attribute, $value, Rule $validator) + /** + * Resolve message + * + * @param Rakit\Validation\Attribute $attribute + * @param mixed $value + * @param Rakit\Validation\Rule $validator + * @return mixed + */ + protected function resolveMessage(Attribute $attribute, $value, Rule $validator): string { $primaryAttribute = $attribute->getPrimaryAttribute(); - $params = $validator->getParameters(); + $params = array_merge($validator->getParameters(), $validator->getParametersTexts()); $attributeKey = $attribute->getKey(); $ruleKey = $validator->getKey(); $alias = $attribute->getAlias() ?: $this->resolveAttributeName($attribute); $message = $validator->getMessage(); // default rule message - $message_keys = [ + $messageKeys = [ $attributeKey.$this->messageSeparator.$ruleKey, $attributeKey, $ruleKey @@ -289,7 +397,7 @@ protected function resolveMessage(Attribute $attribute, $value, Rule $validator) if ($primaryAttribute) { // insert primaryAttribute keys - // $message_keys = [ + // $messageKeys = [ // $attributeKey.$this->messageSeparator.$ruleKey, // >> here [1] << // $attributeKey, @@ -297,11 +405,11 @@ protected function resolveMessage(Attribute $attribute, $value, Rule $validator) // $ruleKey // ]; $primaryAttributeKey = $primaryAttribute->getKey(); - array_splice($message_keys, 1, 0, $primaryAttributeKey.$this->messageSeparator.$ruleKey); - array_splice($message_keys, 3, 0, $primaryAttributeKey); + array_splice($messageKeys, 1, 0, $primaryAttributeKey.$this->messageSeparator.$ruleKey); + array_splice($messageKeys, 3, 0, $primaryAttributeKey); } - foreach($message_keys as $key) { + foreach ($messageKeys as $key) { if (isset($this->messages[$key])) { $message = $this->messages[$key]; break; @@ -314,7 +422,7 @@ protected function resolveMessage(Attribute $attribute, $value, Rule $validator) 'value' => $value, ]); - foreach($vars as $key => $value) { + foreach ($vars as $key => $value) { $value = $this->stringify($value); $message = str_replace(':'.$key, $value, $message); } @@ -336,49 +444,70 @@ protected function resolveMessage(Attribute $attribute, $value, Rule $validator) return $message; } - protected function stringify($value) + /** + * Stringify $value + * + * @param mixed $value + * @return string + */ + protected function stringify($value): string { if (is_string($value) || is_numeric($value)) { return $value; - } elseif(is_array($value) || is_object($value)) { + } elseif (is_array($value) || is_object($value)) { return json_encode($value); } else { return ''; } } - protected function resolveRules($rules) + /** + * Resolve $rules + * + * @param mixed $rules + * @return array + */ + protected function resolveRules($rules): array { if (is_string($rules)) { $rules = explode('|', $rules); } - $resolved_rules = []; + $resolvedRules = []; $validatorFactory = $this->getValidator(); - foreach($rules as $i => $rule) { - if (empty($rule)) continue; + foreach ($rules as $i => $rule) { + if (empty($rule)) { + continue; + } $params = []; if (is_string($rule)) { list($rulename, $params) = $this->parseRule($rule); $validator = call_user_func_array($validatorFactory, array_merge([$rulename], $params)); - } elseif($rule instanceof Rule) { + } elseif ($rule instanceof Rule) { $validator = $rule; - } elseif($rule instanceof Closure) { + } elseif ($rule instanceof Closure) { $validator = call_user_func_array($validatorFactory, ['callback', $rule]); } else { $ruleName = is_object($rule) ? get_class($rule) : gettype($rule); - throw new \Exception("Rule must be a string, closure or Rakit\Validation\Rule instance. ".$ruleName." given"); + $message = "Rule must be a string, Closure or '".Rule::class."' instance. ".$ruleName." given"; + throw new \Exception(); } - $resolved_rules[] = $validator; + $resolvedRules[] = $validator; } - return $resolved_rules; + return $resolvedRules; } - protected function parseRule($rule) + /** + * Parse $rule + * + * @param string $rule + * @return array + */ + protected function parseRule(string $rule): array { $exp = explode(':', $rule, 2); $rulename = $exp[0]; @@ -391,60 +520,114 @@ protected function parseRule($rule) return [$rulename, $params]; } - public function setMessage($key, $message) - { - $this->messages[$key] = $message; - } - - public function setMessages(array $messages) - { - $this->messages = array_merge($this->messages, $messages); - } - - public function setAlias($attributeKey, $alias) + /** + * Given $attributeKey and $alias then assign alias + * + * @param mixed $attributeKey + * @param mixed $alias + * @return void + */ + public function setAlias(string $attributeKey, string $alias) { $this->aliases[$attributeKey] = $alias; } - public function getAlias($attributeKey) + /** + * Get attribute alias from given key + * + * @param mixed $attributeKey + * @return string|null + */ + public function getAlias(string $attributeKey) { return isset($this->aliases[$attributeKey])? $this->aliases[$attributeKey] : null; } - public function setAliases($aliases) + /** + * Set attributes aliases + * + * @param array $aliases + * @return void + */ + public function setAliases(array $aliases) { $this->aliases = array_merge($this->aliases, $aliases); } - public function passes() + /** + * Check validations are passed + * + * @return bool + */ + public function passes(): bool { return $this->errors->count() == 0; } - public function fails() + /** + * Check validations are failed + * + * @return bool + */ + public function fails(): bool { return !$this->passes(); } - public function getValue($key) + /** + * Given $key and get value + * + * @param string $key + * @return mixed + */ + public function getValue(string $key) { return Helper::arrayGet($this->inputs, $key); } - public function hasValue($key) + /** + * Set input value + * + * @param string $key + * @param mixed $value + * @return void + */ + public function setValue(string $key, $value) + { + Helper::arraySet($this->inputs, $key, $value); + } + + /** + * Given $key and check value is exsited + * + * @param string $key + * @return boolean + */ + public function hasValue(string $key): bool { return Helper::arrayHas($this->inputs, $key); } - public function getValidator() + /** + * Get Validator class instance + * + * @return Rakit\Validation\Validator + */ + public function getValidator(): Validator { return $this->validator; } - protected function resolveInputAttributes(array $inputs) + /** + * Given $inputs and resolve input attributes + * + * @param array $inputs + * @return array + */ + protected function resolveInputAttributes(array $inputs): array { $resolvedInputs = []; - foreach($inputs as $key => $rules) { + foreach ($inputs as $key => $rules) { $exp = explode(':', $key); if (count($exp) > 1) { @@ -458,10 +641,23 @@ protected function resolveInputAttributes(array $inputs) return $resolvedInputs; } - public function getValidatedData() { + /** + * Get validated data + * + * @return array + */ + public function getValidatedData(): array + { return array_merge($this->validData, $this->invalidData); } + /** + * Set valid data + * + * @param Rakit\Validation\Attribute $attribute + * @param mixed $value + * @return void + */ protected function setValidData(Attribute $attribute, $value) { $key = $attribute->getKey(); @@ -473,11 +669,23 @@ protected function setValidData(Attribute $attribute, $value) } } - public function getValidData() + /** + * Get valid data + * + * @return array + */ + public function getValidData(): array { return $this->validData; } + /** + * Set invalid data + * + * @param Rakit\Validation\Attribute $attribute + * @param mixed $value + * @return void + */ protected function setInvalidData(Attribute $attribute, $value) { $key = $attribute->getKey(); @@ -489,9 +697,13 @@ protected function setInvalidData(Attribute $attribute, $value) } } - public function getInvalidData() + /** + * Get invalid data + * + * @return void + */ + public function getInvalidData(): array { return $this->invalidData; } - } diff --git a/src/Validator.php b/src/Validator.php index cbd9a19..943952b 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -4,56 +4,96 @@ class Validator { + use Traits\TranslationsTrait, Traits\MessagesTrait; - protected $messages = []; + /** @var translations */ + protected $translations = []; + /** @var array */ protected $validators = []; + /** @var bool */ protected $allowRuleOverride = false; + /** @var bool */ protected $useHumanizedKeys = true; + /** + * Constructor + * + * @param array $messages + * @return void + */ public function __construct(array $messages = []) { $this->messages = $messages; $this->registerBaseValidators(); } - public function setMessage($key, $message) - { - return $this->messages[$key] = $message; - } - - public function setMessages($messages) - { - $this->messages = array_merge($this->messages, $messages); - } - - public function setValidator($key, Rule $rule) + /** + * Register or override existing validator + * + * @param mixed $key + * @param Rakit\Validation\Rule $rule + * @return void + */ + public function setValidator(string $key, Rule $rule) { $this->validators[$key] = $rule; $rule->setKey($key); } + /** + * Get validator object from given $key + * + * @param mixed $key + * @return mixed + */ public function getValidator($key) { return isset($this->validators[$key])? $this->validators[$key] : null; } - public function validate(array $inputs, array $rules, array $messages = array()) + /** + * Validate $inputs + * + * @param array $inputs + * @param array $rules + * @param array $messages + * @return Validation + */ + public function validate(array $inputs, array $rules, array $messages = []): Validation { $validation = $this->make($inputs, $rules, $messages); $validation->validate(); return $validation; } - public function make(array $inputs, array $rules, array $messages = array()) + /** + * Given $inputs, $rules and $messages to make the Validation class instance + * + * @param array $inputs + * @param array $rules + * @param array $messages + * @return Validation + */ + public function make(array $inputs, array $rules, array $messages = []): Validation { $messages = array_merge($this->messages, $messages); - return new Validation($this, $inputs, $rules, $messages); + $validation = new Validation($this, $inputs, $rules, $messages); + $validation->setTranslations($this->getTranslations()); + + return $validation; } - public function __invoke($rule) + /** + * Magic invoke method to make Rule instance + * + * @param string $rule + * @return Rule + * @throws RuleNotFoundException + */ + public function __invoke(string $rule): Rule { $args = func_get_args(); $rule = array_shift($args); @@ -69,6 +109,11 @@ public function __invoke($rule) return $clonedValidator; } + /** + * Initialize base validators array + * + * @return void + */ protected function registerBaseValidators() { $baseValidator = [ @@ -102,6 +147,7 @@ protected function registerBaseValidators() 'present' => new Rules\Present, 'different' => new Rules\Different, 'uploaded_file' => new Rules\UploadedFile, + 'mimes' => new Rules\Mimes, 'callback' => new Rules\Callback, 'before' => new Rules\Before, 'after' => new Rules\After, @@ -114,12 +160,19 @@ protected function registerBaseValidators() 'default' => new Rules\Defaults, // alias of defaults ]; - foreach($baseValidator as $key => $validator) { + foreach ($baseValidator as $key => $validator) { $this->setValidator($key, $validator); } } - public function addValidator($ruleName, Rule $rule) + /** + * Given $ruleName and $rule to add new validator + * + * @param string $ruleName + * @param Rakit\Validation\Rule $rule + * @return void + */ + public function addValidator(string $ruleName, Rule $rule) { if (!$this->allowRuleOverride && array_key_exists($ruleName, $this->validators)) { throw new RuleQuashException( @@ -130,17 +183,34 @@ public function addValidator($ruleName, Rule $rule) $this->setValidator($ruleName, $rule); } - public function allowRuleOverride($status = false) + /** + * Set rule can allow to be overrided + * + * @param boolean $status + * @return void + */ + public function allowRuleOverride(bool $status = false) { $this->allowRuleOverride = $status; } - public function setUseHumanizedKeys($useHumanizedKeys = true) + /** + * Set this can use humanize keys + * + * @param boolean $useHumanizedKeys + * @return void + */ + public function setUseHumanizedKeys(bool $useHumanizedKeys = true) { $this->useHumanizedKeys = $useHumanizedKeys; } - public function getUseHumanizedKeys() + /** + * Get $this->useHumanizedKeys value + * + * @return void + */ + public function isUsingHumanizedKey(): bool { return $this->useHumanizedKeys; } diff --git a/tests/ErrorBagTest.php b/tests/ErrorBagTest.php index 6cb80c6..9f78c61 100644 --- a/tests/ErrorBagTest.php +++ b/tests/ErrorBagTest.php @@ -1,8 +1,11 @@ '1', 'unique' => '2', ], - + 'items.0.id_product' => [ 'numeric' => '3', 'etc' => 'x' @@ -125,7 +128,7 @@ public function testGet() 'items.0.qty' => [ 'numeric' => 'a' ], - + 'items.1.id_product' => [ 'numeric' => '4', 'etc' => 'y' @@ -227,7 +230,7 @@ public function testAll() $this->assertEquals($errors->all('prefix :message suffix'), [ 'prefix 1 suffix', 'prefix 2 suffix', - + 'prefix 3 suffix', 'prefix x suffix', 'prefix a suffix', @@ -262,14 +265,49 @@ public function testFirstOfAll() ]); $this->assertEquals($errors->firstOfAll('prefix :message suffix'), [ - 'prefix 1 suffix', - - 'prefix 3 suffix', - 'prefix a suffix', - - 'prefix 4 suffix', - 'prefix b suffix', + 'email' => 'prefix 1 suffix', + 'items' => [ + [ + 'id_product' => 'prefix 3 suffix', + 'qty' => 'prefix a suffix' + ], + [ + 'id_product' => 'prefix 4 suffix', + 'qty' => 'prefix b suffix' + ], + ] ]); } + public function testFirstOfAllDotNotation() + { + $errors = new ErrorBag([ + 'email' => [ + 'email' => '1', + 'unique' => '2', + ], + 'items.0.id_product' => [ + 'numeric' => '3', + 'etc' => 'x' + ], + 'items.0.qty' => [ + 'numeric' => 'a' + ], + 'items.1.id_product' => [ + 'numeric' => '4', + 'etc' => 'y' + ], + 'items.1.qty' => [ + 'numeric' => 'b' + ] + ]); + + $this->assertEquals($errors->firstOfAll('prefix :message suffix', true), [ + 'email' => 'prefix 1 suffix', + 'items.0.id_product' => 'prefix 3 suffix', + 'items.0.qty' => 'prefix a suffix', + 'items.1.id_product' => 'prefix 4 suffix', + 'items.1.qty' => 'prefix b suffix', + ]); + } } diff --git a/tests/Fixtures/Even.php b/tests/Fixtures/Even.php index cc0ea6d..b556695 100644 --- a/tests/Fixtures/Even.php +++ b/tests/Fixtures/Even.php @@ -1,18 +1,20 @@ assertEquals(Helper::join($pieces0, $separator, $lastSeparator), ''); + $this->assertEquals(Helper::join($pieces1, $separator, $lastSeparator), '1'); + $this->assertEquals(Helper::join($pieces2, $separator, $lastSeparator), '1, and 2'); + $this->assertEquals(Helper::join($pieces3, $separator, $lastSeparator), '1, 2, and 3'); + } + + public function testWraps() + { + $inputs = [1, 2, 3]; + + $this->assertEquals(Helper::wraps($inputs, '-'), ['-1-', '-2-', '-3-']); + $this->assertEquals(Helper::wraps($inputs, '-', '+'), ['-1+', '-2+', '-3+']); + } } diff --git a/tests/Rules/AcceptedTest.php b/tests/Rules/AcceptedTest.php index 3e3eb70..dbad471 100644 --- a/tests/Rules/AcceptedTest.php +++ b/tests/Rules/AcceptedTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check(' 1')); $this->assertFalse($this->rule->check(10)); } - } diff --git a/tests/Rules/AfterTest.php b/tests/Rules/AfterTest.php index a6dd1ed..79a06aa 100644 --- a/tests/Rules/AfterTest.php +++ b/tests/Rules/AfterTest.php @@ -1,7 +1,12 @@ validator = new \Rakit\Validation\Rules\After(); + $this->validator = new After(); } /** diff --git a/tests/Rules/AlphaDashTest.php b/tests/Rules/AlphaDashTest.php index c80f118..269d383 100644 --- a/tests/Rules/AlphaDashTest.php +++ b/tests/Rules/AlphaDashTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('foo bar')); $this->assertFalse($this->rule->check('123 bar ')); } - } diff --git a/tests/Rules/AlphaNumTest.php b/tests/Rules/AlphaNumTest.php index 91ed4dc..dbbff98 100644 --- a/tests/Rules/AlphaNumTest.php +++ b/tests/Rules/AlphaNumTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('123 foo')); $this->assertFalse($this->rule->check(' foo123 ')); } - } diff --git a/tests/Rules/AlphaTest.php b/tests/Rules/AlphaTest.php index cc2c8b5..11e6b4b 100644 --- a/tests/Rules/AlphaTest.php +++ b/tests/Rules/AlphaTest.php @@ -1,8 +1,12 @@ assertFalse($this->rule->check('foo123bar')); $this->assertFalse($this->rule->check('foo bar')); } - } diff --git a/tests/Rules/BeforeTest.php b/tests/Rules/BeforeTest.php index b7ddb43..3a44c3c 100644 --- a/tests/Rules/BeforeTest.php +++ b/tests/Rules/BeforeTest.php @@ -1,7 +1,12 @@ validator = new \Rakit\Validation\Rules\Before(); + $this->validator = new Before(); } /** diff --git a/tests/Rules/BetweenTest.php b/tests/Rules/BetweenTest.php index d50199e..7bcf5f0 100644 --- a/tests/Rules/BetweenTest.php +++ b/tests/Rules/BetweenTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->fillParameters([50, 100])->check(123.4)); } + public function testUploadedFileValue() + { + $mb = function ($n) { + return $n * 1024 * 1024; + }; + + $sampleFile = [ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'text/plain', + 'size' => $mb(2), + 'tmp_name' => __FILE__, + 'error' => 0 + ]; + + $this->assertTrue($this->rule->fillParameters([$mb(2), $mb(5)])->check($sampleFile)); + $this->assertTrue($this->rule->fillParameters(['2M', '5M'])->check($sampleFile)); + $this->assertTrue($this->rule->fillParameters([$mb(1), $mb(2)])->check($sampleFile)); + $this->assertTrue($this->rule->fillParameters(['1M', '2M'])->check($sampleFile)); + + $this->assertFalse($this->rule->fillParameters([$mb(2.1), $mb(5)])->check($sampleFile)); + $this->assertFalse($this->rule->fillParameters(['2.1M', '5M'])->check($sampleFile)); + $this->assertFalse($this->rule->fillParameters([$mb(1), $mb(1.9)])->check($sampleFile)); + $this->assertFalse($this->rule->fillParameters(['1M', '1.9M'])->check($sampleFile)); + } } diff --git a/tests/Rules/CallbackTest.php b/tests/Rules/CallbackTest.php index a71f133..de36ce8 100644 --- a/tests/Rules/CallbackTest.php +++ b/tests/Rules/CallbackTest.php @@ -1,15 +1,18 @@ rule = new Callback; - $this->rule->setCallback(function($value) { - return (is_numeric($value) AND $value % 2 === 0); + $this->rule->setCallback(function ($value) { + return (is_numeric($value) and $value % 2 === 0); }); } @@ -26,5 +29,4 @@ public function testInvalids() $this->assertFalse($this->rule->check('abc12')); $this->assertFalse($this->rule->check("12abc")); } - } diff --git a/tests/Rules/DateTest.php b/tests/Rules/DateTest.php index ca2aa73..1619e28 100644 --- a/tests/Rules/DateTest.php +++ b/tests/Rules/DateTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check("10-10-2010")); $this->assertFalse($this->rule->fillParameters(['Y-m-d'])->check("2010-10-10 10:10")); } - } diff --git a/tests/Rules/DefaultsTest.php b/tests/Rules/DefaultsTest.php index 0bbde29..e3b637c 100644 --- a/tests/Rules/DefaultsTest.php +++ b/tests/Rules/DefaultsTest.php @@ -1,10 +1,12 @@ rule = new Defaults; @@ -12,9 +14,9 @@ public function setUp() public function testDefaults() { - $this->assertEquals($this->rule->fillParameters([10])->check(null), 10); - $this->assertEquals($this->rule->fillParameters(['something'])->check(null), 'something'); - $this->assertEquals($this->rule->fillParameters([[1,2,3]])->check('anything'), [1,2,3]); + $this->assertTrue($this->rule->fillParameters([10])->check(0)); + $this->assertTrue($this->rule->fillParameters(['something'])->check(null)); + $this->assertTrue($this->rule->fillParameters([[1,2,3]])->check(false)); + $this->assertTrue($this->rule->fillParameters([[1,2,3]])->check([])); } - } diff --git a/tests/Rules/DigitsBetweenTest.php b/tests/Rules/DigitsBetweenTest.php index f84e694..0602663 100644 --- a/tests/Rules/DigitsBetweenTest.php +++ b/tests/Rules/DigitsBetweenTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->fillParameters([1, 3])->check(12345)); $this->assertFalse($this->rule->fillParameters([3, 6])->check('foobar')); } - } diff --git a/tests/Rules/DigitsTest.php b/tests/Rules/DigitsTest.php index 2523144..7d9db2e 100644 --- a/tests/Rules/DigitsTest.php +++ b/tests/Rules/DigitsTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->fillParameters([7])->check(12345678)); - $this->assertFalse($this->rule->fillParameters([4])->check(12)); - $this->assertFalse($this->rule->fillParameters([3])->check('foo')); + $this->assertFalse($this->rule->fillParameters([4])->check(12)); + $this->assertFalse($this->rule->fillParameters([3])->check('foo')); } - } diff --git a/tests/Rules/EmailTest.php b/tests/Rules/EmailTest.php index 255155b..66ba2fc 100644 --- a/tests/Rules/EmailTest.php +++ b/tests/Rules/EmailTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('johndoe.gmail.com')); $this->assertFalse($this->rule->check('johndoe.gmail.com')); } - } diff --git a/tests/Rules/InTest.php b/tests/Rules/InTest.php index f0d1169..29e86be 100644 --- a/tests/Rules/InTest.php +++ b/tests/Rules/InTest.php @@ -1,8 +1,11 @@ assertTrue($this->rule->fillParameters(['1', '2', '3'])->check(1)); + $this->assertTrue($this->rule->fillParameters(['1', '2', '3'])->check(1)); $this->assertTrue($this->rule->fillParameters(['1', '2', '3'])->check(true)); - // Strict + // Strict $this->rule->strict(); - $this->assertFalse($this->rule->fillParameters(['1', '2', '3'])->check(1)); - $this->assertFalse($this->rule->fillParameters(['1', '2', '3'])->check(1)); + $this->assertFalse($this->rule->fillParameters(['1', '2', '3'])->check(1)); + $this->assertFalse($this->rule->fillParameters(['1', '2', '3'])->check(1)); } - } diff --git a/tests/Rules/IntegerTest.php b/tests/Rules/IntegerTest.php index 7e0304f..101a4d4 100644 --- a/tests/Rules/IntegerTest.php +++ b/tests/Rules/IntegerTest.php @@ -1,8 +1,11 @@ assertTrue($this->rule->check('-123')); $this->assertTrue($this->rule->check(123)); $this->assertTrue($this->rule->check(-123)); - } public function testInvalids() @@ -29,5 +31,4 @@ public function testInvalids() $this->assertFalse($this->rule->check('123.456')); $this->assertFalse($this->rule->check('-123.456')); } - } diff --git a/tests/Rules/IpTest.php b/tests/Rules/IpTest.php index 5f68c32..0eac7eb 100644 --- a/tests/Rules/IpTest.php +++ b/tests/Rules/IpTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('hf02::2')); $this->assertFalse($this->rule->check('12345:0000:3238:DFE1:0063:0000:0000:FEFB')); } - } diff --git a/tests/Rules/Ipv4Test.php b/tests/Rules/Ipv4Test.php index d0b9a1b..41bd6fe 100644 --- a/tests/Rules/Ipv4Test.php +++ b/tests/Rules/Ipv4Test.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('hf02::2')); $this->assertFalse($this->rule->check('12345:0000:3238:DFE1:0063:0000:0000:FEFB')); } - } diff --git a/tests/Rules/Ipv6Test.php b/tests/Rules/Ipv6Test.php index ea4218a..f3ab8d3 100644 --- a/tests/Rules/Ipv6Test.php +++ b/tests/Rules/Ipv6Test.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('hf02::2')); $this->assertFalse($this->rule->check('12345:0000:3238:DFE1:0063:0000:0000:FEFB')); } - } diff --git a/tests/Rules/JsonTest.php b/tests/Rules/JsonTest.php index aa5377f..9daa860 100644 --- a/tests/Rules/JsonTest.php +++ b/tests/Rules/JsonTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('{"username": John Doe}')); $this->assertFalse($this->rule->check('{number: 12345678}')); } - } - diff --git a/tests/Rules/LowercaseTest.php b/tests/Rules/LowercaseTest.php index 5441140..dcc17b4 100644 --- a/tests/Rules/LowercaseTest.php +++ b/tests/Rules/LowercaseTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('Username')); $this->assertFalse($this->rule->check('userName')); } - } diff --git a/tests/Rules/MaxTest.php b/tests/Rules/MaxTest.php index 304edb0..8c9087b 100644 --- a/tests/Rules/MaxTest.php +++ b/tests/Rules/MaxTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->fillParameters([100])->check(123)); } + public function testUploadedFileValue() + { + $twoMega = 1024 * 1024 * 2; + $sampleFile = [ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'text/plain', + 'size' => $twoMega, + 'tmp_name' => __FILE__, + 'error' => 0 + ]; + + $this->assertTrue($this->rule->fillParameters([$twoMega])->check($sampleFile)); + $this->assertTrue($this->rule->fillParameters(['2M'])->check($sampleFile)); + + $this->assertFalse($this->rule->fillParameters([$twoMega - 1])->check($sampleFile)); + $this->assertFalse($this->rule->fillParameters(['1.9M'])->check($sampleFile)); + } } diff --git a/tests/Rules/MimesTest.php b/tests/Rules/MimesTest.php new file mode 100644 index 0000000..5056f96 --- /dev/null +++ b/tests/Rules/MimesTest.php @@ -0,0 +1,151 @@ +rule = new Mimes(); + } + + public function testValidMimes() + { + $file = [ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'text/plain', + 'size' => filesize(__FILE__), + 'tmp_name' => __FILE__, + 'error' => UPLOAD_ERR_OK + ]; + + $uploadedFileRule = $this->getMockBuilder(Mimes::class) + ->setMethods(['isUploadedFile']) + ->getMock(); + + $uploadedFileRule->expects($this->once()) + ->method('isUploadedFile') + ->willReturn(true); + + $this->assertTrue($uploadedFileRule->check($file)); + } + + /** + * Make sure we can't just passing array like valid $_FILES['key'] + */ + public function testValidateWithoutMockShouldBeInvalid() + { + $this->assertFalse($this->rule->check([ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'text/plain', + 'size' => filesize(__FILE__), + 'tmp_name' => __FILE__, + 'error' => UPLOAD_ERR_OK + ])); + } + + /** + * Missing UPLOAD_ERR_NO_FILE should be valid because it is job for required rule + */ + public function testEmptyMimesShouldBeValid() + { + $this->assertTrue($this->rule->check([ + 'name' => '', + 'type' => '', + 'size' => '', + 'tmp_name' => '', + 'error' => UPLOAD_ERR_NO_FILE + ])); + } + + public function testUploadError() + { + $this->assertFalse($this->rule->check([ + 'name' => '', + 'type' => '', + 'size' => '', + 'tmp_name' => '', + 'error' => 5 + ])); + } + + public function testFileTypes() + { + + $rule = $this->getMockBuilder(Mimes::class) + ->setMethods(['isUploadedFile']) + ->getMock(); + + $rule->expects($this->exactly(3)) + ->method('isUploadedFile') + ->willReturn(true); + + $rule->allowTypes('png|jpeg'); + + $this->assertFalse($rule->check([ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'text/plain', + 'size' => 1024, // 1K + 'tmp_name' => __FILE__, + 'error' => 0 + ])); + + $this->assertTrue($rule->check([ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'image/png', + 'size' => 10 * 1024, + 'tmp_name' => __FILE__, + 'error' => 0 + ])); + + $this->assertTrue($rule->check([ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'image/jpeg', + 'size' => 10 * 1024, + 'tmp_name' => __FILE__, + 'error' => 0 + ])); + } + + /** + * Missing array key(s) should be valid because it is job for required rule + */ + public function testMissingAKeyShouldBeValid() + { + // missing name + $this->assertTrue($this->rule->check([ + 'type' => 'text/plain', + 'size' => filesize(__FILE__), + 'tmp_name' => __FILE__, + 'error' => 0 + ])); + + // missing type + $this->assertTrue($this->rule->check([ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'size' => filesize(__FILE__), + 'tmp_name' => __FILE__, + 'error' => 0 + ])); + + // missing size + $this->assertTrue($this->rule->check([ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'text/plain', + 'tmp_name' => __FILE__, + 'error' => 0 + ])); + + // missing tmp_name + $this->assertTrue($this->rule->check([ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'text/plain', + 'size' => filesize(__FILE__), + 'error' => 0 + ])); + } +} diff --git a/tests/Rules/MinTest.php b/tests/Rules/MinTest.php index 5f51ef3..98b8b99 100644 --- a/tests/Rules/MinTest.php +++ b/tests/Rules/MinTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->fillParameters([7])->check('foobar')); $this->assertFalse($this->rule->fillParameters([4])->check([1,2,3])); - $this->assertFalse($this->rule->fillParameters([200])->check(123)); + $this->assertFalse($this->rule->fillParameters([200])->check(123)); $this->assertFalse($this->rule->fillParameters([4])->check('мин')); $this->assertFalse($this->rule->fillParameters([5])->check('كلمة')); $this->assertFalse($this->rule->fillParameters([4])->check('ワード')); - $this->assertFalse($this->rule->fillParameters([2])->check('字')); + $this->assertFalse($this->rule->fillParameters([2])->check('字')); } + public function testUploadedFileValue() + { + $twoMega = 1024 * 1024 * 2; + $sampleFile = [ + 'name' => pathinfo(__FILE__, PATHINFO_BASENAME), + 'type' => 'text/plain', + 'size' => $twoMega, + 'tmp_name' => __FILE__, + 'error' => 0 + ]; + + $this->assertTrue($this->rule->fillParameters([$twoMega])->check($sampleFile)); + $this->assertTrue($this->rule->fillParameters(['2M'])->check($sampleFile)); + + $this->assertFalse($this->rule->fillParameters([$twoMega + 1])->check($sampleFile)); + $this->assertFalse($this->rule->fillParameters(['2.1M'])->check($sampleFile)); + } } diff --git a/tests/Rules/NotInTest.php b/tests/Rules/NotInTest.php index 403c860..34f0523 100644 --- a/tests/Rules/NotInTest.php +++ b/tests/Rules/NotInTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->fillParameters(['1', '2', '3'])->check(1)); + $this->assertFalse($this->rule->fillParameters(['1', '2', '3'])->check(1)); $this->assertFalse($this->rule->fillParameters(['1', '2', '3'])->check(true)); - // Strict + // Strict $this->rule->strict(); - $this->assertTrue($this->rule->fillParameters(['1', '2', '3'])->check(1)); - $this->assertTrue($this->rule->fillParameters(['1', '2', '3'])->check(1)); + $this->assertTrue($this->rule->fillParameters(['1', '2', '3'])->check(1)); + $this->assertTrue($this->rule->fillParameters(['1', '2', '3'])->check(1)); } - } diff --git a/tests/Rules/NumericTest.php b/tests/Rules/NumericTest.php index 92de70b..cc309df 100644 --- a/tests/Rules/NumericTest.php +++ b/tests/Rules/NumericTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('123foo')); $this->assertFalse($this->rule->check([123])); } - } diff --git a/tests/Rules/RegexTest.php b/tests/Rules/RegexTest.php index 9360115..d5d5f35 100644 --- a/tests/Rules/RegexTest.php +++ b/tests/Rules/RegexTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->fillParameters(["/^F/i"])->check("bar")); } - } diff --git a/tests/Rules/RequiredTest.php b/tests/Rules/RequiredTest.php index 349f650..3f3535f 100644 --- a/tests/Rules/RequiredTest.php +++ b/tests/Rules/RequiredTest.php @@ -1,8 +1,12 @@ assertTrue($this->rule->check(true)); $this->assertTrue($this->rule->check('0')); $this->assertTrue($this->rule->check(0)); - $this->assertTrue($this->rule->check(new \stdClass)); + $this->assertTrue($this->rule->check(new stdClass)); } public function testInvalids() @@ -27,5 +31,4 @@ public function testInvalids() $this->assertFalse($this->rule->check('')); $this->assertFalse($this->rule->check([])); } - } diff --git a/tests/Rules/TypeArrayTest.php b/tests/Rules/TypeArrayTest.php index 2da1ee5..56e8e60 100644 --- a/tests/Rules/TypeArrayTest.php +++ b/tests/Rules/TypeArrayTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('[]')); $this->assertFalse($this->rule->check('[1,2,3]')); } - } diff --git a/tests/Rules/UploadedFileTest.php b/tests/Rules/UploadedFileTest.php index 542c4f3..3f0f6f5 100644 --- a/tests/Rules/UploadedFileTest.php +++ b/tests/Rules/UploadedFileTest.php @@ -1,8 +1,11 @@ getMockBuilder(UploadedFile::class) ->setMethods(['isUploadedFile']) ->getMock(); diff --git a/tests/Rules/UppercaseTest.php b/tests/Rules/UppercaseTest.php index 7c69600..cf7bdc8 100644 --- a/tests/Rules/UppercaseTest.php +++ b/tests/Rules/UppercaseTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->check('Username')); $this->assertFalse($this->rule->check('userName')); } - } diff --git a/tests/Rules/UrlTest.php b/tests/Rules/UrlTest.php index 00a520e..d91fe3d 100644 --- a/tests/Rules/UrlTest.php +++ b/tests/Rules/UrlTest.php @@ -1,8 +1,11 @@ assertFalse($this->rule->forScheme('jdbc')->check('http://www.foobar.com')); $this->assertFalse($this->rule->forScheme(['http', 'https'])->check('any://www.foobar.com')); } - } diff --git a/tests/ValidatonTest.php b/tests/ValidatonTest.php index 9f82e70..4f8f69d 100644 --- a/tests/ValidatonTest.php +++ b/tests/ValidatonTest.php @@ -1,9 +1,13 @@ '', 'type' => '', 'size' => '', @@ -95,7 +97,7 @@ public function testOptionalUploadedFile() ]; $validation = $this->validator->validate([ - 'file' => $empty_file + 'file' => $emptyFile ], [ 'file' => 'uploaded_file' ]); @@ -105,10 +107,10 @@ public function testOptionalUploadedFile() /** * @dataProvider getSamplesMissingKeyFromUploadedFileValue */ - public function testMissingKeyUploadedFile($uploaded_file) + public function testMissingKeyUploadedFile($uploadedFile) { $validation = $this->validator->validate([ - 'file' => $uploaded_file + 'file' => $uploadedFile ], [ 'file' => 'required|uploaded_file' ]); @@ -120,7 +122,7 @@ public function testMissingKeyUploadedFile($uploaded_file) public function getSamplesMissingKeyFromUploadedFileValue() { - $valid_uploaded_file = [ + $validUploadedFile = [ 'name' => 'foo', 'type' => 'text/plain', 'size' => 1000, @@ -129,14 +131,279 @@ public function getSamplesMissingKeyFromUploadedFileValue() ]; $samples = []; - foreach($valid_uploaded_file as $key => $value) { - $uploaded_file = $valid_uploaded_file; - unset($uploaded_file[$key]); - $samples[] = $uploaded_file; + foreach ($validUploadedFile as $key => $value) { + $uploadedFile = $validUploadedFile; + unset($uploadedFile[$key]); + $samples[] = $uploadedFile; } return $samples; } + public function testValidationShouldCorrectlyResolveMultipleFileUploads() + { + // Test from input files: + // + // + $sampleInputFiles = [ + 'photos' => [ + 'name' => [ + 'a.png', + 'b.jpeg', + ], + 'type' => [ + 'image/png', + 'image/jpeg', + ], + 'size' => [ + 1000, + 2000, + ], + 'tmp_name' => [ + __DIR__.'/a.png', + __DIR__.'/b.jpeg', + ], + 'error' => [ + UPLOAD_ERR_OK, + UPLOAD_ERR_OK, + ] + ] + ]; + + $uploadedFileRule = $this->getMockedUploadedFileRule()->fileTypes('jpeg'); + + $validation = $this->validator->validate($sampleInputFiles, [ + 'photos.*' => ['required', $uploadedFileRule] + ]); + + $this->assertFalse($validation->passes()); + $this->assertEquals($validation->getValidData(), [ + 'photos' => [ + 1 => [ + 'name' => 'b.jpeg', + 'type' => 'image/jpeg', + 'size' => 2000, + 'tmp_name' => __DIR__.'/b.jpeg', + 'error' => UPLOAD_ERR_OK, + ] + ] + ]); + $this->assertEquals($validation->getInvalidData(), [ + 'photos' => [ + 0 => [ + 'name' => 'a.png', + 'type' => 'image/png', + 'size' => 1000, + 'tmp_name' => __DIR__.'/a.png', + 'error' => UPLOAD_ERR_OK, + ] + ] + ]); + } + + public function testValidationShouldCorrectlyResolveAssocFileUploads() + { + // Test from input files: + // + // + $sampleInputFiles = [ + 'photos' => [ + 'name' => [ + 'foo' => 'a.png', + 'bar' => 'b.jpeg', + ], + 'type' => [ + 'foo' => 'image/png', + 'bar' => 'image/jpeg', + ], + 'size' => [ + 'foo' => 1000, + 'bar' => 2000, + ], + 'tmp_name' => [ + 'foo' => __DIR__.'/a.png', + 'bar' => __DIR__.'/b.jpeg', + ], + 'error' => [ + 'foo' => UPLOAD_ERR_OK, + 'bar' => UPLOAD_ERR_OK, + ] + ] + ]; + + $uploadedFileRule = $this->getMockedUploadedFileRule()->fileTypes('jpeg'); + + $validation = $this->validator->validate($sampleInputFiles, [ + 'photos.foo' => ['required', clone $uploadedFileRule], + 'photos.bar' => ['required', clone $uploadedFileRule], + ]); + + $this->assertFalse($validation->passes()); + $this->assertEquals($validation->getValidData(), [ + 'photos' => [ + 'bar' => [ + 'name' => 'b.jpeg', + 'type' => 'image/jpeg', + 'size' => 2000, + 'tmp_name' => __DIR__.'/b.jpeg', + 'error' => UPLOAD_ERR_OK, + ] + ] + ]); + $this->assertEquals($validation->getInvalidData(), [ + 'photos' => [ + 'foo' => [ + 'name' => 'a.png', + 'type' => 'image/png', + 'size' => 1000, + 'tmp_name' => __DIR__.'/a.png', + 'error' => UPLOAD_ERR_OK, + ] + ] + ]); + } + + public function testValidationShouldCorrectlyResolveComplexFileUploads() + { + // Test from input files: + // + // + // + // + $sampleInputFiles = [ + 'files' => [ + 'name' => [ + 'foo' => [ + 'bar' => [ + 'baz' => 'foo-bar-baz.jpeg', + 'qux' => 'foo-bar-qux.png', + ] + ], + 'photos' => [ + 'photos-0.png', + 'photos-1.jpeg', + ] + ], + 'type' => [ + 'foo' => [ + 'bar' => [ + 'baz' => 'image/jpeg', + 'qux' => 'image/png', + ] + ], + 'photos' => [ + 'image/png', + 'image/jpeg', + ] + ], + 'size' => [ + 'foo' => [ + 'bar' => [ + 'baz' => 500, + 'qux' => 750, + ] + ], + 'photos' => [ + 1000, + 2000, + ] + ], + 'tmp_name' => [ + 'foo' => [ + 'bar' => [ + 'baz' => __DIR__.'/foo-bar-baz.jpeg', + 'qux' => __DIR__.'/foo-bar-qux.png', + ] + ], + 'photos' => [ + __DIR__.'/photos-0.png', + __DIR__.'/photos-1.jpeg', + ] + ], + 'error' => [ + 'foo' => [ + 'bar' => [ + 'baz' => UPLOAD_ERR_OK, + 'qux' => UPLOAD_ERR_OK, + ] + ], + 'photos' => [ + UPLOAD_ERR_OK, + UPLOAD_ERR_OK, + ] + ] + ] + ]; + + $uploadedFileRule = $this->getMockedUploadedFileRule()->fileTypes('jpeg'); + + $validation = $this->validator->validate($sampleInputFiles, [ + 'files.foo.bar.baz' => ['required', clone $uploadedFileRule], + 'files.foo.bar.qux' => ['required', clone $uploadedFileRule], + 'files.photos.*' => ['required', clone $uploadedFileRule], + ]); + + $this->assertFalse($validation->passes()); + $this->assertEquals($validation->getValidData(), [ + 'files' => [ + 'foo' => [ + 'bar' => [ + 'baz' => [ + 'name' => 'foo-bar-baz.jpeg', + 'type' => 'image/jpeg', + 'size' => 500, + 'tmp_name' => __DIR__.'/foo-bar-baz.jpeg', + 'error' => UPLOAD_ERR_OK, + ] + ] + ], + 'photos' => [ + 1 => [ + 'name' => 'photos-1.jpeg', + 'type' => 'image/jpeg', + 'size' => 2000, + 'tmp_name' => __DIR__.'/photos-1.jpeg', + 'error' => UPLOAD_ERR_OK, + ] + ] + ] + ]); + $this->assertEquals($validation->getInvalidData(), [ + 'files' => [ + 'foo' => [ + 'bar' => [ + 'qux' => [ + 'name' => 'foo-bar-qux.png', + 'type' => 'image/png', + 'size' => 750, + 'tmp_name' => __DIR__.'/foo-bar-qux.png', + 'error' => UPLOAD_ERR_OK, + ] + ] + ], + 'photos' => [ + 0 => [ + 'name' => 'photos-0.png', + 'type' => 'image/png', + 'size' => 1000, + 'tmp_name' => __DIR__.'/photos-0.png', + 'error' => UPLOAD_ERR_OK, + ], + ] + ] + ]); + } + + public function getMockedUploadedFileRule() + { + $rule = $this->getMockBuilder(UploadedFile::class) + ->setMethods(['isUploadedFile']) + ->getMock(); + + $rule->method('isUploadedFile')->willReturn(true); + + return $rule; + } + public function testRequiredIfRule() { $v1 = $this->validator->validate([ @@ -286,7 +553,7 @@ public function testNonExistentValidationRule() 'name' => "some name" ], [ 'name' => 'required|xxx' - ],[ + ], [ 'name.required' => "Fill in your name", 'xxx' => "Oops" ]); @@ -308,7 +575,7 @@ public function testBeforeRule() $validator2 = $this->validator->make($data, [ 'date' => "required|before:last week" - ],[]); + ], []); $validator2->validate(); @@ -329,7 +596,7 @@ public function testAfterRule() $validator2 = $this->validator->make($data, [ 'date' => "required|after:next year" - ],[]); + ], []); $validator2->validate(); @@ -369,7 +636,8 @@ public function testInternalValidationRuleCanBeOverridden() { $this->validator->allowRuleOverride(true); - $this->validator->addValidator('required', new Required()); //This is a custom rule defined in the fixtures directory + //This is a custom rule defined in the fixtures directory + $this->validator->addValidator('required', new Required()); $data = ['s' => json_encode(['name' => 'space x', 'human' => false])]; @@ -629,7 +897,7 @@ public function testSetCustomMessagesInValidation() public function testCustomMessageInCallbackRule() { $evenNumberValidator = function ($value) { - if (!is_numeric($value) OR $value % 2 !== 0) { + if (!is_numeric($value) or $value % 2 !== 0) { return ":attribute must be even number"; } return true; @@ -1010,4 +1278,143 @@ public function testGetInvalidData() $this->assertFalse(isset($stuffs['two'])); } + public function testRuleInInvalidMessages() + { + $validation = $this->validator->validate([ + 'number' => 1 + ], [ + 'number' => 'in:7,8,9', + ]); + + $this->assertEquals($validation->errors()->first('number'), "The Number only allows '7', '8', or '9'"); + + // Using translation + $this->validator->setTranslation('or', 'atau'); + + $validation = $this->validator->validate([ + 'number' => 1 + ], [ + 'number' => 'in:7,8,9', + ]); + + $this->assertEquals($validation->errors()->first('number'), "The Number only allows '7', '8', atau '9'"); + } + + public function testRuleNotInInvalidMessages() + { + $validation = $this->validator->validate([ + 'number' => 1 + ], [ + 'number' => 'not_in:1,2,3', + ]); + + $this->assertEquals($validation->errors()->first('number'), "The Number is not allowing '1', '2', and '3'"); + + // Using translation + $this->validator->setTranslation('and', 'dan'); + + $validation = $this->validator->validate([ + 'number' => 1 + ], [ + 'number' => 'not_in:1,2,3', + ]); + + $this->assertEquals($validation->errors()->first('number'), "The Number is not allowing '1', '2', dan '3'"); + } + + public function testRuleMimesInvalidMessages() + { + $file = [ + 'name' => 'sample.txt', + 'type' => 'plain/text', + 'tmp_name' => __FILE__, + 'size' => 1000, + 'error' => UPLOAD_ERR_OK, + ]; + + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => 'mimes:jpeg,png,bmp', + ]); + + $expectedMessage = "The Sample file type must be 'jpeg', 'png', or 'bmp'"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Using translation + $this->validator->setTranslation('or', 'atau'); + + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => 'mimes:jpeg,png,bmp', + ]); + + $expectedMessage = "The Sample file type must be 'jpeg', 'png', atau 'bmp'"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + } + + public function testRuleUploadedFileInvalidMessages() + { + $file = [ + 'name' => 'sample.txt', + 'type' => 'plain/text', + 'tmp_name' => __FILE__, + 'size' => 1024 * 1024 * 2, // 2M + 'error' => UPLOAD_ERR_OK, + ]; + + $rule = $this->getMockedUploadedFileRule(); + + // Invalid uploaded file (!is_uploaded_file($file['tmp_name'])) + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => 'uploaded_file', + ]); + + $expectedMessage = "The Sample is not valid uploaded file"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Invalid min size + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => [(clone $rule)->minSize('3M')], + ]); + + $expectedMessage = "The Sample file is too small, minimum size is 3M"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Invalid max size + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => [(clone $rule)->maxSize('1M')], + ]); + + $expectedMessage = "The Sample file is too large, maximum size is 1M"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Invalid file types + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => [(clone $rule)->fileTypes(['jpeg', 'png', 'bmp'])], + ]); + + $expectedMessage = "The Sample file type must be 'jpeg', 'png', or 'bmp'"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Invalid file types with translation + $this->validator->setTranslation('or', 'atau'); + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => [(clone $rule)->fileTypes(['jpeg', 'png', 'bmp'])], + ]); + + $expectedMessage = "The Sample file type must be 'jpeg', 'png', atau 'bmp'"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..6c8c4f5 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,3 @@ +