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