Passed
Push — master ( 9bb515...b31114 )
by frey
02:53
created

Adapter   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 493
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 84.07%

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 7
dl 0
loc 493
ccs 153
cts 182
cp 0.8407
rs 6.4799
c 0
b 0
f 0

29 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A getAppId() 0 4 1
A getRegion() 0 5 2
A getSourcePath() 0 6 1
A getUrl() 0 15 3
A getBucket() 0 4 1
A getTemporaryUrl() 0 17 3
A write() 0 8 1
A writeStream() 0 8 1
A update() 0 4 1
A updateStream() 0 4 1
A rename() 0 8 1
A copy() 0 6 1
A delete() 0 7 1
A deleteDir() 0 17 2
A createDir() 0 8 1
A setVisibility() 0 16 2
A has() 0 8 2
A read() 0 22 5
A getHttpClient() 0 8 1
A readStream() 0 17 3
A listContents() 0 12 2
A getMetadata() 0 7 1
A getSize() 0 7 2
A getMimetype() 0 7 2
A getTimestamp() 0 7 2
A getVisibility() 0 18 5
A normalizeFileInfo() 0 15 2
A listObjects() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like Adapter 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 Adapter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Freyo\Flysystem\QcloudCOSv5;
4
5
use Carbon\Carbon;
6
use DateTimeInterface;
7
use League\Flysystem\Adapter\AbstractAdapter;
8
use League\Flysystem\Adapter\CanOverwriteFiles;
9
use League\Flysystem\AdapterInterface;
10
use League\Flysystem\Config;
11
use Qcloud\Cos\Client;
12
use Qcloud\Cos\Exception\NoSuchKeyException;
13
14
/**
15
 * Class Adapter.
16
 */
