1<?php
2/**
3 * Copyright 2017 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 */
24namespace Facebook\GraphNodes;
25
26use Facebook\FacebookResponse;
27use 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 */
43class 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);
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) {
198            $subclassName = static::BASE_GRAPH_OBJECT_PREFIX . $subclassName;
199        }
200
201        return $this->castAsGraphNodeOrGraphEdge($this->decodedBody, $subclassName);
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    public function validateResponseCastableAsGraphNode()
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    public function validateResponseCastableAsGraphEdge()
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;
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