Completed
Pull Request — master (#414)
by Albin
02:59
created

AzureBlobStorage::write()   B

Complexity

Conditions 4
Paths 14

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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