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; 25 26use ArrayIterator; 27use IteratorAggregate; 28use ArrayAccess; 29use Facebook\Authentication\AccessToken; 30use Facebook\Exceptions\FacebookSDKException; 31 32/** 33 * Class BatchRequest 34 * 35 * @package Facebook 36 */ 37class FacebookBatchRequest extends FacebookRequest implements IteratorAggregate, ArrayAccess 38{ 39 /** 40 * @var array An array of FacebookRequest entities to send. 41 */ 42 protected $requests; 43 44 /** 45 * @var array An array of files to upload. 46 */ 47 protected $attachedFiles; 48 49 /** 50 * Creates a new Request entity. 51 * 52 * @param FacebookApp|null $app 53 * @param array $requests 54 * @param AccessToken|string|null $accessToken 55 * @param string|null $graphVersion 56 */ 57 public function __construct(FacebookApp $app = null, array $requests = [], $accessToken = null, $graphVersion = null) 58 { 59 parent::__construct($app, $accessToken, 'POST', '', [], null, $graphVersion); 60 61 $this->add($requests); 62 } 63 64 /** 65 * Adds a new request to the array. 66 * 67 * @param FacebookRequest|array $request 68 * @param string|null|array $options Array of batch request options e.g. 'name', 'omit_response_on_success'. 69 * If a string is given, it is the value of the 'name' option. 70 * 71 * @return FacebookBatchRequest 72 * 73 * @throws \InvalidArgumentException 74 */ 75 public function add($request, $options = null) 76 { 77 if (is_array($request)) { 78 foreach ($request as $key => $req) { 79 $this->add($req, $key); 80 } 81 82 return $this; 83 } 84 85 if (!$request instanceof FacebookRequest) { 86 throw new \InvalidArgumentException('Argument for add() must be of type array or FacebookRequest.'); 87 } 88 89 if (null === $options) { 90 $options = []; 91 } elseif (!is_array($options)) { 92 $options = ['name' => $options]; 93 } 94 95 $this->addFallbackDefaults($request); 96 97 // File uploads 98 $attachedFiles = $this->extractFileAttachments($request); 99 100 $name = isset($options['name']) ? $options['name'] : null; 101 102 unset($options['name']); 103 104 $requestToAdd = [ 105 'name' => $name, 106 'request' => $request, 107 'options' => $options, 108 'attached_files' => $attachedFiles, 109 ]; 110 111 $this->requests[] = $requestToAdd; 112 113 return $this; 114 } 115 116 /** 117 * Ensures that the FacebookApp and access token fall back when missing. 118 * 119 * @param FacebookRequest $request 120 * 121 * @throws FacebookSDKException 122 */ 123 public function addFallbackDefaults(FacebookRequest $request) 124 { 125 if (!$request->getApp()) { 126 $app = $this->getApp(); 127 if (!$app) { 128 throw new FacebookSDKException('Missing FacebookApp on FacebookRequest and no fallback detected on FacebookBatchRequest.'); 129 } 130 $request->setApp($app); 131 } 132 133 if (!$request->getAccessToken()) { 134 $accessToken = $this->getAccessToken(); 135 if (!$accessToken) { 136 throw new FacebookSDKException('Missing access token on FacebookRequest and no fallback detected on FacebookBatchRequest.'); 137 } 138 $request->setAccessToken($accessToken); 139 } 140 } 141 142 /** 143 * Extracts the files from a request. 144 * 145 * @param FacebookRequest $request 146 * 147 * @return string|null 148 * 149 * @throws FacebookSDKException 150 */ 151 public function extractFileAttachments(FacebookRequest $request) 152 { 153 if (!$request->containsFileUploads()) { 154 return null; 155 } 156 157 $files = $request->getFiles(); 158 $fileNames = []; 159 foreach ($files as $file) { 160 $fileName = uniqid(); 161 $this->addFile($fileName, $file); 162 $fileNames[] = $fileName; 163 } 164 165 $request->resetFiles(); 166 167 // @TODO Does Graph support multiple uploads on one endpoint? 168 return implode(',', $fileNames); 169 } 170 171 /** 172 * Return the FacebookRequest entities. 173 * 174 * @return array 175 */ 176 public function getRequests() 177 { 178 return $this->requests; 179 } 180 181 /** 182 * Prepares the requests to be sent as a batch request. 183 */ 184 public function prepareRequestsForBatch() 185 { 186 $this->validateBatchRequestCount(); 187 188 $params = [ 189 'batch' => $this->convertRequestsToJson(), 190 'include_headers' => true, 191 ]; 192 $this->setParams($params); 193 } 194 195 /** 196 * Converts the requests into a JSON(P) string. 197 * 198 * @return string 199 */ 200 public function convertRequestsToJson() 201 { 202 $requests = []; 203 foreach ($this->requests as $request) { 204 $options = []; 205 206 if (null !== $request['name']) { 207 $options['name'] = $request['name']; 208 } 209 210 $options += $request['options']; 211 212 $requests[] = $this->requestEntityToBatchArray($request['request'], $options, $request['attached_files']); 213 } 214 215 return json_encode($requests); 216 } 217 218 /** 219 * Validate the request count before sending them as a batch. 220 * 221 * @throws FacebookSDKException 222 */ 223 public function validateBatchRequestCount() 224 { 225 $batchCount = count($this->requests); 226 if ($batchCount === 0) { 227 throw new FacebookSDKException('There are no batch requests to send.'); 228 } elseif ($batchCount > 50) { 229 // Per: https://developers.facebook.com/docs/graph-api/making-multiple-requests#limits 230 throw new FacebookSDKException('You cannot send more than 50 batch requests at a time.'); 231 } 232 } 233 234 /** 235 * Converts a Request entity into an array that is batch-friendly. 236 * 237 * @param FacebookRequest $request The request entity to convert. 238 * @param string|null|array $options Array of batch request options e.g. 'name', 'omit_response_on_success'. 239 * If a string is given, it is the value of the 'name' option. 240 * @param string|null $attachedFiles Names of files associated with the request. 241 * 242 * @return array 243 */ 244 public function requestEntityToBatchArray(FacebookRequest $request, $options = null, $attachedFiles = null) 245 { 246 247 if (null === $options) { 248 $options = []; 249 } elseif (!is_array($options)) { 250 $options = ['name' => $options]; 251 } 252 253 $compiledHeaders = []; 254 $headers = $request->getHeaders(); 255 foreach ($headers as $name => $value) { 256 $compiledHeaders[] = $name . ': ' . $value; 257 } 258 259 $batch = [ 260 'headers' => $compiledHeaders, 261 'method' => $request->getMethod(), 262 'relative_url' => $request->getUrl(), 263 ]; 264 265 // Since file uploads are moved to the root request of a batch request, 266 // the child requests will always be URL-encoded. 267 $body = $request->getUrlEncodedBody()->getBody(); 268 if ($body) { 269 $batch['body'] = $body; 270 } 271 272 $batch += $options; 273 274 if (null !== $attachedFiles) { 275 $batch['attached_files'] = $attachedFiles; 276 } 277 278 return $batch; 279 } 280 281 /** 282 * Get an iterator for the items. 283 * 284 * @return ArrayIterator 285 */ 286 public function getIterator() 287 { 288 return new ArrayIterator($this->requests); 289 } 290 291 /** 292 * @inheritdoc 293 */ 294 public function offsetSet($offset, $value) 295 { 296 $this->add($value, $offset); 297 } 298 299 /** 300 * @inheritdoc 301 */ 302 public function offsetExists($offset) 303 { 304 return isset($this->requests[$offset]); 305 } 306 307 /** 308 * @inheritdoc 309 */ 310 public function offsetUnset($offset) 311 { 312 unset($this->requests[$offset]); 313 } 314 315 /** 316 * @inheritdoc 317 */ 318 public function offsetGet($offset) 319 { 320 return isset($this->requests[$offset]) ? $this->requests[$offset] : null; 321 } 322} 323