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