1<?php 2 3declare(strict_types=1); 4 5namespace JMS\Serializer\GraphNavigator; 6 7use JMS\Serializer\Accessor\AccessorStrategyInterface; 8use JMS\Serializer\Construction\ObjectConstructorInterface; 9use JMS\Serializer\DeserializationContext; 10use JMS\Serializer\EventDispatcher\EventDispatcher; 11use JMS\Serializer\EventDispatcher\EventDispatcherInterface; 12use JMS\Serializer\EventDispatcher\ObjectEvent; 13use JMS\Serializer\EventDispatcher\PreDeserializeEvent; 14use JMS\Serializer\Exception\ExpressionLanguageRequiredException; 15use JMS\Serializer\Exception\LogicException; 16use JMS\Serializer\Exception\NotAcceptableException; 17use JMS\Serializer\Exception\RuntimeException; 18use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy; 19use JMS\Serializer\Expression\ExpressionEvaluatorInterface; 20use JMS\Serializer\GraphNavigator; 21use JMS\Serializer\GraphNavigatorInterface; 22use JMS\Serializer\Handler\HandlerRegistryInterface; 23use JMS\Serializer\Metadata\ClassMetadata; 24use JMS\Serializer\NullAwareVisitorInterface; 25use JMS\Serializer\Visitor\DeserializationVisitorInterface; 26use Metadata\MetadataFactoryInterface; 27 28/** 29 * Handles traversal along the object graph. 30 * 31 * This class handles traversal along the graph, and calls different methods 32 * on visitors, or custom handlers to process its nodes. 33 * 34 * @author Johannes M. Schmitt <schmittjoh@gmail.com> 35 */ 36final class DeserializationGraphNavigator extends GraphNavigator implements GraphNavigatorInterface 37{ 38 /** 39 * @var DeserializationVisitorInterface 40 */ 41 protected $visitor; 42 43 /** 44 * @var DeserializationContext 45 */ 46 protected $context; 47 48 /** 49 * @var ExpressionLanguageExclusionStrategy 50 */ 51 private $expressionExclusionStrategy; 52 53 /** 54 * @var EventDispatcherInterface 55 */ 56 private $dispatcher; 57 58 /** 59 * @var MetadataFactoryInterface 60 */ 61 private $metadataFactory; 62 63 /** 64 * @var HandlerRegistryInterface 65 */ 66 private $handlerRegistry; 67 68 /** 69 * @var ObjectConstructorInterface 70 */ 71 private $objectConstructor; 72 /** 73 * @var AccessorStrategyInterface 74 */ 75 private $accessor; 76 77 public function __construct( 78 MetadataFactoryInterface $metadataFactory, 79 HandlerRegistryInterface $handlerRegistry, 80 ObjectConstructorInterface $objectConstructor, 81 AccessorStrategyInterface $accessor, 82 ?EventDispatcherInterface $dispatcher = null, 83 ?ExpressionEvaluatorInterface $expressionEvaluator = null 84 ) { 85 $this->dispatcher = $dispatcher ?: new EventDispatcher(); 86 $this->metadataFactory = $metadataFactory; 87 $this->handlerRegistry = $handlerRegistry; 88 $this->objectConstructor = $objectConstructor; 89 $this->accessor = $accessor; 90 if ($expressionEvaluator) { 91 $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator); 92 } 93 } 94 95 /** 96 * Called for each node of the graph that is being traversed. 97 * 98 * @param mixed $data the data depends on the direction, and type of visitor 99 * @param array|null $type array has the format ["name" => string, "params" => array] 100 * 101 * @return mixed the return value depends on the direction, and type of visitor 102 */ 103 public function accept($data, ?array $type = null) 104 { 105 // If the type was not given, we infer the most specific type from the 106 // input data in serialization mode. 107 if (null === $type) { 108 throw new RuntimeException('The type must be given for all properties when deserializing.'); 109 } 110 // Sometimes data can convey null but is not of a null type. 111 // Visitors can have the power to add this custom null evaluation 112 if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) { 113 $type = ['name' => 'NULL', 'params' => []]; 114 } 115 116 switch ($type['name']) { 117 case 'NULL': 118 return $this->visitor->visitNull($data, $type); 119 120 case 'string': 121 return $this->visitor->visitString($data, $type); 122 123 case 'int': 124 case 'integer': 125 return $this->visitor->visitInteger($data, $type); 126 127 case 'bool': 128 case 'boolean': 129 return $this->visitor->visitBoolean($data, $type); 130 131 case 'double': 132 case 'float': 133 return $this->visitor->visitDouble($data, $type); 134 135 case 'array': 136 return $this->visitor->visitArray($data, $type); 137 138 case 'resource': 139 throw new RuntimeException('Resources are not supported in serialized data.'); 140 141 default: 142 $this->context->increaseDepth(); 143 144 // Trigger pre-serialization callbacks, and listeners if they exist. 145 // Dispatch pre-serialization event before handling data to have ability change type in listener 146 if ($this->dispatcher->hasListeners('serializer.pre_deserialize', $type['name'], $this->format)) { 147 $this->dispatcher->dispatch('serializer.pre_deserialize', $type['name'], $this->format, $event = new PreDeserializeEvent($this->context, $data, $type)); 148 $type = $event->getType(); 149 $data = $event->getData(); 150 } 151 152 // First, try whether a custom handler exists for the given type. This is done 153 // before loading metadata because the type name might not be a class, but 154 // could also simply be an artifical type. 155 if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, $type['name'], $this->format)) { 156 $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context); 157 $this->context->decreaseDepth(); 158 159 return $rs; 160 } 161 162 /** @var ClassMetadata $metadata */ 163 $metadata = $this->metadataFactory->getMetadataForClass($type['name']); 164 165 if ($metadata->usingExpression && !$this->expressionExclusionStrategy) { 166 throw new ExpressionLanguageRequiredException(sprintf('To use conditional exclude/expose in %s you must configure the expression language.', $metadata->name)); 167 } 168 169 if (!empty($metadata->discriminatorMap) && $type['name'] === $metadata->discriminatorBaseClass) { 170 $metadata = $this->resolveMetadata($data, $metadata); 171 } 172 173 if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipClass($metadata, $this->context)) { 174 $this->context->decreaseDepth(); 175 176 return null; 177 } 178 179 $this->context->pushClassMetadata($metadata); 180 181 $object = $this->objectConstructor->construct($this->visitor, $metadata, $data, $type, $this->context); 182 183 $this->visitor->startVisitingObject($metadata, $object, $type); 184 foreach ($metadata->propertyMetadata as $propertyMetadata) { 185 if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) { 186 continue; 187 } 188 189 if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) { 190 continue; 191 } 192 193 if ($propertyMetadata->readOnly) { 194 continue; 195 } 196 197 $this->context->pushPropertyMetadata($propertyMetadata); 198 try { 199 $v = $this->visitor->visitProperty($propertyMetadata, $data); 200 $this->accessor->setValue($object, $v, $propertyMetadata, $this->context); 201 } catch (NotAcceptableException $e) { 202 } 203 $this->context->popPropertyMetadata(); 204 } 205 206 $rs = $this->visitor->endVisitingObject($metadata, $data, $type); 207 $this->afterVisitingObject($metadata, $rs, $type); 208 209 return $rs; 210 } 211 } 212 213 /** 214 * @param mixed $data 215 */ 216 private function resolveMetadata($data, ClassMetadata $metadata): ?ClassMetadata 217 { 218 $typeValue = $this->visitor->visitDiscriminatorMapProperty($data, $metadata); 219 220 if (!isset($metadata->discriminatorMap[$typeValue])) { 221 throw new LogicException(sprintf( 222 'The type value "%s" does not exist in the discriminator map of class "%s". Available types: %s', 223 $typeValue, 224 $metadata->name, 225 implode(', ', array_keys($metadata->discriminatorMap)) 226 )); 227 } 228 229 return $this->metadataFactory->getMetadataForClass($metadata->discriminatorMap[$typeValue]); 230 } 231 232 private function afterVisitingObject(ClassMetadata $metadata, object $object, array $type): void 233 { 234 $this->context->decreaseDepth(); 235 $this->context->popClassMetadata(); 236 237 foreach ($metadata->postDeserializeMethods as $method) { 238 $method->invoke($object); 239 } 240 241 if ($this->dispatcher->hasListeners('serializer.post_deserialize', $metadata->name, $this->format)) { 242 $this->dispatcher->dispatch('serializer.post_deserialize', $metadata->name, $this->format, new ObjectEvent($this->context, $object, $type)); 243 } 244 } 245} 246