Completed
Pull Request — master (#435)
by Albin
09:27
created

S3::listKeys()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 11

Duplication

Lines 17
Ratio 100 %

Importance

Changes 0
Metric Value
dl 17
loc 17
rs 9.2
c 0
b 0
f 0
cc 4
eloc 11
nc 6
nop 1
1
<?php
2
3
namespace Gaufrette\Adapter\Aws;
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 View Code Duplication
class S3 implements Adapter, Adapter\MetadataSupporter, Adapter\ListKeysAware, Adapter\SizeCalculator
0 ignored issues
show
Duplication introduced by
This class 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...
16
{
17
    protected $service;
18
    protected $bucket;
19
    protected $options;
20
    protected $bucketExists;
21
    protected $metadata = array();
22
    protected $detectContentType;
23
24
    public function __construct(S3Client $service, $bucket, array $options = array(), $detectContentType = false)
25
    {
26
        $this->service = $service;
27
        $this->bucket = $bucket;
28
        $this->options = array_replace(
29
            array(
30
                'create' => false,
31
                'directory' => '',
32
                'acl' => 'private',
33
            ),
34
            $options
35
        );
36
37
        $this->detectContentType = $detectContentType;
38
    }
39
40
    /**
41
     * Gets the publicly accessible URL of an Amazon S3 object.
42
     *
43
     * @param string $key     Object key
44
     * @param array  $options Associative array of options used to buld the URL
45
     *                        - expires: The time at which the URL should expire
46
     *                        represented as a UNIX timestamp
47
     *                        - Any options available in the Amazon S3 GetObject
48
     *                        operation may be specified.
49
     *
50
     * @return string
51
     */
52
    public function getUrl($key, array $options = array())
53
    {
54
        return $this->service->getObjectUrl(
55
            $this->bucket,
56
            $this->computePath($key),
57
            isset($options['expires']) ? $options['expires'] : null,
58
            $options
59
        );
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function setMetadata($key, $metadata)
66
    {
67
        // BC with AmazonS3 adapter
68
        if (isset($metadata['contentType'])) {
69
            $metadata['ContentType'] = $metadata['contentType'];
70
            unset($metadata['contentType']);
71
        }
72
73
        $this->metadata[$key] = $metadata;
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79
    public function getMetadata($key)
80
    {
81
        return isset($this->metadata[$key]) ? $this->metadata[$key] : array();
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    public function read($key)
88
    {
89
        $this->ensureBucketExists();
90
        $options = $this->getOptions($key);
91
92
        try {
93
            return (string) $this->service->getObject($options)->get('Body');
94
        } catch (\Exception $e) {
95
            return false;
96
        }
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function rename($sourceKey, $targetKey)
103
    {
104
        $this->ensureBucketExists();
105
        $options = $this->getOptions(
106
            $targetKey,
107
            array(
108
                'CopySource' => $this->bucket.'/'.$this->computePath($sourceKey),
109
            )
110
        );
111
112
        try {
113
            $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey)));
114
115
            return $this->delete($sourceKey);
116
        } catch (\Exception $e) {
117
            return false;
118
        }
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function write($key, $content)
125
    {
126
        $this->ensureBucketExists();
127
        $options = $this->getOptions($key, array('Body' => $content));
128
129
        /*
130
         * If the ContentType was not already set in the metadata, then we autodetect
131
         * it to prevent everything being served up as binary/octet-stream.
132
         */
133
        if (!isset($options['ContentType']) && $this->detectContentType) {
134
            $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
135
            if (is_resource($content)) {
136
                $contentType = $fileInfo->file(stream_get_meta_data($content)['uri']);
137
            } else {
138
                $contentType = $fileInfo->buffer($content);
139
            }
140
            $options['ContentType'] = $contentType;
141
        }
142
143
        try {
144
            $this->service->putObject($options);
145
            if (is_resource($content)) {
146
                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...
147
            } else {
148
                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...
149
            }
150
        } catch (\Exception $e) {
151
            return false;
152
        }
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function exists($key)
159
    {
160
        return $this->service->doesObjectExist($this->bucket, $this->computePath($key));
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166
    public function mtime($key)
167
    {
168
        try {
169
            $result = $this->service->headObject($this->getOptions($key));
170
171
            return strtotime($result['LastModified']);
172
        } catch (\Exception $e) {
173
            return false;
174
        }
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function size($key)
181
    {
182
        try {
183
            $result = $this->service->headObject($this->getOptions($key));
184
185
            return $result['ContentLength'];
186
        } catch (\Exception $e) {
187
            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...
188
        }
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194
    public function keys()
195
    {
196
        return $this->listKeys();
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202
    public function listKeys($prefix = '')
203
    {
204
        $options = array('Bucket' => $this->bucket);
205
        if ((string) $prefix != '') {
206
            $options['Prefix'] = $this->computePath($prefix);
207
        } elseif (!empty($this->options['directory'])) {
208
            $options['Prefix'] = $this->options['directory'];
209
        }
210
211
        $keys = array();
212
        $iter = $this->service->getIterator('ListObjects', $options);
213
        foreach ($iter as $file) {
214
            $keys[] = $this->computeKey($file['Key']);
215
        }
216
217
        return $keys;
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     */
223
    public function delete($key)
224
    {
225
        try {
226
            $this->service->deleteObject($this->getOptions($key));
227
228
            return true;
229
        } catch (\Exception $e) {
230
            return false;
231
        }
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237
    public function isDirectory($key)
238
    {
239
        $result = $this->service->listObjects(array(
240
            'Bucket' => $this->bucket,
241
            'Prefix' => rtrim($this->computePath($key), '/').'/',
242
            'MaxKeys' => 1,
243
        ));
244
245
        return count($result['Contents']) > 0;
246
    }
247
248
    /**
249
     * Ensures the specified bucket exists. If the bucket does not exists
250
     * and the create option is set to true, it will try to create the
251
     * bucket. The bucket is created using the same region as the supplied
252
     * client object.
253
     *
254
     * @throws \RuntimeException if the bucket does not exists or could not be
255
     *                           created
256
     */
257
    protected function ensureBucketExists()
258
    {
259
        if ($this->bucketExists) {
260
            return true;
261
        }
262
263
        if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) {
264
            return true;
265
        }
266
267
        if (!$this->options['create']) {
268
            throw new \RuntimeException(sprintf(
269
                'The configured bucket "%s" does not exist.',
270
                $this->bucket
271
            ));
272
        }
273
274
        $options = array('Bucket' => $this->bucket);
275
        if ($this->service->getRegion() != 'us-east-1') {
276
            $options['LocationConstraint'] = $this->service->getRegion();
277
        }
278
279
        $this->service->createBucket($options);
280
        $this->bucketExists = true;
281
282
        return true;
283
    }
284
285
    protected function getOptions($key, array $options = array())
286
    {
287
        $options['ACL'] = $this->options['acl'];
288
        $options['Bucket'] = $this->bucket;
289
        $options['Key'] = $this->computePath($key);
290
291
        /*
292
         * Merge global options for adapter, which are set in the constructor, with metadata.
293
         * Metadata will override global options.
294
         */
295
        $options = array_merge($this->options, $options, $this->getMetadata($key));
296
297
        return $options;
298
    }
299
300
    protected function computePath($key)
301
    {
302
        if (empty($this->options['directory'])) {
303
            return $key;
304
        }
305
306
        return sprintf('%s/%s', $this->options['directory'], $key);
307
    }
308
309
    /**
310
     * Computes the key from the specified path.
311
     *
312
     * @param string $path
313
     *
314
     * return string
315
     */
316
    protected function computeKey($path)
317
    {
318
        return ltrim(substr($path, strlen($this->options['directory'])), '/');
319
    }
320
}
321