Completed
Push — master ( 911d70...b30754 )
by Nicolas
11s
created

AzureBlobStorage   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 530
Duplicated Lines 25.85 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 56
c 1
b 0
f 0
lcom 1
cbo 14
dl 137
loc 530
rs 6.5957

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 3
A getCreateContainerOptions() 0 4 1
A setCreateContainerOptions() 0 4 1
B createContainer() 23 23 4
A deleteContainer() 19 19 3
A read() 15 15 2
B write() 0 30 5
B exists() 0 34 6
B keys() 0 29 3
A mtime() 15 15 2
A size() 0 16 2
A delete() 15 15 2
A rename() 0 21 3
A isDirectory() 0 5 1
A setMetadata() 19 19 2
A getMetadata() 21 21 2
A init() 0 6 2
A failIfContainerNotFound() 0 12 2
A getErrorCodeFromServiceException() 0 10 3
A guessContentType() 10 10 2
A tokenizeKey() 0 18 3
A fetchBlobs() 0 14 2

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 AzureBlobStorage 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 AzureBlobStorage, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Gaufrette\Adapter;
4
5
use Gaufrette\Adapter;
6
use Gaufrette\Util;
7
use Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface;
8
use MicrosoftAzure\Storage\Blob\Models\Blob;
9
use MicrosoftAzure\Storage\Blob\Models\Container;
10
use MicrosoftAzure\Storage\Blob\Models\CreateBlobOptions;
11
use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions;
12
use MicrosoftAzure\Storage\Blob\Models\DeleteContainerOptions;
13
use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions;
14
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
15
16
/**
17
 * Microsoft Azure Blob Storage adapter.
18
 *
19
 * @author Luciano Mammino <[email protected]>
20
 * @author Paweł Czyżewski <[email protected]>
21
 */
22
class AzureBlobStorage 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...
23
                                  MetadataSupporter,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 34 found
Loading history...
24
                                  SizeCalculator
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 34 found
Loading history...
25
26
{
0 ignored issues
show
Coding Style introduced by
Opening brace of a class must be on the line following the class declaration; found 1 line(s)
Loading history...
27
    /**
28
     * Error constants.
29
     */
30
    const ERROR_CONTAINER_ALREADY_EXISTS = 'ContainerAlreadyExists';
31
    const ERROR_CONTAINER_NOT_FOUND = 'ContainerNotFound';
32
33
    /**
34
     * @var AzureBlobStorage\BlobProxyFactoryInterface
35
     */
36
    protected $blobProxyFactory;
37
38
    /**
39
     * @var string
40
     */
41
    protected $containerName;
42
43
    /**
44
     * @var bool
45
     */
46
    protected $detectContentType;
47
48
    /**
49
     * @var \MicrosoftAzure\Storage\Blob\Internal\IBlob
50
     */
51
    protected $blobProxy;
52
53
    /**
54
     * @var bool
55
     */
56
    protected $multiContainerMode = false;
57
58
    /**
59
     * @var CreateContainerOptions
60
     */
61
    protected $createContainerOptions;
62
63
    /**
64
     * @param AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory
65
     * @param string|null                                $containerName
66
     * @param bool                                       $create
67
     * @param bool                                       $detectContentType
68
     *
69
     * @throws \RuntimeException
70
     */
71
    public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName = null, $create = false, $detectContentType = true)
72
    {
73
        $this->blobProxyFactory = $blobProxyFactory;
74
        $this->containerName = $containerName;
75
        $this->detectContentType = $detectContentType;
76
        if (null === $containerName) {
77
            $this->multiContainerMode = true;
78
        } elseif ($create) {
79
            $this->createContainer($containerName);
80
        }
81
    }
82
83
    /**
84
     * @return CreateContainerOptions
85
     */
86
    public function getCreateContainerOptions()
87
    {
88
        return $this->createContainerOptions;
89
    }
90
91
    /**
92
     * @param CreateContainerOptions $options
93
     */
94
    public function setCreateContainerOptions(CreateContainerOptions $options)
95
    {
96
        $this->createContainerOptions = $options;
97
    }
98
99
    /**
100
     * Creates a new container.
101
     *
102
     * @param string                                                     $containerName
103
     * @param \MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions $options
104
     *
105
     * @throws \RuntimeException if cannot create the container
106
     */
