Completed
Push — issue/v4/1096-manage-applicati... ( c1709d...2578ad )
by
unknown
07:36 queued 04:09
created

JsonApi::formatItem()   D

Complexity

Conditions 9
Paths 28

Size

Total Lines 37
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 37
rs 4.909
cc 9
eloc 20
nc 28
nop 3
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2016 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
namespace BEdita\API\Utility;
14
15
use Cake\Collection\CollectionInterface;
16
use Cake\Log\Log;
17
use Cake\ORM\Entity;
18
use Cake\ORM\Query;
19
use Cake\Routing\Exception\MissingRouteException;
20
use Cake\Routing\Router;
21
use Cake\Utility\Hash;
22
23
/**
24
 * JSON API formatter API.
25
 *
26
 * @since 4.0.0
27
 */
28
class JsonApi
29
{
30
    /**
31
     * Format single or multiple data items in JSON API format.
32
     *
33
     * @param mixed $items Items to be formatted.
34
     * @param string|null $type Type of items. If missing, an attempt is made to obtain this info from each item's data.
35
     * @return array
36
     * @throws \InvalidArgumentException Throws an exception if `$item` could not be converted to array, or
37
     *      if required key `id` is unset or empty.
38
     */
39
    public static function formatData($items, $type = null)
40
    {
41
        if ($items instanceof Query || $items instanceof CollectionInterface) {
42
            $items = $items->toList();
43
        }
44
45
        if (!is_array($items) || !Hash::numeric(array_keys($items))) {
46
            return static::formatItem($items, $type, false);
47
        }
48
49
        $data = [];
50
        foreach ($items as $item) {
51
            $data[] = static::formatItem($item, $type, true);
52
        }
53
54
        return $data;
55
    }
56
57
    /**
58
     * Extract type and API endpoint for item.
59
     *
60
     * @param \Cake\ORM\Entity|array $item Item.
61
     * @param string|null $type Original item type.
62
     * @return array Array with item's type and API endpoint.
63
     */
64
    protected static function extractType($item, $type)
65
    {
66
        $endpoint = $type;
67
68
        if (isset($item['type'])) {
69
            $type = $item['type'];
70
        }
71
72
        if ($endpoint === null) {
73
            $endpoint = $type;
74
        }
75
76
        return [$type, $endpoint];
77
    }
78
79
    /**
80
     * Build URL to be used in `links` object.
81
     *
82
     * @param string $name Route name.
83
     * @param string $endpoint Endpoint.
84
     * @param string $type Resource type.
85
     * @param array $options Additional options.
86
     * @return string|null
87
     */
88
    protected static function buildUrl($name, $endpoint, $type, array $options = [])
89
    {
90
        $url = null;
91
92
        $options['_name'] = sprintf('api:resources:%s', $name);
93
        $options['controller'] = $type;
94
95
        try {
96
            $url = Router::url($options, true);
97
        } catch (MissingRouteException $e) {
98
            $options['_name'] = sprintf('api:%s:%s', $endpoint, $name);
99
            unset($options['controller']);
100
101
            try {
102
                $url = Router::url($options, true);
103
            } catch (MissingRouteException $e) {
104
                $options['object_type'] = $type;
105
106
                try {
107
                    $url = Router::url($options, true);
108
                } catch (MissingRouteException $e) {
109
                    // Do not halt if route is missing.
110
                    Log::debug('Unable to build URL', compact('name', 'endpoint', 'type', 'options'));
111
                }
112
            }
113
        }
114
115
        return $url;
116
    }
117
118
    /**
119
     * Extract item's ID and attributes.
120
     *
121
     * @param array $item Item's data.
122
     * @return array Array with item's ID, attributes, and metadata.
123
     */
124
    protected static function extractAttributes(array $item)
125
    {
126
        if (empty($item['id'])) {
127
            throw new \InvalidArgumentException('Key `id` is mandatory');
128
        }
129
        $id = (string)$item['id'];
130
        $meta = Hash::get($item, 'meta', []);
131
        unset($item['id'], $item['type'], $item['meta']);
132
133
        array_walk(
134
            $item,
135
            function (&$attribute) {
136
                if ($attribute instanceof \JsonSerializable) {
137
                    $attribute = json_decode(json_encode($attribute), true);
138
                }
139
            }
140
        );
141
142
        if (!empty($item['_joinData'])) {
143
            $meta += $item['_joinData'];
144
        }
145
        unset($item['_joinData']);
146
147
        return [$id, $item, $meta];
148
    }
149
150
    /**
151
     * Extract relationships for an entity.
152
     *
153
     * @param Entity $entity Entity item.
154
     * @param string $endpoint Default API endpoint for entity type.
155
     * @param string|null $type Type of item.
156
     * @return array
157
     */
158
    protected static function extractRelationships(Entity $entity, $endpoint, $type = null)
159
    {
160
        $associations = (array)$entity->get('relationships') ?: [];
161
162
        $relationships = [];
163
        foreach ($associations as $name) {
164
            unset($related, $self);
165
166
            // Relationships.
167
            $self = static::buildUrl('relationships', $endpoint, $type, [
168
                'id' => $entity->id,
169
                'relationship' => $name,
170
            ]);
171
172
            // Related objects.
173
            $related = static::buildUrl('related', $endpoint, $type, [
174
                'related_id' => $entity->id,
175
                'relationship' => $name,
176
            ]);
177
178
            if (empty($self) && empty($related)) {
179
                continue;
180
            }
181
182
            $relationships[$name] = [
183
                'links' => array_filter(compact('related', 'self')),
184
            ];
185
        }
186
187
        return $relationships;
188
    }
189
190
    /**
191
     * Format single data item in JSON API format.
192
     *
193
     * @param \Cake\ORM\Entity|array $item Single entity item to be formatted.
194
     * @param string|null $type Type of item. If missing, an attempt is made to obtain this info from item's data.
195
     * @param bool $showLink Display item url in 'links.self', default is true
196
     * @return array
197
     * @throws \InvalidArgumentException Throws an exception if `$item` could not be converted to array, or
198
     *      if required key `id` is unset or empty.
199
     */
200
    protected static function formatItem($item, $type = null, $showLink = true)
201
    {
202
        if (!is_array($item) && !($item instanceof Entity)) {
203
            throw new \InvalidArgumentException('Unsupported item type');
204
        }
205
206
        list($type, $endpoint) = static::extractType($item, $type);
207
208
        if ($item instanceof Entity) {
209
            $relationships = static::extractRelationships($item, $endpoint, $type);
210
            if (empty($relationships)) {
211
                unset($relationships);
212
            }
213
214
            $item = $item->toArray();
215
        }
216
217
        if (empty($item)) {
218
            return [];
219
        }
220
221
        list($id, $attributes, $meta) = static::extractAttributes($item);
222
        if (empty($attributes)) {
223
            unset($attributes);
224
        }
225
        if (empty($meta)) {
226
            unset($meta);
227
        }
228
229
        if ($showLink) {
230
            $self = static::buildUrl('resource', $endpoint, $type, compact('id'));
231
232
            $links = compact('self');
233
        }
234
235
        return compact('id', 'type', 'attributes', 'links', 'relationships', 'meta');
236
    }
237
238
    /**
239
     * Parse single or multiple data items from JSON API format.
240
     *
241
     * @param array $data Items to be parsed.
242
     * @return array
243
     * @throws \InvalidArgumentException Throws an exception if one of required keys `id` and `type` is unset or empty.
244
     */
245
    public static function parseData(array $data)
246
    {
247
        if (empty($data)) {
248
            return [];
249
        }
250
251
        if (!Hash::numeric(array_keys($data))) {
252
            return static::parseItem($data);
253
        }
254
255
        $items = [];
256
        foreach ($data as $item) {
257
            $items[] = static::parseItem($item);
258
        }
259
260
        return $items;
261
    }
262
263
    /**
264
     * Parse single data item from JSON API format.
265
     *
266
     * @param array $item Item to be parsed.
267
     * @return array
268
     * @throws \InvalidArgumentException Throws an exception if one of required keys `id` and `type` is unset or empty.
269
     */
270
    protected static function parseItem(array $item)
271
    {
272
        if (empty($item['type'])) {
273
            throw new \InvalidArgumentException('Key `type` is mandatory');
274
        }
275
276
        $data = [
277
            'type' => $item['type'],
278
        ];
279
        if (!empty($item['id'])) {
280
            $data['id'] = $item['id'];
281
        }
282
283
        if (isset($item['attributes']) && is_array($item['attributes'])) {
284
            $data += $item['attributes'];
285
        }
286
287
        if (isset($item['meta']) && is_array($item['meta'])) {
288
            $data['_meta'] = $item['meta'];
289
        }
290
291
        return $data;
292
    }
293
}
294