GraphNodeFactory::safelyMakeGraphNode()   B
last analyzed

Complexity

Conditions 6
Paths 16

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 30
rs 8.439
cc 6
eloc 15
nc 16
nop 2
1
<?php
2
/**
3
 * Copyright 2016 Facebook, Inc.
4
 *
5
 * You are hereby granted a non-exclusive, worldwide, royalty-free license to
6
 * use, copy, modify, and distribute this software in source code or binary
7
 * form for use in connection with the web services and APIs provided by
8
 * Facebook.
9
 *
10
 * As with any software that integrates with the Facebook platform, your use
11
 * of this software is subject to the Facebook Developer Principles and
12
 * Policies [http://developers.facebook.com/policy/]. This copyright notice
13
 * shall be included in all copies or substantial portions of the software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
 * DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
namespace Facebook\GraphNodes;
25
26
use Facebook\FacebookResponse;
27
use Facebook\Exceptions\FacebookSDKException;
28
29
/**
30
 * Class GraphNodeFactory
31
 *
32
 * @package Facebook
33
 *
34
 * ## Assumptions ##
35
 * GraphEdge - is ALWAYS a numeric array
36
 * GraphEdge - is ALWAYS an array of GraphNode types
37
 * GraphNode - is ALWAYS an associative array
38
 * GraphNode - MAY contain GraphNode's "recurrable"
39
 * GraphNode - MAY contain GraphEdge's "recurrable"
40
 * GraphNode - MAY contain DateTime's "primitives"
41
 * GraphNode - MAY contain string's "primitives"
42
 */
43
class GraphNodeFactory
44
{
45
    /**
46
     * @const string The base graph object class.
47
     */
48
    const BASE_GRAPH_NODE_CLASS = '\Facebook\GraphNodes\GraphNode';
49
50
    /**
51
     * @const string The base graph edge class.
52
     */
53
    const BASE_GRAPH_EDGE_CLASS = '\Facebook\GraphNodes\GraphEdge';
54
55
    /**
56
     * @const string The graph object prefix.
57
     */
58
    const BASE_GRAPH_OBJECT_PREFIX = '\Facebook\GraphNodes\\';
59
60
    /**
61
     * @var FacebookResponse The response entity from Graph.
62
     */
63
    protected $response;
64
65
    /**
66
     * @var array The decoded body of the FacebookResponse entity from Graph.
67
     */
68
    protected $decodedBody;
69
70
    /**
71
     * Init this Graph object.
72
     *
73
     * @param FacebookResponse $response The response entity from Graph.
74
     */
75
    public function __construct(FacebookResponse $response)
76
    {
77
        $this->response = $response;
78
        $this->decodedBody = $response->getDecodedBody();
79
    }
80
81
    /**
82
     * Tries to convert a FacebookResponse entity into a GraphNode.
83
     *
84
     * @param string|null $subclassName The GraphNode sub class to cast to.
85
     *
86
     * @return GraphNode
87
     *
88
     * @throws FacebookSDKException
89
     */
90
    public function makeGraphNode($subclassName = null)
91
    {
92
        $this->validateResponseAsArray();
93
        $this->validateResponseCastableAsGraphNode();
94
95
        return $this->castAsGraphNodeOrGraphEdge($this->decodedBody, $subclassName);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->castAsGraphNodeOr...edBody, $subclassName); of type Facebook\GraphNodes\Grap...ok\GraphNodes\GraphEdge adds the type Facebook\GraphNodes\GraphEdge to the return on line 95 which is incompatible with the return type documented by Facebook\GraphNodes\Grap...eFactory::makeGraphNode of type Facebook\GraphNodes\GraphNode.
Loading history...
96
    }
97
98
    /**
99
     * Convenience method for creating a GraphAchievement collection.
100
     *
101
     * @return GraphAchievement
102
     *
103
     * @throws FacebookSDKException
104
     */
105
    public function makeGraphAchievement()
106
    {
107
        return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphAchievement');
108
    }
109
110
    /**
111
     * Convenience method for creating a GraphAlbum collection.
112
     *
113
     * @return GraphAlbum
114
     *
115
     * @throws FacebookSDKException
116
     */
117
    public function makeGraphAlbum()
118
    {
119
        return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphAlbum');
120
    }
121
122
    /**
123
     * Convenience method for creating a GraphPage collection.
124
     *
125
     * @return GraphPage
126
     *
127
     * @throws FacebookSDKException
128
     */
129
    public function makeGraphPage()
130
    {
131
        return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphPage');
132
    }
133
134
    /**
135
     * Convenience method for creating a GraphSessionInfo collection.
136
     *
137
     * @return GraphSessionInfo
138
     *
139
     * @throws FacebookSDKException
140
     */
