JsonTransformer::toCamelCase()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
rs 9.4286
cc 2
eloc 4
nc 2
nop 3
1
<?php
2
3
namespace NilPortugues\Api\Hal;
4
5
use NilPortugues\Api\Hal\Helpers\AttributeFormatterHelper;
6
use NilPortugues\Api\Hal\Helpers\CuriesHelper;
7
use NilPortugues\Api\Transformer\Helpers\RecursiveDeleteHelper;
8
use NilPortugues\Api\Transformer\Helpers\RecursiveFormatterHelper;
9
use NilPortugues\Api\Transformer\Helpers\RecursiveRenamerHelper;
10
use NilPortugues\Api\Transformer\Transformer;
11
use NilPortugues\Serializer\Serializer;
12
13
/**
14
 * This Transformer follows the JSON+HAL specification.
15
 *
16
 * @link http://stateless.co/hal_specification.html
17
 */
18
class JsonTransformer extends Transformer implements HalTransformer
19
{
20
    const EMBEDDED_KEY = '_embedded';
21
    const META_KEY = '_meta';
22
23
    const LINKS_KEY = '_links';
24
    const LINKS_TEMPLATED_KEY = 'templated';
25
    const LINKS_DEPRECATION_KEY = 'deprecation';
26
    const LINKS_TYPE_KEY = 'type';
27
    const LINKS_NAME_KEY = 'name';
28
    const LINKS_PROFILE_KEY = 'profile';
29
    const LINKS_TITLE_KEY = 'title';
30
    const LINKS_HREF_LANG_KEY = 'hreflang';
31
    const LINKS_HREF = 'href';
32
33
    const MEDIA_PROFILE_KEY = 'profile';
34
35
    const SELF_LINK = 'self';
36
    const FIRST_LINK = 'first';
37
    const LAST_LINK = 'last';
38
    const PREV_LINK = 'prev';
39
    const NEXT_LINK = 'next';
40
    const LINKS_CURIES = 'curies';
41
42
    /**
43
     * @var array
44
     */
45
    protected $curies = [];
46
47
    /**
48
     * @param array $value
49
     *
50
     * @return string
51
     */
52
    public function serialize($value)
53
    {
54
        $this->noMappingGuard();
55
56
        if (\is_array($value) && !empty($value[Serializer::MAP_TYPE])) {
57
            $data = ['total' => count($value)];
58
            unset($value[Serializer::MAP_TYPE]);
59
            foreach ($value[Serializer::SCALAR_VALUE] as $v) {
60
                $data[self::EMBEDDED_KEY][] = $this->serializeObject($v);
61
            }
62
        } else {
63
            $data = $this->serializeObject($value);
64
        }
65
66
        return $this->outputStrategy($data);
67
    }
68
69
    /**
70
     * @param array $data
71
     *
72
     * @return string
73
     */
74
    protected function outputStrategy(array &$data)
75
    {
76
        return \json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
77
    }
78
79
    /**
80
     * @param array $value
81
     *
82
     * @return array
83
     */
84
    protected function serializeObject(array $value)
85
    {
86
        $value = $this->preSerialization($value);
87
        $data = $this->serialization($value);
88
89
        return $this->postSerialization($data);
90
    }
91
92
    /**
93
     * @param array $value
94
     *
95
     * @return array
96
     */
97
    protected function preSerialization(array $value)
98
    {
99
        /** @var \NilPortugues\Api\Mapping\Mapping $mapping */
100
        foreach ($this->mappings as $class => $mapping) {
101
            RecursiveDeleteHelper::deleteProperties($this->mappings, $value, $class);
102
            RecursiveRenamerHelper::renameKeyValue($this->mappings, $value, $class);
103
        }
104
105
        return $value;
106
    }
107
108
    /**
109
     * @param array $data
110
     *d
111
     *
112
     * @return array
113
     */
114
    protected function serialization(array $data)
115
    {
116
        $this->setEmbeddedResources($data);
117
        $this->setResponseLinks($data);
118
119
        return $data;
120
    }
121
122
    /**
123
     * @param array $data
124
     */
125
    protected function setEmbeddedResources(array &$data)
126
    {
127
        foreach ($data as $propertyName => &$value) {
128
            if (\is_array($value)) {
129
                $this->setEmbeddedForResource($data, $value, $propertyName);
130
                $this->setEmbeddedForResourceArray($data, $value, $propertyName);
131
            }
132
        }
133
    }
134
135
    /**
136
     * @param array  $data
137
     * @param array  $value
138
     * @param string $propertyName
139
     */
140
    protected function setEmbeddedForResource(array &$data, array &$value, $propertyName)
141
    {
142
        if (!empty($value[Serializer::CLASS_IDENTIFIER_KEY])) {
143
            $type = $value[Serializer::CLASS_IDENTIFIER_KEY];
144
            if (\is_scalar($type) && !empty($this->mappings[$type])) {
145
                $idProperties = $this->mappings[$type]->getIdProperties();
146
                CuriesHelper::addCurieForResource($this->mappings, $this->curies, $type);
147
148
                if (false === \in_array($propertyName, $idProperties)) {
149
                    $data[self::EMBEDDED_KEY][$propertyName] = $value;
150
151
                    list($idValues, $idProperties) = RecursiveFormatterHelper::getIdPropertyAndValues(
152
                        $this->mappings,
153
                        $value,
154
                        $type
155
                    );
156
157
                    $this->addEmbeddedResourceLinks($data, $propertyName, $idProperties, $idValues, $type);
158
                    $this->addEmbeddedResourceAdditionalLinks($data, $value, $propertyName, $type);
159
                    $this->addEmbeddedResourceLinkToLinks($data, $propertyName, $idProperties, $idValues, $type);
160
161
                    unset($data[$propertyName]);
162
                }
163
            }
164
        } else {
165
            $data[$propertyName] = $value;
166
        }
167
    }
168
169
    /**
170
     * @param array  $data
171
     * @param string $propertyName
172
     * @param array  $idProperties
173
     * @param array  $idValues
174
     * @param string $type
175
     */
176
    protected function addEmbeddedResourceLinks(
177
        array &$data,
178
        $propertyName,
179
        array &$idProperties,
180
        array &$idValues,
181
        $type
182
    ) {
183
        $href = self::buildUrl(
184
            $this->mappings,
185
            $idProperties,
186
            $idValues,
187
            $this->mappings[$type]->getResourceUrl(),
188
            $type
189
        );
190
191
        if ($href != $this->mappings[$type]->getResourceUrl()) {
192
            $data[self::EMBEDDED_KEY][$propertyName][self::LINKS_KEY][self::SELF_LINK][self::LINKS_HREF] = $href;
193
        }
194
    }
195
196
    /**
197
     * @param array  $data
198
     * @param array  $value
199
     * @param string $propertyName
200
     * @param string $type
201
     */
202
    protected function addEmbeddedResourceAdditionalLinks(array &$data, array &$value, $propertyName, $type)
203
    {
204
        $links = [];
205
206
        if (!empty($data[self::EMBEDDED_KEY][$propertyName][self::LINKS_KEY])) {
207
            $links = $data[self::EMBEDDED_KEY][$propertyName][self::LINKS_KEY];
208
        }
209
210
        $data[self::EMBEDDED_KEY][$propertyName][self::LINKS_KEY] = \array_merge(
211
            $links,
212
            $this->addHrefToLinks($this->getResponseAdditionalLinks($value, $type))
213
        );
214
    }
215
216
    /**
217
     * @param array  $copy
218
     * @param string $type
219
     *
220
     * @return array
221
     */
222
    protected function getResponseAdditionalLinks(array $copy, $type)
223
    {
224
        $otherUrls = $this->mappings[$type]->getUrls();
225
        list($idValues, $idProperties) = RecursiveFormatterHelper::getIdPropertyAndValues(
226
            $this->mappings,
227
            $copy,
228
            $type
229
        );
230
231
        $newOtherUrls = $otherUrls;
232
        foreach ($newOtherUrls as &$url) {
233
            $url = self::buildUrl($this->mappings, $idProperties, $idValues, $url, $type);
234
        }
235
236
        if ($newOtherUrls == $otherUrls) {
237
            return [];
238
        }
239
240
        $otherUrls = $newOtherUrls;
241
        foreach ($otherUrls as $propertyName => $value) {
242
            $curieName = $this->getPropertyNameWithCurie($type, $propertyName);
243
            $otherUrls[$curieName] = $value;
244
245
            if ($propertyName !== $curieName) {
246
                unset($otherUrls[$propertyName]);
247
            }
248
        }
249
250
        return $otherUrls;
251
    }
252
253
    /**
254
     * @param string $type
255
     * @param string $propertyName
256
     *
257
     * @return string
258
     */
259
    protected function getPropertyNameWithCurie($type, $propertyName)
260
    {
261
        $curie = $this->mappings[$type]->getCuries();
262
        if (!empty($curie)) {
263
            $propertyName = sprintf(
264
                '%s:%s',
265
                $curie['name'],
266
                self::camelCaseToUnderscore($propertyName)
267
            );
268
        }
269
270
        return $propertyName;
271
    }
272
273
    /**
274
     * @param array  $data
275
     * @param string $propertyName
276
     * @param array  $idProperties
277
     * @param array  $idValues
278
     * @param string $type
279
     */
280
    protected function addEmbeddedResourceLinkToLinks(
281
        array &$data,
282
        $propertyName,
283
        array &$idProperties,
284
        array &$idValues,
285
        $type
286
    ) {
287
        $href = self::buildUrl(
288
            $this->mappings,
289
            $idProperties,
290
            $idValues,
291
            $this->mappings[$type]->getResourceUrl(),
292
            $type
293
        );
294
295
        if ($href != $this->mappings[$type]->getResourceUrl()) {
296
            $data[self::LINKS_KEY][$this->getPropertyNameWithCurie($type, $propertyName)][self::LINKS_HREF] = $href;
297
        }
298
    }
299
300
    /**
301
     * @param array  $data
302
     * @param array  $value
303
     * @param string $propertyName
304
     */
305
    protected function setEmbeddedForResourceArray(array &$data, array &$value, $propertyName)
306
    {
307
        if (!empty($value[Serializer::MAP_TYPE])) {
308
            foreach ($value as &$arrayValue) {
309
                if (\is_array($arrayValue)) {
310
                    $this->setEmbeddedArrayValue($data, $propertyName, $arrayValue);
311
                }
312
            }
313
        }
314
    }
315
316
    /**
317
     * @param array  $data
318
     * @param string $propertyName
319
     * @param array  $arrayValue
320
     */
321
    protected function setEmbeddedArrayValue(array &$data, $propertyName, array &$arrayValue)
322
    {
323
        foreach ($arrayValue as $inArrayProperty => &$inArrayValue) {
324
            if ($this->isResourceInArray($inArrayValue)) {
325
                $this->setEmbeddedResources($inArrayValue);
326
327
                $data[self::EMBEDDED_KEY][$propertyName][$inArrayProperty] = $inArrayValue;
328
                $type = $inArrayValue[Serializer::CLASS_IDENTIFIER_KEY];
329
330
                CuriesHelper::addCurieForResource($this->mappings, $this->curies, $type);
331
                $this->addArrayValueResourceToEmbedded($data, $propertyName, $type, $inArrayProperty, $inArrayValue);
332
333
                unset($data[$propertyName]);
334
            }
335
        }
336
    }
337
338
    /**
339
     * @param mixed $inArrayValue
340
     *
341
     * @return bool
342
     */
343
    protected function isResourceInArray($inArrayValue)
344
    {
345
        return \is_array($inArrayValue) && !empty($inArrayValue[Serializer::CLASS_IDENTIFIER_KEY]);
346
    }
347
348
    /**
349
     * @param array  $data
350
     * @param string $propertyName
351
     * @param string $type
352
     * @param string $inArrayProperty
353
     * @param array  $inArrayValue
354
     */
355
    protected function addArrayValueResourceToEmbedded(
356
        array &$data,
357
        $propertyName,
358
        $type,
359
        $inArrayProperty,
360
        array &$inArrayValue
361
    ) {
362
        list($idValues, $idProperties) = RecursiveFormatterHelper::getIdPropertyAndValues(
363
            $this->mappings,
364
            $inArrayValue,
365
            $type
366
        );
367
368
        $href = self::buildUrl(
369
            $this->mappings,
370
            $idProperties,
371
            $idValues,
372
            $this->mappings[$type]->getResourceUrl(),
373
            $type
374
        );
375
376
        if ($href != $this->mappings[$type]->getResourceUrl()) {
377
            $data[self::EMBEDDED_KEY][$propertyName][$inArrayProperty][self::LINKS_KEY][self::SELF_LINK][self::LINKS_HREF] = $href;
378
        }
379
    }
380
381
    /**
382
     * @param array $data
383
     */
384
    protected function setResponseLinks(array &$data)
385
    {
386
        if (!empty($data[Serializer::CLASS_IDENTIFIER_KEY])) {
387
            $data[self::LINKS_KEY] = \array_merge(
388
                CuriesHelper::buildCuries($this->curies),
389
                $this->addHrefToLinks($this->buildLinks()),
390
                (!empty($data[self::LINKS_KEY])) ? $data[self::LINKS_KEY] : [],
391
                $this->addHrefToLinks($this->getResponseAdditionalLinks($data, $data[Serializer::CLASS_IDENTIFIER_KEY]))
392
            );
393
394
            $data[self::LINKS_KEY] = \array_filter($data[self::LINKS_KEY]);
395
396
            if (empty($data[self::LINKS_KEY])) {
397
                unset($data[self::LINKS_KEY]);
398
            }
399
        }
400
    }
401
402
    /**
403
     * @param array $data
404
     *
405
     * @return array
406
     */
407
    protected function postSerialization(array &$data)
408
    {
409
        RecursiveDeleteHelper::deleteKeys($data, [Serializer::CLASS_IDENTIFIER_KEY]);
410
        RecursiveDeleteHelper::deleteKeys($data, [Serializer::MAP_TYPE]);
411
        RecursiveFormatterHelper::formatScalarValues($data);
412
        AttributeFormatterHelper::flattenObjectsWithSingleKeyScalars($data);
413
        $this->recursiveSetKeysToUnderScore($data);
414
        $this->setResponseMeta($data);
415
416
        return $data;
417
    }
418
419
    /**
420
     * @param array $response
421
     */
422
    protected function setResponseMeta(array &$response)
423
    {
424
        if (!empty($this->meta)) {
425
            $response[self::META_KEY] = $this->meta;
426
        }
427
    }
428
429
    /**
430
     * @param \NilPortugues\Api\Mapping\Mapping[] $mappings
431
     * @param                                     $idProperties
432
     * @param                                     $idValues
433
     * @param                                     $url
434
     * @param                                     $type
435
     *
436
     * @return mixed
437
     */
438
    protected static function buildUrl(array &$mappings, $idProperties, $idValues, $url, $type)
439
    {
440
        $outputUrl = \str_replace($idProperties, $idValues, $url);
441
        if ($outputUrl !== $url) {
442
            return $outputUrl;
443
        }
444
445
        $outputUrl = self::secondPassBuildUrl([$mappings[$type]->getClassAlias()], $idValues, $url);
446
447
        if ($outputUrl !== $url) {
448
            return $outputUrl;
449
        }
450
451
        $className = $mappings[$type]->getClassName();
452
        $className = \explode('\\', $className);
453
        $className = \array_pop($className);
454
455
        $outputUrl = self::secondPassBuildUrl([$className], $idValues, $url);
456
        if ($outputUrl !== $url) {
457
            return $outputUrl;
458
        }
459
460
        return $url;
461
    }
462
463
    /**
464
     * @param $idPropertyName
465
     * @param $idValues
466
     * @param $url
467
     *
468
     * @return mixed
469
     */
470
    protected static function secondPassBuildUrl($idPropertyName, $idValues, $url)
471
    {
472
        if (!empty($idPropertyName)) {
473
            $outputUrl = self::toCamelCase($idPropertyName, $idValues, $url);
474
            if ($url !== $outputUrl) {
475
                return $outputUrl;
476
            }
477
478
            $outputUrl = self::toLowerFirstCamelCase($idPropertyName, $idValues, $url);
479
            if ($url !== $outputUrl) {
480
                return $outputUrl;
481
            }
482
483
            $outputUrl = self::toUnderScore($idPropertyName, $idValues, $url);
484
            if ($url !== $outputUrl) {
485
                return $outputUrl;
486
            }
487
        }
488
489
        return $url;
490
    }
491
492
    /**
493
     * @param $original
494
     * @param $idValues
495
     * @param $url
496
     *
497
     * @return mixed
498
     */
499
    protected static function toCamelCase($original, $idValues, $url)
500
    {
501
        foreach ($original as &$o) {
502
            $o = '{'.self::underscoreToCamelCase(self::camelCaseToUnderscore($o)).'}';
503
        }
504
505
        return \str_replace($original, $idValues, $url);
506
    }
507
508
    /**
509
     * @param $original
510
     * @param $idValues
511
     * @param $url
512
     *
513
     * @return mixed
514
     */
515
    protected static function toLowerFirstCamelCase($original, $idValues, $url)
516
    {
517
        foreach ($original as &$o) {
518
            $o = self::underscoreToCamelCase(self::camelCaseToUnderscore($o));
519
            $o[0] = \strtolower($o[0]);
520
            $o = '{'.$o.'}';
521
        }
522
523
        return \str_replace($original, $idValues, $url);
524
    }
525
526
    /**
527
     * @param $original
528
     * @param $idValues
529
     * @param $url
530
     *
531
     * @return mixed
532
     */
533
    protected static function toUnderScore($original, $idValues, $url)
534
    {
535
        foreach ($original as &$o) {
536
            $o = '{'.self::camelCaseToUnderscore($o).'}';
537
        }
538
539
        return \str_replace($original, $idValues, $url);
540
    }
541
542
    /**
543
     * Transforms a given string from camelCase to under_score style.
544
     *
545
     * @param string $camel
546
     * @param string $splitter
547
     *
548
     * @return string
549
     */
550
    protected static function camelCaseToUnderscore($camel, $splitter = '_')
551
    {
552
        $camel = \preg_replace(
553
            '/(?!^)[[:upper:]][[:lower:]]/',
554
            '$0',
555
            \preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel)
556
        );
557
558
        return \strtolower($camel);
559
    }
560
561
    /**
562
     * Converts a underscore string to camelCase.
563
     *
564
     * @param string $string
565
     *
566
     * @return string
567
     */
568
    protected static function underscoreToCamelCase($string)
569
    {
570
        return \str_replace(' ', '', \ucwords(\strtolower(\str_replace(['_', '-'], ' ', $string))));
571
    }
572
}
573