Completed
Pull Request — master (#398)
by Aurélien
02:51
created

AzureBlobStorage::write()   B

Complexity

Conditions 5
Paths 25

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 30
rs 8.439
cc 5
eloc 19
nc 25
nop 2
1
<?php
2
3
namespace Gaufrette\Adapter;
4
5
use Gaufrette\Adapter;
6
use Gaufrette\Util;
7
use Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface;
8
use WindowsAzure\Blob\Models\CreateBlobOptions;
9
use WindowsAzure\Blob\Models\CreateContainerOptions;
10
use WindowsAzure\Blob\Models\DeleteContainerOptions;
11
use WindowsAzure\Blob\Models\ListBlobsOptions;
12
use WindowsAzure\Common\ServiceException;
13
14
/**
15
 * Microsoft Azure Blob Storage adapter.
16
 *
17
 * @author Luciano Mammino <[email protected]>
18
 * @author Paweł Czyżewski <[email protected]>
19
 */
20
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...
21
                                  MetadataSupporter
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 34 found
Loading history...
22
{
23
    /**
24
     * Error constants.
25
     */
26
    const ERROR_CONTAINER_ALREADY_EXISTS = 'ContainerAlreadyExists';
27
    const ERROR_CONTAINER_NOT_FOUND = 'ContainerNotFound';
28
29
    /**
30
     * @var AzureBlobStorage\BlobProxyFactoryInterface
31
     */
32
    protected $blobProxyFactory;
33
34
    /**
35
     * @var string
36
     */
37
    protected $containerName;
38
39
    /**
40
     * @var bool
41
     */
42
    protected $detectContentType;
43
44
    /**
45
     * @var \WindowsAzure\Blob\Internal\IBlob
46
     */
47
    protected $blobProxy;
48
49
    /**
50
     * @param AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory
51
     * @param string                                     $containerName
52
     * @param bool                                       $create
53
     * @param bool                                       $detectContentType
54
     */
55
    public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName, $create = false, $detectContentType = true)
56
    {
57
        $this->blobProxyFactory = $blobProxyFactory;
58
        $this->containerName = $containerName;
59
        $this->detectContentType = $detectContentType;
60
        if ($create) {
61
            $this->createContainer($containerName);
62
        }
63
    }
64
65
    /**
66
     * Creates a new container.
67
     *
68
     * @param string                                           $containerName
69
     * @param \WindowsAzure\Blob\Models\CreateContainerOptions $options
70
     *
71
     * @throws \RuntimeException if cannot create the container
72
     */
73 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...
74
    {
75
        $this->init();
76
77
        try {
78
            $this->blobProxy->createContainer($containerName, $options);
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 73 can also be of type object<WindowsAzure\Blob...CreateContainerOptions>; however, WindowsAzure\Blob\Intern...Blob::createContainer() does only seem to accept object<WindowsAzure\Blob...eContainerOptions>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
79
        } catch (ServiceException $e) {
80
            $errorCode = $this->getErrorCodeFromServiceException($e);
81
82
            if ($errorCode != self::ERROR_CONTAINER_ALREADY_EXISTS) {
83
                throw new \RuntimeException(sprintf(
84
                    'Failed to create the configured container "%s": %s (%s).',
85
                    $containerName,
86
                    $e->getErrorText(),
87
                    $errorCode
88
                ));
89
            }
90
        }
91
    }
92
93
    /**
94
     * Deletes a container.
95
     *
96
     * @param string                 $containerName
97
     * @param DeleteContainerOptions $options
98
     *
99
     * @throws \RuntimeException if cannot delete the container
100
     */
101 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...
102
    {
103
        $this->init();
104
105
        try {
106
            $this->blobProxy->deleteContainer($containerName, $options);
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 101 can also be of type object<WindowsAzure\Blob...DeleteContainerOptions>; however, WindowsAzure\Blob\Intern...Blob::deleteContainer() does only seem to accept object<WindowsAzure\Blob...eContainerOptions>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
107
        } catch (ServiceException $e) {
108
            $errorCode = $this->getErrorCodeFromServiceException($e);
109
110
            if ($errorCode != self::ERROR_CONTAINER_NOT_FOUND) {
111
                throw new \RuntimeException(sprintf(
112
                    'Failed to delete the configured container "%s": %s (%s).',
113
                    $containerName,
114
                    $e->getErrorText(),
115
                    $errorCode
116
                ), $e->getCode());
117
            }
118
        }
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 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...
125
    {
126
        $this->init();
127
128
        try {
129
            $blob = $this->blobProxy->getBlob($this->containerName, $key);
130
131
            return stream_get_contents($blob->getContentStream());
132
        } catch (ServiceException $e) {
133
            $this->failIfContainerNotFound($e, sprintf('read key "%s"', $key));
134
135
            return false;
136
        }
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142
    public function write($key, $content)
143
    {
144
        $this->init();
145
146
        try {
147
            $options = new CreateBlobOptions();
148
149
            if ($this->detectContentType) {
150
                $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
151
                if (is_resource($content)) {
152
                    $contentType = $fileInfo->file(stream_get_meta_data($content)['uri']);
153
                } else {
154
                    $contentType = $fileInfo->buffer($content);
155
                }
156
                $options->setBlobContentType($contentType);
157
            }
158
159
            $this->blobProxy->createBlockBlob($this->containerName, $key, $content, $options);
0 ignored issues
show
Documentation introduced by
$options is of type object<WindowsAzure\Blob...dels\CreateBlobOptions>, but the function expects a object<WindowsAzure\Blob...CreateBlobOptions>|null.

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...
Bug introduced by
It seems like $content can also be of type resource; however, WindowsAzure\Blob\Intern...Blob::createBlockBlob() 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...
160
161
            if (is_resource($content)) {
162
                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...
163
            } else {
164
                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...
165
            }
166
        } catch (ServiceException $e) {
167
            $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key));
168
169
            return false;
170
        }
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function exists($key)
177
    {
178
        $this->init();
179
180
        $listBlobsOptions = new ListBlobsOptions();
181
        $listBlobsOptions->setPrefix($key);
182
183
        try {
184
            $blobsList = $this->blobProxy->listBlobs($this->containerName, $listBlobsOptions);
0 ignored issues
show
Documentation introduced by
$listBlobsOptions is of type object<WindowsAzure\Blob\Models\ListBlobsOptions>, but the function expects a object<WindowsAzure\Blob...\ListBlobsOptions>|null.

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...
185
186
            foreach ($blobsList->getBlobs() as $blob) {
187
                if ($key === $blob->getName()) {
188
                    return true;
189
                }
190
            }
191
        } catch (ServiceException $e) {
192
            $this->failIfContainerNotFound($e, 'check if key exists');
193
            $errorCode = $this->getErrorCodeFromServiceException($e);
194
195
            throw new \RuntimeException(sprintf(
196
                'Failed to check if key "%s" exists in container "%s": %s (%s).',
197
                $key,
198
                $this->containerName,
199
                $e->getErrorText(),
200
                $errorCode
201
            ), $e->getCode());
202
        }
203
204
        return false;
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function keys()
211
    {
212
        $this->init();
213
214
        try {
215
            $blobList = $this->blobProxy->listBlobs($this->containerName);
216
217
            return array_map(
218
                function ($blob) {
219
                    return $blob->getName();
220
                },
221
                $blobList->getBlobs()
222
            );
223
        } catch (ServiceException $e) {
224
            $this->failIfContainerNotFound($e, 'retrieve keys');
225
            $errorCode = $this->getErrorCodeFromServiceException($e);
226
227
            throw new \RuntimeException(sprintf(
228
                'Failed to list keys for the container "%s": %s (%s).',
229
                $this->containerName,
230
                $e->getErrorText(),
231
                $errorCode
232
            ), $e->getCode());
233
        }
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239
    public function mtime($key)
240
    {
241
        $this->init();
242
243
        try {
244
            $properties = $this->blobProxy->getBlobProperties($this->containerName, $key);
245
246
            return $properties->getProperties()->getLastModified()->getTimestamp();
247
        } catch (ServiceException $e) {
248
            $this->failIfContainerNotFound($e, sprintf('read mtime for key "%s"', $key));
249
250
            return false;
251
        }
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257 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...
258
    {
259
        $this->init();
260
261
        try {
262
            $this->blobProxy->deleteBlob($this->containerName, $key);
263
264
            return true;
265
        } catch (ServiceException $e) {
266
            $this->failIfContainerNotFound($e, sprintf('delete key "%s"', $key));
267
268
            return false;
269
        }
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function rename($sourceKey, $targetKey)
276
    {
277
        $this->init();
278
279
        try {
280
            $this->blobProxy->copyBlob($this->containerName, $targetKey, $this->containerName, $sourceKey);
281
            $this->blobProxy->deleteBlob($this->containerName, $sourceKey);
282
283
            return true;
284
        } catch (ServiceException $e) {
285
            $this->failIfContainerNotFound($e, sprintf('rename key "%s"', $sourceKey));
286
287
            return false;
288
        }
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294
    public function isDirectory($key)
295
    {
296
        // Windows Azure Blob Storage does not support directories
297
        return false;
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     */
303 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...
304
    {
305
        $this->init();
306
307
        try {
308
            $this->blobProxy->setBlobMetadata($this->containerName, $key, $content);
309
        } catch (ServiceException $e) {
310
            $errorCode = $this->getErrorCodeFromServiceException($e);
311
312
            throw new \RuntimeException(sprintf(
313
                'Failed to set metadata for blob "%s" in container "%s": %s (%s).',
314
                $key,
315
                $this->containerName,
316
                $e->getErrorText(),
317
                $errorCode
318
            ), $e->getCode());
319
        }
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325 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...
326
    {
327
        $this->init();
328
329
        try {
330
            $properties = $this->blobProxy->getBlobProperties($this->containerName, $key);
331
332
            return $properties->getMetadata();
333
        } catch (ServiceException $e) {
334
            $errorCode = $this->getErrorCodeFromServiceException($e);
335
336
            throw new \RuntimeException(sprintf(
337
                'Failed to get metadata for blob "%s" in container "%s": %s (%s).',
338
                $key,
339
                $this->containerName,
340
                $e->getErrorText(),
341
                $errorCode
342
            ), $e->getCode());
343
        }
344
    }
345
346
    /**
347
     * Lazy initialization, automatically called when some method is called after construction.
348
     */
349
    protected function init()
350
    {
351
        if ($this->blobProxy == null) {
352
            $this->blobProxy = $this->blobProxyFactory->create();
353
        }
354
    }
355
356
    /**
357
     * Throws a runtime exception if a give ServiceException derived from a "container not found" error.
358
     *
359
     * @param ServiceException $exception
360
     * @param string           $action
361
     *
362
     * @throws \RuntimeException
363
     */
364
    protected function failIfContainerNotFound(ServiceException $exception, $action)
365
    {
366
        $errorCode = $this->getErrorCodeFromServiceException($exception);
367
368
        if ($errorCode == self::ERROR_CONTAINER_NOT_FOUND) {
369
            throw new \RuntimeException(sprintf(
370
                'Failed to %s: container "%s" not found.',
371
                $action,
372
                $this->containerName
373
            ), $exception->getCode());
374
        }
375
    }
376
377
    /**
378
     * Extracts the error code from a service exception.
379
     *
380
     * @param ServiceException $exception
381
     *
382
     * @return string
383
     */
384
    protected function getErrorCodeFromServiceException(ServiceException $exception)
385
    {
386
        $xml = @simplexml_load_string($exception->getErrorReason());
387
388
        if ($xml && isset($xml->Code)) {
389
            return (string) $xml->Code;
390
        }
391
392
        return $exception->getErrorReason();
393
    }
394
}
395