Skip to content

Instantly share code, notes, and snippets.

@Stunext
Created November 20, 2018 20:12
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save Stunext/9171b7a8f3633b0b601a0feb8088dca1 to your computer and use it in GitHub Desktop.
Save Stunext/9171b7a8f3633b0b601a0feb8088dca1 to your computer and use it in GitHub Desktop.
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];
}
}
@neoacevedo
Copy link

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

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

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