Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Laravel: Middleware to support multipart/form-data in PUT, PATH and DELETE requests
<?php
namespace App\Http\Middleware;
use Closure;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
* @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);
$files[$fieldName] = array(
'name' => $fileName,
'type' => $headers['content-type'],
'tmp_name' => $localFileName,
'error' => 0,
'size' => filesize($localFileName)
);
// register a shutdown function to cleanup the temporary file
register_shutdown_function(function() {
unlink($localFileName);
});
} else {
$data[$fieldName] = $content;
}
}
}
$fields = new ParameterBag($data);
return ["inputs" => $fields->all(), "files" => $files];
}
}
@matrunchyk
Copy link

matrunchyk commented Dec 7, 2018

What $localFileName stands for?

@iamntz
Copy link

iamntz commented Dec 9, 2018

Couple of things:

Remove notice:

--- register_shutdown_function(function() {
+++ register_shutdown_function(function() use($localFileName) {
  unlink($localFileName);
});

HTML Arrays

If you have HTML array fields (e.g. <input name="foo[]" value="1"> the $fieldName would be wrong. If you dump $request->all() you should have

[
  'foo' => [ 1 ]
]

And, instead, you have: [ "foo[]" => 1 ].

Any ideas how to fix this?

@mahmudinm
Copy link

mahmudinm commented Feb 12, 2019

thanks

@programatom
Copy link

programatom commented Mar 11, 2019

This worked for me! Thank you so much

@beecoder77
Copy link

beecoder77 commented May 14, 2019

thanks bro

@renbooo
Copy link

renbooo commented May 26, 2019

thanks bro

@itpcc
Copy link

itpcc commented May 30, 2019

HTML Arrays

If you have HTML array fields (e.g. <input name="foo[]" value="1"> the $fieldName would be wrong. If you dump $request->all() you should have

[
  'foo' => [ 1 ]
]

And, instead, you have: [ "foo[]" => 1 ].

Any ideas how to fix this?

For this issue, I use PHP parse_str function to parse the array key, then flatten, pull the first dotted array key, and merge with the existing one.

Just add use Illuminate\Support\Arr; at the top of the file, and replace line 91 with:

parse_str($fieldName.'=__INPUT__', $parsedInput);
$dottedInput = Arr::dot($parsedInput);
$targetInput = Arr::add([], array_key_first($dottedInput), $content);

$data = array_merge_recursive($data, $targetInput);

I hope this can help all of you.

Reference: https://gist.github.com/devmycloud/df28012101fbc55d8de1737762b70348#file-parseinputstream-php-L79

@neoacevedo
Copy link

neoacevedo commented Dec 2, 2019

Works only for HTTP environment. Fails for HTTPS environment with

explode(): Empty delimiter {"exception":"[object] (ErrorException(code: 0): explode(): Empty delimiter at /application/app/Http/Middleware/HandlePutFormData.php:54)

@sidigi
Copy link

sidigi commented Dec 18, 2019

thx man

@JhonatanRaul
Copy link

JhonatanRaul commented Apr 3, 2020

HTML Arrays

If you have HTML array fields (e.g. <input name="foo[]" value="1"> the $fieldName would be wrong. If you dump $request->all() you should have

[
  'foo' => [ 1 ]
]

And, instead, you have: [ "foo[]" => 1 ].
Any ideas how to fix this?

For this issue, I use PHP parse_str function to parse the array key, then flatten, pull the first dotted array key, and merge with the existing one.

Just add use Illuminate\Support\Arr; at the top of the file, and replace line 91 with:

parse_str($fieldName.'=__INPUT__', $parsedInput);
$dottedInput = Arr::dot($parsedInput);
$targetInput = Arr::add([], array_key_first($dottedInput), $content);

$data = array_merge_recursive($data, $targetInput);

I hope this can help all of you.

Reference: https://gist.github.com/devmycloud/df28012101fbc55d8de1737762b70348#file-parseinputstream-php-L79

array_key_first not working:

$targetInput = Arr::add([], array_key_first($dottedInput), $content);

Use:

$targetInput = Arr::add([], array_keys($dottedInput)[0], $content);

@JhonatanRaul
Copy link

JhonatanRaul commented Apr 7, 2020

This solution returns in $request->all() but returns nothing in $request->file('arquivos')

I noticed one thing, that when doing POST without middleware the result (which works normally):

'arquivos' => 
  array (
    0 => 
    Illuminate\Http\UploadedFile::__set_state(array(
       'test' => false,
       'originalName' => 'BUG_Refeicoes_Lanches_T12.png',
       'mimeType' => 'image/png',
       'size' => 21626,
       'error' => 0,
       'hashName' => NULL,
    )),
  ),

And when doing PUT with middleware the result is:

'arquivos[]' => 
  Illuminate\Http\UploadedFile::__set_state(array(
     'test' => false,
     'originalName' => 'BUG_Refeicoes2.png',
     'mimeType' => 'image/png',
     'size' => 21626,
     'error' => 0,
     'hashName' => NULL,
  )),

As a consequence, it only returns in $request->all(), but it does not return in $request->file('arquivos'), and also returns only 1 file with the middleware, even if I upload 2 files (for POST Request without middleware returns 2 normal).

I made the corrections in fork: https://gist.github.com/JhonatanRaul/cb2f9670ad0a8aa2fc32d263f948342a

Tyvm folks,
Good Jobs!

@sahilkashyap64
Copy link

sahilkashyap64 commented Apr 17, 2020

Thank you very much.

@StanleyMasinde
Copy link

StanleyMasinde commented Aug 10, 2020

I thought it had sorted out my problem but no.
The best way is to use method spoofing in your request body.

const formData = new FormData()
formData.append('_method', 'PUT')

Then make your XMLHTTPRequest as POST laravel will redirect this to the update method

@XuanYanKhoh
Copy link

XuanYanKhoh commented May 24, 2022

image

i want to get the result as the picture above rather than this,

image

Can anyone help me ? I was using the method that suggest by @JhonatanRaul for changing the line 91 by use PHP parse_str function to parse the array key, then flatten, pull the first dotted array key, and merge with the existing one.

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