Skip to content

Instantly share code, notes, and snippets.

@JhonatanRaul
Forked from iamntz/HandlePutFormData.php
Last active March 8, 2023 12:58
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save JhonatanRaul/cb2f9670ad0a8aa2fc32d263f948342a to your computer and use it in GitHub Desktop.
Save JhonatanRaul/cb2f9670ad0a8aa2fc32d263f948342a to your computer and use it in GitHub Desktop.
Laravel: Middleware to support multipart/form-data in PUT, PATH and DELETE requests. Deals with one level of form arrays.
<?php
namespace App\Http\Middleware;
use Closure;
use Symfony\Component\HttpFoundation\ParameterBag;
use Illuminate\Support\Arr;
/**
* @author https://github.com/Stunext
*
* PHP, and by extension, Laravel does not support multipart/form-data requests when using any request method other than POST.
* This limits the ability to implement RESTful architectures. This is a middleware for Laravel 5.7 that manually decoding
* the php://input stream when the request type is PUT, DELETE or PATCH and the Content-Type header is mutlipart/form-data.
*
* The implementation is based on an example by [netcoder at stackoverflow](http://stackoverflow.com/a/9469615).
* This is necessary due to an underlying limitation of PHP, as discussed here: https://bugs.php.net/bug.php?id=55815.
*
*/
class HandlePutFormData
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->method() == 'POST' or $request->method() == 'GET')
{
return $next($request);
}
if (preg_match('/multipart\/form-data/', $request->headers->get('Content-Type')) or preg_match('/multipart\/form-data/', $request->headers->get('content-type')))
{
$parameters = $this->decode();
$request->merge($parameters['inputs']);
$request->files->add($parameters['files']);
}
return $next($request);
}
public function decode()
{
$files = array();
$data = array();
// Fetch content and determine boundary
$rawData = file_get_contents('php://input');
$boundary = substr($rawData, 0, strpos($rawData, "\r\n"));
// Fetch and process each part
$parts = array_slice(explode($boundary, $rawData), 1);
foreach ($parts as $part) {
// If this is the last part, break
if ($part == "--\r\n") {
break;
}
// Separate content from headers
$part = ltrim($part, "\r\n");
list($rawHeaders, $content) = explode("\r\n\r\n", $part, 2);
$content = substr($content, 0, strlen($content) - 2);
// Parse the headers list
$rawHeaders = explode("\r\n", $rawHeaders);
$headers = array();
foreach ($rawHeaders as $header) {
list($name, $value) = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
}
// Parse the Content-Disposition to get the field name, etc.
if (isset($headers['content-disposition'])) {
$filename = null;
preg_match('/^form-data; *name="([^"]+)"(; *filename="([^"]+)")?/', $headers['content-disposition'], $matches);
$fieldName = $matches[1];
$fileName = (isset($matches[3]) ? $matches[3] : null);
// If we have a file, save it. Otherwise, save the data.
if ($fileName !== null) {
$localFileName = tempnam(sys_get_temp_dir(), 'sfy');
file_put_contents($localFileName, $content);
$arr = array(
'name' => $fileName,
'type' => $headers['content-type'],
'tmp_name' => $localFileName,
'error' => 0,
'size' => filesize($localFileName)
);
if(substr($fieldName, -2, 2) == '[]') {
$fieldName = substr($fieldName, 0, strlen($fieldName)-2);
}
if(array_key_exists($fieldName, $files)) {
array_push($files[$fieldName], $arr);
} else {
$files[$fieldName] = array($arr);
}
// register a shutdown function to cleanup the temporary file
register_shutdown_function(function() use($localFileName) {
unlink($localFileName);
});
} else {
parse_str($fieldName.'=__INPUT__', $parsedInput);
$dottedInput = Arr::dot($parsedInput);
$targetInput = Arr::add([], array_keys($dottedInput)[0], $content);
$data = array_merge_recursive($data, $targetInput);
}
}
}
$fields = new ParameterBag($data);
return ["inputs" => $fields->all(), "files" => $files];
}
}
@ericvvc9
Copy link

Digging deeper, it happened that the validation is failing because of Symfony UploadedFile.php class testing file upload validity internally using PHP is_uploaded_file function in the isValid() function, and this PHP function tells whether the file was uploaded via HTTP POST. More here.

Did you find a workaround to get this work?

@diamondobama
Copy link

Digging deeper, it happened that the validation is failing because of Symfony UploadedFile.php class testing file upload validity internally using PHP is_uploaded_file function in the isValid() function, and this PHP function tells whether the file was uploaded via HTTP POST. More here.

Did you find a workaround to get this work?

Unfortunately, not. Until the PHP team resolves that issue, no workaround for that.

@DazDotOne
Copy link

Digging deeper, it happened that the validation is failing because of Symfony UploadedFile.php class testing file upload validity internally using PHP is_uploaded_file function in the isValid() function, and this PHP function tells whether the file was uploaded via HTTP POST. More here.

Did you find a workaround to get this work?

Did you try the PECL extension? That basically populates the POST data so should fix your issue. I'd be interested to find out if that is the case.

@luizgdi
Copy link

luizgdi commented Sep 18, 2020

Hey guys! Ty for the great workaround!! But I've found one thing that I suppose that isn't intended...
When setting a file to the request, $request->image should return directly the UploadedFile, but insteads it returns an array with the UploadedFile being the index 0, I THINK this isn't the expected return, as the middleware is trying to workaround, and laravel wouldn't handle that file like that... I've solved this editing the line 97 from: $files[$fieldName] = array($arr); to $files[$fieldName] = $arr; but I didn't tested enough to tell if that would affect anything else since my application is new and still pretty small. If you can give me some feedback about that, would appreciate! And again: tyvm!

@cAstraea
Copy link

cAstraea commented Mar 8, 2023

The extension didn't fix for me. $request->allFiles() is still empty on PUT requests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment