AmazonS3   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 355
Duplicated Lines 2.82 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 5.41%

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 2
dl 10
loc 355
ccs 8
cts 148
cp 0.0541
rs 9.3999
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A read() 0 21 2
A write() 0 6 1
A writeContent() 0 21 1
A rename() 0 18 1
A delete() 0 11 1
A getFiles() 0 19 2
B copyFiles() 0 33 5
A exists() 0 6 1
A isDirectory() 0 10 2
A checkBucket() 0 4 1
B ensureBucketExists() 0 36 5
A getURL() 0 6 1
A getFileSize() 0 9 1
A copyToLocalTemporaryFile() 10 10 1
A getService() 0 4 1
B pathOrUrlToPath() 0 40 6

How to fix   Duplicated Code   

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:

1
<?php
2
3
namespace Partnermarketing\FileSystemBundle\Adapter;
4
5
use Aws\S3\S3Client as AmazonClient;
6
use Guzzle\Http\EntityBody;
7
use Guzzle\Http\Mimetypes;
8
9
/**
10
 * Amazon specific file system adapter
11
 */
12
class AmazonS3 implements AdapterInterface
13
{
14
    private $service;
15
    private $bucket;
16
    private $localTmpDir;
17
    private $options;
18
    private $haveEnsuredBucketExists = false;
19
20
    /**
21
     * Constructor for AmazonS3 adapter
22
     *
23
     * @param \Aws\S3\S3Client $service
24
     * @param $bucket
25
     * @param string           $acl
26
     * @param array            $options
27
     */
28 3
    public function __construct(AmazonClient $service, $bucket, $localTmpDir, $acl = 'public-read', $options = array())
29
    {
30 3
        $this->service = $service;
31 3
        $this->bucket  = $bucket;
32 3
        $this->localTmpDir = $localTmpDir;
33 3
        $this->options = array_replace_recursive(
34 3
            array('create' => false, 'region' => 'eu-west-1', 'ACL' => $acl),
35
            $options
36 3
        );
37 3
    }
38
39
    /**
40
     * {@inheritDoc}
41
     */
42
    public function read($path)
43
    {
44
        // In this method only, $path can be absolute, for reading files from
45
        // outside of the file system directory, e.g. uploads, fixtures.
46
        if (file_exists($path)) {
47
            return file_get_contents($path);
48
        }
49
50
        list($path, $bucket) = $this->pathOrUrlToPath($path);
51
52
        $this->ensureBucketExists();
53
54
        $response = $this->service->getObject(array(
55
            'Bucket' => $bucket,
56
            'Key' => $path
57
        ));
58
59
        $response['Body']->rewind();
60
61
        return (string) $response['Body'];
62
    }
63
64
    /**
65
     * {@inheritDoc}
66
     */
67
    public function write($path, $source)
68
    {
69
        list($path, $bucket) = $this->pathOrUrlToPath($path);
0 ignored issues
show
Unused Code introduced by
The assignment to $bucket is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
70
71
        return $this->writeContent($path, EntityBody::factory(fopen($source, 'r')));
72
    }
73
74
    /**
75
     * {@inheritDoc}
76
     */
77
    public function writeContent($path, $content)
78
    {
79
        list($path, $bucket) = $this->pathOrUrlToPath($path);
80
81
        $this->ensureBucketExists();
82
83
        $response = $this->service->putObject(array(
0 ignored issues
show
Unused Code introduced by
$response 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...
84
            'Bucket' => $bucket,
85
            'Key' => $path,
86
            'Body' => $content,
87
            'ACL' => $this->options['ACL'],
88
            'ContentType' => Mimetypes::getInstance()->fromFilename($path)
89
        ));
90
91
        $this->service->waitUntil('ObjectExists', array(
92
            'Bucket' => $bucket,
93
            'Key'    => $path
94
        ));
95
96
        return $path;
97
    }
98
99
    /**
100
     * {@inheritDoc}
101
     */
102
    public function rename($sourcePath, $targetPath)
103
    {
104
        list($sourcePath, $sourceBucket) = $this->pathOrUrlToPath($sourcePath);
105
        list($targetPath, $targetBucket) = $this->pathOrUrlToPath($targetPath);
106
107
        $this->ensureBucketExists();
108
109
        $this->service->copyObject(array(
110
            'Bucket' => $targetBucket,
111
            'Key' => $targetPath,
112
            'CopySource' => urlencode($sourceBucket.'/'.$sourcePath),
113
            'ACL' => $this->options['ACL']
114
        ));
115
116
        $this->delete($sourcePath);
117
118
        return true;
119
    }
120
121
    /**
122
     * {@inheritDoc}
123
     */
124
    public function delete($path)
125
    {
126
        list($path, $bucket) = $this->pathOrUrlToPath($path);
127
128
        $this->ensureBucketExists();
129
130
        return $this->service->deleteObject(array(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->service->d...cket, 'Key' => $path)); (Aws\Result) is incompatible with the return type declared by the interface Partnermarketing\FileSys...dapterInterface::delete of type 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...
131
            'Bucket' => $bucket,
132
            'Key' => $path
133
        ));
