Passed
Pull Request — master (#888)
by Tobias
01:50
created

BatchRequest::add()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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