Browse Source

Add allowEmptyByFlags() and type-specific allowEmpty*() methods

Deprecate allowEmpty() and notEmpty().
Also add isEmpty() and deprecate _fieldIsEmpty().
chinpei215 7 years ago
parent
commit
2d4889151f
2 changed files with 308 additions and 8 deletions
  1. 307 8
      src/Validation/Validator.php
  2. 1 0
      tests/TestCase/Validation/ValidatorTest.php

+ 307 - 8
src/Validation/Validator.php

@@ -38,6 +38,48 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
     const NESTED = '_nested';
 
     /**
+     * A flag for allowEmptyByFlags()
+     *
+     * @var int
+     */
+    const EMPTY_STRING = 1;
+
+    /**
+     * A flag for allowEmptyByFlags()
+     *
+     * @var int
+     */
+    const EMPTY_ARRAY = 2;
+
+    /**
+     * A flag for allowEmptyByFlags()
+     *
+     * @var int
+     */
+    const EMPTY_FILE = 4;
+
+    /**
+     * A flag for allowEmptyByFlags()
+     *
+     * @var int
+     */
+    const EMPTY_DATE = 8;
+
+    /**
+     * A flag for allowEmptyByFlags()
+     *
+     * @var int
+     */
+    const EMPTY_TIME = 16;
+
+    /**
+     * A combination of the all EMPTY_* flags
+     *
+     * @var int
+     */
+    const EMPTY_ALL = self::EMPTY_STRING | self::EMPTY_ARRAY | self::EMPTY_FILE | self::EMPTY_DATE | self::EMPTY_TIME;
+
+    /**
      * Holds the ValidationSet objects array
      *
      * @var array
@@ -83,6 +125,13 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
     protected $_allowEmptyMessages = [];
 
     /**
+     * Contains the flags which specify what is empty for each corresponding field.
+     *
+     * @var array
+     */
+    protected $_allowEmptyFlags = [];
+
+    /**
      * Constructor
      *
      */
@@ -119,7 +168,13 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
             }
 
             $canBeEmpty = $this->_canBeEmpty($field, $context);
