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