107 View Code Duplication
    public function createContainer($containerName, CreateContainerOptions $options = null)
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...
108
    {
109
        $this->init();
110
111
        if (null === $options) {
112
            $options = $this->getCreateContainerOptions();
113
        }
114
115
        try {
116
            $this->blobProxy->createContainer($containerName, $options);
117
        } catch (ServiceException $e) {
118
            $errorCode = $this->getErrorCodeFromServiceException($e);
119
120
            if ($errorCode !== self::ERROR_CONTAINER_ALREADY_EXISTS) {
121
                throw new \RuntimeException(sprintf(
122
                    'Failed to create the configured container "%s": %s (%s).',
123
                    $containerName,
124
                    $e->getErrorText(),
125
                    $errorCode
126
                ));
127
            }
128
        }
129
    }
130
131
    /**
132
     * Deletes a container.
133
     *
134
     * @param string                 $containerName
135
     * @param DeleteContainerOptions $options
136
     *
137
     * @throws \RuntimeException if cannot delete the container
138
     */
139 View Code Duplication
    public function deleteContainer($containerName, DeleteContainerOptions $options = null)
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...
140
    {
141
        $this->init();
142
143
        try {
144
            $this->blobProxy->deleteContainer($containerName, $options);
145
        } catch (ServiceException $e) {
146
            $errorCode = $this->getErrorCodeFromServiceException($e);
147
148
            if ($errorCode !== self::ERROR_CONTAINER_NOT_FOUND) {
149
                throw new \RuntimeException(sprintf(
150
                    'Failed to delete the configured container "%s": %s (%s).',
151
                    $containerName,
152
                    $e->getErrorText(),
153
                    $errorCode
154
                ), $e->getCode());
155
            }
156
        }
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     * @throws \RuntimeException
162
     * @throws \InvalidArgumentException
163
     */
164 View Code Duplication
    public function read($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...
165
    {
166
        $this->init();
167
        list($containerName, $key) = $this->tokenizeKey($key);
168
169
        try {
170
            $blob = $this->blobProxy->getBlob($containerName, $key);
171
172
            return stream_get_contents($blob->getContentStream());
173
        } catch (ServiceException $e) {
174
            $this->failIfContainerNotFound($e, sprintf('read key "%s"', $key), $containerName);
175
176
            return false;
177
        }
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     * @throws \RuntimeException
183
     * @throws \InvalidArgumentException
184
     */
185
    public function write($key, $content)
186
    {
187
        $this->init();
188
        list($containerName, $key) = $this->tokenizeKey($key);
189
190
        $options = new CreateBlobOptions();
191
192
        if ($this->detectContentType) {
193
            $contentType = $this->guessContentType($content);
194
195
            $options->setContentType($contentType);
196
        }
197
198
        try {
199
            if ($this->multiContainerMode) {
200
                $this->createContainer($containerName);
201
            }
202
203
            $this->blobProxy->createBlockBlob($containerName, $key, $content, $options);
204
        } catch (ServiceException $e) {
205
            $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key), $containerName);
206
207
            return false;
208
        }
209
        if (is_resource($content)) {
210
            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...
211
        }
212
213
        return Util\Size::fromContent($content);
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     * @throws \RuntimeException
219
     * @throws \InvalidArgumentException
220
     */
221
    public function exists($key)
222
    {
223
        $this->init();
224
        list($containerName, $key) = $this->tokenizeKey($key);
225
226
        $listBlobsOptions = new ListBlobsOptions();
227
        $listBlobsOptions->setPrefix($key);
228
229
        try {
230
            $blobsList = $this->blobProxy->listBlobs($containerName, $listBlobsOptions);
231
232
            foreach ($blobsList->getBlobs() as $blob) {
233
                if ($key === $blob->getName()) {
234
                    return true;
235
                }
236
            }
237
        } catch (ServiceException $e) {
238
            $errorCode = $this->getErrorCodeFromServiceException($e);
239
            if ($this->multiContainerMode && self::ERROR_CONTAINER_NOT_FOUND === $errorCode) {
240
                return false;
241
            }
242
            $this->failIfContainerNotFound($e, 'check if key exists', $containerName);
243
244
            throw new \RuntimeException(sprintf(
245
                'Failed to check if key "%s" exists in container "%s": %s (%s).',
246
                $key,
247
                $containerName,
248
                $e->getErrorText(),
249
                $errorCode
250
            ), $e->getCode());
251
        }
252
253
        return false;
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     * @throws \RuntimeException
259
     */
260
    public function keys()
261
    {
262
        $this->init();
263
264
        try {
265
            if ($this->multiContainerMode) {
266
                $containersList = $this->blobProxy->listContainers();
267
                return call_user_func_array('array_merge', array_map(
268
                    function(Container $container) {
269
                        $containerName = $container->getName();
270
                        return $this->fetchBlobs($containerName, $containerName);
271
                    },
272
                    $containersList->getContainers()
273
                ));
274
            }
275
276
            return $this->fetchBlobs($this->containerName);
277
        } catch (ServiceException $e) {
278
            $this->failIfContainerNotFound($e, 'retrieve keys', $this->containerName);
279
            $errorCode = $this->getErrorCodeFromServiceException($e);
280
281
            throw new \RuntimeException(sprintf(
282
                'Failed to list keys for the container "%s": %s (%s).',
283
                $this->containerName,
284
                $e->getErrorText(),
285
                $errorCode
286
            ), $e->getCode());
287
        }
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     * @throws \RuntimeException
293
     * @throws \InvalidArgumentException
294
     */
295 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...
296
    {
297
        $this->init();
298
        list($containerName, $key) = $this->tokenizeKey($key);
299
300
        try {
301
            $properties = $this->blobProxy->getBlobProperties($containerName, $key);
302
303
            return $properties->getProperties()->getLastModified()->getTimestamp();
304
        } catch (ServiceException $e) {
305
            $this->failIfContainerNotFound($e, sprintf('read mtime for key "%s"', $key), $containerName);
306
307
            return false;
308
        }
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314
    public function size($key)
315
    {
316
        $this->init();
317
        list($containerName, $key) = $this->tokenizeKey($key);
318
319
        try {
320
            $properties = $this->blobProxy->getBlobProperties($containerName, $key);
321
322
            return $properties->getProperties()->getContentLength();
323
        } catch (ServiceException $e) {
324
            $this->failIfContainerNotFound($e, sprintf('read content length for key "%s"', $key), $containerName);
325
326
            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...
327
        }
328
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     * @throws \RuntimeException
334
     * @throws \InvalidArgumentException
335
     */
336 View Code Duplication
    public function delete($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...
337
    {
338
        $this->init();
339
        list($containerName, $key) = $this->tokenizeKey($key);
340
341
        try {
342
            $this->blobProxy->deleteBlob($containerName, $key);
343
344
            return true;
345
        } catch (ServiceException $e) {
346
            $this->failIfContainerNotFound($e, sprintf('delete key "%s"', $key), $containerName);
347
348
            return false;
349
        }
350
    }
351
352
    /**
353
     * {@inheritdoc}
354
     * @throws \RuntimeException
355
     * @throws \InvalidArgumentException
356
     */
357
    public function rename($sourceKey, $targetKey)
358
    {
359
        $this->init();
360
361
        list($sourceContainerName, $sourceKey) = $this->tokenizeKey($sourceKey);
362
        list($targetContainerName, $targetKey) = $this->tokenizeKey($targetKey);
363
364
        try {
365
            if ($this->multiContainerMode) {
366
                $this->createContainer($targetContainerName);
367
            }
368
            $this->blobProxy->copyBlob($targetContainerName, $targetKey, $sourceContainerName, $sourceKey);
369
            $this->blobProxy->deleteBlob($sourceContainerName, $sourceKey);
370
371
            return true;
372
        } catch (ServiceException $e) {
373
            $this->failIfContainerNotFound($e, sprintf('rename key "%s"', $sourceKey), $sourceContainerName);
374
375
            return false;
376
        }
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382
    public function isDirectory($key)
383
    {
384
        // Windows Azure Blob Storage does not support directories
385
        return false;
386
    }
387
388
    /**
389
     * {@inheritdoc}
390
     * @throws \RuntimeException
391
     * @throws \InvalidArgumentException
392
     */
393 View Code Duplication
    public function setMetadata($key, $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...
394
    {
395
        $this->init();
396
        list($containerName, $key) = $this->tokenizeKey($key);
397
398
        try {
399
            $this->blobProxy->setBlobMetadata($containerName, $key, $content);
400
        } catch (ServiceException $e) {
401
            $errorCode = $this->getErrorCodeFromServiceException($e);
402
403
            throw new \RuntimeException(sprintf(
404
                'Failed to set metadata for blob "%s" in container "%s": %s (%s).',
405
                $key,
406
                $containerName,
407
                $e->getErrorText(),
408
                $errorCode
409
            ), $e->getCode());
410
        }
411
    }
412
413
    /**
414
     * {@inheritdoc}
415
     * @throws \RuntimeException
416
     * @throws \InvalidArgumentException
417
     */
418 View Code Duplication
    public function getMetadata($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...
419
    {
420
        $this->init();
421
        list($containerName, $key) = $this->tokenizeKey($key);
422
423
        try {
424
            $properties = $this->blobProxy->getBlobProperties($containerName, $key);
425
426
            return $properties->getMetadata();
427
        } catch (ServiceException $e) {
428
            $errorCode = $this->getErrorCodeFromServiceException($e);
429
430
            throw new \RuntimeException(sprintf(
431
                'Failed to get metadata for blob "%s" in container "%s": %s (%s).',
432
                $key,
433
                $containerName,
434
                $e->getErrorText(),
435
                $errorCode
436
            ), $e->getCode());
437
        }
438
    }
439
440
    /**
441
     * Lazy initialization, automatically called when some method is called after construction.
442
     */
443
    protected function init()
444
    {
445
        if ($this->blobProxy === null) {
446
            $this->blobProxy = $this->blobProxyFactory->create();
447
        }
448
    }
449
450
    /**
451
     * Throws a runtime exception if a give ServiceException derived from a "container not found" error.
452
     *
453
     * @param ServiceException $exception
454
     * @param string           $action
455
     * @param string           $containerName
456
     *
457
     * @throws \RuntimeException
458
     */
459
    protected function failIfContainerNotFound(ServiceException $exception, $action, $containerName)
460
    {
461
        $errorCode = $this->getErrorCodeFromServiceException($exception);
462
463
        if ($errorCode === self::ERROR_CONTAINER_NOT_FOUND) {
464
            throw new \RuntimeException(sprintf(
465
                'Failed to %s: container "%s" not found.',
466
                $action,
467
                $containerName
468
            ), $exception->getCode());
469
        }
470
    }
471
472
    /**
473
     * Extracts the error code from a service exception.
474
     *
475
     * @param ServiceException $exception
476
     *
477
     * @return string
478
     */
479
    protected function getErrorCodeFromServiceException(ServiceException $exception)
480
    {
481
        $xml = @simplexml_load_string($exception->getResponse()->getBody());
482
483
        if ($xml && isset($xml->Code)) {
484
            return (string) $xml->Code;
485
        }
486
487
        return $exception->getErrorText();
488
    }
489
490
    /**
491
     * @param string|resource $content
492
     *
493
     * @return string
494
     */
495 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...
496
    {
497
        $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
498
499
        if (is_resource($content)) {
500
            return $fileInfo->file(stream_get_meta_data($content)['uri']);
501
        }
502
503
        return $fileInfo->buffer($content);
504
    }
505
506
    /**
507
     * @param string $key
508
     *
509
     * @return array
510
     * @throws \InvalidArgumentException
511
     */
512
    private function tokenizeKey($key)
513
    {
514
        $containerName = $this->containerName;
515
        if (false === $this->multiContainerMode) {
516
            return [$containerName, $key];
517
        }
518
519
        if (false === ($index = strpos($key, '/'))) {
520
            throw new \InvalidArgumentException(sprintf(
521
                'Failed to establish container name from key "%s", container name is required in multi-container mode',
522
                $key
523
            ));
524
        }
525
        $containerName = substr($key, 0, $index);
526
        $key = substr($key, $index + 1);
527
528
        return [$containerName, $key];
529
    }
530
531
    /**
532
     * @param string $containerName
533
     * @param null   $prefix
534
     *
535
     * @return array
536
     */
537
    private function fetchBlobs($containerName, $prefix = null)
538
    {
539
        $blobList = $this->blobProxy->listBlobs($containerName);
540
        return array_map(
541
            function (Blob $blob) use ($prefix) {
542
                $name = $blob->getName();
543
                if (null !== $prefix) {
544
                    $name = $prefix .'/'. $name;
545
                }
546
                return $name;
547
            },
548
            $blobList->getBlobs()
549
        );
550
    }
551
}
552