Completed
Pull Request — master (#486)
by Dalibor
02:20
created

AzureBlobStorage::tokenizeKey()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 1
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\CreateBlobOptions;
10
use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions;
11
use MicrosoftAzure\Storage\Blob\Models\DeleteContainerOptions;
12
use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions;
13
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
14
15
/**
16
 * Microsoft Azure Blob Storage adapter.
17
 *
18
 * @author Luciano Mammino <[email protected]>
19
 * @author Paweł Czyżewski <[email protected]>
20
 */
21
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...
22
                                  MetadataSupporter
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 34 found
Loading history...
23
{
24
    /**
25
     * Error constants.
26
     */
27
    const ERROR_CONTAINER_ALREADY_EXISTS = 'ContainerAlreadyExists';
28
    const ERROR_CONTAINER_NOT_FOUND = 'ContainerNotFound';
29
30
    /**
31
     * @var AzureBlobStorage\BlobProxyFactoryInterface
32
     */
33
    protected $blobProxyFactory;
34
35
    /**
36
     * @var string
37
     */
38
    protected $containerName;
39
40
    /**
41
     * @var bool
42
     */
43
    protected $detectContentType;
44
45
    /**
46
     * @var \MicrosoftAzure\Storage\Blob\Internal\IBlob
47
     */
48
    protected $blobProxy;
49
50
    /**
51
     * @var bool
52
     */
53
    protected $multiContainerMode = false;
54
55
    /**
56
     * @var CreateContainerOptions
57
     */
58
    protected $createContainerOptions;
59
60
    /**
61
     * @param AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory
62
     * @param string|null                                $containerName
63
     * @param bool                                       $create
64
     * @param bool                                       $detectContentType
65
     *
66
     * @throws \RuntimeException
67
     */
68
    public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName = null, $create = false, $detectContentType = true)
69
    {
70
        $this->blobProxyFactory = $blobProxyFactory;
71
        $this->containerName = $containerName;
72
        $this->detectContentType = $detectContentType;
73
        if (null === $containerName) {
74
            $this->multiContainerMode = true;
75
        } elseif ($create) {
76
            $this->createContainer($containerName);
77
        }
78
    }
79
80
    /**
81
     * @return CreateContainerOptions
82
     */
83
    public function getCreateContainerOptions()
84
    {
85
        return $this->createContainerOptions;
86
    }
87
88
    /**
89
     * @param CreateContainerOptions $options
90
     */
91
    public function setCreateContainerOptions(CreateContainerOptions $options)
92
    {
93
        $this->createContainerOptions = $options;
94
    }
95
96
    /**
97
     * Creates a new container.
98
     *
99
     * @param string                                                     $containerName
100
     * @param \MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions $options
101
     *
102
     * @throws \RuntimeException if cannot create the container
103
     */
104 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...
105
    {
106
        $this->init();
107
108
        if (null === $options) {
109
            $options = $this->getCreateContainerOptions();
110
        }
111
112
        try {
113
            $this->blobProxy->createContainer($containerName, $options);
114
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
115
            $errorCode = $this->getErrorCodeFromServiceException($e);
116
117
            if ($errorCode !== self::ERROR_CONTAINER_ALREADY_EXISTS) {
118
                throw new \RuntimeException(sprintf(
119
                    'Failed to create the configured container "%s": %s (%s).',
120
                    $containerName,
121
                    $e->getErrorText(),
122
                    $errorCode
123
                ));
124
            }
125
        }
126
    }
127
128
    /**
129
     * Deletes a container.
130
     *
131
     * @param string                 $containerName
132
     * @param DeleteContainerOptions $options
133
     *
134
     * @throws \RuntimeException if cannot delete the container
135
     */
136 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...
137
    {
138
        $this->init();
139
140
        try {
141
            $this->blobProxy->deleteContainer($containerName, $options);
142
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
143
            $errorCode = $this->getErrorCodeFromServiceException($e);
144
145
            if ($errorCode !== self::ERROR_CONTAINER_NOT_FOUND) {
146
                throw new \RuntimeException(sprintf(
147
                    'Failed to delete the configured container "%s": %s (%s).',
148
                    $containerName,
149
                    $e->getErrorText(),
150
                    $errorCode
151
                ), $e->getCode());
152
            }
153
        }
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     * @throws \RuntimeException
159
     * @throws \InvalidArgumentException
160
     */
161 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...
162
    {
163
        $this->init();
164
        list($containerName, $key) = $this->tokenizeKey($key);
165
166
        try {
167
            $blob = $this->blobProxy->getBlob($containerName, $key);
168
169
            return stream_get_contents($blob->getContentStream());
170
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
171
            $this->failIfContainerNotFound($e, sprintf('read key "%s"', $key), $containerName);
172
173
            return false;
174
        }
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     * @throws \RuntimeException
180
     * @throws \InvalidArgumentException
181
     */
182
    public function write($key, $content)
183
    {
184
        $this->init();
185
        list($containerName, $key) = $this->tokenizeKey($key);
186
187
        $options = new CreateBlobOptions();
188
189
        if ($this->detectContentType) {
190
            $contentType = $this->guessContentType($content);
191
192
            $options->setBlobContentType($contentType);
193
        }
194
195
        try {
196
            $this->createContainer($containerName);
197
198
            $this->blobProxy->createBlockBlob($containerName, $key, $content, $options);
199
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
200
            $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key), $containerName);
201
202
            return false;
203
        }
204
        if (is_resource($content)) {
205
            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...
206
        }
207
208
        return Util\Size::fromContent($content);
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     * @throws \RuntimeException
214
     * @throws \InvalidArgumentException
215
     */
216
    public function exists($key)
217
    {
218
        $this->init();
219
        list($containerName, $key) = $this->tokenizeKey($key);
220
221
        $listBlobsOptions = new ListBlobsOptions();
222
        $listBlobsOptions->setPrefix($key);
223
224
        try {
225
            $blobsList = $this->blobProxy->listBlobs($containerName, $listBlobsOptions);
226
227
            foreach ($blobsList->getBlobs() as $blob) {
228
                if ($key === $blob->getName()) {
229
                    return true;
230
                }
231
            }
232
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
233
            $errorCode = $this->getErrorCodeFromServiceException($e);
234
            if ($this->multiContainerMode && self::ERROR_CONTAINER_NOT_FOUND === $errorCode) {
235
                return false;
236
            }
237
            $this->failIfContainerNotFound($e, 'check if key exists', $containerName);
238
239
            throw new \RuntimeException(sprintf(
240
                'Failed to check if key "%s" exists in container "%s": %s (%s).',
241
                $key,
242
                $containerName,
243
                $e->getErrorText(),
244
                $errorCode
245
            ), $e->getCode());
246
        }
247
248
        return false;
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     * @throws \RuntimeException
254
     */
255
    public function keys()
256
    {
257
        $this->init();
258
        // list($containerName, $key) = $this->tokenizeKey($key);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
259
260
        try {
261
            $blobList = $this->blobProxy->listBlobs($this->containerName);
262
263
            return array_map(
264
                function (Blob $blob) {
265
                    return $blob->getName();
266
                },
267
                $blobList->getBlobs()
268
            );
269
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
270
            $this->failIfContainerNotFound($e, 'retrieve keys', $this->containerName);
271
            $errorCode = $this->getErrorCodeFromServiceException($e);
272
273
            throw new \RuntimeException(sprintf(
274
                'Failed to list keys for the container "%s": %s (%s).',
275
                $this->containerName,
276
                $e->getErrorText(),
277
                $errorCode
278
            ), $e->getCode());
279
        }
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     * @throws \RuntimeException
285
     * @throws \InvalidArgumentException
286
     */
287 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...
288
    {
289
        $this->init();
290
        list($containerName, $key) = $this->tokenizeKey($key);
291
292
        try {
293
            $properties = $this->blobProxy->getBlobProperties($containerName, $key);
294
295
            return $properties->getProperties()->getLastModified()->getTimestamp();
296
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
297
            $this->failIfContainerNotFound($e, sprintf('read mtime for key "%s"', $key), $containerName);
298
299
            return false;
300
        }
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     * @throws \RuntimeException
306
     * @throws \InvalidArgumentException
307
     */
308 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...
309
    {
310
        $this->init();
311
        list($containerName, $key) = $this->tokenizeKey($key);
312
313
        try {
314
            $this->blobProxy->deleteBlob($containerName, $key);
315
316
            return true;
317
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
318
            $this->failIfContainerNotFound($e, sprintf('delete key "%s"', $key), $containerName);
319
320
            return false;
321
        }
322
    }
323
324
    /**
325
     * {@inheritdoc}
326
     * @throws \RuntimeException
327
     * @throws \InvalidArgumentException
328
     */
329
    public function rename($sourceKey, $targetKey)
330
    {
331
        $this->init();
332
333
        list($sourceContainerName, $sourceKey) = $this->tokenizeKey($sourceKey);
334
        list($targetContainerName, $targetKey) = $this->tokenizeKey($targetKey);
335
336
        try {
337
            $this->createContainer($targetContainerName);
338
            $this->blobProxy->copyBlob($targetContainerName, $targetKey, $sourceContainerName, $sourceKey);
339
            $this->blobProxy->deleteBlob($sourceContainerName, $sourceKey);
340
341
            return true;
342
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
343
            $this->failIfContainerNotFound($e, sprintf('rename key "%s"', $sourceKey), $sourceContainerName);
344
345
            return false;
346
        }
347
    }
348
349
    /**
350
     * {@inheritdoc}
351
     */
352
    public function isDirectory($key)
353
    {
354
        // Windows Azure Blob Storage does not support directories
355
        return false;
356
    }
357
358
    /**
359
     * {@inheritdoc}
360
     * @throws \RuntimeException
361
     * @throws \InvalidArgumentException
362
     */
363 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...
364
    {
365
        $this->init();
366
        list($containerName, $key) = $this->tokenizeKey($key);
367
368
        try {
369
            $this->blobProxy->setBlobMetadata($containerName, $key, $content);
370
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
371
            $errorCode = $this->getErrorCodeFromServiceException($e);
372
373
            throw new \RuntimeException(sprintf(
374
                'Failed to set metadata for blob "%s" in container "%s": %s (%s).',
375
                $key,
376
                $containerName,
377
                $e->getErrorText(),
378
                $errorCode
379
            ), $e->getCode());
380
        }
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     * @throws \RuntimeException
386
     * @throws \InvalidArgumentException
387
     */
388 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...
389
    {
390
        $this->init();
391
        list($containerName, $key) = $this->tokenizeKey($key);
392
393
        try {
394
            $properties = $this->blobProxy->getBlobProperties($containerName, $key);
395
396
            return $properties->getMetadata();
397
        } catch (ServiceException $e) {
0 ignored issues
show
Bug introduced by
The class MicrosoftAzure\Storage\C...ptions\ServiceException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
398
            $errorCode = $this->getErrorCodeFromServiceException($e);
399
400
            throw new \RuntimeException(sprintf(
401
                'Failed to get metadata for blob "%s" in container "%s": %s (%s).',
402
                $key,
403
                $containerName,
404
                $e->getErrorText(),
405
                $errorCode
406
            ), $e->getCode());
407
        }
408
    }
409
410
    /**
411
     * Lazy initialization, automatically called when some method is called after construction.
412
     */
413
    protected function init()
414
    {
415
        if ($this->blobProxy === null) {
416
            $this->blobProxy = $this->blobProxyFactory->create();
417
        }
418
    }
419
420
    /**
421
     * Throws a runtime exception if a give ServiceException derived from a "container not found" error.
422
     *
423
     * @param ServiceException $exception
424
     * @param string           $action
425
     * @param string           $containerName
426
     *
427
     * @throws \RuntimeException
428
     */
429
    protected function failIfContainerNotFound(ServiceException $exception, $action, $containerName)
430
    {
431
        $errorCode = $this->getErrorCodeFromServiceException($exception);
432
433
        if ($errorCode === self::ERROR_CONTAINER_NOT_FOUND) {
434
            throw new \RuntimeException(sprintf(
435
                'Failed to %s: container "%s" not found.',
436
                $action,
437
                $containerName
438
            ), $exception->getCode());
439
        }
440
    }
441
442
    /**
443
     * Extracts the error code from a service exception.
444
     *
445
     * @param ServiceException $exception
446
     *
447
     * @return string
448
     */
449
    protected function getErrorCodeFromServiceException(ServiceException $exception)
450
    {
451
        $xml = @simplexml_load_string($exception->getResponse()->getBody());
452
453
        if ($xml && isset($xml->Code)) {
454
            return (string) $xml->Code;
455
        }
456
457
        return $exception->getErrorText();
458
    }
459
460
    /**
461
     * @param string|resource $content
462
     *
463
     * @return string
464
     */
465 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...
466
    {
467
        $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
468
469
        if (is_resource($content)) {
470
            return $fileInfo->file(stream_get_meta_data($content)['uri']);
471
        }
472
473
        return $fileInfo->buffer($content);
474
    }
475
476
    /**
477
     * @param string $key
478
     *
479
     * @return array
480
     * @throws \InvalidArgumentException
481
     */
482
    private function tokenizeKey($key)
483
    {
484
        $containerName = $this->containerName;
485
        if (true === $this->multiContainerMode) {
486
            if (false === ($index = strpos($key, '/'))) {
487
                // TODO: specify better error message here
488
                throw new \InvalidArgumentException('No /');
489
            }
490
            $containerName = substr($key, 0, $index);
491
            $key = substr($key, $index + 1);
492
        }
493
        return [$containerName, $key];
494
    }
495
}
496