141
    public function makeGraphSessionInfo()
142
    {
143
        return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphSessionInfo');
144
    }
145
146
    /**
147
     * Convenience method for creating a GraphUser collection.
148
     *
149
     * @return GraphUser
150
     *
151
     * @throws FacebookSDKException
152
     */
153
    public function makeGraphUser()
154
    {
155
        return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphUser');
156
    }
157
158
    /**
159
     * Convenience method for creating a GraphEvent collection.
160
     *
161
     * @return GraphEvent
162
     *
163
     * @throws FacebookSDKException
164
     */
165
    public function makeGraphEvent()
166
    {
167
        return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphEvent');
168
    }
169
170
    /**
171
     * Convenience method for creating a GraphGroup collection.
172
     *
173
     * @return GraphGroup
174
     *
175
     * @throws FacebookSDKException
176
     */
177
    public function makeGraphGroup()
178
    {
179
        return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphGroup');
180
    }
181
182
    /**
183
     * Tries to convert a FacebookResponse entity into a GraphEdge.
184
     *
185
     * @param string|null $subclassName The GraphNode sub class to cast the list items to.
186
     * @param boolean     $auto_prefix  Toggle to auto-prefix the subclass name.
187
     *
188
     * @return GraphEdge
189
     *
190
     * @throws FacebookSDKException
191
     */
192
    public function makeGraphEdge($subclassName = null, $auto_prefix = true)
193
    {
194
        $this->validateResponseAsArray();
195
        $this->validateResponseCastableAsGraphEdge();
196
197
        if ($subclassName && $auto_prefix) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subclassName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
198
            $subclassName = static::BASE_GRAPH_OBJECT_PREFIX . $subclassName;
199
        }
200
201
        return $this->castAsGraphNodeOrGraphEdge($this->decodedBody, $subclassName);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->castAsGraphNodeOr...edBody, $subclassName); of type Facebook\GraphNodes\Grap...ok\GraphNodes\GraphEdge adds the type Facebook\GraphNodes\GraphNode to the return on line 201 which is incompatible with the return type documented by Facebook\GraphNodes\Grap...eFactory::makeGraphEdge of type Facebook\GraphNodes\GraphEdge.
Loading history...
202
    }
203
204
    /**
205
     * Validates the decoded body.
206
     *
207
     * @throws FacebookSDKException
208
     */
209
    public function validateResponseAsArray()
210
    {
211
        if (!is_array($this->decodedBody)) {
212
            throw new FacebookSDKException('Unable to get response from Graph as array.', 620);
213
        }
214
    }
215
216
    /**
217
     * Validates that the return data can be cast as a GraphNode.
218
     *
219
     * @throws FacebookSDKException
220
     */
221 View Code Duplication
    public function validateResponseCastableAsGraphNode()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
222
    {
223
        if (isset($this->decodedBody['data']) && static::isCastableAsGraphEdge($this->decodedBody['data'])) {
224
            throw new FacebookSDKException(
225
                'Unable to convert response from Graph to a GraphNode because the response looks like a GraphEdge. Try using GraphNodeFactory::makeGraphEdge() instead.',
226
                620
227
            );
228
        }
229
    }
230
231
    /**
232
     * Validates that the return data can be cast as a GraphEdge.
233
     *
234
     * @throws FacebookSDKException
235
     */
236 View Code Duplication
    public function validateResponseCastableAsGraphEdge()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
237
    {
238
        if (!(isset($this->decodedBody['data']) && static::isCastableAsGraphEdge($this->decodedBody['data']))) {
239
            throw new FacebookSDKException(
240
                'Unable to convert response from Graph to a GraphEdge because the response does not look like a GraphEdge. Try using GraphNodeFactory::makeGraphNode() instead.',
241
                620
242
            );
243
        }
244
    }
245
246
    /**
247
     * Safely instantiates a GraphNode of $subclassName.
248
     *
249
     * @param array       $data         The array of data to iterate over.
250
     * @param string|null $subclassName The subclass to cast this collection to.
251
     *
252
     * @return GraphNode
253
     *
254
     * @throws FacebookSDKException
255
     */
256
    public function safelyMakeGraphNode(array $data, $subclassName = null)
257
    {
258
        $subclassName = $subclassName ?: static::BASE_GRAPH_NODE_CLASS;
259
        static::validateSubclass($subclassName);
260
261
        // Remember the parent node ID
262
        $parentNodeId = isset($data['id']) ? $data['id'] : null;
263
264
        $items = [];
265
266
        foreach ($data as $k => $v) {
267
            // Array means could be recurable
268
            if (is_array($v)) {
269
                // Detect any smart-casting from the $graphObjectMap array.
270
                // This is always empty on the GraphNode collection, but subclasses can define
271
                // their own array of smart-casting types.
272
                $graphObjectMap = $subclassName::getObjectMap();
273
                $objectSubClass = isset($graphObjectMap[$k])
274
                    ? $graphObjectMap[$k]
275
                    : null;
276
277
                // Could be a GraphEdge or GraphNode
278
                $items[$k] = $this->castAsGraphNodeOrGraphEdge($v, $objectSubClass, $k, $parentNodeId);
279
            } else {
280
                $items[$k] = $v;
281
            }
282
        }
283
284
        return new $subclassName($items);
285
    }
286
287
    /**
288
     * Takes an array of values and determines how to cast each node.
289
     *
290
     * @param array       $data         The array of data to iterate over.
291
     * @param string|null $subclassName The subclass to cast this collection to.
292
     * @param string|null $parentKey    The key of this data (Graph edge).
293
     * @param string|null $parentNodeId The parent Graph node ID.
294
     *
295
     * @return GraphNode|GraphEdge
296
     *
297
     * @throws FacebookSDKException
298
     */
299
    public function castAsGraphNodeOrGraphEdge(array $data, $subclassName = null, $parentKey = null, $parentNodeId = null)
300
    {
301
        if (isset($data['data'])) {
302
            // Create GraphEdge
303
            if (static::isCastableAsGraphEdge($data['data'])) {
304
                return $this->safelyMakeGraphEdge($data, $subclassName, $parentKey, $parentNodeId);
305
            }
306
            // Sometimes Graph is a weirdo and returns a GraphNode under the "data" key
307
            $data = $data['data'];
308
        }
309
310
        // Create GraphNode
311
        return $this->safelyMakeGraphNode($data, $subclassName);
312
    }
313
314
    /**
315
     * Return an array of GraphNode's.
316
     *
317
     * @param array       $data         The array of data to iterate over.
318
     * @param string|null $subclassName The GraphNode subclass to cast each item in the list to.
319
     * @param string|null $parentKey    The key of this data (Graph edge).
320
     * @param string|null $parentNodeId The parent Graph node ID.
321
     *
322
     * @return GraphEdge
323
     *
324
     * @throws FacebookSDKException
325
     */
326
    public function safelyMakeGraphEdge(array $data, $subclassName = null, $parentKey = null, $parentNodeId = null)
327
    {
328
        if (!isset($data['data'])) {
329
            throw new FacebookSDKException('Cannot cast data to GraphEdge. Expected a "data" key.', 620);
330
        }
331
332
        $dataList = [];
333
        foreach ($data['data'] as $graphNode) {
334
            $dataList[] = $this->safelyMakeGraphNode($graphNode, $subclassName);
335
        }
336
337
        $metaData = $this->getMetaData($data);
338
339
        // We'll need to make an edge endpoint for this in case it's a GraphEdge (for cursor pagination)
340
        $parentGraphEdgeEndpoint = $parentNodeId && $parentKey ? '/' . $parentNodeId . '/' . $parentKey : null;
0 ignored issues
show
Bug Best Practice introduced by
The expression $parentNodeId of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $parentKey of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
341
        $className = static::BASE_GRAPH_EDGE_CLASS;
342
343
        return new $className($this->response->getRequest(), $dataList, $metaData, $parentGraphEdgeEndpoint, $subclassName);
344
    }
345
346
    /**
347
     * Get the meta data from a list in a Graph response.
348
     *
349
     * @param array $data The Graph response.
350
     *
351
     * @return array
352
     */
353
    public function getMetaData(array $data)
354
    {
355
        unset($data['data']);
356
357
        return $data;
358
    }
359
360
    /**
361
     * Determines whether or not the data should be cast as a GraphEdge.
362
     *
363
     * @param array $data
364
     *
365
     * @return boolean
366
     */
367
    public static function isCastableAsGraphEdge(array $data)
368
    {
369
        if ($data === []) {
370
            return true;
371
        }
372
373
        // Checks for a sequential numeric array which would be a GraphEdge
374
        return array_keys($data) === range(0, count($data) - 1);
375
    }
376
377
    /**
378
     * Ensures that the subclass in question is valid.
379
     *
380
     * @param string $subclassName The GraphNode subclass to validate.
381
     *
382
     * @throws FacebookSDKException
383
     */
384
    public static function validateSubclass($subclassName)
385
    {
386
        if ($subclassName == static::BASE_GRAPH_NODE_CLASS || is_subclass_of($subclassName, static::BASE_GRAPH_NODE_CLASS)) {
387
            return;
388
        }
389
390
        throw new FacebookSDKException('The given subclass "' . $subclassName . '" is not valid. Cannot cast to an object that is not a GraphNode subclass.', 620);
391
    }
392
}
393