Completed
Pull Request — master (#343)
by
unknown
12:06
created

AwsS3   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 343
Duplicated Lines 13.41 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 40
lcom 1
cbo 3
dl 46
loc 343
rs 8.2608
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getUrl() 0 9 2
A __construct() 15 15 1
A getAuthUrl() 0 17 1
A setMetadata() 0 10 2
A getMetadata() 0 4 2
A read() 0 11 2
A rename() 0 18 2
B write() 0 30 6
A exists() 0 4 1
A mtime() 9 10 2
A size() 9 10 2
A keys() 0 4 1
A listKeys() 5 17 4
A delete() 0 10 2
A isDirectory() 0 10 1
B ensureBucketExists() 0 27 5
A getOptions() 0 14 1
A computePath() 8 8 2
A computeKey() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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 Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
3
namespace Gaufrette\Adapter;
4
5
use Gaufrette\Adapter;
6
use Aws\S3\S3Client;
7
use Gaufrette\Util;
8
9
10
/**
11
 * Amazon S3 adapter using the AWS SDK for PHP v2.x.
12
 *
13
 * @author  Michael Dowling <[email protected]>
14
 */
15
class AwsS3 implements Adapter,
0 ignored issues
show
Coding Style introduced by
The first item in a multi-line implements list must be on the line following the implements keyword
Loading history...
16
                       MetadataSupporter,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
17
                       ListKeysAware,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
18
                       SizeCalculator
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
19
{
20
    protected $service;
21
    protected $bucket;
22
    protected $options;
23
    protected $bucketExists;
24
    protected $metadata = array();
25
    protected $detectContentType;
26
27 View Code Duplication
    public function __construct(S3Client $service, $bucket, array $options = array(), $detectContentType = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
28
    {
29
        $this->service = $service;
30
        $this->bucket = $bucket;
31
        $this->options = array_replace(
32
            array(
33
                'create' => false,
34
                'directory' => '',
35
                'acl' => 'private',
36
            ),
37
            $options
38
        );
39
40
        $this->detectContentType = $detectContentType;
41
    }
42
43
    /**
44
     * Gets the publicly accessible URL of an Amazon S3 object.
45
     *
46
     * @param string $key     Object key
47
     * @param array  $options Associative array of options used to buld the URL
48
     *                        - expires: The time at which the URL should expire
49
     *                        represented as a UNIX timestamp
50
     *                        - Any options available in the Amazon S3 GetObject
51
     *                        operation may be specified.
52
     *
53
     * @return string
54
     */
55
    public function getUrl($key, array $options = array())
56
    {
57
        return $this->service->getObjectUrl(
58
            $this->bucket,
59
            $this->computePath($key),
60
            isset($options['expires']) ? $options['expires'] : null,
61
            $options
62
        );
63
    }
64
65
    /**
66
     * Gets the pre-authorized URL of an Amazon S3 object
67
     *
68
     * @param string $key     Object key
69
     * @param int|string|\DateTime $expires The time at which the URL should
70
     *      expire. This can be a Unix timestamp, a PHP DateTime object, or a
71
     *      string that can be evaluated by strtotime.
72
     * @param array $parameters Optional parameters.
73
     *      Exempli gratia:
74
     *      - ['ResponseContentType' => 'application/pdf']
75
     *      to force Response Content-Type header to "application/pdf"
76
     *      - ['ResponseContentDisposition' => 'attachment; filename="file.pdf"']
77
     *      to force resource download using a particular file-name
78
     *
79
     * @return string
80
     */
81
    public function getAuthUrl($key, $expires = '+15 minutes', $parameters = [])
82
    {
83
        $cmdOptions = array_merge(
84
            $parameters,
85
            [
86
                'Bucket' => $this->bucket,
87
                'Key'    => $this->computePath($key),
88
            ]
89
        );
90
91
        $cmd = $this->service->getCommand('GetObject', $cmdOptions);
92
        $request = $this->service->createPresignedRequest($cmd, $expires);
93
94
        $presignedUrl = (string) $request->getUri();
95
96
        return $presignedUrl;
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function setMetadata($key, $metadata)
103
    {
104
        // BC with AmazonS3 adapter
105
        if (isset($metadata['contentType'])) {
106
            $metadata['ContentType'] = $metadata['contentType'];
107
            unset($metadata['contentType']);
108
        }
109
110
        $this->metadata[$key] = $metadata;
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function getMetadata($key)
117
    {
118
        return isset($this->metadata[$key]) ? $this->metadata[$key] : array();
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function read($key)
125
    {
126
        $this->ensureBucketExists();
127
        $options = $this->getOptions($key);
128
129
        try {
130
            return (string) $this->service->getObject($options)->get('Body');
131
        } catch (\Exception $e) {
132
            return false;
133
        }
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139
    public function rename($sourceKey, $targetKey)
140
    {
141
        $this->ensureBucketExists();
142
        $options = $this->getOptions(
143
            $targetKey,
144
            array(
145
                'CopySource' => $this->bucket.'/'.$this->computePath($sourceKey),
146
            )
147
        );
148
149
        try {
150
            $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey)));
151
152
            return $this->delete($sourceKey);
153
        } catch (\Exception $e) {
154
            return false;
155
        }
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function write($key, $content)
162
    {
163
        $this->ensureBucketExists();
164
        $options = $this->getOptions($key, array('Body' => $content));
165
166
        /*
167
         * If the ContentType was not already set in the metadata, then we autodetect
168
         * it to prevent everything being served up as binary/octet-stream.
169
         */
170
        if (!isset($options['ContentType']) && $this->detectContentType) {
171
            $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
172
            if (is_resource($content)) {
173
                $contentType = $fileInfo->file(stream_get_meta_data($content)['uri']);
174
            } else {
175
                $contentType = $fileInfo->buffer($content);
176
            }
177
            $options['ContentType'] = $contentType;
178
        }
179
180
        try {
181
            $this->service->putObject($options);
182
            if (is_resource($content)) {
183
                return Util\Size::fromResource($content);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Gaufrette\Util\S...fromResource($content); (string) is incompatible with the return type declared by the interface Gaufrette\Adapter::write of type integer|boolean.

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...
184
            } else {
185
                return Util\Size::fromContent($content);
0 ignored issues
show
Bug introduced by
It seems like $content can also be of type resource; however, Gaufrette\Util\Size::fromContent() does only seem to accept string, 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...
186
            }
187
        } catch (\Exception $e) {
188
            return false;
189
        }
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function exists($key)
196
    {
197
        return $this->service->doesObjectExist($this->bucket, $this->computePath($key));
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203 View Code Duplication
    public function mtime($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
204
    {
205
        try {
206
            $result = $this->service->headObject($this->getOptions($key));
207
208
            return strtotime($result['LastModified']);
209
        } catch (\Exception $e) {
210
            return false;
211
        }
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217 View Code Duplication
    public function size($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
218
    {
219
        try {
220
            $result = $this->service->headObject($this->getOptions($key));
221
222
            return $result['ContentLength'];
223
        } catch (\Exception $e) {
224
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Gaufrette\Adapter\SizeCalculator::size of type integer.

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...
225
        }
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231
    public function keys()
232
    {
233
        return $this->listKeys();
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239
    public function listKeys($prefix = '')
240
    {
241
        $options = array('Bucket' => $this->bucket);
242 View Code Duplication
        if ((string) $prefix != '') {
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...
243
            $options['Prefix'] = $this->computePath($prefix);
244
        } elseif (!empty($this->options['directory'])) {
245
            $options['Prefix'] = $this->options['directory'];
246
        }
247
248
        $keys = array();
249
        $iter = $this->service->getIterator('ListObjects', $options);
250
        foreach ($iter as $file) {
251
            $keys[] = $this->computeKey($file['Key']);
252
        }
253
254
        return $keys;
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function delete($key)
261
    {
262
        try {
263
            $this->service->deleteObject($this->getOptions($key));
264
265
            return true;
266
        } catch (\Exception $e) {
267
            return false;
268
        }
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function isDirectory($key)
275
    {
276
        $result = $this->service->listObjects(array(
277
            'Bucket' => $this->bucket,
278
            'Prefix' => rtrim($this->computePath($key), '/').'/',
279
            'MaxKeys' => 1,
280
        ));
281
282
        return count($result['Contents']) > 0;
283
    }
284
285
    /**
286
     * Ensures the specified bucket exists. If the bucket does not exists
287
     * and the create option is set to true, it will try to create the
288
     * bucket. The bucket is created using the same region as the supplied
289
     * client object.
290
     *
291
     * @throws \RuntimeException if the bucket does not exists or could not be
292
     *                           created
293
     */
294
    protected function ensureBucketExists()
295
    {
296
        if ($this->bucketExists) {
297
            return true;
298
        }
299
300
        if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) {
301
            return true;
302
        }
303
304
        if (!$this->options['create']) {
305
            throw new \RuntimeException(sprintf(
306
                'The configured bucket "%s" does not exist.',
307
                $this->bucket
308
            ));
309
        }
310
311
        $options = array('Bucket' => $this->bucket);
312
        if ($this->service->getRegion() != 'us-east-1') {
313
            $options['LocationConstraint'] = $this->service->getRegion();
314
        }
315
316
        $this->service->createBucket($options);
317
        $this->bucketExists = true;
318
319
        return true;
320
    }
321
322
    protected function getOptions($key, array $options = array())
323
    {
324
        $options['ACL'] = $this->options['acl'];
325
        $options['Bucket'] = $this->bucket;
326
        $options['Key'] = $this->computePath($key);
327
328
        /*
329
         * Merge global options for adapter, which are set in the constructor, with metadata.
330
         * Metadata will override global options.
331
         */
332
        $options = array_merge($this->options, $options, $this->getMetadata($key));
333
334
        return $options;
335
    }
336
337 View Code Duplication
    protected function computePath($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
338
    {
339
        if (empty($this->options['directory'])) {
340
            return $key;
341
        }
342
343
        return sprintf('%s/%s', $this->options['directory'], $key);
344
    }
345
346
    /**
347
     * Computes the key from the specified path.
348
     *
349
     * @param string $path
350
     *
351
     * return string
352
     */
353
    protected function computeKey($path)
354
    {
355
        return ltrim(substr($path, strlen($this->options['directory'])), '/');
356
    }
357
}
358