-
-
Notifications
You must be signed in to change notification settings - Fork 188
/
JsonView.php
316 lines (296 loc) · 12.1 KB
/
JsonView.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
<?php
namespace Neos\Flow\Mvc\View;
/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ControllerContext;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Utility\ObjectAccess;
use Neos\Utility\TypeHandling;
/**
* A JSON view
*
* @api
*/
class JsonView extends AbstractView
{
/**
* Supported options
* @var array
*/
protected $supportedOptions = [
'jsonEncodingOptions' => [0, 'Bitmask of supported Encoding options. See https://php.net/manual/en/json.constants.php', 'integer'],
'datetimeFormat' => [\DateTime::ATOM, 'The datetime format to use for all DateTime objects. See https://www.php.net/manual/en/class.datetime.php#datetime.synopsis', 'string']
];
/**
* Definition for the class name exposure configuration,
* that is, if the class name of an object should also be
* part of the output JSON, if configured.
*
* Setting this value, the object's class name is fully
* put out, including the namespace.
*/
const EXPOSE_CLASSNAME_FULLY_QUALIFIED = 1;
/**
* Puts out only the actual class name without namespace.
* See EXPOSE_CLASSNAME_FULL for the meaning of the constant at all.
*/
const EXPOSE_CLASSNAME_UNQUALIFIED = 2;
/**
* @var ControllerContext
*/
protected $controllerContext;
/**
* Only variables whose name is contained in this array will be rendered
*
* @var array
*/
protected $variablesToRender = ['value'];
/**
* The rendering configuration for this JSON view which
* determines which properties of each variable to render.
*
* The configuration array must have the following structure:
*
* Example 1:
*
* [
* 'variable1' => [
* '_only' => ['property1', 'property2', ...]
* ),
* 'variable2' => [
* '_exclude' => ['property3', 'property4, ...]
* ),
* 'variable3' => [
* '_exclude' => ['secretTitle'],
* '_descend' => [
* 'customer' => [
* '_only' => ['firstName', 'lastName']
* ]
* ]
* ],
* 'somearrayvalue' => [
* '_descendAll' => [
* '_only' => ['property1']
* ]
* ]
* ]
*
* Of variable1 only property1 and property2 will be included.
* Of variable2 all properties except property3 and property4
* are used.
* Of variable3 all properties except secretTitle are included.
*
* If a property value is an array or object, it is not included
* by default. If, however, such a property is listed in a "_descend"
* section, the renderer will descend into this sub structure and
* include all its properties (of the next level).
*
* The configuration of each property in "_descend" has the same syntax
* like at the top level. Therefore - theoretically - infinitely nested
* structures can be configured.
*
* To export indexed arrays the "_descendAll" section can be used to
* include all array keys for the output. The configuration inside a
* "_descendAll" will be applied to each array element.
*
*
* Example 2: exposing object identifier
*
* [
* 'variableFoo' => [
* '_exclude' => ['secretTitle'],
* '_descend' => [
* 'customer' => [ // consider 'customer' being a persisted entity
* '_only' => ['firstName'],
* '_exposeObjectIdentifier' => true,
* '_exposedObjectIdentifierKey' => 'guid'
* ]
* ]
* ]
* ]
*
* Note for entity objects you are able to expose the object's identifier
* also, just add an "_exposeObjectIdentifier" directive set to true and
* an additional property '__identity' will appear keeping the persistence
* identifier. Renaming that property name instead of '__identity' is also
* possible with the directive "_exposedObjectIdentifierKey".
* Example 2 above would output (summarized):
* {"customer":{"firstName":"John","guid":"892693e4-b570-46fe-af71-1ad32918fb64"}}
*
*
* Example 3: exposing object's class name
*
* [
* 'variableFoo' => [
* '_exclude' => ['secretTitle'],
* '_descend' => [
* 'customer' => [ // consider 'customer' being an object
* '_only' => ['firstName'],
* '_exposeClassName' => Neos\Flow\Mvc\View\JsonView::EXPOSE_CLASSNAME_FULLY_QUALIFIED
* ]
* ]
* ]
* ]
*
* The ``_exposeClassName`` is similar to the objectIdentifier one, but the class name is added to the
* JSON object output, for example (summarized):
* {"customer":{"firstName":"John","__class":"Acme\Foo\Domain\Model\Customer"}}
*
* The other option is EXPOSE_CLASSNAME_UNQUALIFIED which only will give the last part of the class
* without the namespace, for example (summarized):
* {"customer":{"firstName":"John","__class":"Customer"}}
* This might be of interest to not provide information about the package or domain structure behind.
*
* @var array
*/
protected $configuration = [];
/**
* @var PersistenceManagerInterface
* @Flow\Inject
*/
protected $persistenceManager;
/**
* Specifies which variables this JsonView should render
* By default only the variable 'value' will be rendered
*
* @param array $variablesToRender
* @return void
* @api
*/
public function setVariablesToRender(array $variablesToRender)
{
$this->variablesToRender = $variablesToRender;
}
/**
* @param array $configuration The rendering configuration for this JSON view
* @return void
*/
public function setConfiguration(array $configuration)
{
$this->configuration = $configuration;
}
/**
* Transforms the value view variable to a serializable
* array represantion using a YAML view configuration and JSON encodes
* the result.
*
* @return string The JSON encoded variables
* @api
*/
public function render()
{
$this->controllerContext->getResponse()->setContentType('application/json');
$propertiesToRender = $this->renderArray();
$options = $this->getOption('jsonEncodingOptions');
return json_encode($propertiesToRender, JSON_THROW_ON_ERROR | $options);
}
/**
* Loads the configuration and transforms the value to a serializable
* array.
*
* @return array|string|int|float|null An array containing the values, ready to be JSON encoded
* @api
*/
protected function renderArray()
{
if (count($this->variablesToRender) === 1) {
$variableName = current($this->variablesToRender);
$valueToRender = $this->variables[$variableName] ?? null;
$configuration = $this->configuration[$variableName] ?? [];
} else {
$valueToRender = [];
foreach ($this->variablesToRender as $variableName) {
$valueToRender[$variableName] = $this->variables[$variableName] ?? null;
}
$configuration = $this->configuration;
}
return $this->transformValue($valueToRender, $configuration);
}
/**
* Transforms a value depending on type recursively using the
* supplied configuration.
*
* @param mixed $value The value to transform
* @param array $configuration Configuration for transforming the value
* @return array|string|int|float|null The transformed value
*/
protected function transformValue($value, array $configuration)
{
if (is_array($value) || $value instanceof \ArrayAccess) {
$array = [];
foreach ($value as $key => $element) {
if (isset($configuration['_descendAll']) && is_array($configuration['_descendAll'])) {
$array[$key] = $this->transformValue($element, $configuration['_descendAll']);
} else {
if (isset($configuration['_only']) && is_array($configuration['_only']) && !in_array($key, $configuration['_only'])) {
continue;
}
if (isset($configuration['_exclude']) && is_array($configuration['_exclude']) && in_array($key, $configuration['_exclude'])) {
continue;
}
$array[$key] = $this->transformValue($element, $configuration[$key] ?? []);
}
}
return $array;
} elseif (is_object($value) && $value instanceof \JsonSerializable) {
return $this->transformValue($value->jsonSerialize(), $configuration);
} elseif (is_object($value)) {
return $this->transformObject($value, $configuration);
} else {
return $value;
}
}
/**
* Traverses the given object structure in order to transform it into an
* array structure.
*
* @param object $object Object to traverse
* @param array $configuration Configuration for transforming the given object or NULL
* @return array|string Object structure as an array
*/
protected function transformObject($object, array $configuration)
{
if ($object instanceof \DateTimeInterface) {
return $object->format($this->getOption('datetimeFormat'));
} else {
$propertyNames = ObjectAccess::getGettablePropertyNames($object);
$propertiesToRender = [];
foreach ($propertyNames as $propertyName) {
if (isset($configuration['_only']) && is_array($configuration['_only']) && !in_array($propertyName, $configuration['_only'])) {
continue;
}
if (isset($configuration['_exclude']) && is_array($configuration['_exclude']) && in_array($propertyName, $configuration['_exclude'])) {
continue;
}
$propertyValue = ObjectAccess::getProperty($object, $propertyName);
if (!is_array($propertyValue) && !is_object($propertyValue)) {
$propertiesToRender[$propertyName] = $propertyValue;
} elseif (isset($configuration['_descend']) && array_key_exists($propertyName, $configuration['_descend'])) {
$propertiesToRender[$propertyName] = $this->transformValue($propertyValue, $configuration['_descend'][$propertyName]);
}
}
if (isset($configuration['_exposeObjectIdentifier']) && $configuration['_exposeObjectIdentifier'] === true) {
if (isset($configuration['_exposedObjectIdentifierKey']) && strlen($configuration['_exposedObjectIdentifierKey']) > 0) {
$identityKey = $configuration['_exposedObjectIdentifierKey'];
} else {
$identityKey = '__identity';
}
$propertiesToRender[$identityKey] = $this->persistenceManager->getIdentifierByObject($object);
}
if (isset($configuration['_exposeClassName']) && ($configuration['_exposeClassName'] === self::EXPOSE_CLASSNAME_FULLY_QUALIFIED || $configuration['_exposeClassName'] === self::EXPOSE_CLASSNAME_UNQUALIFIED)) {
$className = TypeHandling::getTypeForValue($object);
$classNameParts = explode('\\', $className);
$propertiesToRender['__class'] = ($configuration['_exposeClassName'] === self::EXPOSE_CLASSNAME_FULLY_QUALIFIED ? $className : array_pop($classNameParts));
}
return $propertiesToRender;
}
}
}