-            $isEmpty = $this->_fieldIsEmpty($data[$name]);
+
+            $flags = self::EMPTY_ALL;
+            if (isset($this->_allowEmptyFlags[$name])) {
+                $flags = $this->_allowEmptyFlags[$name];
+            }
+
+            $isEmpty = $this->isEmpty($data[$name], $flags);
 
             if (!$canBeEmpty && $isEmpty) {
                 $errors[$name]['_empty'] = $this->getNotEmptyMessage($name);
@@ -615,6 +670,8 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
      * Because this and `notEmpty()` modify the same internal state, the last
      * method called will take precedence.
      *
+     * @deprecated Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(),
+     *   allowEmptyDate(), allowEmptyTime() or allowEmptyDateTime() instead.
      * @param string|array $field the name of the field or a list of fields
      * @param bool|string|callable $when Indicates when the field is allowed to be empty
      * Valid values are true (always), 'create', 'update'. If a callable is passed then
@@ -624,7 +681,91 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
      */
     public function allowEmpty($field, $when = true, $message = null)
     {
+        return $this->allowEmptyByFlags($field, null, $when, $message);
+    }
+
+    /**
+     * Allows a field to be empty. You can also pass array.
+     * Using an array will let you provide the following keys:
+     *
+     * - `flags` individual flags for field
+     * - `when` individual when condition for field
+     * - 'message' individual message for field
+     *
+     * You can also set flags, when and message for all passed fields, the individual
+     * setting takes precedence over group settings.
+     *
+     * ### Example:
+     *
+     * ```
+     * // Email can be empty
+     * $validator->allowEmptyByFlags('email', Validator::EMPTY_STRING);
+     *
+     * // Email can be empty on create
+     * $validator->allowEmptyByFlags('email', Validator::EMPTY_STRING, 'create');
+     *
+     * // Email can be empty on update
+     * $validator->allowEmptyByFlags('email', Validator::EMPTY_STRING, 'update');
+     *
+     * // Email and subject can be empty on update
+     * $validator->allowEmptyByFlags(['email', 'subject'], Validator::EMPTY_STRING, 'update');
+     *
+     * // Email can be always empty, subject and content can be empty on update.
+     * $validator->allowEmptyByFlags(
+     *      [
+     *          'email' => [
+     *              'when' => true
+     *          ],
+     *          'content' => [
+     *              'message' => 'Content cannot be empty'
+     *          ],
+     *          'subject'
+     *      ],
+     *      Validator::EMPTY_STRING,
+     *      'update'
+     * );
+     * ```
+     *
+     * It is possible to conditionally allow emptiness on a field by passing a callback
+     * as a second argument. The callback will receive the validation context array as
+     * argument:
+     *
+     * ```
+     * $validator->allowEmpty('email', Validator::EMPTY_STRING, function ($context) {
+     *   return !$context['newRecord'] || $context['data']['role'] === 'admin';
+     * });
+     * ```
+     *
+     * If you want to allow other kind of empty data on a field, you need to pass other
+     * flags:
+     *
+     * ```
+     * $validator->allowEmptyByFlags('photo', Validator::EMPTY_FILE);
+     * $validator->allowEmptyByFlags('published', Validator::EMPTY_STRING | Validator::EMPTY_DATE | Validator::EMPTY_TIME);
+     * $validator->allowEmptyByFlags('items', Validator::EMPTY_STRING | Validator::EMPTY_ARRAY);
+     * ```
+     *
+     * You can also use convenience wrappers of this method. The following calls are the
+     * same as above:
+     *
+     * ```
+     * $validator->allowEmptyFile('photo');
+     * $validator->allowEmptyDateTime('published');
+     * $validator->allowEmptyArray('items');
+     * ```
+     *
+     * @param string|array $field the name of the field or a list of fields
+     * @param int|null $flags A bitmask of EMPTY_* flags which specify what is empty
+     * @param bool|string|callable $when Indicates when the field is allowed to be empty
+     * Valid values are true (always), 'create', 'update'. If a callable is passed then
+     * the field will allowed to be empty only when the callback returns true.
+     * @param string|null $message The message to show if the field is not
+     * @return $this
+     */
+    public function allowEmptyByFlags($field, $flags, $when = true, $message = null)
+    {
         $settingsDefault = [
+            'flags' => $flags,
             'when' => $when,
             'message' => $message
         ];
@@ -641,12 +782,127 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
             if ($settings[$fieldName]['message']) {
                 $this->_allowEmptyMessages[$fieldName] = $settings[$fieldName]['message'];
             }
+            if ($settings[$fieldName]['flags'] !== null) {
+                $this->_allowEmptyFlags[$fieldName] = $settings[$fieldName]['flags'];
+            }
         }
 
         return $this;
     }
 
     /**
+     * Allows a field to be an empty string.
+     *
+     * This method is equivalent to calling allowEmptyByFlags() with EMPTY_STRING flag.
+     *
+     * @param string|array $field the name of the field or a list of fields
+     * @param bool|string|callable $when Indicates when the field is allowed to be empty
+     * Valid values are true (always), 'create', 'update'. If a callable is passed then
+     * the field will allowed to be empty only when the callback returns true.
+     * @param string|null $message The message to show if the field is not
+     * @return $this
+     * @see \Cake\Validation\Validator::allowEmptyByFlags() For detail usage
+     */
+    public function allowEmptyString($field, $when = true, $message = null)
+    {
+        return $this->allowEmptyByFlags($field, self::EMPTY_STRING, $when, $message);
+    }
+
+    /**
+     * Allows a field to be an empty array.
+     *
+     * This method is equivalent to calling allowEmptyByFlags() with EMPTY_STRING +
+     * EMPTY_ARRAY flags.
+     *
+     * @param string|array $field the name of the field or a list of fields
+     * @param bool|string|callable $when Indicates when the field is allowed to be empty
+     * Valid values are true (always), 'create', 'update'. If a callable is passed then
+     * the field will allowed to be empty only when the callback returns true.
+     * @param string|null $message The message to show if the field is not
+     * @return $this
+     * @see \Cake\Validation\Validator::allowEmptyByFlags() For detail usage
+     */
+    public function allowEmptyArray($field, $when = true, $message = null)
+    {
+        return $this->allowEmptyByFlags($field, self::EMPTY_STRING | self::EMPTY_ARRAY, $when, $message);
+    }
+
+    /**
+     * Allows a field to be an empty file.
+     *
+     * This method is equivalent to calling allowEmptyByFlags() with EMPTY_FILE flag.
+     *
+     * @param string|array $field the name of the field or a list of fields
+     * @param bool|string|callable $when Indicates when the field is allowed to be empty
+     * Valid values are true (always), 'create', 'update'. If a callable is passed then
+     * the field will allowed to be empty only when the callback returns true.
+     * @param string|null $message The message to show if the field is not
+     * @return $this
+     * @see \Cake\Validation\Validator::allowEmptyByFlags() For detail usage
+     */
+    public function allowEmptyFile($field, $when = true, $message = null)
+    {
+        return $this->allowEmptyByFlags($field, self::EMPTY_FILE, $when, $message);
+    }
+
+    /**
+     * Allows a field to be an empty date.
+     *
+     * This method is equivalent to calling allowEmptyByFlags() with EMPTY_STRING +
+     * EMPTY_DATE flags.
+     *
+     * @param string|array $field the name of the field or a list of fields
+     * @param bool|string|callable $when Indicates when the field is allowed to be empty
+     * Valid values are true (always), 'create', 'update'. If a callable is passed then
+     * the field will allowed to be empty only when the callback returns true.
+     * @param string|null $message The message to show if the field is not
+     * @return $this
+     * @see \Cake\Validation\Validator::allowEmptyByFlags() For detail usage
+     */
+    public function allowEmptyDate($field, $when = true, $message = null)
+    {
+        return $this->allowEmptyByFlags($field, self::EMPTY_STRING | self::EMPTY_DATE, $when, $message);
+    }
+
+    /**
+     * Allows a field to be an empty time.
+     *
+     * This method is equivalent to calling allowEmptyByFlags() with EMPTY_STRING +
+     * EMPTY_TIME flags.
+     *
+     * @param string|array $field the name of the field or a list of fields
+     * @param bool|string|callable $when Indicates when the field is allowed to be empty
+     * Valid values are true (always), 'create', 'update'. If a callable is passed then
+     * the field will allowed to be empty only when the callback returns true.
+     * @param string|null $message The message to show if the field is not
+     * @return $this
+     * @see \Cake\Validation\Validator::allowEmptyByFlags() For detail usage
+     */
+    public function allowEmptyTime($field, $when = true, $message = null)
+    {
+        return $this->allowEmptyByFlags($field, self::EMPTY_STRING | self::EMPTY_TIME, $when, $message);
+    }
+
+    /**
+     * Allows a field to be an empty date/time.
+     *
+     * This method is equivalent to calling allowEmptyByFlags() with EMPTY_STRING +
+     * EMPTY_DATE + EMPTY_TIME flags.
+     *
+     * @param string|array $field the name of the field or a list of fields
+     * @param bool|string|callable $when Indicates when the field is allowed to be empty
+     * Valid values are true (always), 'create', 'update'. If a callable is passed then
+     * the field will allowed to be empty only when the callback returns true.
+     * @param string|null $message The message to show if the field is not
+     * @return $this
+     * @see \Cake\Validation\Validator::allowEmptyByFlags() For detail usage
+     */
+    public function allowEmptyDateTime($field, $when = true, $message = null)
+    {
+        return $this->allowEmptyByFlags($field, self::EMPTY_STRING | self::EMPTY_DATE | self::EMPTY_TIME, $when, $message);
+    }
+
+    /**
      * Converts validator to fieldName => $settings array
      *
      * @param int|string $fieldName name of field
@@ -728,6 +984,9 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
      * Because this and `allowEmpty()` modify the same internal state, the last
      * method called will take precedence.
      *
+     * @deprecated Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(),
+     *   allowEmptyDate(), allowEmptyTime() or allowEmptyDateTime() with oppsite
+     *   conditions instead.
      * @param string|array $field the name of the field or list of fields
      * @param string|null $message The message to show if the field is not
      * @param bool|string|callable $when Indicates when the field is not allowed
@@ -2053,20 +2312,59 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
      *
      * @param mixed $data value to check against
      * @return bool
+     * @deprecated Use isEmpty() instead
      */
     protected function _fieldIsEmpty($data)
     {
-        if (empty($data) && !is_bool($data) && !is_numeric($data)) {
+        return $this->isEmpty($data, self::EMPTY_ALL);
+    }
+
+    /**
+     * Returns true if the field is empty in the passed data array
+     *
+     * @param mixed $data value to check against
+     * @param int $flags A bitmask of EMPTY_* flags which specify what is empty
+     * @return bool
+     */
+    protected function isEmpty($data, $flags)
+    {
+        if ($data === null) {
+            return true;
+        }
+
+        if ($data === '' && ($flags & self::EMPTY_STRING)) {
             return true;
         }
-        $isArray = is_array($data);
-        if ($isArray && (isset($data['year']) || isset($data['hour']))) {
-            $value = implode('', $data);
 
-            return strlen($value) === 0;
+        if ($data === [] && ($flags & self::EMPTY_ARRAY)) {
+            return true;
         }
-        if ($isArray && isset($data['name'], $data['type'], $data['tmp_name'], $data['error'])) {
-            return (int)$data['error'] === UPLOAD_ERR_NO_FILE;
+
+        if (is_array($data)) {
+            if (($flags & self::EMPTY_FILE)
+                && isset($data['name'], $data['type'], $data['tmp_name'], $data['error'])
+                && (int)$data['error'] === UPLOAD_ERR_NO_FILE
+            ) {
+                return true;
+            }
+
+            $allFieldsAreEmpty = true;
+            foreach ($data as $field) {
+                if ($field !== null && $field !== '') {
+                    $allFieldsAreEmpty = false;
+                    break;
+                }
+            }
+
+            if ($allFieldsAreEmpty) {
+                if (($flags & self::EMPTY_DATE) && isset($data['year'])) {
+                    return true;
+                }
+
+                if (($flags & self::EMPTY_TIME) && isset($data['hour'])) {
+                    return true;
+                }
+            }
         }
 
         return false;
@@ -2134,6 +2432,7 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
         return [
             '_presenceMessages' => $this->_presenceMessages,
             '_allowEmptyMessages' => $this->_allowEmptyMessages,
+            '_allowEmptyFlags' => $this->_allowEmptyFlags,
             '_useI18n' => $this->_useI18n,
             '_providers' => array_keys($this->_providers),
             '_fields' => $fields

+ 1 - 0
tests/TestCase/Validation/ValidatorTest.php

@@ -1370,6 +1370,7 @@ class ValidatorTest extends TestCase
             ],
             '_presenceMessages' => [],
             '_allowEmptyMessages' => [],
+            '_allowEmptyFlags' => [],
             '_useI18n' => true,
         ];
         $this->assertEquals($expected, $result);