Passed
Pull Request — master (#925)
by Asmir
02:44
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
/*
6
 * Copyright 2016 Johannes M. Schmitt <[email protected]>
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 *     http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
namespace JMS\Serializer;
22
23
use JMS\Serializer\Accessor\AccessorStrategyInterface;
24
use JMS\Serializer\Construction\ObjectConstructorInterface;
25
use JMS\Serializer\EventDispatcher\EventDispatcher;
26
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
27
use JMS\Serializer\EventDispatcher\ObjectEvent;
28
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
29
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
30
use JMS\Serializer\Exception\CircularReferenceDetectedException;
31
use JMS\Serializer\Exception\ExcludedClassException;
32
use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
33
use JMS\Serializer\Exception\InvalidArgumentException;
34
use JMS\Serializer\Exception\NotAcceptableException;
35
use JMS\Serializer\Exception\RuntimeException;
36
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
37
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
38
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
39
use JMS\Serializer\Handler\HandlerRegistryInterface;
40
use JMS\Serializer\Metadata\ClassMetadata;
41
use Metadata\MetadataFactoryInterface;
42
43
/**
44
 * Handles traversal along the object graph.
45
 *
46
 * This class handles traversal along the graph, and calls different methods
47
 * on visitors, or custom handlers to process its nodes.
48
 *
49
 * @author Johannes M. Schmitt <[email protected]>
50
 */
51
final class SerializationGraphNavigator extends GraphNavigator implements GraphNavigatorInterface
52
{
53
    /**
54
     * @var SerializationVisitorInterface
55
     */
56
    protected $visitor;
57
58
    /**
59
     * @var ExpressionLanguageExclusionStrategy
60
     */
61
    private $expressionExclusionStrategy;
62
63
    private $dispatcher;
64
    private $metadataFactory;
65
    private $handlerRegistry;
66
    /**
67
     * @var AccessorStrategyInterface
68
     */
69
    private $accessor;
70
71
    /**
72
     * @var bool
73
     */
74
    private $shouldSerializeNull;
75
76 275
    public function __construct(
77
        MetadataFactoryInterface $metadataFactory,
78
        HandlerRegistryInterface $handlerRegistry,
79
        AccessorStrategyInterface $accessor,
80
        EventDispatcherInterface $dispatcher = null,
81
        ExpressionEvaluatorInterface $expressionEvaluator = null
82
    )
83
    {
84 275
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
85 275
        $this->metadataFactory = $metadataFactory;
86 275
        $this->handlerRegistry = $handlerRegistry;
87 275
        $this->accessor = $accessor;
88
89 275
        if ($expressionEvaluator) {
90 21
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
91
        }
92 275
    }
93
94 275
    public function initialize(VisitorInterface $visitor, Context $context):void
95
    {
96 275
        parent::initialize($visitor, $context);
97 275
        $this->shouldSerializeNull = $context->shouldSerializeNull();
98 275
    }
99
100
    /**
101
     * Called for each node of the graph that is being traversed.
102
     *
103
     * @param mixed $data the data depends on the direction, and type of visitor
104
     * @param null|array $type array has the format ["name" => string, "params" => array]
105
     * @return mixed the return value depends on the direction, and type of visitor
106
     */
107 274
    public function accept($data, array $type = null)
108
    {
109
        // If the type was not given, we infer the most specific type from the
110
        // input data in serialization mode.
111 274
        if (null === $type) {
112
113 254
            $typeName = \gettype($data);
114 254
            if ('object' === $typeName) {
115 180
                $typeName = \get_class($data);
116
            }
117
118 254
            $type = array('name' => $typeName, 'params' => array());
119
        }
120
        // If the data is null, we have to force the type to null regardless of the input in order to
121
        // guarantee correct handling of null values, and not have any internal auto-casting behavior.
122 142
        else if (null === $data) {
123 5
            $type = array('name' => 'NULL', 'params' => array());
124
        }
125
        // Sometimes data can convey null but is not of a null type.
126
        // Visitors can have the power to add this custom null evaluation
127 274
        if ($this->visitor instanceof NullAwareVisitorInterface && $this->visitor->isNull($data) === true) {
128
            $type = array('name' => 'NULL', 'params' => array());
129
        }
130
131 274
        switch ($type['name']) {
132 274
            case 'NULL':
133 38
                if (!$this->shouldSerializeNull) {
134 15
                    throw new NotAcceptableException();
135
                }
136 23
                return $this->visitor->visitNull($data, $type);
137
138 250
            case 'string':
139 156
                return $this->visitor->visitString((string)$data, $type);
140
141 246
            case 'int':
142 246
            case 'integer':
143 42
                return $this->visitor->visitInteger((int)$data, $type);
144
145 243
            case 'bool':
146 243
            case 'boolean':
147 13
                return $this->visitor->visitBoolean((bool)$data, $type);
148
149 238
            case 'double':
150 229
            case 'float':
151 20
                return $this->visitor->visitDouble((float)$data, $type);
152
153 229
            case 'array':
154 100
                return $this->visitor->visitArray((array)$data, $type);
155
156 191
            case 'resource':
157 1
                $msg = 'Resources are not supported in serialized data.';
158 1
                if (null !== $path = $this->context->getPath()) {
0 ignored issues
show
Bug introduced by
The method getPath() does not exist on JMS\Serializer\Context. It seems like you code against a sub-type of JMS\Serializer\Context such as JMS\Serializer\SerializationContext. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

158
                if (null !== $path = $this->context->/** @scrutinizer ignore-call */ getPath()) {
Loading history...
159 1
                    $msg .= ' Path: ' . $path;
160
                }
161
162 1
                throw new RuntimeException($msg);
163
164
            default:
165
166 190
                if (null !== $data) {
167 190
                    if ($this->context->isVisiting($data)) {
0 ignored issues
show
Bug introduced by
The method isVisiting() does not exist on JMS\Serializer\Context. It seems like you code against a sub-type of JMS\Serializer\Context such as JMS\Serializer\SerializationContext. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

167
                    if ($this->context->/** @scrutinizer ignore-call */ isVisiting($data)) {
Loading history...
168 4
                        throw new CircularReferenceDetectedException();
169
                    }
170 190
                    $this->context->startVisiting($data);
0 ignored issues
show
Bug introduced by
The method startVisiting() does not exist on JMS\Serializer\Context. It seems like you code against a sub-type of JMS\Serializer\Context such as JMS\Serializer\SerializationContext. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

170
                    $this->context->/** @scrutinizer ignore-call */ 
171
                                    startVisiting($data);
Loading history...
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 190
                if (class_exists($type['name'], false) || interface_exists($type['name'], false)) {
176 190
                    if (is_subclass_of($data, $type['name'], false)) {
177 4
                        $type = array('name' => \get_class($data), 'params' => array());
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 190
                if ($this->dispatcher->hasListeners('serializer.pre_serialize', $type['name'], $this->format)) {
184 184
                    $this->dispatcher->dispatch('serializer.pre_serialize', $type['name'], $this->format, $event = new PreSerializeEvent($this->context, $data, $type));
185 184
                    $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 190
                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
192 47
                    $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
193 47
                    $this->context->stopVisiting($data);
0 ignored issues
show
Bug introduced by
The method stopVisiting() does not exist on JMS\Serializer\Context. It seems like you code against a sub-type of JMS\Serializer\Context such as JMS\Serializer\SerializationContext. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

193
                    $this->context->/** @scrutinizer ignore-call */ 
194
                                    stopVisiting($data);
Loading history...
194
195 47
                    return $rs;
196
                }
197
198
199
                /** @var $metadata ClassMetadata */
200 168
                $metadata = $this->metadataFactory->getMetadataForClass($type['name']);
201
202 166
                if ($metadata->usingExpression && $this->expressionExclusionStrategy === null) {
203 2
                    throw new ExpressionLanguageRequiredException("To use conditional exclude/expose in {$metadata->name} you must configure the expression language.");
204
                }
205
206 164
                if ($this->exclusionStrategy->shouldSkipClass($metadata, $this->context)) {
207 10
                    $this->context->stopVisiting($data);
208
209 10
                    throw new ExcludedClassException();
210
                }
211
212 160
                $this->context->pushClassMetadata($metadata);
213
214 160
                foreach ($metadata->preSerializeMethods as $method) {
215 2
                    $method->invoke($data);
216
                }
217
218 160
                $this->visitor->startVisitingObject($metadata, $data, $type, $this->context);
0 ignored issues
show
Unused Code introduced by
The call to JMS\Serializer\Serializa...::startVisitingObject() has too many arguments starting with $this->context. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

218
                $this->visitor->/** @scrutinizer ignore-call */ 
219
                                startVisitingObject($metadata, $data, $type, $this->context);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
219 160
                foreach ($metadata->propertyMetadata as $propertyMetadata) {
220 159
                    if ($this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
221 16
                        continue;
222
                    }
223
224 159
                    if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
225 16
                        continue;
226
                    }
227
228 159
                    $v = $this->accessor->getValue($data, $propertyMetadata);
229
230 157
                    if (null === $v && $this->shouldSerializeNull !== true) {
231 22
                        continue;
232
                    }
233
234 155
                    $this->context->pushPropertyMetadata($propertyMetadata);
235 155
                    $this->visitor->visitProperty($propertyMetadata, $v);
236 154
                    $this->context->popPropertyMetadata();
237
                }
238
239 156
                $this->afterVisitingObject($metadata, $data, $type);
240
241 156
                return $this->visitor->endVisitingObject($metadata, $data, $type);
242
        }
243
    }
244
245 156
    private function afterVisitingObject(ClassMetadata $metadata, $object, array $type)
246
    {
247 156
        $this->context->stopVisiting($object);
248 156
        $this->context->popClassMetadata();
249
250 156
        foreach ($metadata->postSerializeMethods as $method) {
251 2
            $method->invoke($object);
252
        }
253
254 156
        if ($this->dispatcher->hasListeners('serializer.post_serialize', $metadata->name, $this->format)) {
255 2
            $this->dispatcher->dispatch('serializer.post_serialize', $metadata->name, $this->format, new ObjectEvent($this->context, $object, $type));
256
        }
257 156
    }
258
}
259