1<?php 2 3/* 4 * This file is part of the JsonSchema package. 5 * 6 * For the full copyright and license information, please view the LICENSE 7 * file that was distributed with this source code. 8 */ 9 10namespace JsonSchema\Constraints; 11 12use JsonSchema\Entity\JsonPointer; 13use JsonSchema\Exception\InvalidArgumentException; 14use UnexpectedValueException as StandardUnexpectedValueException; 15 16/** 17 * The TypeConstraint Constraints, validates an element against a given type 18 * 19 * @author Robert Schönthal <seroscho@googlemail.com> 20 * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> 21 */ 22class TypeConstraint extends Constraint 23{ 24 /** 25 * @var array|string[] type wordings for validation error messages 26 */ 27 public static $wording = array( 28 'integer' => 'an integer', 29 'number' => 'a number', 30 'boolean' => 'a boolean', 31 'object' => 'an object', 32 'array' => 'an array', 33 'string' => 'a string', 34 'null' => 'a null', 35 'any' => null, // validation of 'any' is always true so is not needed in message wording 36 0 => null, // validation of a false-y value is always true, so not needed as well 37 ); 38 39 /** 40 * {@inheritdoc} 41 */ 42 public function check(&$value = null, $schema = null, JsonPointer $path = null, $i = null) 43 { 44 $type = isset($schema->type) ? $schema->type : null; 45 $isValid = false; 46 $wording = array(); 47 48 if (is_array($type)) { 49 $this->validateTypesArray($value, $type, $wording, $isValid, $path); 50 } elseif (is_object($type)) { 51 $this->checkUndefined($value, $type, $path); 52 53 return; 54 } else { 55 $isValid = $this->validateType($value, $type); 56 } 57 58 if ($isValid === false) { 59 if (!is_array($type)) { 60 $this->validateTypeNameWording($type); 61 $wording[] = self::$wording[$type]; 62 } 63 $this->addError($path, ucwords(gettype($value)) . ' value found, but ' . 64 $this->implodeWith($wording, ', ', 'or') . ' is required', 'type'); 65 } 66 } 67 68 /** 69 * Validates the given $value against the array of types in $type. Sets the value 70 * of $isValid to true, if at least one $type mateches the type of $value or the value 71 * passed as $isValid is already true. 72 * 73 * @param mixed $value Value to validate 74 * @param array $type TypeConstraints to check agains 75 * @param array $validTypesWording An array of wordings of the valid types of the array $type 76 * @param bool $isValid The current validation value 77 * @param $path 78 */ 79 protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, $path) 80 { 81 foreach ($type as $tp) { 82 // $tp can be an object, if it's a schema instead of a simple type, validate it 83 // with a new type constraint 84 if (is_object($tp)) { 85 if (!$isValid) { 86 $validator = $this->factory->createInstanceFor('type'); 87 $subSchema = new \stdClass(); 88 $subSchema->type = $tp; 89 $validator->check($value, $subSchema, $path, null); 90 $error = $validator->getErrors(); 91 $isValid = !(bool) $error; 92 $validTypesWording[] = self::$wording['object']; 93 } 94 } else { 95 $this->validateTypeNameWording($tp); 96 $validTypesWording[] = self::$wording[$tp]; 97 if (!$isValid) { 98 $isValid = $this->validateType($value, $tp); 99 } 100 } 101 } 102 } 103 104 /** 105 * Implodes the given array like implode() with turned around parameters and with the 106 * difference, that, if $listEnd isn't false, the last element delimiter is $listEnd instead of 107 * $delimiter. 108 * 109 * @param array $elements The elements to implode 110 * @param string $delimiter The delimiter to use 111 * @param bool $listEnd The last delimiter to use (defaults to $delimiter) 112 * 113 * @return string 114 */ 115 protected function implodeWith(array $elements, $delimiter = ', ', $listEnd = false) 116 { 117 if ($listEnd === false || !isset($elements[1])) { 118 return implode($delimiter, $elements); 119 } 120 $lastElement = array_slice($elements, -1); 121 $firsElements = join($delimiter, array_slice($elements, 0, -1)); 122 $implodedElements = array_merge(array($firsElements), $lastElement); 123 124 return join(" $listEnd ", $implodedElements); 125 } 126 127 /** 128 * Validates the given $type, if there's an associated self::$wording. If not, throws an 129 * exception. 130 * 131 * @param string $type The type to validate 132 * 133 * @throws StandardUnexpectedValueException 134 */ 135 protected function validateTypeNameWording($type) 136 { 137 if (!isset(self::$wording[$type])) { 138 throw new StandardUnexpectedValueException( 139 sprintf( 140 'No wording for %s available, expected wordings are: [%s]', 141 var_export($type, true), 142 implode(', ', array_filter(self::$wording))) 143 ); 144 } 145 } 146 147 /** 148 * Verifies that a given value is of a certain type 149 * 150 * @param mixed $value Value to validate 151 * @param string $type TypeConstraint to check against 152 * 153 * @throws InvalidArgumentException 154 * 155 * @return bool 156 */ 157 protected function validateType(&$value, $type) 158 { 159 //mostly the case for inline schema 160 if (!$type) { 161 return true; 162 } 163 164 if ('any' === $type) { 165 return true; 166 } 167 168 if ('object' === $type) { 169 return $this->getTypeCheck()->isObject($value); 170 } 171 172 if ('array' === $type) { 173 return $this->getTypeCheck()->isArray($value); 174 } 175 176 $coerce = $this->factory->getConfig(Constraint::CHECK_MODE_COERCE_TYPES); 177 178 if ('integer' === $type) { 179 if ($coerce) { 180 $value = $this->toInteger($value); 181 } 182 183 return is_int($value); 184 } 185 186 if ('number' === $type) { 187 if ($coerce) { 188 $value = $this->toNumber($value); 189 } 190 191 return is_numeric($value) && !is_string($value); 192 } 193 194 if ('boolean' === $type) { 195 if ($coerce) { 196 $value = $this->toBoolean($value); 197 } 198 199 return is_bool($value); 200 } 201 202 if ('string' === $type) { 203 return is_string($value); 204 } 205 206 if ('email' === $type) { 207 return is_string($value); 208 } 209 210 if ('null' === $type) { 211 return is_null($value); 212 } 213 214 throw new InvalidArgumentException((is_object($value) ? 'object' : $value) . ' is an invalid type for ' . $type); 215 } 216 217 /** 218 * Converts a value to boolean. For example, "true" becomes true. 219 * 220 * @param $value The value to convert to boolean 221 * 222 * @return bool|mixed 223 */ 224 protected function toBoolean($value) 225 { 226 if ($value === 'true') { 227 return true; 228 } 229 230 if ($value === 'false') { 231 return false; 232 } 233 234 return $value; 235 } 236 237 /** 238 * Converts a numeric string to a number. For example, "4" becomes 4. 239 * 240 * @param mixed $value the value to convert to a number 241 * 242 * @return int|float|mixed 243 */ 244 protected function toNumber($value) 245 { 246 if (is_numeric($value)) { 247 return $value + 0; // cast to number 248 } 249 250 return $value; 251 } 252 253 protected function toInteger($value) 254 { 255 if (is_numeric($value) && (int) $value == $value) { 256 return (int) $value; // cast to number 257 } 258 259 return $value; 260 } 261} 262