134
    }
135
136
    /**
137
     * {@inheritDoc}
138
     */
139
    public function getFiles($directory = "")
140
    {
141
        list($directory, $bucket) = $this->pathOrUrlToPath($directory);
142
143
        $this->ensureBucketExists();
144
145
        $list = $this->service->getIterator('ListObjects', array(
146
            'Bucket' => $bucket, 'Prefix' => $directory
147
        ));
148
149
        $files = [];
150
        foreach ($list as $object) {
151
            $files[] = $object['Key'];
152
        }
153
154
        sort($files);
155
156
        return $files;
157
    }
158
159
    /**
160
     * {@inheritDoc}
161
     */
162
    public function copyFiles($sourceDir, $targetDir)
163
    {
164
        list($sourceDir, $sourceBucket) = $this->pathOrUrlToPath($sourceDir);
165
        list($targetDir, $targetBucket) = $this->pathOrUrlToPath($targetDir);
166
167
        /*
168
         * Add '/' character to the directories if necessary
169
         */
170
        $sourceDir = $sourceDir . (substr($sourceDir, -1) == '/' ? '' : '/');
171
        $targetDir = $targetDir . (substr($targetDir, -1) == '/' ? '' : '/');
172
173
        $this->ensureBucketExists();
174
175
        $files = $this->getFiles($sourceDir);
176
177
        $batch = array();
178
        for ($i = 0; $i < count($files); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
179
            $targetFile = str_replace($sourceDir, "", $files[$i]);
180
            $batch[] =  $this->service->getCommand('CopyObject', array(
181
                'Bucket'     => $targetBucket,
182
                'Key'        => "{$targetDir}{$targetFile}",
183
                'CopySource' => "{$sourceBucket}/{$files[$i]}",
184
            ));
185
        }
186
187
        try {
188
            $this->service->execute($batch);
0 ignored issues
show
Documentation introduced by
$batch is of type array, but the function expects a object<Aws\CommandInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
189
        } catch (\Exception $e) {
190
            throw new \RuntimeException(sprintf('Failed to copy files from %s to %s.', $sourceDir, $targetDir));
191
        }
192
193
        return true;
194
    }
195
196
    /**
197
     * {@inheritDoc}
198
     */
199
    public function exists($path)
200
    {
201
        list($path, $bucket) = $this->pathOrUrlToPath($path);
202
203
        return $this->service->doesObjectExist($bucket, $path);
204
    }
205
206
    /**
207
     * {@inheritDoc}
208
     */
209
    public function isDirectory($path)
210
    {
211
        list($path, $bucket) = $this->pathOrUrlToPath($path);
0 ignored issues
show
Unused Code introduced by
The assignment to $bucket is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
212
213
        if ($this->exists($path.'/')) {
214
            return true;
215
        }
216
217
        return false;
218
    }
219
220
    /**
221
     * Calls the bucket exist method, and returns its result.
222
     *
223
     * @return boolean
224
     */
225
    public function checkBucket()
226
    {
227
        return $this->ensureBucketExists();
228
    }
229
230
    /**
231
    * Ensures the specified bucket exists. If is does not, and create is true, it will try to create it.
232
    *
233
    * @return boolean
234
    */
