Completed
Push — master ( 7113c4...8875d3 )
by Yassine
14s
created

BatchRequest::add()   C

Complexity

Conditions 7
Paths 9

Size

Total Lines 39
Code Lines 21

Duplication

Lines 5
Ratio 12.82 %

Code Coverage

Tests 21
CRAP Score 7

Importance

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