Uploadable behavior extension for Doctrine 2

Uploadable behavior provides the tools to manage the persistence of files with Doctrine 2, including automatic handling of moving, renaming and removal of files and other features.

Features:

  • Extension moves, removes and renames files according to configuration automatically
  • Lots of options: Allow overwrite, append a number if file exists, filename generators, post-move callbacks, etc.
  • It can be extended to work not only with uploaded files, but with files coming from any source (an URL, another file in the same server, etc).
  • Validation of size and mime type

Content:

Setup and autoloading

Read the documentation or check the example code on how to setup and use the extensions in most optimized way.

Uploadable Entity example:

Uploadable annotations:

  1. @Gedmo\Mapping\Annotation\Uploadable this class annotation tells if a class is Uploadable. Available configuration options:
    • allowOverwrite - If this option is true, it will overwrite a file if it already exists. If you set "false", an exception will be thrown. Default: false
    • appendNumber - If this option is true and "allowOverwrite" is false, in the case that the file already exists, it will append a number to the filename. Example: if you're uploading a file named "test.txt", if the file already exists and this option is true, the extension will modify the name of the uploaded file to "test-1.txt", where "1" could be any number. The extension will check if the file exists until it finds a filename with a number as its postfix that is not used. If you use a filename generator and this option is true, it will append a number to the filename anyway if a file with the same name already exists. Default value: false
    • path - This option expects a string containing the path where the files represented by this entity will be moved. Default: "". Path can be set in other ways: From the listener or from a method. More details later.
    • pathMethod - Similar to option "path", but this time it represents the name of a method on the entity that will return the path to which the files represented by this entity will be moved. This is useful in several cases. For example, you can set specific paths for specific entities, or you can get the path from other sources (like a framework configuration) instead of hardcoding it in the entity. Default: "". As first argument this method takes default path, so you can return path relative to default.
    • callback - This option allows you to set a method name. If this option is set, the method will be called after the file is moved. Default value: "". As first argument, this method can receive an array with information about the uploaded file, which includes the following keys:
      1. fileName: The filename.
      2. fileExtension: The extension of the file (including the dot). Example: .jpg
      3. fileWithoutExt: The filename without the extension.
      4. filePath: The file path. Example: /my/path/filename.jpg
      5. fileMimeType: The mime-type of the file. Example: text/plain.
      6. fileSize: Size of the file in bytes. Example: 140000.
    • filenameGenerator: This option allows you to set a filename generator for the file. There are two already included by the extension: SHA1, which generates a sha1 filename for the file, and ALPHANUMERIC, which "normalizes" the filename, leaving only alphanumeric characters in the filename, and replacing anything else with a "-". You can even create your own FilenameGenerator class (implementing the Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface) and set this option with the fully qualified class name. The other option available is "NONE" which, as you may guess, means no generation for the filename will occur. Default: "NONE".
    • maxSize: This option allows you to set a maximum size for the file in bytes. If file size exceeds the value set in this configuration, an exception of type "UploadableMaxSizeException" will be thrown. By default, its value is set to 0, meaning that no size validation will occur.
    • allowedTypes: With this option you can set a comma-separated list of allowed mime types for the file. The extension will use a simple mime type guesser to guess the file type, and then it will compare it to the list of allowed types. If the mime type is not valid, then an exception of type "UploadableInvalidMimeTypeException" will be thrown. If you set this option, you can't set the disallowedTypes option described next. By default, no validation of mime type occurs. If you want to use a custom mime type guesser, see this.
    • disallowedTypes: Similar to the option allowedTypes, but with this one you configure a "black list" of mime types. If the mime type of the file is on this list, n exception of type "UploadableInvalidMimeTypeException" will be thrown. If you set this option, you can't set the allowedTypes option described next. By default, no validation of mime type occurs. If you want to use a custom mime type guesser, see this.
  2. @Gedmo\Mapping\Annotation\UploadableFilePath: This annotation is used to set which field will receive the path to the file. The field MUST be of type "string". Either this one or UploadableFileName annotation is REQUIRED to be set.
  3. @Gedmo\Mapping\Annotation\UploadableFileName: This annotation is used to set which field will receive the name of the file. The field MUST be of type "string". Either this one or UploadableFilePath annotation is REQUIRED to be set.
  4. @Gedmo\Mapping\Annotation\UploadableFileMimeType: This is an optional annotation used to set which field will receive the mime type of the file as its value. This field MUST be of type "string".
  5. @Gedmo\Mapping\Annotation\UploadableFileSize: This is an optional annotation used to set which field will receive the size in bytes of the file as its value. This field MUST be of type "decimal".

