Version 5 supported

File manipulation

Asset storage is provided out of the box via a Flysystem backend store. This abstraction allows for files to be stored in any number of different ways, such as storing them in the cloud, so you cannot rely on having a local file path in order to get and manipulate the contents of any given asset.

Silverstripe CMS provides a well-abstracted API for creating, manipulating, and storing assets.

See images for some image-specific manipulation methods.

Creating new files in PHP

When working with files in PHP you can upload a file into a File dataobject using one of the below methods:

MethodDescription
File::setFromLocalFileLoad a local file into the asset store
File::setFromStreamWill store content from a stream
File::setFromStringWill store content from a binary string

For example:

use SilverStripe\Assets\File;

// Store a file named "example-file.txt".
$fileRecord = File::create();
$fileRecord->setFromString('This is some file content', 'example-file.txt');
$fileRecord->write();

If you want to store your file in a [DBFile] field directly, or you don't want to use the File model for some reason, you can also use the default AssetStore directly:

use SilverStripe\Assets\File;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\FieldType\DBField;

// Store a file named "example-file.txt".
$store = Injector::inst()->get(AssetStore::class);
$result = $store->setFromString('This is some file content', 'example-file.txt');

// Save a database record that points to the stored file.
// Note that we pull the file name from the result because the asset store might have renamed it.
$dbFile = DBField::create_field('DBFile', $result);
$fileRecord = File::create(['Name' => $result['Filename'], 'File' => $dbFile]);
$fileRecord->write();

Storage conflict resolution

When storing new files, it's possible to determine the mechanism the backend should use when it encounters an existing file name pattern. The conflict resolution to use can be passed into the third parameter of the above methods (after content and filename). The available constants are:

ConstantIf an existing file is found then:
AssetStore::CONFLICT_EXCEPTIONAn exception will be thrown
AssetStore::CONFLICT_OVERWRITEThe existing file will be replaced
AssetStore::CONFLICT_RENAMEThe backend will choose a new name
AssetStore::CONFLICT_USE_EXISTINGThe existing file will be used

If no conflict resolution scheme is chosen, or an unsupported one is requested, then the backend will choose one. The default asset store supports each of these.

The conflict resolution is passed in to the config argument like so:

use SilverStripe\Assets\File;
use SilverStripe\Assets\Storage\AssetStore;

// Store a file named "example-file.txt".
$fileRecord = File::create();
$fileRecord->setFromString(
    'This is some file content',
    'example-file.txt',
    // If a file with that name already exists, let the file store rename this one.
    config: ['conflict' => AssetStore::CONFLICT_RENAME]
);
$fileRecord->write();

See file storage for more details about the way files are stored.

Accessing files via PHP

As with storage, there are also different ways of loading the content (or properties) of the file:

MethodDescription
File::getStreamWill get an output stream of the file content
File::getStringGets the binary content
File::getURLGets the URL for this resource. May or may not be absolute
File::getAbsoluteURLGets the absolute URL to this resource
File::getMimeTypeGet the mime type of this file
File::getMetaDataGets other metadata from the file as an array
File::getFileTypeReturn the type of file for the given extension

Additional file types

Silverstripe CMS has a pre-defined list of common file types. File::getFileType will return "unknown" for files outside that list.

You can add your own file extensions and their description with the following configuration:

SilverStripe\Assets\File:
  file_types:
    ai: 'Adobe Illustrator'
    psd: 'Adobe Photoshop File'

Renaming and moving files

In order to move or rename a file you can simply update the Name property, or assign the ParentID to a new folder. Please note that these modifications are made simply on the draft stage, and will not be copied to live until a publish is made via the CMS (either on this object, or cascading from a parent).

When files are renamed using the ORM, all file variants are automatically renamed at the same time.

use SilverStripe\Assets\File;

$file = File::get()->filter('Name', 'oldname.jpg')->first();
if ($file) {
    // The below will move 'oldname.jpg' and 'oldname__variant.jpg'
    // to 'newname.jpg' and 'newname__variant.jpg' respectively
    $file->Name = 'newname.jpg';
    $file->write();
}

