Completed
Pull Request — master (#491)
by Albin
02:26
created

AwsS3::mimeType()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 9
Ratio 100 %

Importance

Changes 0
Metric Value
dl 9
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 6
nc 3
nop 1
1
<?php
2
3
namespace Gaufrette\Adapter;
4
5
use Gaufrette\Adapter;
6
use Aws\S3\S3Client;
7
use Gaufrette\Util;
8
9
/**
10
 * Amazon S3 adapter using the AWS SDK for PHP v2.x.
11
 *
12
 * @author  Michael Dowling <[email protected]>
13
 */
14
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...
15
                       MetadataSupporter,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
16
                       ListKeysAware,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
17
                       SizeCalculator,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
18
                       MimeTypeProvider
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
19
{
20
    /** @var S3Client */
21
    protected $service;
22
    /** @var string */
23
    protected $bucket;
24
    /** @var array */
25
    protected $options;
26
    /** @var bool */
27
    protected $bucketExists;
28
    /** @var array */
29
    protected $metadata = [];
30
    /** @var bool */
31
    protected $detectContentType;
32
33
    /**
34
     * @param S3Client $service
35
     * @param string   $bucket
36
     * @param array    $options
37
     * @param bool     $detectContentType
38
     */
39
    public function __construct(S3Client $service, $bucket, array $options = [], $detectContentType = false)
40
    {
41
        $this->service = $service;
42
        $this->bucket = $bucket;
43
        $this->options = array_replace(
44
            [
45
                'create' => false,
46
                'directory' => '',
47
                'acl' => 'private',
48
            ],
49
            $options
50
        );
51
52
        $this->detectContentType = $detectContentType;
53
    }
54
55
    /**
56
     * Gets the publicly accessible URL of an Amazon S3 object.
57
     *
58
     * @param string $key     Object key
59
     * @param array  $options Associative array of options used to buld the URL
60
     *                        - expires: The time at which the URL should expire
61
     *                        represented as a UNIX timestamp
62
     *                        - Any options available in the Amazon S3 GetObject
63
     *                        operation may be specified.
64
     *
65
     * @return string
66
     */
67
    public function getUrl($key, array $options = [])
68
    {
69
        return $this->service->getObjectUrl(
70
            $this->bucket,
71
            $this->computePath($key),
72
            isset($options['expires']) ? $options['expires'] : null,
0 ignored issues
show
Unused Code introduced by
The call to S3Client::getObjectUrl() has too many arguments starting with isset($options['expires'...tions['expires'] : null.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
73
            $options
74
        );
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function setMetadata($key, $metadata)
81
    {
82
        // BC with AmazonS3 adapter
83
        if (isset($metadata['contentType'])) {
84
            $metadata['ContentType'] = $metadata['contentType'];
85
            unset($metadata['contentType']);
86
        }
87
88
        $this->metadata[$key] = $metadata;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function getMetadata($key)
95
    {
96
        return isset($this->metadata[$key]) ? $this->metadata[$key] : [];
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function read($key)
103
    {
104
        $this->ensureBucketExists();
105
        $options = $this->getOptions($key);
106
107
        try {
108
            // Get remote object
109
            $object = $this->service->getObject($options);
110
            // If there's no metadata array set up for this object, set it up
111
            if (!array_key_exists($key, $this->metadata) || !is_array($this->metadata[$key])) {
112
                $this->metadata[$key] = [];
113
            }
114
            // Make remote ContentType metadata available locally
115
            $this->metadata[$key]['ContentType'] = $object->get('ContentType');
116
117
            return (string) $object->get('Body');
118
        } catch (\Exception $e) {
119
            return false;
120
        }
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function rename($sourceKey, $targetKey)
127
    {
128
        $this->ensureBucketExists();
129
        $options = $this->getOptions(
130
            $targetKey,
131
            ['CopySource' => $this->bucket.'/'.$this->computePath($sourceKey)]
132
        );
133
134
        try {
135
            $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey)));
136
137
            return $this->delete($sourceKey);
138
        } catch (\Exception $e) {
139
            return false;
140
        }
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function write($key, $content)
147
    {
148
        $this->ensureBucketExists();
149
        $options = $this->getOptions($key, ['Body' => $content]);
150
151
        /*
152
         * If the ContentType was not already set in the metadata, then we autodetect
153
         * it to prevent everything being served up as binary/octet-stream.
154
         */
155
        if (!isset($options['ContentType']) && $this->detectContentType) {
156
            $options['ContentType'] = $this->guessContentType($content);
157
        }
158
159
        try {
160
            $this->service->putObject($options);
161
162
            if (is_resource($content)) {
163
                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...
164
            }
165
166
            return Util\Size::fromContent($content);
167
        } catch (\Exception $e) {
168
            return false;
169
        }
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175
    public function exists($key)
176
    {
177
        return $this->service->doesObjectExist($this->bucket, $this->computePath($key));
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 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...
184
    {
185
        try {
186
            $result = $this->service->headObject($this->getOptions($key));
187
188
            return strtotime($result['LastModified']);
189
        } catch (\Exception $e) {
190
            return false;
191
        }
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197 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...
198
    {
199
        try {
200
            $result = $this->service->headObject($this->getOptions($key));
201
202
            return $result['ContentLength'];
203
        } catch (\Exception $e) {
204
            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...
205
        }
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211
    public function keys()
212
    {
213
        return $this->listKeys();
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function listKeys($prefix = '')
220
    {
221
        $options = ['Bucket' => $this->bucket];
222 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...
223
            $options['Prefix'] = $this->computePath($prefix);
224
        } elseif (!empty($this->options['directory'])) {
225
            $options['Prefix'] = $this->options['directory'];
226
        }
227
228
        $keys = [];
229
        $iter = $this->service->getIterator('ListObjects', $options);
230
        foreach ($iter as $file) {
231
            $keys[] = $this->computeKey($file['Key']);
232
        }
233
234
        return $keys;
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     */
240
    public function delete($key)
241
    {
242
        try {
243
            $this->service->deleteObject($this->getOptions($key));
244
245
            return true;
246
        } catch (\Exception $e) {
247
            return false;
248
        }
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254
    public function isDirectory($key)
255
    {
256
        $result = $this->service->listObjects([
257
            'Bucket' => $this->bucket,
258
            'Prefix' => rtrim($this->computePath($key), '/').'/',
259
            'MaxKeys' => 1,
260
        ]);
261
262
        return count($result['Contents']) > 0;
263
    }
264
265
    /**
266
     * Ensures the specified bucket exists. If the bucket does not exists
267
     * and the create option is set to true, it will try to create the
268
     * bucket. The bucket is created using the same region as the supplied
269
     * client object.
270
     *
271
     * @throws \RuntimeException if the bucket does not exists or could not be
272
     *                           created
273
     */
274
    protected function ensureBucketExists()
275
    {
276
        if ($this->bucketExists) {
277
            return true;
278
        }
279
280
        if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) {
281
            return true;
282
        }
283
284
        if (!$this->options['create']) {
285
            throw new \RuntimeException(sprintf(
286
                'The configured bucket "%s" does not exist.',
287
                $this->bucket
288
            ));
289
        }
290
291
        $this->service->createBucket([
292
            'Bucket' => $this->bucket,
293
            'LocationConstraint' => $this->service->getRegion()
294
        ]);
295
        $this->bucketExists = true;
296
297
        return true;
298
    }
299
300
    protected function getOptions($key, array $options = [])
301
    {
302
        $options['ACL'] = $this->options['acl'];
303
        $options['Bucket'] = $this->bucket;
304
        $options['Key'] = $this->computePath($key);
305
306
        /*
307
         * Merge global options for adapter, which are set in the constructor, with metadata.
308
         * Metadata will override global options.
309
         */
310
        $options = array_merge($this->options, $options, $this->getMetadata($key));
311
312
        return $options;
313
    }
314
315 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...
316
    {
317
        if (empty($this->options['directory'])) {
318
            return $key;
319
        }
320
321
        return sprintf('%s/%s', $this->options['directory'], $key);
322
    }
323
324
    /**
325
     * Computes the key from the specified path.
326
     *
327
     * @param string $path
328
     *
329
     * return string
330
     */
331
    protected function computeKey($path)
332
    {
333
        return ltrim(substr($path, strlen($this->options['directory'])), '/');
334
    }
335
336
    /**
337
     * @param string $content
338
     *
339
     * @return string
340
     */
341 View Code Duplication
    private function guessContentType($content)
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...
342
    {
343
        $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
344
345
        if (is_resource($content)) {
346
            return $fileInfo->file(stream_get_meta_data($content)['uri']);
347
        }
348
349
        return $fileInfo->buffer($content);
350
    }
351
352 View Code Duplication
    public function mimeType($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...
353
    {
354
        try {
355
            $result = $this->service->headObject($this->getOptions($key));
356
            return ($result['ContentType']);
357
        } catch (\Exception $e) {
358
            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\MimeTypeProvider::mimeType of type string.

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...
359
        }
360
    }
361
}
362