How can you allow uploading files via your API?
Zend Framework 2 provides a variety of classes surrounding file upload functionality, including a set of validators (used to validate whether the file was uploaded, as well as whether it meets specific criteria such as file size, extension, MIME type, etc.), a set of filters (used to allow renaming an uploaded file, as well as, more rarely, to manipulate the contents of the file), and file-upload-specific inputs for input filters (because validation of files needs to follow different rules than regular data).
There are three ways to upload files to an API:
- As the only content.
- As part of a
multipart/form-datapayload, mixed with other "normal" data.
- As part of a
multipart/mixedpayload, mixed with other multiple uploads.
When uploading a file as the only expected content, you can set the MIME type to exactly what you
expect, and nothing special needs to be done. As an example, consider an API that allows you to
upload GIF images; you would add
image/gif to the Content Negotiation Content-Type whitelist in
your service, and in your RPC controller or REST resource, grab the request content and process it:
$request = $this->getRequest(); $imageContent = $request->getContent(); // Do something with the content -- most likely write it to a file file_put_contents($fileName, $imageContent);
Jumping to the last item,
multipart/mixed, your content would have multiple parts, each likely
representing a different content type; you might have one with a JSON structure, another with an
image, and so on. The problems with this approach are numerous:
Few clients support
multipart/mixedwell: many just outright do not support it (
wget, HTTPie, Postman, Advanced REST Client, Angular's
$http, jQuery, etc.), and some do not allow specifying a
From the server side, there's the question of how to handle the various parts. Are they named? If so, does that mean that a part with a JSON structure would go under that name? or is the name used to identify that particular structure so it can be merged into another structure? If the parts are not named, how should you deal with them? Do you go through them one by one?
- How should content negotiation be handled? Should the whitelist apply to each part? What happens if one part does not pass the whitelist -- is the entire request rejected?
Due to the difficulties with both sending
multipart/mixed responses as well as handling them,
Apigility does not support that media type at this time.
Now, finally, we'll look at the middle option,
multipart/form-data. This media type is natively
supported by every client we've reviewed, and, in fact, is the only multipart type that is available
on most of them. Requests in this media type have the following types of parts:
Named data. These are parts with a
Content-Dispositionheader that includes a
namesegment, but no
filenamesegment. The body of the part is the data associated with that particular name. In terms of your API, each data part represents a field you are sending in the request.
- Files. These are parts with a
Content-Dispositionheader that includes both a
filenamesegment. The file in the content will be associated with the given
name, and the
filenamewill be present as part of either the
$_FILESPHP superglobal, or, in the case of PUT or PATCH requests, the files composed in the request.
There are still some down-sides to using
- Nested structures are difficult to handle properly. Typically, you will need to serialize them on the client-side, and have logic server-side to deserialize.
- Most clients will pass a media type of
application/octet-streamfor any files sent as part of a
Zend\Validator\File\*usually handles this situation well, however.
In order to upload files using
multipart/form-data in your API, you will need to add the media
type to the Content Type Whitelist for your service.
Once that is done, you can follow the rest of this tutorial to handle file uploads.
Apigility allows you to mark a field as a file upload:
You must mark this if the field will be used for file uploads; failure to do so will mean the validators you select will not run correctly.
Once you have done that, you may add filters and validators. We recommend the following:
Zend\Filter\File\RenameUploadis used to rename the upload file. We recommend doing this, and setting the appropriate
targetdirectory (this can often be somewhere in your
data/path), and enabling the
randomizeflag (which both prevents file collisions, as well as ensures the filename cannot contain characters that might lead to overwriting existing files).
Zend\Validator\File\MimeTypecan be used to prevent files that do not fall within a defined set of MIME types from being successfully uploaded.
Zend\Validator\File\IsImagecan be used to determine whether the file is an image file.
Zend\InputFilter\FileInput also automatically adds the
which will cause the validation to fail if the upload cannot complete.
Configuring and validating your file upload is only half of the story. You need to complete the upload and retrieve the information from within your service classes. First, you need access to your input filter.
- In a REST service, use this:
$inputFilter = $this->getInputFilter();
- In an RPC service, use this:
$inputFilter = $this->getEvent()->getParam('ZF\ContentValidation\InputFilter');
Now that you have the input filter, call its
getValues() method to get all filtered, validated
$data = $inputFilter->getValues(); $image = $data['image'];
Alternately, you can retrieve just the file upload field; in the example below, we use the field
image, which corresponds with the screenshot from earlier:
$image = $inputFilter->getValue('image');
The value will for the upload file will be an array. This array mimics the
$_FILES array, and will
have the following elements:
error, corresponding to the appropriate
UPLOAD_ERROR_*constant (this should always be
name, the original filename when uploaded.
size, the file's size in bytes.
tmp_name, the actual upload file path and filename; use this one to retrieve the file!
type, the file's MIME type.
You can now use this information for any later purposes -- including storing the path in a database, copying the file to a cloud service, etc.