Note that you can cause the file to be moved immediately by setting the Versioned reading mode to draft temporarily.

use SilverStripe\Assets\File;
use SilverStripe\Versioned\Versioned;

$file = File::get()->filter('Name', 'oldname.jpg')->first();
if ($file) {
    // The below will immediately move 'oldname.jpg' and 'oldname__variant.jpg'
    // to 'newname.jpg' and 'newname__variant.jpg' respectively
    $file->Name = 'newname.jpg';
    Versioned::withVersionedMode(function () use ($file) {
        Versioned::set_reading_mode('Stage.' . Versioned::DRAFT);
        $file->write();
        $file->publishSingle();
    });
}

Convert a file to a different format

You can use the manipulateExtension() method on any File or DBFile object to create a variant with a different file extension than the original.

This can be very useful if you want to convert a file to a different format for the user to download or view, while leaving the original file intact. Some examples of when you might want this are:

  • Generating thumbnails for videos, documents, etc
  • Converting images to .webp for faster page load times
  • Converting documents to .pdf so downloaded documents are more portable

Converting between image formats

Converting between image formats is the easiest example, because we can let Intervention Image do the heavy lifting for us.

All we need to do is tell it what extension we want to convert to and how to handle conflicts, and if that conversion is supported it will be done.

See Supported Formats | Intervention Image for supported formats.

namespace App\Extension;

use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Core\Extension;

class ImageFormatExtension extends Extension
{
    /**
     * Create a variant of the image in a different format.
     *
     * @param string $newExtension The file extension of the formatted file, e.g. "webp"
     */
    public function format(string $newExtension): DBFile
    {
        $original = $this->getOwner();
        return $original->manipulateExtension(
            $newExtension,
            function (AssetStore $store, string $filename, string $hash, string $variant) use ($original) {
                $backend = $original->getImageBackend();
                $config = ['conflict' => AssetStore::CONFLICT_USE_EXISTING];
                $tuple = $backend->writeToStore($store, $filename, $hash, $variant, $config);
                return [$tuple, $backend];
            }
        );
    }
}

Let's look at what's actually happening here, piece by piece.

return $original->manipulateExtension($newExtension /* ... */);

We call the manipulateExtension() method and pass in the file extension we want to convert our image to. If that variant file already exists, it won't call the callback method - the asset store system won't generate the file again if it already exists.

We'll be returning the result of this manipulation, which will be a DBFile containing all of the relevant information about our new variant.

function (AssetStore $store, string $filename, string $hash, string $variant) use ($original) {
    $backend = $original->getImageBackend();
    // ...
};

We define a callback, which will be called by manipulateExtension() if our variant file doesn't exist yet. This callback will be responsible for generating and storing the variant file.

The parameters for the callback function are as follows:

TypeNameDescription
AssetStorestoreThe mechanism used to store the actual file
stringfilenameThe name of the original file, including the original file extension
stringhashAn sha1 hash of the original file content
stringvariantA base64 encoded string with information about the variant file you're creating

We also want access to the original file record here so that we can use its Image_Backend to store the new file and do the conversion for us.

$config = ['conflict' => AssetStore::CONFLICT_USE_EXISTING];
$tuple = $backend->writeToStore($store, $filename, $hash, $variant, $config);