Notes about setting the path where the files will be moved:

You have three choices to configure the path. You can set a default path on the listener, which will be used on every entity which doesn't have a path or pathMethod defined:

$listener->setDefaultPath('/my/path');

You can use the Uploadable "path" option to set the path:

/**
 * @ORM\Entity
 * @Gedmo\Uploadable(path="/my/path")
 */
class File
{
    //...
}

Or you can use the Uploadable "pathMethod" option to set the name of the method which will return the path:

/**
 * @ORM\Entity
 * @Gedmo\Uploadable(pathMethod="getPath")
 */
class File
{
    public function getPath()
    {
        return '/my/path';
    }
}

Note regarding the Uploadable interface:

The Uploadable interface is not necessary, except in cases there you need to identify an entity as Uploadable. The metadata is loaded only once then you need to identify an entity as Uploadable. The metadata is loaded only once then cache is activated

Minimum configuration needed:

<?php
namespace Entity;

use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;

// If you don't set the path here, remember that you must set it on the listener!

/**
 * @ORM\Entity
 * @Gedmo\Uploadable
 */
class File
{
    // Other fields..

    /**
     * @ORM\Column
     * @Gedmo\UploadableFilePath
     */
    private $path;
}

Example of an entity with all the configurations set:

<?php
namespace Entity;

use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @Gedmo\Uploadable(path="/my/path", callback="myCallbackMethod", filenameGenerator="SHA1", allowOverwrite=true, appendNumber=true)
 */
class File
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @ORM\Column
     * @Gedmo\UploadableFilePath
     */
    private $path;

    /**
     * @ORM\Column
     * @Gedmo\UploadableFileName
     */
    private $name;

    /**
     * @ORM\Column
     * @Gedmo\UploadableFileMimeType
     */
    private $mimeType;

    /**
     * @ORM\Column(type="decimal")
     * @Gedmo\UploadableFileSize
     */
    private $size;

    public function myCallbackMethod(array $info)
    {
        // Do some stuff with the file..
    }

    // Other methods..
}

Yaml mapping example:

Yaml mapped Article: /mapping/yaml/Entity.Article.dcm.yml

---
Entity\File:
  type: entity
  table: files
  gedmo:
    uploadable:
      allowOverwrite: true
      appendNumber: true
      path: '/my/path'
      pathMethod: getPath
      callback: callbackMethod
      filenameGenerator: SHA1
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    path:
      type: string
      gedmo:
        - uploadableFilePath
    name:
      type: string
      gedmo:
        - uploadableFileName
    mimeType:
      type: string
      gedmo:
        - uploadableFileMimeType
    size:
      type: decimal
      gedmo:
        - uploadableFileSize

Xml mapping example

<?xml version="1.0" encoding="UTF-8"?>

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">

    <entity name="Entity\File" table="files">

        <id name="id" type="integer" column="id">
            <generator strategy="AUTO"/>
        </id>

        <field name="mimeType" column="mime" type="string">
            <gedmo:uploadable-file-mime-type />
        </field>

        <field name="size" column="size" type="decimal">
            <gedmo:uploadable-file-size />
        </field>

        <field name="name" column="name" type="string">
            <gedmo:uploadable-file-name />
        </field>

        <field name="path" column="path" type="string">
            <gedmo:uploadable-file-path />
        </field>

        <gedmo:uploadable
            allow-overwrite="true"
            append-number="true"
            path="/my/path"
            path-method="getPath"
            callback="callbackMethod"
            filename-generator="SHA1" />

    </entity>

