Completed
Push — master ( d64c7f...d0ede0 )
by Nicolas
11s
created

src/Gaufrette/Adapter/AzureBlobStorage.php (12 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\CreateBlockBlobOptions;
12
use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions;
13
use MicrosoftAzure\Storage\Blob\Models\DeleteContainerOptions;
14
use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions;
15
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
16
17
/**
18
 * Microsoft Azure Blob Storage adapter.
19
 *
20
 * @author Luciano Mammino <[email protected]>
21
 * @author Paweł Czyżewski <[email protected]>
22
 */
23
class AzureBlobStorage implements Adapter,
24
                                  MetadataSupporter,
25
                                  SizeCalculator
26
27
{
28
    /**
29
     * Error constants.
30
     */
31
    const ERROR_CONTAINER_ALREADY_EXISTS = 'ContainerAlreadyExists';
32
    const ERROR_CONTAINER_NOT_FOUND = 'ContainerNotFound';
33
34
    /**
35
     * @var AzureBlobStorage\BlobProxyFactoryInterface
36
     */
37
    protected $blobProxyFactory;
38
39
    /**
40
     * @var string
41
     */
42
    protected $containerName;
43
44
    /**
45
     * @var bool
46
     */
47
    protected $detectContentType;
48
49
    /**
50
     * @var \MicrosoftAzure\Storage\Blob\Internal\IBlob
51
     */
52
    protected $blobProxy;
53
54
    /**
55
     * @var bool
56
     */
57
    protected $multiContainerMode = false;
58
59
    /**
60
     * @var CreateContainerOptions
61
     */
62
    protected $createContainerOptions;
63
64
    /**
65
     * @param AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory
66
     * @param string|null                                $containerName
67
     * @param bool                                       $create
68
     * @param bool                                       $detectContentType
69
     *
70
     * @throws \RuntimeException
71
     */
72
    public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName = null, $create = false, $detectContentType = true)
73
    {
74
        $this->blobProxyFactory = $blobProxyFactory;
75
        $this->containerName = $containerName;
76
        $this->detectContentType = $detectContentType;
77
        if (null === $containerName) {
78
            $this->multiContainerMode = true;
79
        } elseif ($create) {
80
            $this->createContainer($containerName);
81
        }
82
    }
83
84
    /**
85
     * @return CreateContainerOptions
86
     */
87
    public function getCreateContainerOptions()
88
    {
89
        return $this->createContainerOptions;
90
    }
91
92
    /**
93
     * @param CreateContainerOptions $options
94
     */
95
    public function setCreateContainerOptions(CreateContainerOptions $options)
96
    {
97
        $this->createContainerOptions = $options;
98
    }
99
100
    /**
101
     * Creates a new container.
102
     *
103
     * @param string                                                     $containerName
104
     * @param \MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions $options
105
     *
106
     * @throws \RuntimeException if cannot create the container
107
     */
108 View Code Duplication
    public function createContainer($containerName, CreateContainerOptions $options = null)
0 ignored issues
show
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...
109
    {
110
        $this->init();
111
112
        if (null === $options) {
113
            $options = $this->getCreateContainerOptions();
114
        }
115
116
        try {
117
            $this->blobProxy->createContainer($containerName, $options);
118
        } catch (ServiceException $e) {
119
            $errorCode = $this->getErrorCodeFromServiceException($e);
120
121
            if ($errorCode !== self::ERROR_CONTAINER_ALREADY_EXISTS) {
122
                throw new \RuntimeException(sprintf(
123
                    'Failed to create the configured container "%s": %s (%s).',
124
                    $containerName,
125
                    $e->getErrorText(),
126
                    $errorCode
127
                ));
128
            }
129
        }
130
    }
131
132
    /**
133
     * Deletes a container.
134
     *
135
     * @param string                 $containerName
136
     * @param DeleteContainerOptions $options
137
     *
138
     * @throws \RuntimeException if cannot delete the container
139
     */
140 View Code Duplication
    public function deleteContainer($containerName, DeleteContainerOptions $options = null)
0 ignored issues
show
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...
141
    {
142
        $this->init();
143
144
        try {
145
            $this->blobProxy->deleteContainer($containerName, $options);
146
        } catch (ServiceException $e) {
147
            $errorCode = $this->getErrorCodeFromServiceException($e);
148
149
            if ($errorCode !== self::ERROR_CONTAINER_NOT_FOUND) {
150
                throw new \RuntimeException(sprintf(
151
                    'Failed to delete the configured container "%s": %s (%s).',
152
                    $containerName,
153
                    $e->getErrorText(),
154
                    $errorCode
155
                ), $e->getCode());
156
            }
157
        }
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     * @throws \RuntimeException
163
     * @throws \InvalidArgumentException
164
     */
165 View Code Duplication
    public function read($key)
0 ignored issues
show
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...
166
    {
167
        $this->init();
168
        list($containerName, $key) = $this->tokenizeKey($key);
169
170
        try {
171
            $blob = $this->blobProxy->getBlob($containerName, $key);
172
173
            return stream_get_contents($blob->getContentStream());
174
        } catch (ServiceException $e) {
175
            $this->failIfContainerNotFound($e, sprintf('read key "%s"', $key), $containerName);
176
177
            return false;
178
        }
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     * @throws \RuntimeException
184
     * @throws \InvalidArgumentException
185
     */
186
    public function write($key, $content)
187
    {
188
        $this->init();
189
        list($containerName, $key) = $this->tokenizeKey($key);
190
191
        if (class_exists(CreateBlockBlobOptions::class)) {
192
            $options = new CreateBlockBlobOptions();
193
        } else {
194
            // for microsoft/azure-storage < 1.0
195
            $options = new CreateBlobOptions();
196
        }
197
198
        if ($this->detectContentType) {
199
            $contentType = $this->guessContentType($content);
200
201
            $options->setContentType($contentType);
202
        }
203
204
        try {
205
            if ($this->multiContainerMode) {
206
                $this->createContainer($containerName);
207
            }
208
209
            $this->blobProxy->createBlockBlob($containerName, $key, $content, $options);
0 ignored issues
show
$options is of type object<MicrosoftAzure\St...dels\CreateBlobOptions>, but the function expects a null|object<MicrosoftAzu...CreateBlockBlobOptions>.

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...
210
        } catch (ServiceException $e) {
211
            $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key), $containerName);
212
213
            return false;
214
        }
215
        if (is_resource($content)) {
216
            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...
217
        }
218
219
        return Util\Size::fromContent($content);
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     * @throws \RuntimeException
225
     * @throws \InvalidArgumentException
226
     */
227
    public function exists($key)
228
    {
229
        $this->init();
230
        list($containerName, $key) = $this->tokenizeKey($key);
231
232
        $listBlobsOptions = new ListBlobsOptions();
233
        $listBlobsOptions->setPrefix($key);
234
235
        try {
236
            $blobsList = $this->blobProxy->listBlobs($containerName, $listBlobsOptions);
237
238
            foreach ($blobsList->getBlobs() as $blob) {
239
                if ($key === $blob->getName()) {
240
                    return true;
241
                }
242
            }
243
        } catch (ServiceException $e) {
244
            $errorCode = $this->getErrorCodeFromServiceException($e);
245
            if ($this->multiContainerMode && self::ERROR_CONTAINER_NOT_FOUND === $errorCode) {
246
                return false;
247
            }
248
            $this->failIfContainerNotFound($e, 'check if key exists', $containerName);
249
250
            throw new \RuntimeException(sprintf(
251
                'Failed to check if key "%s" exists in container "%s": %s (%s).',
252
                $key,
253
                $containerName,
254
                $e->getErrorText(),
255
                $errorCode
256
            ), $e->getCode());
257
        }
258
259
        return false;
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     * @throws \RuntimeException
265
     */
266
    public function keys()
267
    {
268
        $this->init();
269
270
        try {
271
            if ($this->multiContainerMode) {
272
                $containersList = $this->blobProxy->listContainers();
273
                return call_user_func_array('array_merge', array_map(
274
                    function(Container $container) {
275
                        $containerName = $container->getName();
276
                        return $this->fetchBlobs($containerName, $containerName);
277
                    },
278
                    $containersList->getContainers()
279
                ));
280
            }
281
282
            return $this->fetchBlobs($this->containerName);
283
        } catch (ServiceException $e) {
284
            $this->failIfContainerNotFound($e, 'retrieve keys', $this->containerName);
285
            $errorCode = $this->getErrorCodeFromServiceException($e);
286
287
            throw new \RuntimeException(sprintf(
288
                'Failed to list keys for the container "%s": %s (%s).',
289
                $this->containerName,
290
                $e->getErrorText(),
291
                $errorCode
292
            ), $e->getCode());
293
        }
294
    }
295
296
    /**
297
     * {@inheritdoc}
298
     * @throws \RuntimeException
299
     * @throws \InvalidArgumentException
300
     */
301 View Code Duplication
    public function mtime($key)
0 ignored issues
show
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...
302
    {
303
        $this->init();
304
        list($containerName, $key) = $this->tokenizeKey($key);
305
306
        try {
307
            $properties = $this->blobProxy->getBlobProperties($containerName, $key);
308
309
            return $properties->getProperties()->getLastModified()->getTimestamp();
310
        } catch (ServiceException $e) {
311
            $this->failIfContainerNotFound($e, sprintf('read mtime for key "%s"', $key), $containerName);
312
313
            return false;
314
        }
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     */
320 View Code Duplication
    public function size($key)
0 ignored issues
show
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...
321
    {
322
        $this->init();
323
        list($containerName, $key) = $this->tokenizeKey($key);
324
325
        try {
326
            $properties = $this->blobProxy->getBlobProperties($containerName, $key);
327
328
            return $properties->getProperties()->getContentLength();
329
        } catch (ServiceException $e) {
330
            $this->failIfContainerNotFound($e, sprintf('read content length for key "%s"', $key), $containerName);
331
332
            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...
333
        }
334
335
    }
336
337
    /**
338
     * {@inheritdoc}
339
     * @throws \RuntimeException
340
     * @throws \InvalidArgumentException
341
     */
342 View Code Duplication
    public function delete($key)
0 ignored issues
show
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...
343
    {
344
        $this->init();
345
        list($containerName, $key) = $this->tokenizeKey($key);
346
347
        try {
348
            $this->blobProxy->deleteBlob($containerName, $key);
349
350
            return true;
351
        } catch (ServiceException $e) {
352
            $this->failIfContainerNotFound($e, sprintf('delete key "%s"', $key), $containerName);
353
354
            return false;
355
        }
356
    }
357
358
    /**
359
     * {@inheritdoc}
360
     * @throws \RuntimeException
361
     * @throws \InvalidArgumentException
362
     */
363
    public function rename($sourceKey, $targetKey)
364
    {
365
        $this->init();
366
367
        list($sourceContainerName, $sourceKey) = $this->tokenizeKey($sourceKey);
368
        list($targetContainerName, $targetKey) = $this->tokenizeKey($targetKey);
369
370
        try {
371
            if ($this->multiContainerMode) {
372
                $this->createContainer($targetContainerName);
373
            }
374
            $this->blobProxy->copyBlob($targetContainerName, $targetKey, $sourceContainerName, $sourceKey);
375
            $this->blobProxy->deleteBlob($sourceContainerName, $sourceKey);
376
377
            return true;
378
        } catch (ServiceException $e) {
379
            $this->failIfContainerNotFound($e, sprintf('rename key "%s"', $sourceKey), $sourceContainerName);
380
381
            return false;
382
        }
383
    }
384
385
    /**
386
     * {@inheritdoc}
387
     */
388
    public function isDirectory($key)
389
    {
390
        // Windows Azure Blob Storage does not support directories
391
        return false;
392
    }
393
394
    /**
395
     * {@inheritdoc}
396
     * @throws \RuntimeException
397
     * @throws \InvalidArgumentException
398
     */
399 View Code Duplication
    public function setMetadata($key, $content)
0 ignored issues
show
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...
400
    {
401
        $this->init();
402
        list($containerName, $key) = $this->tokenizeKey($key);
403
404
        try {
405
            $this->blobProxy->setBlobMetadata($containerName, $key, $content);
406
        } catch (ServiceException $e) {
407
            $errorCode = $this->getErrorCodeFromServiceException($e);
408
409
            throw new \RuntimeException(sprintf(
410
                'Failed to set metadata for blob "%s" in container "%s": %s (%s).',
411
                $key,
412
                $containerName,
413
                $e->getErrorText(),
414
                $errorCode
415
            ), $e->getCode());
416
        }
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     * @throws \RuntimeException
422
     * @throws \InvalidArgumentException
423
     */
424 View Code Duplication
    public function getMetadata($key)
0 ignored issues
show
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...
425
    {
426
        $this->init();
427
        list($containerName, $key) = $this->tokenizeKey($key);
428
429
        try {
430
            $properties = $this->blobProxy->getBlobProperties($containerName, $key);
431
432
            return $properties->getMetadata();
433
        } catch (ServiceException $e) {
434
            $errorCode = $this->getErrorCodeFromServiceException($e);
435
436
            throw new \RuntimeException(sprintf(
437
                'Failed to get metadata for blob "%s" in container "%s": %s (%s).',
438
                $key,
439
                $containerName,
440
                $e->getErrorText(),
441
                $errorCode
442
            ), $e->getCode());
443
        }
444
    }
445
446
    /**
447
     * Lazy initialization, automatically called when some method is called after construction.
448
     */
449
    protected function init()
450
    {
451
        if ($this->blobProxy === null) {
452
            $this->blobProxy = $this->blobProxyFactory->create();
453
        }
454
    }
455
456
    /**
457
     * Throws a runtime exception if a give ServiceException derived from a "container not found" error.
458
     *
459
     * @param ServiceException $exception
460
     * @param string           $action
461
     * @param string           $containerName
462
     *
463
     * @throws \RuntimeException
464
     */
465
    protected function failIfContainerNotFound(ServiceException $exception, $action, $containerName)
466
    {
467
        $errorCode = $this->getErrorCodeFromServiceException($exception);
468
469
        if ($errorCode === self::ERROR_CONTAINER_NOT_FOUND) {
470
            throw new \RuntimeException(sprintf(
471
                'Failed to %s: container "%s" not found.',
472
                $action,
473
                $containerName
474
            ), $exception->getCode());
475
        }
476
    }
477
478
    /**
479
     * Extracts the error code from a service exception.
480
     *
481
     * @param ServiceException $exception
482
     *
483
     * @return string
484
     */
485
    protected function getErrorCodeFromServiceException(ServiceException $exception)
486
    {
487
        $xml = @simplexml_load_string($exception->getResponse()->getBody());
488
489
        if ($xml && isset($xml->Code)) {
490
            return (string) $xml->Code;
491
        }
492
493
        return $exception->getErrorText();
494
    }
495
496
    /**
497
     * @param string|resource $content
498
     *
499
     * @return string
500
     */
501 View Code Duplication
    private function guessContentType($content)
0 ignored issues
show
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...
502
    {
503
        $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
504
505
        if (is_resource($content)) {
506
            return $fileInfo->file(stream_get_meta_data($content)['uri']);
507
        }
508
509
        return $fileInfo->buffer($content);
510
    }
511
512
    /**
513
     * @param string $key
514
     *
515
     * @return array
516
     * @throws \InvalidArgumentException
517
     */
518
    private function tokenizeKey($key)
519
    {
520
        $containerName = $this->containerName;
521
        if (false === $this->multiContainerMode) {
522
            return [$containerName, $key];
523
        }
524
525
        if (false === ($index = strpos($key, '/'))) {
526
            throw new \InvalidArgumentException(sprintf(
527
                'Failed to establish container name from key "%s", container name is required in multi-container mode',
528
                $key
529
            ));
530
        }
531
        $containerName = substr($key, 0, $index);
532
        $key = substr($key, $index + 1);
533
534
        return [$containerName, $key];
535
    }
536
537
    /**
538
     * @param string $containerName
539
     * @param null   $prefix
540
     *
541
     * @return array
542
     */
543
    private function fetchBlobs($containerName, $prefix = null)
544
    {
545
        $blobList = $this->blobProxy->listBlobs($containerName);
546
        return array_map(
547
            function (Blob $blob) use ($prefix) {
548
                $name = $blob->getName();
549
                if (null !== $prefix) {
550
                    $name = $prefix .'/'. $name;
551
                }
552
                return $name;
553
            },
554
            $blobList->getBlobs()
555
        );
556
    }
557
}
558