Passed
Branch master (bf85d9)
by Johannes
05:40
created

SerializationGraphNavigator   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 179
Duplicated Lines 0 %

Test Coverage

Coverage 98.78%

Importance

Changes 0
Metric Value
dl 0
loc 179
ccs 81
cts 82
cp 0.9878
rs 8.3999
c 0
b 0
f 0
wmc 38

3 Methods

Rating   Name   Duplication   Size   Complexity  
F accept() 0 131 32
A afterVisitingObject() 0 11 3
A __construct() 0 12 3
1
<?php
2
3
/*
4
 * Copyright 2016 Johannes M. Schmitt <[email protected]>
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace JMS\Serializer;
20
21
use JMS\Serializer\Construction\ObjectConstructorInterface;
22
use JMS\Serializer\EventDispatcher\EventDispatcher;
23
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
24
use JMS\Serializer\EventDispatcher\ObjectEvent;
25
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
26
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
27
use JMS\Serializer\Exception\CircularReferenceDetectedException;
28
use JMS\Serializer\Exception\ExcludedClassException;
29
use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
30
use JMS\Serializer\Exception\InvalidArgumentException;
31
use JMS\Serializer\Exception\RuntimeException;
32
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
33
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
34
use JMS\Serializer\Handler\HandlerRegistryInterface;
35
use JMS\Serializer\Metadata\ClassMetadata;
36
use Metadata\MetadataFactoryInterface;
37
38
/**
39
 * Handles traversal along the object graph.
40
 *
41
 * This class handles traversal along the graph, and calls different methods
42
 * on visitors, or custom handlers to process its nodes.
43
 *
44
 * @author Johannes M. Schmitt <[email protected]>
45
 */