As mentioned earlier, Intervention Image will be converting the image for us. The $backend variable (unless you've replaced it with something else) is an instance of InterventionBackend which implements Image_Backend and uses the Intervention Image API.

The $variant variable holds information about the file conversion we want to make, so this line is just us saying "take this image, convert it to this new file type, and store the result."

Notice that we're using the CONFLICT_USE_EXISTING conflict resolution strategy. Our callback shouldn't be called if our variant file already exists, but just in case it does, we can just use the existing file instead of generating a new one.

The value returned from writeToStore() is an associative array with information about the new variant file you've created.

return [$tuple, $backend];

Finally, our callback returns both the information about the variant file and the Image_Backend object we used to generate it. Returning the Image_Backend here is important, because it will be used to perform any image-related manipulations we want to perform afterwards.

Now we just need to apply the extension to both the Image and DBFile classes.

SilverStripe\Assets\Image:
  extensions:
    - App\Extension\ImageFormatExtension
SilverStripe\Assets\Storage\DBFile:
  extensions:
    - App\Extension\ImageFormatExtension

You can use this method in PHP code or in templates on any instance of Image or DBFile. It will create a variant with the new file extension.

For example, if your page has a relation called MyImage to an Image record:

$MyImage.format('webp').ScaleWidth(150)

See images for more information about image-specific manipulation methods.

Converting between other formats

Converting between other formats (including a non-image to an image) is a little bit more involved, because we have to find another library that will do the conversion for us and then store the new content.

Below are two examples for these conversions - one where the file is converted to an image, and another where the file is converted to a PDF.

These examples won't include performing the actual conversion from one format to another, because that would need to be handled by some third-party library. Instead, they demonstrate how to use the manipulateExtension() API to store the converted files as variants.

namespace App\Extension;

use SilverStripe\Assets\Image_Backend;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Core\Extension;
use SilverStripe\Core\Injector\Injector;

class FileConversionExtension extends Extension
{
    /**
     * Create a variant of the file as an image.
     *
     * @param string $newExtension The file extension of the image to create, e.g. "webp"
     */
    public function toImage(string $newExtension): DBFile
    {
        /** Add some logic here to validate the conversion is supported */

        $original = $this->getOwner();
        return $original->manipulateExtension(
            $newExtension,
            function (AssetStore $store, string $filename, string $hash, string $variant) {
                $tmpFilePath = /* some conversion logic goes here */;
                $backend = Injector::inst()->create(Image_Backend::class);
                $backend->loadFrom($tmpFilePath);
                $config = ['conflict' => AssetStore::CONFLICT_USE_EXISTING];
                $tuple = $backend->writeToStore($store, $filename, $hash, $variant, $config);
                return [$tuple, $backend];
            }
        );
    }

    /**
     * Create a variant of the file as a pdf.
     */
    public function toPdf(): DBFile
    {
        /** Add some logic here to validate the conversion is supported */

        $original = $this->getOwner();
        return $file->manipulateExtension(
            'pdf',
            function (AssetStore $store, string $filename, string $hash, string $variant) {
                $tmpFilePath = /* some conversion logic goes here */;
                $config = ['conflict' => AssetStore::CONFLICT_USE_EXISTING];
                $tuple = $store->setFromLocalFile($tmpFilePath, $filename, $hash, $variant, $config);
                return [$tuple, null];
            }
        );
    }
}

After applying the extension to both the File and DBFile classes, you can use these methods in PHP or in templates.

SilverStripe\Assets\File:
  extensions:
    - App\Extension\ImageFormatExtension
SilverStripe\Assets\Storage\DBFile:
  extensions:
    - App\Extension\ImageFormatExtension

Okay, now lets step through those and take a look at what's going on. We'll only look at the parts that are different from the image-to-image conversion we looked at earlier.

Converting something to an image

The main difference between converting between images, and converting a non-image to an image, is that you have to get a third-party to perform the conversion for you.

$tmpFilePath = /* some conversion logic goes here */;
$backend = Injector::inst()->create(Image_Backend::class);
$backend->loadFrom($tmpFilePath);

After the actual file conversion has happened, and you have the new file contents stored in some temporary location (e.g. using tmpfile), we need to load that content into a Image_Backend. Unlike before, we don't have an existing image, so we need to get a new backend using the Injector.

The rest is the same as when we were converting from an image - we still get Intervention Image to store the variant file for us, and we make sure to include the Image_Backend object in our returned value.

Converting something to something else

When the format we're converting to is not an image, things are a little simpler. Again, we have to perform the conversion ourselves.

$tmpFilePath = /* some conversion logic goes here */;
$config = ['conflict' => AssetStore::CONFLICT_USE_EXISTING];
$tuple = $store->setFromLocalFile($tmpFilePath, $filename, $hash, $variant, $config);

Then, since we're not saving an image, we can just use the normal asset store logic.

return [$tuple, null];

Our new file variant isn't an image in this case, so we won't need access to the image manipulation methods provided by an Image_Backend. So instead, we just put null in its place.