Total Complexity | 54 |
Total Lines | 690 |
Duplicated Lines | 10.29 % |
Coverage | 94.41% |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
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.
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 |
||
21 | class Client |
||
22 | { |
||
23 | /** |
||
24 | * ShareFile token. |
||
25 | * |
||
26 | * @var array |
||
27 | */ |
||
28 | public $token; |
||
29 | |||
30 | /** |
||
31 | * Guzzle Client. |
||
32 | * |
||
33 | * @var \GuzzleHttp\Client |
||
34 | */ |
||
35 | public $client; |
||
36 | |||
37 | /** |
||
38 | * Thumbnail size. |
||
39 | */ |
||
40 | const THUMBNAIL_SIZE_M = 75; |
||
41 | const THUMBNAIL_SIZE_L = 600; |
||
42 | |||
43 | /* |
||
44 | * ShareFile Folder |
||
45 | */ |
||
46 | const FOLDER_TOP = 'top'; |
||
47 | const FOLDER_HOME = 'home'; |
||
48 | const FOLDER_FAVORITES = 'favorites'; |
||
49 | const FOLDER_ALLSHARED = 'allshared'; |
||
50 | |||
51 | /* |
||
52 | * Default Chunk Size for uploading files |
||
53 | */ |
||
54 | const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024; // 8 megabytes |
||
55 | |||
56 | /** |
||
57 | * Client constructor. |
||
58 | * |
||
59 | * @param string $hostname ShareFile hostname |
||
60 | * @param string $client_id OAuth2 client_id |
||
61 | * @param string $client_secret OAuth2 client_secret |
||
62 | * @param string $username ShareFile username |
||
63 | * @param string $password ShareFile password |
||
64 | * @param MockHandler|HandlerStack $handler Guzzle Handler |
||
65 | * |
||
66 | * @throws Exception |
||
67 | */ |
||
68 | 75 | public function __construct(string $hostname, string $client_id, string $client_secret, string $username, string $password, $handler = null) |
|
82 | ], |
||
83 | ] |
||
84 | ); |
||
85 | 66 | } |
|
86 | |||
87 | /** |
||
88 | * ShareFile authentication using username/password. |
||
89 | * |
||
90 | * @param string $hostname ShareFile hostname |
||
91 | * @param string $client_id OAuth2 client_id |
||
92 | * @param string $client_secret OAuth2 client_secret |
||
93 | * @param string $username ShareFile username |
||
94 | * @param string $password ShareFile password |
||
95 | * @param MockHandler|HandlerStack $handler Guzzle Handler |
||
96 | * |
||
97 | * @throws Exception |
||
98 | * |
||
99 | * @return array |
||
100 | */ |
||
101 | 75 | protected function authenticate(string $hostname, string $client_id, string $client_secret, string $username, string $password, $handler = null):array |
|
102 | { |
||
103 | 75 | $uri = "https://{$hostname}/oauth/token"; |
|
104 | |||
105 | $parameters = [ |
||
106 | 75 | 'grant_type' => 'password', |
|
107 | 75 | 'client_id' => $client_id, |
|
108 | 75 | 'client_secret' => $client_secret, |
|
109 | 75 | 'username' => $username, |
|
110 | 75 | 'password' => $password, |
|
111 | ]; |
||
112 | |||
113 | try { |
||
114 | 75 | $client = new GuzzleClient(['handler' => $handler]); |
|
115 | 75 | $response = $client->post( |
|
116 | 75 | $uri, |
|
117 | 75 | ['form_params' => $parameters] |
|
118 | ); |
||
119 | 3 | } catch (ClientException $exception) { |
|
120 | 3 | throw $exception; |
|
121 | } |
||
122 | |||
123 | 72 | if ($response->getStatusCode() == '200') { |
|
124 | 69 | return json_decode($response->getBody(), true); |
|
125 | } else { |
||
126 | 3 | throw new Exception('Authentication error', $response->getStatusCode()); |
|
127 | } |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * Get user details. |
||
132 | * |
||
133 | * @param string $userId ShareFile user id (optional) |
||
134 | * |
||
135 | * @return array |
||
136 | */ |
||
137 | 3 | public function getUser(string $userId = ''):array |
|
138 | { |
||
139 | 3 | return $this->get("Users({$userId})"); |
|
140 | } |
||
141 | |||
142 | /** |
||
143 | * Create a folder. |
||
144 | * |
||
145 | * @param string $parentId Id of the parent folder |
||
146 | * @param string $name Name |
||
147 | * @param string $description Description |
||
148 | * @param bool $overwrite Overwrite folder |
||
149 | * |
||
150 | * @return array |
||
151 | */ |
||
152 | 6 | public function createFolder(string $parentId, string $name, string $description = '', bool $overwrite = false):array |
|
153 | { |
||
154 | 6 | $parameters = $this->buildHttpQuery( |
|
155 | [ |
||
156 | 6 | 'overwrite' => $overwrite, |
|
157 | 'passthrough' => false, |
||
158 | ] |
||
159 | ); |
||
160 | |||
161 | $data = [ |
||
162 | 6 | 'name' => $name, |
|
163 | 6 | 'description' => $description, |
|
164 | ]; |
||
165 | |||
166 | 6 | return $this->post("Items({$parentId})/Folder?{$parameters}", $data); |
|
167 | } |
||
168 | |||
169 | /** |
||
170 | * Get Folder/File using Id. |
||
171 | * |
||
172 | * @param string $itemId Item id |
||
173 | * @param bool $getChildren Include children |
||
174 | * |
||
175 | * @return array |
||
176 | */ |
||
177 | 6 | public function getItemById(string $itemId, bool $getChildren = false):array |
|
178 | { |
||
179 | 6 | $parameters = $getChildren === true ? '$expand=Children' : ''; |
|
180 | |||
181 | 6 | return $this->get("Items({$itemId})?{$parameters}"); |
|
182 | } |
||
183 | |||
184 | /** |
||
185 | * Get Folder/File using path. |
||
186 | * |
||
187 | * @param string $path Path |
||
188 | * @param string $itemId Id of the root folder (optional) |
||
189 | * |
||
190 | * @return array |
||
191 | */ |
||
192 | 3 | View Code Duplication | public function getItemByPath(string $path, string $itemId = ''):array |
|
|||
193 | { |
||
194 | 3 | if (empty($itemId)) { |
|
195 | 3 | return $this->get("Items/ByPath?Path={$path}"); |
|
196 | } else { |
||
197 | return $this->get("Items({$itemId})/ByPath?Path={$path}"); |
||
198 | } |
||
199 | } |
||
200 | |||
201 | /** |
||
202 | * Get breadcrumps of an item. |
||
203 | * |
||
204 | * @param string $itemId Item Id |
||
205 | * |
||
206 | * @return array |
||
207 | */ |
||
208 | 3 | public function getItemBreadcrumps(string $itemId):array |
|
211 | } |
||
212 | |||
213 | /** |
||
214 | * Copy an item. |
||
215 | * |
||
216 | * @param string $targetId Id of the target folder |
||
217 | * @param string $itemId Id of the copied item |
||
218 | * @param bool $overwrite Indicates whether items with the same name will be overwritten or not (optional) |
||
219 | * |
||
220 | * @return array |
||
221 | */ |
||
222 | 6 | public function copyItem(string $targetId, string $itemId, bool $overwrite = false):array |
|
223 | { |
||
224 | 6 | $parameters = $this->buildHttpQuery( |
|
225 | [ |
||
226 | 6 | 'targetid' => $targetId, |
|
227 | 6 | 'overwrite' => $overwrite, |
|
228 | ] |
||
229 | ); |
||
230 | |||
231 | 6 | return $this->post("Items({$itemId})/Copy?{$parameters}"); |
|
232 | } |
||
233 | |||
234 | /** |
||
235 | * Update an item. |
||
236 | * |
||
237 | * @param string $itemId Id of the item |
||
238 | * @param array $data New data |
||
239 | * @param bool $forceSync Indicates whether operation is to be executed synchronously (optional) |
||
240 | * @param bool $notify Indicates whether an email should be sent to users subscribed to Upload Notifications (optional) |
||
241 | * |
||
242 | * @return array |
||
243 | */ |
||
244 | 3 | View Code Duplication | public function updateItem(string $itemId, array $data, bool $forceSync = true, bool $notify = true):array |
245 | { |
||
246 | 3 | $parameters = $this->buildHttpQuery( |
|
247 | [ |
||
248 | 3 | 'forceSync' => $forceSync, |
|
249 | 3 | 'notify' => $notify, |
|
250 | ] |
||
251 | ); |
||
252 | |||
253 | 3 | return $this->patch("Items({$itemId})?{$parameters}", $data); |
|
254 | } |
||
255 | |||
256 | /** |
||
257 | * Delete an item. |
||
258 | * |
||
259 | * @param string $itemId Item id |
||
260 | * @param bool $singleversion True it will delete only the specified version rather than all sibling files with the same filename (optional) |
||
261 | * @param bool $forceSync True will block the operation from taking place asynchronously (optional) |
||
262 | * |
||
263 | * @return string |
||
264 | */ |
||
265 | 3 | View Code Duplication | public function deleteItem(string $itemId, bool $singleversion = false, bool $forceSync = false):string |
275 | } |
||
276 | |||
277 | /** |
||
278 | * Get temporary download URL for an item. |
||
279 | * |
||
280 | * @param string $itemId Item id |
||
281 | * @param bool $includeallversions For folder downloads only, includes old versions of files in the folder in the zip when true, current versions only when false (default) |
||
282 | * |
||
283 | * @return array |
||
284 | */ |
||
285 | 3 | View Code Duplication | public function getItemDownloadUrl(string $itemId, bool $includeallversions = false):array |
286 | { |
||
287 | 3 | $parameters = $this->buildHttpQuery( |
|
288 | [ |
||
289 | 3 | 'includeallversions' => $includeallversions, |
|
290 | 'redirect' => false, |
||
291 | ] |
||
292 | ); |
||
293 | |||
294 | 3 | return $this->get("Items({$itemId})/Download?{$parameters}"); |
|
295 | } |
||
296 | |||
297 | /** |
||
298 | * Get contents of and item. |
||
299 | * |
||
300 | * @param string $itemId Item id |
||
301 | * @param bool $includeallversions $includeallversions For folder downloads only, includes old versions of files in the folder in the zip when true, current versions only when false (default) |
||
302 | * |
||
303 | * @return mixed |
||
304 | */ |
||
305 | 3 | View Code Duplication | public function getItemContents(string $itemId, bool $includeallversions = false) |
315 | } |
||
316 | |||
317 | /** |
||
318 | * Get the Chunk Uri to start a file-upload. |
||
319 | * |
||
320 | * @param string $method Upload method (Standard or Streamed) |
||
321 | * @param string $filename Name of file |
||
322 | * @param string $folderId Id of the parent folder |
||
323 | * @param bool $unzip Indicates that the upload is a Zip file, and contents must be extracted at the end of upload. The resulting files and directories will be placed in the target folder. If set to false, the ZIP file is uploaded as a single file. Default is false (optional) |
||
324 | * @param bool $overwrite Indicates whether items with the same name will be overwritten or not (optional) |
||
325 | * @param bool $notify Indicates whether users will be notified of this upload - based on folder preferences (optional) |
||
326 | * @param bool $raw Send contents contents directly in the POST body (=true) or send contents in MIME format (=false) (optional) |
||
327 | * @param resource $stream Resource stream of the contents (optional) |
||
328 | * |
||
329 | * @return array |
||
330 | */ |
||
331 | 9 | public function getChunkUri(string $method, string $filename, string $folderId, bool $unzip = false, $overwrite = true, bool $notify = true, bool $raw = false, $stream = null):array |
|
332 | { |
||
333 | 9 | $parameters = $this->buildHttpQuery( |
|
334 | [ |
||
335 | 9 | 'method' => $method, |
|
336 | 9 | 'raw' => $raw, |
|
337 | 9 | 'fileName' => basename($filename), |
|
338 | 9 | 'fileSize' => $stream == null ? filesize($filename) : fstat($stream)['size'], |
|
339 | 'canResume' => false, |
||
340 | 'startOver' => false, |
||
341 | 9 | 'unzip' => $unzip, |
|
342 | 9 | 'tool' => 'apiv3', |
|
343 | 9 | 'overwrite' => $overwrite, |
|
344 | 9 | 'title' => basename($filename), |
|
345 | 'isSend' => false, |
||
346 | 9 | 'responseFormat' => 'json', |
|
347 | 9 | 'notify' => $notify, |
|
348 | 9 | 'clientCreatedDateUTC' => $stream == null ? filectime($filename) : fstat($stream)['ctime'], |
|
349 | 9 | 'clientModifiedDateUTC' => $stream == null ? filemtime($filename) : fstat($stream)['mtime'], |
|
350 | ] |
||
351 | ); |
||
352 | |||
353 | 9 | return $this->post("Items({$folderId})/Upload?{$parameters}"); |
|
354 | } |
||
355 | |||
356 | /** |
||
357 | * Upload a file using a single HTTP POST. |
||
358 | * |
||
359 | * @param string $filename Name of file |
||
360 | * @param string $folderId Id of the parent folder |
||
361 | * @param bool $unzip Indicates that the upload is a Zip file, and contents must be extracted at the end of upload. The resulting files and directories will be placed in the target folder. If set to false, the ZIP file is uploaded as a single file. Default is false (optional) |
||
362 | * @param bool $overwrite Indicates whether items with the same name will be overwritten or not (optional) |
||
363 | * @param bool $notify Indicates whether users will be notified of this upload - based on folder preferences (optional) |
||
364 | * |
||
365 | * @return string |
||
366 | */ |
||
367 | 3 | public function uploadFileStandard(string $filename, string $folderId, bool $unzip = false, bool $overwrite = true, bool $notify = true):string |
|
385 | } |
||
386 | |||
387 | /** |
||
388 | * Upload a file using multiple HTTP POSTs. |
||
389 | * |
||
390 | * @param mixed $stream Stream resource |
||
391 | * @param string $folderId Id of the parent folder |
||
392 | * @param string $filename Filename (optional) |
||
393 | * @param bool $unzip Indicates that the upload is a Zip file, and contents must be extracted at the end of upload. The resulting files and directories will be placed in the target folder. If set to false, the ZIP file is uploaded as a single file. Default is false (optional) |
||
394 | * @param bool $overwrite Indicates whether items with the same name will be overwritten or not (optional) |
||
395 | * @param bool $notify Indicates whether users will be notified of this upload - based on folder preferences (optional) |
||
396 | * @param int $chunkSize Maximum size of the individual HTTP posts in bytes |
||
397 | * |
||
398 | * @return string |
||
399 | */ |
||
400 | 3 | public function uploadFileStreamed($stream, string $folderId, string $filename = null, bool $unzip = false, bool $overwrite = true, bool $notify = true, int $chunkSize = null):string |
|
445 | } |
||
446 | |||
447 | /** |
||
448 | * Get Thumbnail of an item. |
||
449 | * |
||
450 | * @param string $itemId Item id |
||
451 | * @param int $size Thumbnail size: THUMBNAIL_SIZE_M or THUMBNAIL_SIZE_L (optional) |
||
452 | * |
||
453 | * @return array |
||
454 | */ |
||
455 | 3 | View Code Duplication | public function getThumbnailUrl(string $itemId, int $size = 75):array |
456 | { |
||
457 | 3 | $parameters = $this->buildHttpQuery( |
|
458 | [ |
||
459 | 3 | 'size' => $size, |
|
460 | 'redirect' => false, |
||
461 | ] |
||
462 | ); |
||
463 | |||
464 | 3 | return $this->get("Items({$itemId})/Thumbnail?{$parameters}"); |
|
465 | } |
||
466 | |||
467 | /** |
||
468 | * Get browser link for an item. |
||
469 | * |
||
470 | * @param string $itemId Item id |
||
471 | * |
||
472 | * @return array |
||
473 | */ |
||
474 | 3 | public function getWebAppLink(string $itemId):array |
|
475 | { |
||
476 | 3 | return $this->post("Items({$itemId})/WebAppLink"); |
|
477 | } |
||
478 | |||
479 | /** |
||
480 | * Share Share for external user. |
||
481 | * |
||
482 | * @param array $options Share options |
||
483 | * @param bool $notify Indicates whether user will be notified if item is downloaded (optional) |
||
484 | * |
||
485 | * @return array |
||
486 | */ |
||
487 | 3 | public function createShare(array $options, $notify = false):array |
|
488 | { |
||
489 | 3 | $parameters = $this->buildHttpQuery( |
|
490 | [ |
||
491 | 3 | 'notify' => $notify, |
|
492 | 'direct' => true, |
||
493 | ] |
||
494 | ); |
||
495 | |||
496 | 3 | return $this->post("Shares?{$parameters}", $options); |
|
497 | } |
||
498 | |||
499 | /** |
||
500 | * Get AccessControl List for an item. |
||
501 | * |
||
502 | * @param string $itemId Id of an item |
||
503 | * @param string $userId Id of an user |
||
504 | * |
||
505 | * @return array |
||
506 | */ |
||
507 | 6 | View Code Duplication | public function getItemAccessControls(string $itemId, string $userId = ''):array |
508 | { |
||
509 | 6 | if (! empty($userId)) { |
|
510 | 3 | return $this->get("AccessControls(principalid={$userId},itemid={$itemId})"); |
|
511 | } else { |
||
512 | 3 | return $this->get("Items({$itemId})/AccessControls"); |
|
513 | } |
||
514 | } |
||
515 | |||
516 | /** |
||
517 | * Build API uri. |
||
518 | * |
||
519 | * @param string $endpoint API endpoint |
||
520 | * |
||
521 | * @return string |
||
522 | */ |
||
523 | 63 | protected function buildUri(string $endpoint): string |
|
524 | { |
||
525 | 63 | return "https://{$this->token['subdomain']}.sf-api.com/sf/v3/{$endpoint}"; |
|
526 | } |
||
527 | |||
528 | /** |
||
529 | * Make a request to the API. |
||
530 | * |
||
531 | * @param string $method HTTP Method |
||
532 | * @param string $endpoint API endpoint |
||
533 | * @param mixed|string|array $json POST body (optional) |
||
534 | * |
||
535 | * @throws Exception |
||
536 | * |
||
537 | * @return mixed |
||
538 | */ |
||
539 | 63 | protected function request(string $method, string $endpoint, $json = null) |
|
540 | { |
||
541 | 63 | $uri = $this->buildUri($endpoint); |
|
542 | 63 | $options = $json != null ? ['json' => $json] : []; |
|
543 | |||
544 | try { |
||
545 | 63 | $response = $this->client->request($method, $uri, $options); |
|
546 | } catch (ClientException $exception) { |
||
547 | throw $this->determineException($exception); |
||
548 | } |
||
549 | |||
550 | 63 | $body = (string) $response->getBody(); |
|
551 | |||
552 | 63 | return $this->jsonValidator($body) ? json_decode($body, true) : $body; |
|
553 | } |
||
554 | |||
555 | /** |
||
556 | * Shorthand for GET-request. |
||
557 | * |
||
558 | * @param string $endpoint API endpoint |
||
559 | * |
||
560 | * @return mixed |
||
561 | */ |
||
562 | 30 | protected function get(string $endpoint) |
|
563 | { |
||
564 | 30 | return $this->request('GET', $endpoint); |
|
565 | } |
||
566 | |||
567 | /** |
||
568 | * Shorthand for POST-request. |
||
569 | * |
||
570 | * @param string $endpoint API endpoint |
||
571 | * @param mixed|string|array $json POST body (optional) |
||
572 | * |
||
573 | * @return mixed |
||
574 | */ |
||
575 | 27 | protected function post(string $endpoint, $json = null) |
|
576 | { |
||
577 | 27 | return $this->request('POST', $endpoint, $json); |
|
578 | } |
||
579 | |||
580 | /** |
||
581 | * Shorthand for PATCH-request. |
||
582 | * |
||
583 | * @param string $endpoint API endpoint |
||
584 | * @param mixed|string|array $json POST body (optional) |
||
585 | * |
||
586 | * @return mixed |
||
587 | */ |
||
588 | 3 | protected function patch(string $endpoint, $json = null) |
|
589 | { |
||
590 | 3 | return $this->request('PATCH', $endpoint, $json); |
|
591 | } |
||
592 | |||
593 | /** |
||
594 | * Shorthand for DELETE-request. |
||
595 | * |
||
596 | * @param string $endpoint API endpoint |
||
597 | * |
||
598 | * @return string|array |
||
599 | */ |
||
600 | 3 | protected function delete(string $endpoint) |
|
601 | { |
||
602 | 3 | return $this->request('DELETE', $endpoint); |
|
603 | } |
||
604 | |||
605 | /** |
||
606 | * Upload a chunk of data using HTTP POST body. |
||
607 | * |
||
608 | * @param string $uri Upload URI |
||
609 | * @param string $data Contents to upload |
||
610 | * |
||
611 | * @return string|array |
||
612 | */ |
||
613 | 3 | protected function uploadChunk($uri, $data) |
|
614 | { |
||
615 | 3 | $response = $this->client->request( |
|
616 | 3 | 'POST', |
|
617 | 3 | $uri, |
|
618 | [ |
||
619 | 'headers' => [ |
||
620 | 3 | 'Content-Length' => strlen($data), |
|
621 | 3 | 'Content-Type' => 'application/octet-stream', |
|
622 | ], |
||
623 | 3 | 'body' => $data, |
|
624 | ] |
||
625 | ); |
||
626 | |||
627 | 3 | return (string) $response->getBody(); |
|
628 | } |
||
629 | |||
630 | /** |
||
631 | * Sometimes fread() returns less than the request number of bytes (for example, when reading |
||
632 | * from network streams). This function repeatedly calls fread until the requested number of |
||
633 | * bytes have been read or we've reached EOF. |
||
634 | * |
||
635 | * @param resource $stream |
||
636 | * @param int $chunkSize |
||
637 | * |
||
638 | * @throws Exception |
||
639 | * @return string |
||
640 | */ |
||
641 | 3 | protected function readChunk($stream, int $chunkSize) |
|
654 | } |
||
655 | |||
656 | /** |
||
657 | * Handle ClientException. |
||
658 | * |
||
659 | * @param ClientException $exception ClientException |
||
660 | * |
||
661 | * @return Exception |
||
662 | */ |
||
663 | protected function determineException(ClientException $exception): Exception |
||
670 | } |
||
671 | |||
672 | /** |
||
673 | * Build HTTP query. |
||
674 | * |
||
675 | * @param array $parameters Query parameters |
||
676 | * |
||
677 | * @return string |
||
678 | */ |
||
679 | 39 | protected function buildHttpQuery(array $parameters):string |
|
691 | ) |
||
692 | ); |
||
693 | } |
||
694 | |||
695 | /** |
||
696 | * Validate JSON. |
||
697 | * |
||
698 | * @param mixed $data JSON variable |
||
699 | * |
||
700 | * @return bool |
||
701 | */ |
||
702 | 63 | protected function jsonValidator($data = null):bool |
|
713 |
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.