File Upload

The Editor PHP libraries provide an Upload class that can be used with the upload and uploadMany field types to provide end users with the ability to upload files to a server. The goal of the Upload class is to make the server-side aspect of file uploading as simple as possible, while retaining the same flexibility that you would expect from the Editor libraries.

The documentation here refers to the PHP aspect of the upload field type. Please refer to the client-side documentation for details on how to configure the Javascript aspect of the file upload options in Editor.

Overview

The Upload class provides the following features:

  • Save files to local file system or database
  • Store information about each file in a database table - what information is stored is configurable
  • File saving override options for custom actions (for example creating thumbnails)
  • Easy validation of file extensions
  • Custom validation for more complex options

In a typical situation it is expected that you will store the file on the server's file system (not the database, although that is possible if you require) and meta information about the file on the database so it can be accessed through the file() method.

Consider for example the following two tables:

Table: staff                      Table: image
+----+---------+-------+-------+  +----+-----------------+----------+
| id | name    | title | image |  | id | fileName        | fileSize |
+----+---------+-------+-------+  +----+-----------------+----------+
| 1  | Allan   | CEO   | 1     |  | 1  | Allan.png       | 4532     |
| 2  | Charlie | CTO   | 4     |  | 4  | Charlie-CTO.png | 9408     |
| 3  | Fred    | CFO   | 9     |  | 9  | Photo.png       | 2054     |
+----+---------+-------+-------+  +----+-----------------+----------+

Here we have a staff table where each row has a reference to the image table. The Editor initialisation to read the data from the staff table might look like the following:

$data = Editor::inst( $db, 'staff' )
    ->field( 
        Field::inst( 'name' ),
        Field::inst( 'title' ),
        Field::inst( 'image' ),
    )
    ->process( $_POST )
    ->data();

This gives us the information required on the client-side to show staff information, but not yet to upload files and display information about files.

The documentation below will build upon this basic example to add the ability to upload a new file in the Editor interface. Note that although this example focuses on images, any file type can be used - what files you use will be dependent upon your own requirements. Additionally the database structure shown above is not fixed - for example you can use a link table if you require by adding an additional left join.

The Upload class

The Upload class is constructed using the same method as all other Editor classes and provides the ability to make use of chaining. Specifically an Upload::inst() static method for PHP 5.3+ - in PHP 5.4+ you can just use new Upload() as 5.4 allows chaining from the constructor.

The new Upload instance is attached to the field that should have its value edited when a new image is selected - in the example discussed here that is the image column. This is done using the Field->upload() method:

Field::inst( 'image' )
    ->upload( Upload::inst( ... ) )
    ->setFormatter( 'Format::nullEmpty' );

Note that we also use a set formatter, the nullEmpty formatter in this case, which will cause Editor to write a null value to the database when no image value is submitted to the client-side. This is optional, and will depend upon the exact schema used in your database, but this is typically desirable.

One-to-many support

The uploadMany field type provides the ability to assign multiple files to a single field in an Editor form - i.e. one-to-many. Editor's one-to-many support is provided through the Mjoin class (see the Mjoin documentation).

The following documentation will focus primarily on using the upload type with fields in the main Editor instance, but the Upload class can be attached to fields defined in an Mjoin instance in exactly the same was as fields in the main Editor instance to provide the data required for the uploadMany field type.

File location

The Upload constructor accepts a single, optional, parameter - a string with the full system path where the uploaded file should be stored. Note that it is important you use the full path rather than a relative path to reduce complexities of include files! Use $_SERVER['DOCUMENT_ROOT'] to get the path to the root directory of your web-server if required.

The path given can have three special strings in it that the Upload class will replace so the file can be easily referenced from a database and to remove the chance of a file name collision:

  • __ID__ - The primary key value of the row that refers to this file
  • __EXTN__ - The file extension
  • __NAME__ - The full file name (including the extension).

These parameters are simply inserted into the string where you would like them to appear:

