GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 22f0d5...0f2783 )
by Freek
19s
created

Client   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 640
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 64
lcom 1
cbo 11
dl 0
loc 640
rs 3.24
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 3
A createFolder() 0 12 1
A createSharedLinkWithSettings() 0 12 2
A listSharedLinks() 0 12 2
A delete() 0 8 1
A download() 0 10 1
A getMetadata() 0 8 1
A getThumbnail() 0 12 1
A listFolderContinue() 0 4 1
A shouldUploadChunked() 0 14 4
A isPipe() 0 4 2
A upload() 0 19 2
A uploadChunked() 0 16 4
B uploadChunk() 0 31 7
A uploadSessionStart() 0 11 2
A uploadSessionAppend() 0 11 3
A uploadSessionFinish() 0 17 2
A getAccountInfo() 0 4 1
A revokeToken() 0 4 1
A normalizePath() 0 10 3
A getEndpointUrl() 0 8 2
A contentEndpointRequest() 0 19 3
A rpcEndpointRequest() 0 18 3
A determineException() 0 8 2
A getStream() 0 16 3
A getAccessToken() 0 4 1
A setAccessToken() 0 6 1
A getHeaders() 0 6 1
A copy() 0 9 1
A getTemporaryLink() 0 10 1
A listFolder() 0 9 1
A move() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Spatie\Dropbox;
4
5
use Exception;
6
use GuzzleHttp\Psr7;
7
use GuzzleHttp\Psr7\PumpStream;
8
use GuzzleHttp\Psr7\StreamWrapper;
9
use Psr\Http\Message\StreamInterface;
10
use GuzzleHttp\Client as GuzzleClient;
11
use Psr\Http\Message\ResponseInterface;
12
use GuzzleHttp\Exception\ClientException;
13
use Spatie\Dropbox\Exceptions\BadRequest;
14
use GuzzleHttp\Exception\RequestException;
15
16
class Client
17
{
18
    const THUMBNAIL_FORMAT_JPEG = 'jpeg';
19
    const THUMBNAIL_FORMAT_PNG = 'png';
20
21
    const THUMBNAIL_SIZE_XS = 'w32h32';
22
    const THUMBNAIL_SIZE_S = 'w64h64';
23
    const THUMBNAIL_SIZE_M = 'w128h128';
24
    const THUMBNAIL_SIZE_L = 'w640h480';
25
    const THUMBNAIL_SIZE_XL = 'w1024h768';
26
27
    const MAX_CHUNK_SIZE = 1024 * 1024 * 150;
28
29
    const UPLOAD_SESSION_START = 0;
30
    const UPLOAD_SESSION_APPEND = 1;
31
32
    /** @var string */
33
    protected $accessToken;
34
35
    /** @var \GuzzleHttp\Client */
36
    protected $client;
37
38
    /** @var int */
39
    protected $maxChunkSize;
40
41
    /** @var int */
42
    protected $maxUploadChunkRetries;
43
44
    /**
45
     * @param string $accessToken
46
     * @param GuzzleClient|null $client
47
     * @param int $maxChunkSize Set max chunk size per request (determines when to switch from "one shot upload" to upload session and defines chunk size for uploads via session).
48
     * @param int $maxUploadChunkRetries How many times to retry an upload session start or append after RequestException.
49
     */
50
    public function __construct(string $accessToken, GuzzleClient $client = null, int $maxChunkSize = self::MAX_CHUNK_SIZE, int $maxUploadChunkRetries = 0)
51
    {
52
        $this->accessToken = $accessToken;
53
54
        $this->client = $client ?? new GuzzleClient();
55
56
        $this->maxChunkSize = ($maxChunkSize < self::MAX_CHUNK_SIZE ? ($maxChunkSize > 1 ? $maxChunkSize : 1) : self::MAX_CHUNK_SIZE);
57
        $this->maxUploadChunkRetries = $maxUploadChunkRetries;
58
    }
59
60
    /**
61
     * Copy a file or folder to a different location in the user's Dropbox.
62
     *
63
     * If the source path is a folder all its contents will be copied.
64
     *
65
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-copy_v2
66
     */
67
    public function copy(string $fromPath, string $toPath): array
68
    {
69
        $parameters = [
70
            'from_path' => $this->normalizePath($fromPath),
71
            'to_path' => $this->normalizePath($toPath),
72
        ];
73
74
        return $this->rpcEndpointRequest('files/copy_v2', $parameters);
75
    }
76
77
    /**
78
     * Create a folder at a given path.
79
     *
80
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-create_folder
81
     */
82
    public function createFolder(string $path): array
83
    {
84
        $parameters = [
85
            'path' => $this->normalizePath($path),
86
        ];
87
88
        $object = $this->rpcEndpointRequest('files/create_folder', $parameters);
89
90
        $object['.tag'] = 'folder';
91
92
        return $object;
93
    }
94
95
    /**
96
     * Create a shared link with custom settings.
97
     *
98
     * If no settings are given then the default visibility is RequestedVisibility.public.
99
     * The resolved visibility, though, may depend on other aspects such as team and
100
     * shared folder settings). Only for paid users.
101
     *
102
     * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-create_shared_link_with_settings
103
     */
104
    public function createSharedLinkWithSettings(string $path, array $settings = [])
105
    {
106
        $parameters = [
107
            'path' => $this->normalizePath($path),
108
        ];
109
110
        if (count($settings)) {
111
            $parameters = array_merge(compact('settings'), $parameters);
112
        }
113
114
        return $this->rpcEndpointRequest('sharing/create_shared_link_with_settings', $parameters);
115
    }
116
117
    /**
118
     * List shared links.
119
     *
120
     * For empty path returns a list of all shared links. For non-empty path
121
     * returns a list of all shared links with access to the given path.
122
     *
123
     * If direct_only is set true, only direct links to the path will be returned, otherwise
124
     * it may return link to the path itself and parent folders as described on docs.
125
     *
126
     * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-list_shared_links
127
     */
128
    public function listSharedLinks(string $path = null, bool $direct_only = false, string $cursor = null): array
129
    {
130
        $parameters = [
131
            'path' => $path ? $this->normalizePath($path) : null,
132
            'cursor' => $cursor,
133
            'direct_only' => $direct_only,
134
        ];
135
136
        $body = $this->rpcEndpointRequest('sharing/list_shared_links', $parameters);
137
138
        return $body['links'];
139
    }
140
141
    /**
142
     * Delete the file or folder at a given path.
143
     *
144
     * If the path is a folder, all its contents will be deleted too.
145
     * A successful response indicates that the file or folder was deleted.
146
     *
147
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-delete
148
     */
149
    public function delete(string $path): array
150
    {
151
        $parameters = [
152
            'path' => $this->normalizePath($path),
153
        ];
154
155
        return $this->rpcEndpointRequest('files/delete', $parameters);
156
    }
157
158
    /**
159
     * Download a file from a user's Dropbox.
160
     *
161
     * @param string $path
162
     *
163
     * @return resource
164
     *
165
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-download
166
     */
167
    public function download(string $path)
168
    {
169
        $arguments = [
170
            'path' => $this->normalizePath($path),
171
        ];
172
173
        $response = $this->contentEndpointRequest('files/download', $arguments);
174
175
        return StreamWrapper::getResource($response->getBody());
176
    }
177
178
    /**
179
     * Returns the metadata for a file or folder.
180
     *
181
     * Note: Metadata for the root folder is unsupported.
182
     *
183
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_metadata
184
     */
185
    public function getMetadata(string $path): array
186
    {
187
        $parameters = [
188
            'path' => $this->normalizePath($path),
189
        ];
190
191
        return $this->rpcEndpointRequest('files/get_metadata', $parameters);
192
    }
193
194
    /**
195
     * Get a temporary link to stream content of a file.
196
     *
197
     * This link will expire in four hours and afterwards you will get 410 Gone.
198
     * Content-Type of the link is determined automatically by the file's mime type.
199
     *
200
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_temporary_link
201
     */
202
    public function getTemporaryLink(string $path): string
203
    {
204
        $parameters = [
205
            'path' => $this->normalizePath($path),
206
        ];
207
208
        $body = $this->rpcEndpointRequest('files/get_temporary_link', $parameters);
209
210
        return $body['link'];
211
    }
212
213
    /**
214
     * Get a thumbnail for an image.
215
     *
216
     * This method currently supports files with the following file extensions:
217
     * jpg, jpeg, png, tiff, tif, gif and bmp.
218
     *
219
     * Photos that are larger than 20MB in size won't be converted to a thumbnail.
220
     *
221
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail
222
     */
223
    public function getThumbnail(string $path, string $format = 'jpeg', string $size = 'w64h64'): string
224
    {
225
        $arguments = [
226
            'path' => $this->normalizePath($path),
227
            'format' => $format,
228
            'size' => $size,
229
        ];
230
231
        $response = $this->contentEndpointRequest('files/get_thumbnail', $arguments);
232
233
        return (string) $response->getBody();
234
    }
235
236
    /**
237
     * Starts returning the contents of a folder.
238
     *
239
     * If the result's ListFolderResult.has_more field is true, call
240
     * list_folder/continue with the returned ListFolderResult.cursor to retrieve more entries.
241
     *
242
     * Note: auth.RateLimitError may be returned if multiple list_folder or list_folder/continue calls
243
     * with same parameters are made simultaneously by same API app for same user. If your app implements
244
     * retry logic, please hold off the retry until the previous request finishes.
245
     *
246
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder
247
     */
248
    public function listFolder(string $path = '', bool $recursive = false): array
249
    {
250
        $parameters = [
251
            'path' => $this->normalizePath($path),
252
            'recursive' => $recursive,
253
        ];
254
255
        return $this->rpcEndpointRequest('files/list_folder', $parameters);
256
    }
257
258
    /**
259
     * Once a cursor has been retrieved from list_folder, use this to paginate through all files and
260
     * retrieve updates to the folder, following the same rules as documented for list_folder.
261
     *
262
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder-continue
263
     */
264
    public function listFolderContinue(string $cursor = ''): array
265
    {
266
        return $this->rpcEndpointRequest('files/list_folder/continue', compact('cursor'));
267
    }
268
269
    /**
270
     * Move a file or folder to a different location in the user's Dropbox.
271
     *
272
     * If the source path is a folder all its contents will be moved.
273
     *
274
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-move_v2
275
     */
276
    public function move(string $fromPath, string $toPath): array
277
    {
278
        $parameters = [
279
            'from_path' => $this->normalizePath($fromPath),
280
            'to_path' => $this->normalizePath($toPath),
281
        ];
282
283
        return $this->rpcEndpointRequest('files/move_v2', $parameters);
284
    }
285
286
    /**
287
     * The file should be uploaded in chunks if it size exceeds the 150 MB threshold
288
     * or if the resource size could not be determined (eg. a popen() stream).
289
     *
290
     * @param string|resource $contents
291
     *
292
     * @return bool
293
     */
294
    protected function shouldUploadChunked($contents): bool
295
    {
296
        $size = is_string($contents) ? strlen($contents) : fstat($contents)['size'];
297
298
        if ($this->isPipe($contents)) {
299
            return true;
300
        }
301
302
        if ($size === null) {
303
            return true;
304
        }
305
306
        return $size > $this->maxChunkSize;
307
    }
308
309
    /**
310
     * Check if the contents is a pipe stream (not seekable, no size defined).
311
     *
312
     * @param string|resource $contents
313
     *
314
     * @return bool
315
     */
316
    protected function isPipe($contents): bool
317
    {
318
        return is_resource($contents) ? (fstat($contents)['mode'] & 010000) != 0 : false;
319
    }
320
321
    /**
322
     * Create a new file with the contents provided in the request.
323
     *
324
     * Do not use this to upload a file larger than 150 MB. Instead, create an upload session with upload_session/start.
325
     *
326
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload
327
     *
328
     * @param string $path
329
     * @param string|resource $contents
330
     * @param string $mode
331
     *
332
     * @return array
333
     */
334
    public function upload(string $path, $contents, $mode = 'add'): array
335
    {
336
        if ($this->shouldUploadChunked($contents)) {
337
            return $this->uploadChunked($path, $contents, $mode);
338
        }
339
340
        $arguments = [
341
            'path' => $this->normalizePath($path),
342
            'mode' => $mode,
343
        ];
344
345
        $response = $this->contentEndpointRequest('files/upload', $arguments, $contents);
346
347
        $metadata = json_decode($response->getBody(), true);
348
349
        $metadata['.tag'] = 'file';
350
351
        return $metadata;
352
    }
353
354
    /**
355
     * Upload file split in chunks. This allows uploading large files, since
356
     * Dropbox API v2 limits the content size to 150MB.
357
     *
358
     * The chunk size will affect directly the memory usage, so be careful.
359
     * Large chunks tends to speed up the upload, while smaller optimizes memory usage.
360
     *
361
     * @param string $path
362
     * @param string|resource $contents
363
     * @param string $mode
364
     * @param int $chunkSize
365
     *
366
     * @return array
367
     */
368
    public function uploadChunked(string $path, $contents, $mode = 'add', $chunkSize = null): array
369
    {
370
        if ($chunkSize === null || $chunkSize > $this->maxChunkSize) {
371
            $chunkSize = $this->maxChunkSize;
372
        }
373
374
        $stream = $this->getStream($contents);
375
376
        $cursor = $this->uploadChunk(self::UPLOAD_SESSION_START, $stream, $chunkSize, null);
0 ignored issues
show
Bug introduced by
It seems like $stream defined by $this->getStream($contents) on line 374 can also be of type object<GuzzleHttp\Psr7\PumpStream>; however, Spatie\Dropbox\Client::uploadChunk() does only seem to accept object<GuzzleHttp\Psr7\Stream>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
377
378
        while (! $stream->eof()) {
379
            $cursor = $this->uploadChunk(self::UPLOAD_SESSION_APPEND, $stream, $chunkSize, $cursor);
380
        }
381
382
        return $this->uploadSessionFinish('', $cursor, $path, $mode);
383
    }
384
385
    /**
386
     * @param int $type
387
     * @param Psr7\Stream $stream
388
     * @param int $chunkSize
389
     * @param \Spatie\Dropbox\UploadSessionCursor|null $cursor
390
     * @return \Spatie\Dropbox\UploadSessionCursor
391
     * @throws Exception
392
     */
393
    protected function uploadChunk($type, &$stream, $chunkSize, $cursor = null): UploadSessionCursor
394
    {
395
        $maximumTries = $stream->isSeekable() ? $this->maxUploadChunkRetries : 0;
396
        $pos = $stream->tell();
397
398
        $tries = 0;
399
400
        tryUpload:
401
        try {
402
            $tries++;
403
404
            $chunkStream = new Psr7\LimitStream($stream, $chunkSize, $stream->tell());
405
406
            if ($type === self::UPLOAD_SESSION_START) {
407
                return $this->uploadSessionStart($chunkStream);
408
            }
409
410
            if ($type === self::UPLOAD_SESSION_APPEND && $cursor !== null) {
411
                return $this->uploadSessionAppend($chunkStream, $cursor);
412
            }
413
414
            throw new Exception('Invalid type');
415
        } catch (RequestException $exception) {
416
            if ($tries < $maximumTries) {
417
                // rewind
418
                $stream->seek($pos, SEEK_SET);
419
                goto tryUpload;
420
            }
421
            throw $exception;
422
        }
423
    }
424
425
    /**
426
     * Upload sessions allow you to upload a single file in one or more requests,
427
     * for example where the size of the file is greater than 150 MB.
428
     * This call starts a new upload session with the given data.
429
     *
430
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-start
431
     *
432
     * @param string|StreamInterface $contents
433
     * @param bool $close
434
     *
435
     * @return UploadSessionCursor
436
     */
437
    public function uploadSessionStart($contents, bool $close = false): UploadSessionCursor
438
    {
439
        $arguments = compact('close');
440
441
        $response = json_decode(
442
            $this->contentEndpointRequest('files/upload_session/start', $arguments, $contents)->getBody(),
443
            true
444
        );
445
446
        return new UploadSessionCursor($response['session_id'], ($contents instanceof StreamInterface ? $contents->tell() : strlen($contents)));
447
    }
448
449
    /**
450
     * Append more data to an upload session.
451
     * When the parameter close is set, this call will close the session.
452
     * A single request should not upload more than 150 MB.
453
     *
454
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-append_v2
455
     *
456
     * @param string|StreamInterface $contents
457
     * @param UploadSessionCursor $cursor
458
     * @param bool $close
459
     *
460
     * @return \Spatie\Dropbox\UploadSessionCursor
461
     */
462
    public function uploadSessionAppend($contents, UploadSessionCursor $cursor, bool $close = false): UploadSessionCursor
463
    {
464
        $arguments = compact('cursor', 'close');
465
466
        $pos = $contents instanceof StreamInterface ? $contents->tell() : 0;
467
        $this->contentEndpointRequest('files/upload_session/append_v2', $arguments, $contents);
468
469
        $cursor->offset += $contents instanceof StreamInterface ? ($contents->tell() - $pos) : strlen($contents);
470
471
        return $cursor;
472
    }
473
474
    /**
475
     * Finish an upload session and save the uploaded data to the given file path.
476
     * A single request should not upload more than 150 MB.
477
     *
478
     * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-finish
479
     *
480
     * @param string|StreamInterface $contents
481
     * @param \Spatie\Dropbox\UploadSessionCursor $cursor
482
     * @param string $path
483
     * @param string|array $mode
484
     * @param bool $autorename
485
     * @param bool $mute
486
     *
487
     * @return array
488
     */
489
    public function uploadSessionFinish($contents, UploadSessionCursor $cursor, string $path, $mode = 'add', $autorename = false, $mute = false): array
490
    {
491
        $arguments = compact('cursor');
492
        $arguments['commit'] = compact('path', 'mode', 'autorename', 'mute');
493
494
        $response = $this->contentEndpointRequest(
495
            'files/upload_session/finish',
496
            $arguments,
497
            ($contents == '') ? null : $contents
498
        );
499
500
        $metadata = json_decode($response->getBody(), true);
501
502
        $metadata['.tag'] = 'file';
503
504
        return $metadata;
505
    }
506
507
    /**
508
     * Get Account Info for current authenticated user.
509
     *
510
     * @link https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account
511
     *
512
     * @return array
513
     */
514
    public function getAccountInfo(): array
515
    {
516
        return $this->rpcEndpointRequest('users/get_current_account');
517
    }
518
519
    /**
520
     * Revoke current access token.
521
     *
522
     * @link https://www.dropbox.com/developers/documentation/http/documentation#auth-token-revoke
523
     */
524
    public function revokeToken()
525
    {
526
        $this->rpcEndpointRequest('auth/token/revoke');
527
    }
528
529
    protected function normalizePath(string $path): string
530
    {
531
        if (preg_match("/^id:.*|^rev:.*|^(ns:[0-9]+(\/.*)?)/", $path) === 1) {
532
            return $path;
533
        }
534
535
        $path = trim($path, '/');
536
537
        return ($path === '') ? '' : '/'.$path;
538
    }
539
540
    protected function getEndpointUrl(string $subdomain, string $endpoint): string
541
    {
542
        if (count($parts = explode('::', $endpoint)) === 2) {
543
            [$subdomain, $endpoint] = $parts;
544
        }
545
546
        return "https://{$subdomain}.dropboxapi.com/2/{$endpoint}";
547
    }
548
549
    /**
550
     * @param string $endpoint
551
     * @param array $arguments
552
     * @param string|resource|StreamInterface $body
553
     *
554
     * @return \Psr\Http\Message\ResponseInterface
555
     *
556
     * @throws \Exception
557
     */
558
    public function contentEndpointRequest(string $endpoint, array $arguments, $body = ''): ResponseInterface
559
    {
560
        $headers = ['Dropbox-API-Arg' => json_encode($arguments)];
561
562
        if ($body !== '') {
563
            $headers['Content-Type'] = 'application/octet-stream';
564
        }
565
566
        try {
567
            $response = $this->client->post($this->getEndpointUrl('content', $endpoint), [
568
                'headers' => $this->getHeaders($headers),
569
                'body' => $body,
570
            ]);
571
        } catch (ClientException $exception) {
572
            throw $this->determineException($exception);
573
        }
574
575
        return $response;
576
    }
577
578
    public function rpcEndpointRequest(string $endpoint, array $parameters = null): array
579
    {
580
        try {
581
            $options = ['headers' => $this->getHeaders()];
582
583
            if ($parameters) {
584
                $options['json'] = $parameters;
585
            }
586
587
            $response = $this->client->post($this->getEndpointUrl('api', $endpoint), $options);
588
        } catch (ClientException $exception) {
589
            throw $this->determineException($exception);
590
        }
591
592
        $response = json_decode($response->getBody(), true);
593
594
        return $response ?? [];
595
    }
596
597
    protected function determineException(ClientException $exception): Exception
598
    {
599
        if (in_array($exception->getResponse()->getStatusCode(), [400, 409])) {
600
            return new BadRequest($exception->getResponse());
0 ignored issues
show
Bug introduced by
It seems like $exception->getResponse() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
601
        }
602
603
        return $exception;
604
    }
605
606
    /**
607
     * @param $contents
608
     *
609
     * @return \GuzzleHttp\Psr7\PumpStream|\GuzzleHttp\Psr7\Stream
610
     */
611
    protected function getStream($contents)
612
    {
613
        if ($this->isPipe($contents)) {
614
            /* @var resource $contents */
615
            return new PumpStream(function ($length) use ($contents) {
616
                $data = fread($contents, $length);
617
                if (strlen($data) === 0) {
618
                    return false;
619
                }
620
621
                return $data;
622
            });
623
        }
624
625
        return Psr7\stream_for($contents);
626
    }
627
628
    /**
629
     * Get the access token.
630
     */
631
    public function getAccessToken(): string
632
    {
633
        return $this->accessToken;
634
    }
635
636
    /**
637
     * Set the access token.
638
     */
639
    public function setAccessToken(string $accessToken): self
640
    {
641
        $this->accessToken = $accessToken;
642
643
        return $this;
644
    }
645
646
    /**
647
     * Get the HTTP headers.
648
     */
649
    protected function getHeaders(array $headers = []): array
650
    {
651
        return array_merge([
652
            'Authorization' => "Bearer {$this->accessToken}",
653
        ], $headers);
654
    }
655
}
656