17
class Adapter extends AbstractAdapter implements CanOverwriteFiles
18
{
19
    /**
20
     * @var Client
21
     */
22
    protected $client;
23
24
    /**
25
     * @var array
26
     */
27
    protected $config = [];
28
29
    /**
30
     * @var array
31
     */
32
    protected $regionMap = [
33
        'cn-east'      => 'ap-shanghai',
34
        'cn-sorth'     => 'ap-guangzhou',
35
        'cn-north'     => 'ap-beijing-1',
36
        'cn-south-2'   => 'ap-guangzhou-2',
37
        'cn-southwest' => 'ap-chengdu',
38
        'sg'           => 'ap-singapore',
39
        'tj'           => 'ap-beijing-1',
40
        'bj'           => 'ap-beijing',
41
        'sh'           => 'ap-shanghai',
42
        'gz'           => 'ap-guangzhou',
43
        'cd'           => 'ap-chengdu',
44
        'sgp'          => 'ap-singapore',
45
    ];
46
47
    /**
48
     * Adapter constructor.
49
     *
50
     * @param Client $client
51
     * @param array  $config
52
     */
53
    public function __construct(Client $client, array $config)
54
    {
55
        $this->client = $client;
56
        $this->config = $config;
57
58
        $this->setPathPrefix($config['cdn']);
59
    }
60
61
    /**
62
     * @return string
63
     */
64 19
    public function getBucket()
65 1
    {
66 19
        return $this->config['bucket'];
67
    }
68
69
    /**
70
     * @return string
71
     */
72 2
    public function getAppId()
73
    {
74 2
        return $this->config['credentials']['appId'];
75
    }
76
77
    /**
78
     * @return string
79
     */
80 2
    public function getRegion()
81
    {
82 2
        return array_key_exists($this->config['region'], $this->regionMap)
83 2
            ? $this->regionMap[$this->config['region']] : $this->config['region'];
84
    }
85
86
    /**
87
     * @param $path
88
     *
89
     * @return string
90
     */
91 2
    public function getSourcePath($path)
92
    {
93 2
        return sprintf('%s-%s.cos.%s.myqcloud.com/%s',
94 2
            $this->getBucket(), $this->getAppId(), $this->getRegion(), $path
95 2
        );
96
    }
97
98
    /**
99
     * @param string $path
100
     *
101
     * @return string
102
     */
103 4
    public function getUrl($path)
104 4
    {
105 3
        if ($this->config['cdn']) {
106 3
            return $this->applyPathPrefix($path);
107
        }
108
109
        $objectUrl = $this->client->getObjectUrl(
110 4
            $this->getBucket(), $path, null,
111
                ['Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http']
112 4
        );
113
114
        $url = parse_url($objectUrl);
115
116
        return sprintf('%s://%s%s', $url['scheme'], $url['host'], urldecode($url['path']));
117
    }
118
119
    /**
120
     * @param string             $path
121
     * @param \DateTimeInterface $expiration
122
     * @param array              $options
123
     *
124
     * @return string
125
     */
126 1
    public function getTemporaryUrl($path, DateTimeInterface $expiration, array $options = [])
127
    {
128 1
        $options = array_merge($options,
129 1
            ['Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http']);
130
131 1
        $objectUrl = $this->client->getObjectUrl(
132 1
            $this->getBucket(), $path, $expiration->format('c'), $options
133 1
        );
134
135 1
        $url = parse_url($objectUrl);
136
137 1
        if ($this->config['cdn']) {
138 1
            return $this->config['cdn'].urldecode($url['path']).'?'.$url['query'];
139
        }
140
141
        return sprintf('%s://%s%s?%s', $url['scheme'], $url['host'], urldecode($url['path']), $url['query']);
142
    }
143
144
    /**
145
     * @param string $path
146
     * @param string $contents
147
     * @param Config $config
148
     *
149
     * @return array|bool
150
     */
151 2
    public function write($path, $contents, Config $config)
152
    {
153 2
        return $this->client->upload($this->getBucket(), $path, $contents, [
154
            'params' => [
155 2
                'ACL' => $config->get('visibility', 'public-read'),
156 2
            ],
157 2
        ]);
158
    }
159
160
    /**
161
     * @param string   $path
162
     * @param resource $resource
163
     * @param Config   $config
164
     *
165
     * @return array|bool
166
     */
167 4
    public function writeStream($path, $resource, Config $config)
168
    {
169 4
        return $this->client->upload($this->getBucket(), $path, stream_get_contents($resource, -1, 0), [
170
            'params' => [
171 4
                'ACL' => $config->get('visibility', 'public-read'),
172 2
            ],
173 2
        ]);
174
    }
175
176
    /**
177
     * @param string $path
178
     * @param string $contents
179
     * @param Config $config
180
     *
181
     * @return array|bool
182
     */
183 1
    public function update($path, $contents, Config $config)
184
    {
185 1
        return $this->write($path, $contents, $config);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->write($path, $contents, $config); of type array|boolean adds the type boolean to the return on line 185 which is incompatible with the return type declared by the interface League\Flysystem\AdapterInterface::update of type array|false.
Loading history...
186
    }
187
188
    /**
189
     * @param string   $path
190
     * @param resource $resource
191
     * @param Config   $config
192
     *
193
     * @return array|bool
194
     */
195 1
    public function updateStream($path, $resource, Config $config)
196
    {
197 1
        return $this->writeStream($path, $resource, $config);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->writeStream($path, $resource, $config); of type array|boolean adds the type boolean to the return on line 197 which is incompatible with the return type declared by the interface League\Flysystem\AdapterInterface::updateStream of type array|false.
Loading history...
198
    }
199
200
    /**
201
     * @param string $path
202
     * @param string $newpath
203
     *
204
     * @return bool
205
     */
206 1
    public function rename($path, $newpath)
207
    {
208 1
        $result = $this->copy($path, $newpath);
209
210 1
        $this->delete($path);
211
212 1
        return $result;
213
    }
214
215
    /**
216
     * @param string $path
217
     * @param string $newpath
218
     *
219
     * @return bool
220
     */
221 2
    public function copy($path, $newpath)
222
    {
223 2
        $source = $this->getSourcePath($path);
224
225 2
        return (bool) $this->client->copy($this->getBucket(), $newpath, $source);
226
    }
227
228
    /**
229
     * @param string $path
230
     *
231
     * @return bool
232
     */
233 2
    public function delete($path)
234
    {
235 2
        return (bool) $this->client->deleteObject([
236 2
            'Bucket' => $this->getBucket(),
237 2
            'Key'    => $path,
238 2
        ]);
239
    }
240
241
    /**
242
     * @param string $dirname
243
     *
244
     * @return bool
245
     */
246 1
    public function deleteDir($dirname)
247
    {
248 1
        $response = $this->listObjects($dirname);
249
250 1
        if (!isset($response['Contents'])) {
251
            return true;
252
        }
253
254 1
        $keys = array_map(function ($item) {
255 1
            return ['Key' => $item['Key']];
256 1
        }, (array) $response['Contents']);
257
258 1
        return (bool) $this->client->deleteObjects([
259 1
            'Bucket'  => $this->getBucket(),
260 1
            'Objects' => $keys,
261 1
        ]);
262
    }
263
264
    /**
265
     * @param string $dirname
266
     * @param Config $config
267
     *
268
     * @return array|bool
269
     */
270 1
    public function createDir($dirname, Config $config)
271
    {
272 1
        return $this->client->putObject([
273 1
            'Bucket' => $this->getBucket(),
274 1
            'Key'    => $dirname.'/_blank',
275 1
            'Body'   => '',
276 1
        ]);
277
    }
278
279
    /**
280
     * @param string $path
281
     * @param string $visibility
282
     *
283
     * @return bool
284
     */
285 1
    public function setVisibility($path, $visibility)
286
    {
287
        switch ($visibility) {
288 1
            case AdapterInterface::VISIBILITY_PUBLIC:
289
                $visibility = 'public-read';
290
                break;
291 1
            default:
292
                //
293 1
        }
294
295 1
        return (bool) $this->client->PutObjectAcl([
0 ignored issues
show
Bug Best Practice introduced by
The return type of return (bool) $this->cli...'ACL' => $visibility)); (boolean) is incompatible with the return type declared by the interface League\Flysystem\AdapterInterface::setVisibility of type array|false.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
296 1
            'Bucket' => $this->getBucket(),
297 1
            'Key'    => $path,
298 1
            'ACL'    => $visibility,
299 1
        ]);