235
    private function ensureBucketExists()
236
    {
237
        if ($this->haveEnsuredBucketExists) {
238
            return true;
239
        }
240
241
        if ($this->service->doesBucketExist($this->bucket)) {
242
            $this->haveEnsuredBucketExists = true;
243
244
            return true;
245
        }
246
247
        if (!$this->options['create']) {
248
            throw new \RuntimeException(sprintf(
249
                'The configured bucket "%s" does not exist.',
250
                $this->bucket
251
            ));
252
        }
253
254
        $response = $this->service->createBucket(
255
            array('Bucket' => $this->bucket, 'LocationConstraint' => $this->options['region'])
256
        );
257
258
        $this->service->waitUntil('BucketExists', array('Bucket' => $this->bucket));
259
260
        if (!$response['Location']) {
261
            throw new \RuntimeException(sprintf(
262
                'Failed to create the configured bucket "%s".',
263
                $this->bucket
264
            ));
265
        }
266
267
        $this->haveEnsuredBucketExists = true;
268
269
        return true;
270
    }
271
272
    /**
273
     * {@inheritDoc}
274
     */
275
    public function getURL($path)
276
    {
277
        list($path, $bucket) = $this->pathOrUrlToPath($path);
278
279
        return $this->service->getObjectUrl($bucket, $path);
280
    }
281
282
    /**
283
     * {@inheritDoc}
284
     */
285
    public function getFileSize($path)
286
    {
287
        list($path, $bucket) = $this->pathOrUrlToPath($path);
288
289
        return (int) $this->service->headObject([
290
            'Bucket' => $bucket,
291
            'Key' => $path,
292
        ])->get('ContentLength');
293
    }
294
295
    /**
296
     * {@inheritDoc}
297
     */
298 View Code Duplication
    public function copyToLocalTemporaryFile($path)
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...
299
    {
300
        $content = $this->read($path);
301
        $extension = pathinfo($path, PATHINFO_EXTENSION);
302
        $target = tempnam($this->localTmpDir, null) . '.' . $extension;
303
304
        file_put_contents($target, $content);
305
306
        return $target;
307
    }
308
309
    /**
310
     * {@inheritDoc}
311
     */
312
    public function getService()
313
    {
314
        return $this->service;
315
    }
316
317
    /**
318
     * Returns an s3 location in normalised format, plus parses the bucket name
319
     * from the URL if a URL is used.
320
     *
321
     * @param  string $input
322
     * @return array  Contains two values:
323
     *                Index 0: the path to the file in S3.
324
     *                Index 1: the S3 bucket name.
325
     */
326
    private function pathOrUrlToPath($input)
327
    {
328
        $bucket = $this->bucket;
329
330
        if (empty($input)) {
331
            return ['', $bucket];
332
        }
333
        if (strpos($input, 'http://') === 0 || strpos($input, 'https://') === 0) {
334
            $path = parse_url($input, PHP_URL_PATH);
335
336
            /**
337
             *  Try to detect the bucket name from the hostname
338
             */
339
            $host = parse_url($input, PHP_URL_HOST);
340
            if (preg_match('/s3[-\.a-z0-9]*\.amazonaws\.com$/', $host)) {
341
                // In this case we have a hostname which is just
342
                // something like: s3-eu-west.amazonaws.com. In this
343
                // case the bucket name is not part of the hostname,
344
                // which means the root of the path is instead (this is
345
                // a path-style name). EG:
346
                // s3-eu-west.amazonaws.com/pm2/somefile, the bucket
347
                // name here is pm2
348
                if (strpos($host, '.s3') === false) {
349
                    $path = ltrim($path, '/');
350
                    $path = substr($path, strpos($path, '/'));
351
                // In this case, the hostname is something like:
352
                // pm2.s3-eu-west.amazonaws.com, meaning that the bucket
353
                // name is pm2
354
                } else {
355
                    $bucket = substr($host, 0, strpos($host, '.s3'));
356
                }
357
            } else {
358
                $bucket = $host;
359
            }
360
        } else {
361
            $path = $input;
362
        }
363
364
        return [ltrim($path, '/'), $bucket];
365
    }
366
}
367