Passed
Pull Request — master (#964)
by Asmir
14:09
created

SerializationGraphNavigator::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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