300
    }
301
302
    /**
303
     * @param string $path
304
     *
305
     * @return bool
306
     */
307 1
    public function has($path)
308
    {
309
        try {
310 1
            return (bool) $this->getMetadata($path);
311
        } catch (NoSuchKeyException $e) {
312
            return false;
313
        }
314
    }
315
316
    /**
317
     * @param string $path
318
     *
319
     * @return array|bool
320
     */
321 1
    public function read($path)
322
    {
323
        try {
324 1
            if (isset($this->config['read_from_cdn']) && $this->config['read_from_cdn']) {
325
                $response = $this->getHttpClient()
326
                                 ->get($this->getTemporaryUrl($path, Carbon::now()->addMinutes(5)))
327
                                 ->getBody()
328
                                 ->getContents();
329
            } else {
330 1
                $response = $this->client->getObject([
0 ignored issues
show
Bug introduced by
The method getObject() does not exist on Qcloud\Cos\Client. Did you maybe mean getObjectUrl()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
331 1
                    'Bucket' => $this->getBucket(),
332 1
                    'Key'    => $path,
333 1
                ])->get('Body');
334
            }
335
336 1
            return ['contents' => (string)$response];
337
        } catch (NoSuchKeyException $e) {
338
            return false;
339
        } catch (\GuzzleHttp\Exception\ClientException $e) {
340
            return false;
341
        }
342
    }
343
344
    /**
345
     * @return \GuzzleHttp\Client
346
     */
347 1
    public function getHttpClient()
348
    {
349 1
        return $client = (new \GuzzleHttp\Client([
0 ignored issues
show
Unused Code introduced by
$client is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
350 1
            'verify'          => false,
351 1
            'timeout'         => $this->config['timeout'],
352 1
            'connect_timeout' => $this->config['connect_timeout'],
353 1
        ]));
354
    }
355
356
    /**
357
     * @param string $path
358
     *
359
     * @return array|bool
360
     */
361 1
    public function readStream($path)