Field::inst( 'image' )
    ->upload( Upload::inst( $_SERVER['DOCUMENT_ROOT'].'/uploads/__ID__.__EXTN__' ) )
    ->setFormatter( 'Format::nullEmpty' );

If you wish to perform more complex actions for storing files, such as using a directory structure to limit the number of files in each directory to something safe for your operating system if you are working with large numbers of files, please see the Custom upload actions section below.

If no value is given in the constructor for the file path the Upload class will not attempt to write the uploaded file to the file system unless Upload->action() is used (which operates in exactly the same way as the constructor). This can be useful if you are storing the file's binary data in the database.

Database information

For efficiency you will typically wish to store information about each file in a database table. In combination with the file() method, this makes it very easy to get information about each file such as the file's path to load it in a web-browser, the file type and even if the file exists!

The Upload class provides a db() method that is used for this purpose. It takes up to four parameters:

  1. string - The database table name where the file information should be stored
  2. string - The database primary key column name. The Upload class requires that the database table have a single primary key so each row can be uniquely identified.
  3. array - A list of the columns to be written to on upload. The array keys are the database column names and the values can be one of:
    • A Upload::DB_* constant - describes information that should be obtains from the file - see the table below for a full list of the options available
    • A value (e.g a string, number, etc) which is written directly to the database
    • A closure function which is executed and the return value written to the database. The closure function takes a single parameter - the $_FILES array for the uploaded file.
  4. callback (optional) - A formatting function for each row read from the database. A single parameter is passed in, the associative array for the file from the database. It expects no return value - any manipulation of the array is propagated (make sure you use the variable by reference by using function(&$row){...}).

The constants that can be used for the values are:

Constant Meaning
Upload::DB_CONTENT File content
Upload::DB_CONTENT_TYPE Content type
Upload::DB_EXTN File extension
Upload::DB_FILE_NAME File name (with extension)
Upload::DB_FILE_SIZE File size (bytes)
Upload::DB_MIME_TYPE MIME type
Upload::DB_READ_ONLY Editor will not write a value to this field, just read its value which might come from a default, trigger or be updated from some other location. Since 1.6.2
Upload::DB_SYSTEM_PATH Full system path to the file
Upload::DB_WEB_PATH HTTP path to the file. This is derived from the system path by removing $_SERVER['DOCUMENT_ROOT']. If your images are stored outside of the document root a custom value would need to be used.

The db() method is used for both writing information to the database, and also automatically populating the data for the file() method. The parameters defined here are also available on the client-side (with the exception of the DB_CONTENT option).

Continuing the illustrative example we now have:

Field::inst( 'image' )
    ->upload(
        Upload::inst( $_SERVER['DOCUMENT_ROOT'].'/uploads/__ID__.__EXTN__' )
            ->db( 'image', 'id', array(
                'fileName' => Upload::DB_FILE_NAME,
                'fileSize' => Upload::DB_FILE_SIZE
            ) )
    )
    ->setFormatter( 'Format::nullEmpty' );

Validation

