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

BatchRequest::requestEntityToBatchArray()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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