362
    {
363
        try {
364 1
            $temporaryUrl = $this->getTemporaryUrl($path, Carbon::now()->addMinutes(5));
365
366 1
            $stream = $this->getHttpClient()
367 1
                           ->get($temporaryUrl, ['stream' => true])
368 1
                           ->getBody()
369 1
                           ->detach();
370
371 1
            return ['stream' => $stream];
372
        } catch (NoSuchKeyException $e) {
373
            return false;
374
        } catch (\GuzzleHttp\Exception\ClientException $e) {
375
            return false;
376
        }
377
    }
378
379
    /**
380
     * @param string $directory
381
     * @param bool   $recursive
382
     *
383
     * @return array|bool
384
     */
385 1
    public function listContents($directory = '', $recursive = false)
386
    {
387 1
        $list = [];
388
389 1
        $response = $this->listObjects($directory, $recursive);
390
391 1
        foreach ((array) $response->get('Contents') as $content) {
392 1
            $list[] = $this->normalizeFileInfo($content);
393 1
        }
394
395 1
        return $list;
396
    }
397
398
    /**
399
     * @param string $path
400
     *
401
     * @return array|bool
402
     */
403 5
    public function getMetadata($path)
404
    {
405 5
        return $this->client->headObject([
406 5
            'Bucket' => $this->getBucket(),
407 5
            'Key'    => $path,
408 5
        ])->toArray();
409
    }
410
411
    /**
412
     * @param string $path
413
     *
414
     * @return array|bool
415
     */
416 1
    public function getSize($path)
417
    {
418 1
        $meta = $this->getMetadata($path);
419
420 1
        return isset($meta['ContentLength'])
421 1
            ? ['size' => $meta['ContentLength']] : false;
422
    }
423
424
    /**
425
     * @param string $path
426
     *
427
     * @return array|bool
428
     */
429 1
    public function getMimetype($path)
430
    {
431 1
        $meta = $this->getMetadata($path);
432
433 1
        return isset($meta['ContentType'])
434 1
            ? ['mimetype' => $meta['ContentType']] : false;
435
    }
436
437
    /**
438
     * @param string $path
439
     *
440
     * @return array|bool
441
     */
442 1
    public function getTimestamp($path)
443
    {
444 1
        $meta = $this->getMetadata($path);
445
446 1
        return isset($meta['LastModified'])
447 1
            ? ['timestamp' => strtotime($meta['LastModified'])] : false;
448
    }
449
450
    /**
451
     * @param string $path
452
     *
453
     * @return array|bool
454
     */
455 1
    public function getVisibility($path)
456
    {
457 1
        $meta = $this->client->getObjectAcl([
458 1
            'Bucket' => $this->getBucket(),
459 1
            'Key'    => $path,
460 1
        ]);
461
462 1
        foreach ($meta->get('Grants') as $grant) {
463 1
            if (isset($grant['Grantee']['URI'])
464 1
                && $grant['Permission'] === 'READ'
465 1
                && strpos($grant['Grantee']['URI'], 'global/AllUsers') !== false
466 1
            ) {
467
                return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
468
            }
469 1
        }
470
471 1
        return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
472
    }
473
474
    /**
475
     * @param array $content
476
     *
477
     * @return array
478
     */
479 1
    private function normalizeFileInfo(array $content)
480
    {
481 1
        $path = pathinfo($content['Key']);
482
483
        return [
484 1
            'type'      => 'file',
485 1
            'path'      => $content['Key'],
486 1
            'timestamp' => Carbon::parse($content['LastModified'])->getTimestamp(),
487 1
            'size'      => (int) $content['Size'],
488 1
            'dirname'   => (string) $path['dirname'],
489 1
            'basename'  => (string) $path['basename'],
490 1
            'extension' => isset($path['extension']) ? $path['extension'] : '',
491 1
            'filename'  => (string) $path['filename'],
492 1
        ];
493
    }
494
495
    /**
496
     * @param string $directory
497
     * @param bool   $recursive
498
     *
499
     * @return mixed
500
     */
501 2
    private function listObjects($directory = '', $recursive = false)
502
    {
503 2
        return $this->client->listObjects([
504 2
            'Bucket'    => $this->getBucket(),
505 2
            'Prefix'    => ((string) $directory === '') ? '' : ($directory.'/'),
506 2
            'Delimiter' => $recursive ? '' : '/',
507 2
        ]);
508
    }
509
}
510