1
|
|
|
<?php |
2
|
|
|
namespace Mathielen\ImportEngine\Storage\Parser; |
3
|
|
|
|
4
|
|
|
use JMS\Serializer\Exclusion\GroupsExclusionStrategy; |
5
|
|
|
use JMS\Serializer\SerializationContext; |
6
|
|
|
use Metadata\MetadataFactoryInterface; |
7
|
|
|
use JMS\Serializer\Metadata\PropertyMetadata; |
8
|
|
|
use JMS\Serializer\Naming\PropertyNamingStrategyInterface; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Uses the JMS metadata factory to extract input/output model information |
12
|
|
|
*/ |
13
|
|
|
class JmsMetadataParser |
14
|
|
|
{ |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* @var \Metadata\MetadataFactoryInterface |
18
|
|
|
*/ |
19
|
|
|
private $factory; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var PropertyNamingStrategyInterface |
23
|
|
|
*/ |
24
|
|
|
private $namingStrategy; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Constructor, requires JMS Metadata factory |
28
|
|
|
*/ |
29
|
2 |
|
public function __construct( |
30
|
|
|
MetadataFactoryInterface $factory, |
31
|
|
|
PropertyNamingStrategyInterface $namingStrategy |
32
|
|
|
) { |
33
|
2 |
|
$this->factory = $factory; |
34
|
2 |
|
$this->namingStrategy = $namingStrategy; |
35
|
2 |
|
} |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* {@inheritdoc} |
39
|
|
|
*/ |
40
|
2 |
|
public function parse(array $input) |
41
|
|
|
{ |
42
|
2 |
|
$className = $input['class']; |
43
|
2 |
|
$groups = $input['groups']; |
44
|
|
|
|
45
|
2 |
|
return $this->doParse($className, array(), $groups); |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Recursively parse all metadata for a class |
50
|
|
|
* |
51
|
|
|
* @param string $className Class to get all metadata for |
52
|
|
|
* @param array $visited Classes we've already visited to prevent infinite recursion. |
53
|
|
|
* @param array $groups Serialization groups to include. |
54
|
|
|
* @return array metadata for given class |
55
|
|
|
* @throws \InvalidArgumentException |
56
|
|
|
*/ |
57
|
2 |
|
protected function doParse($className, $visited = array(), array $groups = array()) |
58
|
|
|
{ |
59
|
2 |
|
$meta = $this->factory->getMetadataForClass($className); |
60
|
|
|
|
61
|
2 |
|
if (null === $meta) { |
62
|
|
|
throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className)); |
63
|
|
|
} |
64
|
|
|
|
65
|
2 |
|
$exclusionStrategies = array(); |
66
|
2 |
|
if ($groups) { |
|
|
|
|
67
|
1 |
|
$exclusionStrategies[] = new GroupsExclusionStrategy($groups); |
68
|
|
|
} |
69
|
|
|
|
70
|
2 |
|
$params = array(); |
71
|
|
|
|
72
|
|
|
// iterate over property metadata |
73
|
2 |
|
foreach ($meta->propertyMetadata as $item) { |
74
|
2 |
|
if (!is_null($item->type)) { |
75
|
2 |
|
$name = $this->namingStrategy->translateName($item); |
76
|
|
|
|
77
|
2 |
|
$dataType = $this->processDataType($item); |
78
|
|
|
|
79
|
|
|
// apply exclusion strategies |
80
|
2 |
|
foreach ($exclusionStrategies as $strategy) { |
81
|
1 |
|
if (true === $strategy->shouldSkipProperty($item, SerializationContext::create())) { |
82
|
1 |
|
continue 2; |
83
|
|
|
} |
84
|
|
|
} |
85
|
|
|
|
86
|
2 |
|
$params[$name] = array( |
87
|
2 |
|
'dataType' => $dataType['normalized'], |
88
|
|
|
'required' => false, |
89
|
2 |
|
'readonly' => $item->readOnly, |
90
|
2 |
|
'sinceVersion' => $item->sinceVersion, |
91
|
2 |
|
'untilVersion' => $item->untilVersion, |
92
|
|
|
); |
93
|
|
|
|
94
|
2 |
|
if (!is_null($dataType['class'])) { |
95
|
1 |
|
$params[$name]['class'] = $dataType['class']; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
// if class already parsed, continue, to avoid infinite recursion |
99
|
2 |
|
if (in_array($dataType['class'], $visited)) { |
100
|
1 |
|
continue; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
// check for nested classes with JMS metadata |
104
|
2 |
|
if ($dataType['class'] && null !== $this->factory->getMetadataForClass($dataType['class'])) { |
105
|
1 |
|
$visited[] = $dataType['class']; |
106
|
2 |
|
$params[$name]['children'] = $this->doParse($dataType['class'], $visited, $groups); |
107
|
|
|
} |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
|
111
|
2 |
|
return $params; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Figure out a normalized data type (for documentation), and get a |
116
|
|
|
* nested class name, if available. |
117
|
|
|
* |
118
|
|
|
* @return array |
119
|
|
|
*/ |
120
|
2 |
|
protected function processDataType(PropertyMetadata $item) |
121
|
|
|
{ |
122
|
|
|
// check for a type inside something that could be treated as an array |
123
|
2 |
|
if ($nestedType = $this->getNestedTypeInArray($item)) { |
124
|
2 |
|
if ($this->isPrimitive($nestedType)) { |
125
|
|
|
return array( |
126
|
2 |
|
'normalized' => sprintf("array of %ss", $nestedType), |
127
|
|
|
'class' => null |
128
|
|
|
); |
129
|
|
|
} |
130
|
|
|
|
131
|
2 |
|
$exp = explode("\\", $nestedType); |
132
|
|
|
|
133
|
|
|
return array( |
134
|
2 |
|
'normalized' => sprintf("array of objects (%s)", end($exp)), |
135
|
2 |
|
'class' => $nestedType |
136
|
|
|
); |
137
|
|
|
} |
138
|
|
|
|
139
|
2 |
|
$type = $item->type['name']; |
140
|
|
|
|
141
|
|
|
// could be basic type |
142
|
2 |
|
if ($this->isPrimitive($type)) { |
143
|
|
|
return array( |
144
|
1 |
|
'normalized' => $type, |
145
|
|
|
'class' => null |
146
|
|
|
); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
// we can use type property also for custom handlers, then we don't have here real class name |
150
|
2 |
|
if (!class_exists($type)) { |
151
|
|
|
return array( |
152
|
2 |
|
'normalized' => sprintf("custom handler result for (%s)", $type), |
153
|
|
|
'class' => null |
154
|
|
|
); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
// if we got this far, it's a general class name |
158
|
2 |
|
$exp = explode("\\", $type); |
159
|
|
|
|
160
|
|
|
return array( |
161
|
2 |
|
'normalized' => sprintf("object (%s)", end($exp)), |
162
|
2 |
|
'class' => $type |
163
|
|
|
); |
164
|
|
|
} |
165
|
|
|
|
166
|
2 |
|
protected function isPrimitive($type) |
167
|
|
|
{ |
168
|
2 |
|
return in_array($type, array('boolean', 'integer', 'string', 'float', 'double', 'array', 'DateTime')); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Check the various ways JMS describes values in arrays, and |
173
|
|
|
* get the value type in the array |
174
|
|
|
* |
175
|
|
|
* @param PropertyMetadata $item |
176
|
|
|
* @return string|null |
177
|
|
|
*/ |
178
|
2 |
|
protected function getNestedTypeInArray(PropertyMetadata $item) |
179
|
|
|
{ |
180
|
2 |
|
if (isset($item->type['name']) && in_array($item->type['name'], array('array', 'ArrayCollection'))) { |
181
|
2 |
View Code Duplication |
if (isset($item->type['params'][1]['name'])) { |
|
|
|
|
182
|
|
|
// E.g. array<string, MyNamespaceMyObject> |
183
|
2 |
|
return $item->type['params'][1]['name']; |
184
|
|
|
} |
185
|
2 |
View Code Duplication |
if (isset($item->type['params'][0]['name'])) { |
|
|
|
|
186
|
|
|
// E.g. array<MyNamespaceMyObject> |
187
|
2 |
|
return $item->type['params'][0]['name']; |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
2 |
|
return null; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
} |
195
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.