46
final class SerializationGraphNavigator implements GraphNavigatorInterface
47
{
48
    /**
49
     * @var ExpressionLanguageExclusionStrategy
50
     */
51
    private $expressionExclusionStrategy;
52
53
    private $dispatcher;
54
    private $metadataFactory;
55
    private $handlerRegistry;
56
57 304
    public function __construct(
58
        MetadataFactoryInterface $metadataFactory,
59
        HandlerRegistryInterface $handlerRegistry,
60
        EventDispatcherInterface $dispatcher = null,
61
        ExpressionEvaluatorInterface $expressionEvaluator = null
62
    )
63
    {
64 304
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
65 304
        $this->metadataFactory = $metadataFactory;
66 304
        $this->handlerRegistry = $handlerRegistry;
67 304
        if ($expressionEvaluator) {
68 21
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
69
        }
70 304
    }
71
72
    /**
73
     * Called for each node of the graph that is being traversed.
74
     *
75
     * @param mixed $data the data depends on the direction, and type of visitor
76
     * @param null|array $type array has the format ["name" => string, "params" => array]
77
     * @param Context|SerializationContext $context
78
     * @return mixed the return value depends on the direction, and type of visitor
79
     */
80 256
    public function accept($data, array $type = null, Context $context)
81
    {
82 256
        $visitor = $context->getVisitor();
83
84
        // If the type was not given, we infer the most specific type from the
85
        // input data in serialization mode.
86 256
        if (null === $type) {
87
88 236
            $typeName = \gettype($data);
89 236
            if ('object' === $typeName) {
90 174
                $typeName = \get_class($data);
91
            }
92
93 236
            $type = array('name' => $typeName, 'params' => array());
94
        }
95
        // If the data is null, we have to force the type to null regardless of the input in order to
96
        // guarantee correct handling of null values, and not have any internal auto-casting behavior.
97 139
        else if (null === $data) {
98 5
            $type = array('name' => 'NULL', 'params' => array());
99
        }
100
        // Sometimes data can convey null but is not of a null type.
101
        // Visitors can have the power to add this custom null evaluation
102 256
        if ($visitor instanceof NullAwareVisitorInterface && $visitor->isNull($data) === true) {
103
            $type = array('name' => 'NULL', 'params' => array());
104
        }
105
106 256
        switch ($type['name']) {
107 256
            case 'NULL':
108 23
                return $visitor->visitNull($data, $type);
109
110 244
            case 'string':
111 150
                return $visitor->visitString($data, $type);
112
113 240
            case 'int':
114 240
            case 'integer':
115 42
                return $visitor->visitInteger($data, $type);
116
117 237
            case 'bool':
118 237
            case 'boolean':
119 12
                return $visitor->visitBoolean($data, $type);
120
121 232
            case 'double':
122 223
            case 'float':
123 20
                return $visitor->visitDouble($data, $type);
124
125 223
            case 'array':
126 98
                return $visitor->visitArray($data, $type);
127
128 185
            case 'resource':
129 1
                $msg = 'Resources are not supported in serialized data.';
130 1
                if (null !== $path = $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

130
                if (null !== $path = $context->/** @scrutinizer ignore-call */ getPath()) {
Loading history...
131 1
                    $msg .= ' Path: ' . $path;
132
                }
133
134 1
                throw new RuntimeException($msg);
135
136
            default:
137
138 184
                if (null !== $data) {
139 184
                    if ($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

139
                    if ($context->/** @scrutinizer ignore-call */ isVisiting($data)) {
Loading history...
140 4
                        throw new CircularReferenceDetectedException();
141
                    }
142 184
                    $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

142
                    $context->/** @scrutinizer ignore-call */ 
143
                              startVisiting($data);
Loading history...
143
                }
144
145
                // If we're serializing a polymorphic type, then we'll be interested in the
146
                // metadata for the actual type of the object, not the base class.
147 184
                if (class_exists($type['name'], false) || interface_exists($type['name'], false)) {
148 184
                    if (is_subclass_of($data, $type['name'], false)) {
149 4
                        $type = array('name' => \get_class($data), 'params' => array());
150
                    }
151
                }
152
153 184
                $format = $context->getFormat();
154
155
                // Trigger pre-serialization callbacks, and listeners if they exist.
156
                // Dispatch pre-serialization event before handling data to have ability change type in listener
157 184
                if ($this->dispatcher->hasListeners('serializer.pre_serialize', $type['name'], $format)) {
158 178
                    $this->dispatcher->dispatch('serializer.pre_serialize', $type['name'], $format, $event = new PreSerializeEvent($context, $data, $type));
159 178
                    $type = $event->getType();
160
                }
161
162
                // First, try whether a custom handler exists for the given type. This is done
163
                // before loading metadata because the type name might not be a class, but
164
                // could also simply be an artifical type.
165 184
                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $format)) {
166 47
                    $rs = \call_user_func($handler, $visitor, $data, $type, $context);
167 47
                    $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

167
                    $context->/** @scrutinizer ignore-call */ 
168
                              stopVisiting($data);
Loading history...
168
169 47
                    return $rs;
170
                }
171
172 162
                $exclusionStrategy = $context->getExclusionStrategy();
173
174
                /** @var $metadata ClassMetadata */
175 162
                $metadata = $this->metadataFactory->getMetadataForClass($type['name']);
176
177 160
                if ($metadata->usingExpression && $this->expressionExclusionStrategy === null) {
178 2
                    throw new ExpressionLanguageRequiredException("To use conditional exclude/expose in {$metadata->name} you must configure the expression language.");
179
                }
180
181 158
                if ($exclusionStrategy->shouldSkipClass($metadata, $context)) {
182 10
                    $context->stopVisiting($data);
183
184 10
                    throw new ExcludedClassException();
185
                }
186
187 154
                $context->pushClassMetadata($metadata);
188
189 154
                foreach ($metadata->preSerializeMethods as $method) {
190 2
                    $method->invoke($data);
191
                }
192
193 154
                $visitor->startVisitingObject($metadata, $data, $type, $context);
0 ignored issues
show
Unused Code introduced by
The call to JMS\Serializer\Deseriali...::startVisitingObject() has too many arguments starting with $context. ( Ignorable by Annotation )

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

193
                $visitor->/** @scrutinizer ignore-call */ 
194
                          startVisitingObject($metadata, $data, $type, $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...
Unused Code introduced by
The call to JMS\Serializer\Serializa...::startVisitingObject() has too many arguments starting with $context. ( Ignorable by Annotation )

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

193
                $visitor->/** @scrutinizer ignore-call */ 
194
                          startVisitingObject($metadata, $data, $type, $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...
194 154
                foreach ($metadata->propertyMetadata as $propertyMetadata) {
195 153
                    if ($exclusionStrategy->shouldSkipProperty($propertyMetadata, $context)) {
196 12
                        continue;
197
                    }
198
199 153
                    if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $context)) {
200 16
                        continue;
201
                    }
202
203 153
                    $context->pushPropertyMetadata($propertyMetadata);
204 153
                    $visitor->visitProperty($propertyMetadata, $data);
205 150
                    $context->popPropertyMetadata();
206
                }
207
208 150
                $this->afterVisitingObject($metadata, $data, $type, $context, $format);
209
210 150
                return $visitor->endVisitingObject($metadata, $data, $type);
211
        }
212
    }
213
214 150
    private function afterVisitingObject(ClassMetadata $metadata, $object, array $type, Context $context, $format)
215
    {
216 150
        $context->stopVisiting($object);
217 150
        $context->popClassMetadata();
218
219 150
        foreach ($metadata->postSerializeMethods as $method) {
220 2
            $method->invoke($object);
221
        }
222
223 150
        if ($this->dispatcher->hasListeners('serializer.post_serialize', $metadata->name, $format)) {
224 2
            $this->dispatcher->dispatch('serializer.post_serialize', $metadata->name, $format, new ObjectEvent($context, $object, $type));
225
        }
226 150
    }
227
}
228