Component spiral/storage
provides a powerful storage abstraction thanks to the
wonderful Flysystem PHP package by Frank de Jonge. The Storage component
integration provides simple drivers for working with local filesystems and Amazon S3. Even better, it's amazingly simple
to switch between these storage options between your local development machine and production server as the API remains
the same for each system.
Please note that, unlike classical file systems, the store component provides an API which provides the operations of writing a file, checking its existence, reading and getting the public address of this file. All operations (which classic file systems have) for working with directories, or a list of files are not available.
To install the component:
composer require spiral/storage
Please note that the spiral/framework >= 2.8 already includes this component.
Make sure to add Spiral\Bootloader\Storage\StorageBootloader
to your App class:
protected const LOAD = [
// ...
Spiral\Bootloader\Storage\StorageBootloader::class
];
Storage config file should be located (by default) at app/config/storage.php
. Within this file, you may configure the
servers (the "servers
" section) and the specific storages (the "buckets
" section) that your servers will use and
which you will use.
For example, the simplest configuration with one server and two storages might look like this:
return [
/**
* -------------------------------------------------------------------------
* Default Storage Bucket Name
* -------------------------------------------------------------------------
*
* Here you can specify which of the buckets you want to use by default for
* all work with storages.
*
*/
'default' => env('STORAGE_SERVER', 'uploads'),
/**
* -------------------------------------------------------------------------
* Storage Servers
* -------------------------------------------------------------------------
*
* Here are each of the servers is configured for your application. Of
* course, examples of customizing each available server supported by
* Spiral are shown below to simplify development.
*
*/
'servers' => [
'static' => [
'adapter' => 'local',
'directory' => __DIR__ . '/../../runtime/static',
],
's3' => [
'adapter' => 's3', // or "s3-async"
'region' => env('S3_REGION'),
'bucket' => env('S3_BUCKET'),
'key' => env('S3_KEY'),
'secret' => env('S3_SECRET'),
],
],
/**
* -------------------------------------------------------------------------
* Storage Buckets
* -------------------------------------------------------------------------
*
* Here are a list of specific buckets (or storages) that use the above
* server settings. Each "server" section in this list must refer to a
* valid server name in the list above.
*
* The list of settings in this case is also an example of use. You can
* freely change the number of buckets and the type of settings as you wish.
*
*/
'buckets' => [
'uploads' => [
'server' => 'static',
'prefix' => 'upload',
],
'images' => [
'server' => 'static',
'prefix' => 'img',
],
'videos' => [
'server' => 's3',
],
],
];
Note
Please note that this configuration is only available when used with the Spiral Framework.
This way of using the component is required only if it is installed separately, outside the framework.
First you need to create a storage instance where all your buckets will be stored. After that, you can add and get arbitrary bucket from it by the desired name.
$storage = new \Spiral\Storage\Storage();
$storage->add('example', \Spiral\Storage\Bucket::fromAdapter(
new \League\Flysystem\Local\LocalFilesystemAdapter(__DIR__ . '/path/to/directory')
));
$file = $storage->bucket('example')
->write('file.txt', 'content');
As you may have noticed, you can use existing flysystem adapters
to create a bucket. Just install the one you want and add it to the store using the Bucket::fromAdapter()
method.
The local server, as the name implies, is located in the local file system: In the same place where the executable code of your application is located.
You have already seen an example of local server settings earlier, however, to simplify them, some optional sections have been specially removed. Let's now take a look at the complete configuration of this type of server, leaving all the possible configuration sections.
return [
'servers' => [
'local' => [
//
// Server type name. For a local server, this value must be
// a string value "local".
//
'adapter' => 'local',
//
// The required path to the local directory where your files will
// be stored.
//
'directory' => '/example/directory',
//
// Visibility mapping. Here you can set the default visibility for
// files and the permissions for files and directories corresponding
// to a certain type of visibility.
//
// The visibility value can only be "private" or "public".
//
'visibility' => [
'public' => ['file' => 0644, 'dir' => 0755],
'private' => ['file' => 0600, 'dir' => 0700],
'default' => 'public',
],
],
],
'buckets' => [
'bucket' => [
//
// Relation to an existing local server. Note that all further
// bucket options are applicable only for this (i.e. local) server
// type.
//
'server' => 'local',
//
// For buckets that use local servers, you can add a directory
// prefix. In this case, the real physical path to the file will
// look like: "/example/directory/prefix", where
// the "/example/directory" string is the physical directory
// specified in the server, and "prefix" - prefix for file
// directories specified in the bucket.
//
'prefix' => 'prefix',
]
]
];
This type of server is designed to interact with an external distributed file system using the S3 protocol. S3 is the main protocol for communicating with Amazon servers. In addition to Amazon S3 itself, there are free alternatives that you can install and use on your own server, for example Minio Server.
To set up this type of server, you will need an existing bucket and personal authentication data. In addition, for completeness, the example below will include optional parameters that are optional and have default values.
Please note that in order to interact with this type of servers, you must have one of the two available packages
installed (either). You must install the league/flysystem-aws-s3-v3
or league/flysystem-async-aws-s3
package using
the Composer.
composer require league/flysystem-aws-s3-v3 ^2.0
// OR
composer require league/flysystem-async-aws-s3 ^2.0
During configuration, you should specify which of the packages you will use in the server's "adapter" section.
The "s3"
value corresponds to the league/flysystem-aws-s3-v3
package, while the "s3-async"
value corresponds
to the league/flysystem-async-aws-s3
package.
return [
'servers' => [
'local' => [
//
// Server type name. For a S3 server, this value must be a string
// value "s3" or "s3-async".
//
'adapter' => 's3',
//
// Required string key of S3 region like "eu-north-1".
//
// Region can be found on "Amazon S3" page here:
// - https://s3.console.aws.amazon.com/s3/home
//
'region' => env('S3_REGION'),
//
// Optional key of S3 API version.
//
'version' => env('S3_VERSION', 'latest'),
//
// Required key of S3 bucket.
//
// Bucket name can be found on "Amazon S3" page here:
// - https://s3.console.aws.amazon.com/s3/home
//
'bucket' => env('S3_BUCKET'),
//
// Required key of S3 credentials key like "AAAABBBBCCCCDDDDEEEE".
//
// Credentials key can be found on "Security Credentials" page here:
// - https://console.aws.amazon.com/iam/home#/security_credentials
//
'key' => env('S3_KEY'),
//
// Required key of S3 credentials private key.
// This must be a private key string value or a path to a private key file.
//
// Identifier can be also found on "Security Credentials" page here:
// - https://console.aws.amazon.com/iam/home#/security_credentials
//
'secret' => env('S3_SECRET'),
//
// Optional key of S3 credentials token.
//
'token' => env('S3_TOKEN', null),
//
// Optional key of S3 credentials expiration time.
//
'expires' => env('S3_EXPIRES', null),
//
// Optional key of S3 files visibility. Visibility is "public"
// by default.
//
'visibility' => env('S3_VISIBILITY', 'public'),
//
// For buckets that use S3 servers, you can add a directory
// prefix.
//
'prefix' => '',
//
// Optional key of S3 API endpoint URI. This value is required when
// using a server other than Amazon.
//
'endpoint' => env('S3_ENDPOINT', null),
//
// Optional additional S3 options.
// For example, option "use_path_style_endpoint" is required to work
// with a Minio S3 Server.
//
// Note: This "options" section is available since framework >= 2.8.5
// See also https://github.com/spiral/framework/issues/416
//
'options' => [
'use_path_style_endpoint' => true,
]
],
],
'buckets' => [
'bucket' => [
//
// Relation to an existing S3 server. Note that all further bucket
// options are applicable only for this (i.e. s3 or s3-async) server
// type.
//
'server' => 's3',
//
// The visibility value for a specific bucket type. Although you can
// specify server-wide visibility, you can also override this value
// for a specific bucket type.
//
'visibility' => env('S3_VISIBILITY', 'public'),
//
// In the case that you want to use another bucket using the main
// server settings, then you can redefine it by specifying the
// appropriate configuration key.
//
'bucket' => env('S3_BUCKET', null),
//
// If the new bucket is in a different region, then you can also
// override this value.
//
'region' => env('S3_REGION', null),
//
// A similar way can be done with the directory prefix in cases
// where a particular bucket must refer to some other root directory.
//
'prefix' => 'custom-directory',
]
]
];
In some cases, standard adapters may not be enough and in this case you may need to specify your own. You can also use your config file to configure your custom adapter.
In this case, the adapter section must refer to the League\Flysystem\FilesystemAdapter
implementation, and the
"options" section will contain an array of arguments passed to the constructor of this adapter.
return [
'servers' => [
'custom' => [
//
// Server type name that contains class name of adapter.
//
'adapter' => \Custom\FlysystemAdapter::Class,
//
// Adapter's constructor arguments.
//
'options' => [
// ...
]
],
],
'buckets' => [
'bucket' => [
//
// Relation to custom server.
//
'server' => 'custom'
]
]
];
Finally, after we have familiarized ourselves with what types of servers the component supports, we can move on to using them.
The storage architecture assumes 3 different levels of access to identical operations: Storage, Bucket and File. At each of these levels, you can operate on files, but the differences are in how much data you have to transfer to a particular method. At the highest "Storage" level you have to pass information about the bucket and the file, at the "Bucket" level only about the file, and finally at the "File" level you will not have to pass any additional information.
In practice, it will look like this. Let's try to create a file example.txt
in 3 different ways inside some controller
of our application.
use Spiral\Storage\StorageInterface;
class UploadController
{
public function createFile(StorageInterface $storage): array
{
$result = [];
// 1. Storage level
$result[] = $storage->create('bucket://example.txt');
// 2. Bucket level
$result[] = $storage->bucket('bucket')
->create('example.txt');
// 3. File level
$result[] = $storage->bucket('bucket')
->file('example.txt')
->create();
return $result;
}
}
The differences between the methods are as follows:
When working with a storage, you should pass the file name in the URI-like format [BUCKET_NAME]://[FILE_NAME]
.
In all methods of working with storage, this is the first string argument.
In all methods of working with a bucket, this is a file name in an arbitrary format. Please also note that the leading
slash does not affect the file location in any way and the names file.txt
and /file.txt
will be completely
identical.
In all methods of working with a specific file, no additional arguments are required.
Each of the methods has both its advantages and disadvantages. Just use what you like best.
Since in the example above we are using a controller, then at the same time we will use an alternative dependency, which is suitable in cases of simple file saving, when many implementations of buckets are not required.
In such cases, as below, the bucket used in the system by default will be selected. You can define this bucket in
the 'default'
section of your configuration.
use Spiral\Storage\BucketInterface;
use Psr\Http\Message\ServerRequestInterface;
class UploadingController
{
public function upload(ServerRequestInterface $request, BucketInterface $bucket): string
{
/** @var \Psr\Http\Message\UploadedFileInterface $file */
foreach ($request->getUploadedFiles() as $i => $file) {
$bucket->write("file-{$i}.txt", $file->getStream());
}
return \count($request->getUploadedFiles()) . ' files uploaded';
}
}
From each of the levels, you can refer to the child.
From The Storage:
$storage->bucket('[bucket-name]'): BucketInterface
$storage->file('[bucket-name]://[file-name]'): FileInterface
From The Bucket:
$bucket->file('[file-name]'): FileInterface
After we have familiarized ourselves with the options for using the same methods at different levels of the store, we should go on to describe the possibilities directly. Let's start!
In case to create a file, you can use one of the two available methods: create()
or write()
. The first creates an
empty file in cases where it is not created, and the second allows you to additionally write arbitrary string
or resource
stream content there.
For example, the code that creates the file might look like this.
// Creating file from bucket
$file = $bucket->create('file.txt');
// Creating file from bucket with string content
$file = $bucket->write('file.txt', 'message');
// Creating file from bucket with resource stream content
$file = $bucket->write('file.txt', fopen(__DIR__ . '/local/file.txt', 'rb+'));
To copy files, use the copy()
method, which contains one required argument with the name of the new file and one
optional - the bucket where this file should be copied. If the second argument is not specified, then the compilation
bucket will be identical to the original one.
The move()
method is completely similar to using the copy()
method, but instead of copying it moves the file.
$backup = $firstBucket->copy('from.txt', 'backup.txt');
$moved = $firstBucket->move('backup.txt', 'to.txt', $secondBucket);
Please note that when a file is moved (or copied) from a bucket with private permissions to a distribution with public permissions, the visibility of the file will also change to the one corresponding to this bucket.
To delete a file, just use the delete()
method. This method accepts an optional boolean argument meaning deletion an
empty directory in which this file was located.
$bucket->delete('file.txt');
There are two ways to read the contents of an existing file: Using the getContents()
and getStream()
methods. The
first one returns the string content of the file, and the second resource is a stream for working with streaming data.
$string = $bucket->getContents('text.txt');
$resource = $bucket->getStream('music.mp3');
To check the existence of a file, use the exists()
method, which returns a boolean value true
if the file exists
in the bucket, or false
if it does not exist.
$isExists = $bucket->exists('file.txt');
To get information about the size of a file, use the getSize()
method, which returns the size of an existing file in
bytes.
$bytes = $bucket->getSize('file.txt');
To retrieve information about the date of the last modification of a file, use the getLastModified()
method, which
returns the time in UNIX timestamp format.
$timestamp = $bucket->getLastModified('file.txt');
To get the file mime type, use the getMimeType()
method, which returns a string mime type representation of the file.
$mime = $bucket->getMimeType('file.txt');
In addition to such characteristics as the existence of a file, there is also the visibility of the file. You can get
information about the visibility of a file using the getVisibility()
method, and update the visibility using the
setVisibility()
method.
These methods operate on constants that have been defined in the Spiral\Storage\Visibility
enum-like interface.
Thus, the sample code with file visibility control will look like the following.
$visibility = $bucket->getVisibility('file.txt');
// If file is "private" then publish it
if ($visibility === Visibility::VISIBILITY_PRIVATE) {
$bucket->setVisibility('file.txt', Visibility::VISIBILITY_PUBLIC);
}
Note
In the case of using a bucket located on Windows OS, this functionality may not work.
The storage component initially provides only operations for working with files on arbitrary file systems. However, some systems, in addition to storage, allow HTTP to organize an access point to such files to receive their contents through a browser.
The storage component allows you to add an arbitrary URI resolver of public addresses for these files using the distribution component.
To configure the resolver, you should familiarize yourself with the configuration of this component. After that, for a specific bucket, simply add a section containing a link to a specific distribution resolver.
Each bucket has such an opportunity to specify the distribution.
return [
// ...
'buckets' => [
'uploads' => [
'server' => '...',
//
// + Add relation to existing distribution.
//
'distribution' => 'NAME_OF_DISTRIBUTION'
],
],
];
To use this URI resolver, simply call the toUri()
method. In case you need any other distribution, you can specify it
explicitly in the toUriFrom(...)
method.
Note
Unlike other methods, this one can only be called on a specific file.
use Spiral\Storage\BucketInterface;
use Spiral\Distribution\UriResolverInterface;
class UriController
{
// Using default URI resolver
public function getUri(BucketInterface $bucket): string
{
return (string)$bucket
->file('picture.jpg')
->toUri();
}
// Using another URI resolver
public function getAnotherUri(BucketInterface $bucket, UriResolverInterface $resolver): string
{
return (string)$bucket
->file('picture.jpg')
->toUriFrom($resolver);
}
}
Some generators may accept additional options. Such arguments can be passed to the toUri([...$arguments])
or toUriFrom($resolver, [...$arguments])
methods. For example, if you create a link to the CloudFront, you can
additionally specify the expiration time of this link.
You can read more about possible additional arguments on the corresponding page of the distribution documentation.
$uri = $file->toUri(new \DateInterval('PT30S'));
// And
$uri = $file->toUriFrom($resolver, new \DateInterval('PT30S'));