1<?php 2 3declare(strict_types=1); 4 5namespace JMS\Serializer; 6 7use JMS\Serializer\Exception\LogicException; 8use JMS\Serializer\Exception\NotAcceptableException; 9use JMS\Serializer\Exception\RuntimeException; 10use JMS\Serializer\Metadata\ClassMetadata; 11use JMS\Serializer\Metadata\PropertyMetadata; 12use JMS\Serializer\Visitor\DeserializationVisitorInterface; 13 14final class JsonDeserializationVisitor extends AbstractVisitor implements DeserializationVisitorInterface 15{ 16 /** 17 * @var int 18 */ 19 private $options = 0; 20 21 /** 22 * @var int 23 */ 24 private $depth = 512; 25 26 /** 27 * @var \SplStack 28 */ 29 private $objectStack; 30 31 /** 32 * @var object|null 33 */ 34 private $currentObject; 35 36 public function __construct( 37 int $options = 0, 38 int $depth = 512 39 ) { 40 $this->objectStack = new \SplStack(); 41 $this->options = $options; 42 $this->depth = $depth; 43 } 44 45 /** 46 * {@inheritdoc} 47 */ 48 public function visitNull($data, array $type): void 49 { 50 } 51 52 /** 53 * {@inheritdoc} 54 */ 55 public function visitString($data, array $type): string 56 { 57 return (string) $data; 58 } 59 60 /** 61 * {@inheritdoc} 62 */ 63 public function visitBoolean($data, array $type): bool 64 { 65 return (bool) $data; 66 } 67 68 /** 69 * {@inheritdoc} 70 */ 71 public function visitInteger($data, array $type): int 72 { 73 return (int) $data; 74 } 75 76 /** 77 * {@inheritdoc} 78 */ 79 public function visitDouble($data, array $type): float 80 { 81 return (float) $data; 82 } 83 84 /** 85 * {@inheritdoc} 86 */ 87 public function visitArray($data, array $type): array 88 { 89 if (!\is_array($data)) { 90 throw new RuntimeException(sprintf('Expected array, but got %s: %s', \gettype($data), json_encode($data))); 91 } 92 93 // If no further parameters were given, keys/values are just passed as is. 94 if (!$type['params']) { 95 return $data; 96 } 97 98 switch (\count($type['params'])) { 99 case 1: // Array is a list. 100 $listType = $type['params'][0]; 101 102 $result = []; 103 104 foreach ($data as $v) { 105 $result[] = $this->navigator->accept($v, $listType); 106 } 107 108 return $result; 109 110 case 2: // Array is a map. 111 [$keyType, $entryType] = $type['params']; 112 113 $result = []; 114 115 foreach ($data as $k => $v) { 116 $result[$this->navigator->accept($k, $keyType)] = $this->navigator->accept($v, $entryType); 117 } 118 119 return $result; 120 121 default: 122 throw new RuntimeException(sprintf('Array type cannot have more than 2 parameters, but got %s.', json_encode($type['params']))); 123 } 124 } 125 126 /** 127 * {@inheritdoc} 128 */ 129 public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string 130 { 131 if (isset($data[$metadata->discriminatorFieldName])) { 132 return (string) $data[$metadata->discriminatorFieldName]; 133 } 134 135 throw new LogicException(sprintf( 136 'The discriminator field name "%s" for base-class "%s" was not found in input data.', 137 $metadata->discriminatorFieldName, 138 $metadata->name 139 )); 140 } 141 142 /** 143 * {@inheritdoc} 144 */ 145 public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void 146 { 147 $this->setCurrentObject($object); 148 } 149 150 /** 151 * {@inheritdoc} 152 */ 153 public function visitProperty(PropertyMetadata $metadata, $data) 154 { 155 $name = $metadata->serializedName; 156 157 if (null === $data) { 158 return; 159 } 160 161 if (!\is_array($data)) { 162 throw new RuntimeException(sprintf('Invalid data %s (%s), expected "%s".', json_encode($data), $metadata->type['name'], $metadata->class)); 163 } 164 165 if (true === $metadata->inline) { 166 if (!$metadata->type) { 167 throw new RuntimeException(sprintf( 168 'You must define a type for %s::$%s.', 169 $metadata->class, 170 $metadata->name 171 )); 172 } 173 return $this->navigator->accept($data, $metadata->type); 174 } 175 176 if (!array_key_exists($name, $data)) { 177 throw new NotAcceptableException(); 178 } 179 180 if (!$metadata->type) { 181 throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->class, $metadata->name)); 182 } 183 184 return null !== $data[$name] ? $this->navigator->accept($data[$name], $metadata->type) : null; 185 } 186 187 /** 188 * {@inheritdoc} 189 */ 190 public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object 191 { 192 $obj = $this->currentObject; 193 $this->revertCurrentObject(); 194 195 return $obj; 196 } 197 198 /** 199 * {@inheritdoc} 200 */ 201 public function getResult($data) 202 { 203 return $data; 204 } 205 206 public function setCurrentObject(object $object): void 207 { 208 $this->objectStack->push($this->currentObject); 209 $this->currentObject = $object; 210 } 211 212 public function getCurrentObject(): ?object 213 { 214 return $this->currentObject; 215 } 216 217 public function revertCurrentObject(): ?object 218 { 219 return $this->currentObject = $this->objectStack->pop(); 220 } 221 222 /** 223 * {@inheritdoc} 224 */ 225 public function prepare($str) 226 { 227 $decoded = json_decode($str, true, $this->depth, $this->options); 228 229 switch (json_last_error()) { 230 case JSON_ERROR_NONE: 231 return $decoded; 232 233 case JSON_ERROR_DEPTH: 234 throw new RuntimeException('Could not decode JSON, maximum stack depth exceeded.'); 235 236 case JSON_ERROR_STATE_MISMATCH: 237 throw new RuntimeException('Could not decode JSON, underflow or the nodes mismatch.'); 238 239 case JSON_ERROR_CTRL_CHAR: 240 throw new RuntimeException('Could not decode JSON, unexpected control character found.'); 241 242 case JSON_ERROR_SYNTAX: 243 throw new RuntimeException('Could not decode JSON, syntax error - malformed JSON.'); 244 245 case JSON_ERROR_UTF8: 246 throw new RuntimeException('Could not decode JSON, malformed UTF-8 characters (incorrectly encoded?)'); 247 248 default: 249 throw new RuntimeException('Could not decode JSON.'); 250 } 251 } 252} 253