It has been a long time since I first realized that handling file uploads in non-POST requests (like PUT) wasn't an easy task.

One could assume the $_FILES array should be populated regardless the HTTP verb, but actually, PHP doesn't do it on its own.

After a long time wanting to find a solution to this problem, I've finally dedicated the time to get something functional, that allows file uploads to be transparently handled regardless the HTTP verb (it works the same way in POST, PUT and PATCH requests).

Since nowadays I try to work with psr-7/middleware based applications, I have created a Zend Expressive app that registers a middleware capable of parsing a multipart/form-data request body, populating the request's uploaded files array and parsed body array.

This way, you can call $request->getUploadedFiles() or $request->getParsedBody() in any PUT or PATCH action, the same way you would do in a POST action.

The first couple of lines is simple. This middleware should only be executed when the request uses one of the HTTP verbs that allows body, but POST, which is automatically parsed by PHP. Also, the content type of the request should be multipart/form-data

Using a couple of explodes, an array_reduce and a regular expression, this method separates the headers from the body of every part, and then, depending on the information present in the content-disposition header of the part, it determines if it belongs to a properly uploaded file, a file element that has not been uploaded or a regular body parameter.

When a properly uploaded file is found, it is written in the directory configured in the ini upload_tmp_dir option, using the same file pattern used by PHP when storing files uploaded to a POST request.

Finally, it appends the parsed field to the $bodyParams array or the $files array.

There's only one thing left to see. When an uploaded file is found, the addFile protected method is called. Let's see it:

Since files can be uploaded as arrays, we have to take it into account when generating the $files array.

If the name of the array uses the array notation (including brackets at the end, like someFile[] instead of someFile), we have to make sure the value of that file element is an array of UploadedFileInterface objects, and any part that is a file and uses that same name is appended to the same array under the same name.

And that's it. If you have made some tests with the example app, you have already seen how it works.

Considerations

Regardless this works, it is just an experiment, and I wouldn't recommend you to do this in your project, unless it is essential for the application to be able to upload files in a PUT or PATCH request.

If you can, I would rather change the endpoint, so that it works with the POST method.

These are the main reasons:

The UploadedFile object implementation included in zend/diactoros (and thus, in zend expressive), calls PHP's move_uploaded_file when the $file->moveTo() method is called (probably, other implementations do the same).
This throws an exception in PUT and PATCH requests, since PHP doesn't consider those files to have been uploaded.
In order to get this working, in this example I have included a new UploadedFile implementation, which extend's from diactoros' implementation, but always moves the file using the resource, without checking if the file is really an uploaded file.
While this solution works, it has some security concerns, and could be exploited by a malicious attacker.

This solution implies loading the whole body into memory, in order to parse files and temporarily save them in disk, which is a much less optimized process than having them already in disk, like in POST requests.

Generated temporary files are not deleted at the end of the request, like in POST requests. However, we could use tmpfile instead of tempnam, or delete the files after calling $next, if they already exist and have not been handled by other middlewares.

Apart from that, I hope you learned reading the article as much as I did writing it.