It is important that the files uploaded are validated to ensure that they are of an expected type (you don't want a Word document uploaded where your software expects an image for example!). There are two validation methods provided by the Upload class:

  • Simple extension validation
  • Custom validation methods

Provided validation functions

The Validate class provides two built in validation methods that can be used to validate file uploads. The function signatures for the file validations differ from the field validators in that they do not accept a ValidateOptions parameter - instead they take parameters which are defined by the validator, including the error message to be returned.

The built in validators are:

  • fileExtensions( string[] $valid, string $errorMessage ) - Check that the uploaded file's extension is one of those provided in the valid list. This check is case-insensitive, so png in the list would accept .PNG, .pnG, etc, as valid extensions). Also, it is worth noting that you should not rely upon extension validation only for secure validation as it is trivial to modify a file name extension. It is however a useful and simple sanity check.
  • fileSize( integer $size, string $errorMessage ) - Check that the uploaded file is equal to or below a given size (in bytes).

As an example, to check that only image files are uploaded you might use:

Upload::inst( ... )
    ->validator(
        Validate::fileExtensions(
            array( 'png', 'jpg', 'gif' ),
            'Only image files can be uploaded (png, jpg and gif)'
        )
    )

Custom validation

A custom validation method can perform any check you wish, for example actually going into the file's binary content to ensure that it is of the type expected, limiting file size, etc. This is done through use of a closure method that is attached using the Upload->validator() method - a single parameter is required, the closure validation method.

The closure function, when executed will be given a single argument: the $_FILES array entry for the uploaded file. The return value should be null if validation passes and a string containing an error message if it fails.

Consider the following example

Upload::inst( ... )
    ->validator( function ( $file ) {
        if ( $file['size'] >= 500000 ) {
            return "Files must be smaller than 500K";
        }
        return null;
    } )

Multiple validation methods can be used for a single Upload instance by simply calling Upload->validator() multiple times. The validators are executed in sequence when a file is uploaded.

Custom upload actions

There may be occasions when you find that the simple file move options provided by the Upload class constructor aren't enough for your application. This might be because you want to create thumbnails from an uploaded image so they are immediately available for use, or to use a complex directory structure for the uploaded files. Whatever the reason the Upload class provides the option of specifying a closure function that will be executed in place of the detail action.

As with the simple file path the closure method is specified using the constructor or the Upload->action() method. When executed it will receive two parameters:

  1. The $_FILES array entry for the uploaded file.
  2. The primary key value for the file if db() was used to store information in the database (otherwise this value will be null).

The return value is the value given to the client-side to identify the file. Typically this will be the primary key value, but you could also use a file path or any other information.

Consider the following example:

Field::inst( 'image' )
    ->upload(
        Upload::inst( function ( $file, $id ) {
            move_uploaded_file( $file['tmp_name'], '/uploads/'.$id );
            return $id;
        } )
            ->db( 'image', 'id', array(
                'fileName' => Upload::DB_FILE_NAME,
                'fileSize' => Upload::DB_FILE_SIZE
            ) )
    )
    ->setFormatter( 'Format::nullEmpty' );

In this case we are simply storing the file using move_uploaded_file(), but much more complex actions can be taken if required.

Deletion of orphaned files

Providing users the ability to upload files to a server also requires the ability to remove files - although users only have this ability indirectly (by deleting the data that has a reference to the file). The Upload class has a dbClean() method available that gives you this ability.

The method takes either one or two arguments:

  1. A table and field name string (in table.field format) to tell Editor where to look for files that are in use. This parameter is optional for a standard field, but is required when used inside an Mjoin field.
  2. A callback function that is executed whenever Editor performs a database clean (which is on create, edit and remove - not upload). It is passed a single argument, an array of information from the database about rows which are no longer referenced by the external table (note this only checks the host Editor table, not any other database tables - so if you have multiple tables using the same files table, you will need to execute your own queries). The information provided in the passed in array is defined by the options specified for the Upload->db() method.

The callback function can return true to indicate that the rows should be removed from the database. Any other return value (including none) will result in no action being performed by the libraries.

The callback function can therefore be used to delete files from the file system, or potentially to execute your own queries on the database to ensure there are no unwanted orphaned records.

Field::inst( 'image' )
    ->upload(
        Upload::inst( $_SERVER['DOCUMENT_ROOT'].'/uploads/__ID__.__EXTN__' )
            ->db( 'image', 'id', array(
                'fileName'   => Upload::DB_FILE_NAME,
                'webPath'    => Upload::DB_WEB_PATH,
                'systemPath' => Upload::DB_SYSTEM_PATH
            ) )
            ->dbClean( function ( $data ) {
                // Remove the files from the file system
                for ( $i=0, $ien=count($data) ; $i<$ien ; $i++ ) {
                    unlink( $data[$i]['systemPath'] );
                }

                // Have Editor remove the rows from the database
                return true;
            } )
    )

Events

Events can also be used to handle the deletion of files that are no longer referenced. The preRemove event for example can be used to remove files immediately prior to a row being deleted.

PHP API documentation

The PHP API developer documentation for the Editor PHP classes is available for detailed and technical discussion about the methods and classes discussed above.