</doctrine-mapping>

Usage:

<?php
// Example setting the path directly on the listener:

$listener->setDefaultPath('/my/app/web/upload');

if (isset($_FILES['images']) && is_array($_FILES['images'])) {
    foreach ($_FILES['images'] as $fileInfo) {
        $file = new File();

        $listener->addEntityFileInfo($file, $fileInfo);

        // You can set the file info directly with a FileInfoInterface object, like this:
        //
        // $listener->addEntityFileInfo($file, new FileInfoArray($fileInfo));
        //
        // Or create your own class which implements FileInfoInterface
        //
        // $listener->addEntityFileInfo($file, new MyOwnFileInfo($fileInfo));

        $em->persist($file);
    }
}

$em->flush();

Easy like that, any suggestions on improvements are very welcome.

Using the extension to handle not only uploaded files

Maybe you want to handle files obtained from an URL, or even files that are already located in the same server than your app. This can be handled in a very simple way. First, you need to create a class that implements the FileInfoInterface interface. As an example:

use Gedmo\Uploadable\FileInfo\FileInfoInterface;

class CustomFileInfo implements FileInfoInterface
{
    protected $path;
    protected $name;
    protected $size;
    protected $type;
    protected $filename;
    protected $error = 0;

    public function __construct($path)
    {
        $this->path = $path;

        // Now, process the file and fill the rest of the properties.
    }

    // This returns the actual path of the file
    public function getTmpName()
    {
        return $this->path;
    }

    // This returns the filename
    public function getName()
    {
        return $this->name;
    }

    // This returns the file size in bytes
    public function getSize()
    {
        return $this->size;
    }

    // This returns the mime type
    public function getType()
    {
        return $this->type;
    }

    public function getError()
    {
        // This should return 0, as it's only used to return the codes from PHP file upload errors.
        return $this->error;
    }

    // If this method returns true, it will produce that the extension uses "move_uploaded_file" function to move
    // the file. If it returns false, the extension will use the "copy" function.
    public function isUploadedFile()
    {
        return false;
    }
}

Or you could simply extend the FileInfoArray class and do the following:

use Gedmo\Uploadable\FileInfo\FileInfoArray;

class CustomFileInfo extends FileInfoArray
{
    public function __construct($path)
    {
        // There's already a $fileInfo property, which needs to be an array with the
        // following keys: tmp_name, name, size, type, error
        $this->fileInfo = array(
            'tmp_name'      => '',
            'name'          => '',
            'size'          => 0,
            'type'          => '',
            'error'         => 0
        );

        // Now process the file at $path and fill the keys with the correct values.
        //
        // In this example we use a $path as the first argument, but it could be an URL
        // to the file we need to obtain, etc.
    }

    public function isUploadedFile()
    {
        // Remember to set this to false so we use "copy" instead of "move_uploaded_file"

        return false;
    }
}

And that's it. Then, instead of getting the file info from the $_FILES array, you would do:

// We set the default path in the listener again
$listener->setDefaultPath('/my/path');

$file = new File();

$listener->addEntityFileInfo($file, new CustomFileInfo('/path/to/file.txt'));

$em->persist($file);
$em->flush();

Custom Mime type guessers

If you want to use your own mime type guesser, you need to implement the interface "Gedmo\Uploadable\MimeType\MimeTypeGuesserInterface", which has only one method: "guess($filePath)". Then, you can set the mime type guesser used on the listener in the following way:

$listener->setMimeTypeGuesser(new MyCustomMimeTypeGuesser());