# Reading Request Data
Usually data sent in a POST request is structured key/value pairs with a MIME type of application/x-www-form-urlencoded . However many applications such as web services require raw data, often in XML or JSON format, to be sent instead. This data can be read using one of two methods.
php://input is a stream that provides access to the raw request body.
$rawdata = file_get_contents("php://input"); // Let's say we got JSON $decoded = json_decode($rawdata);
$HTTP_RAW_POST_DATA is a global variable that contains the raw POST data. It is only available if the always_populate_raw_post_data directive in php.ini is enabled.
$rawdata = $HTTP_RAW_POST_DATA; // Or maybe we get XML $decoded = simplexml_load_string($rawdata);
This variable has been deprecated since PHP version 5.6, and was removed in PHP 7.0.
Note that neither of these methods are available when the content type is set to multipart/form-data , which is used for file uploads.
# Reading POST data
Data from a POST request is stored in the superglobal
(opens new window) $_POST in the form of an associative array.
Note that accessing a non-existent array item generates a notice, so existence should always be checked with the isset() or empty() functions, or the null coalesce operator.
$from = isset($_POST["name"]) ? $_POST["name"] : "NO NAME"; $message = isset($_POST["message"]) ? $_POST["message"] : "NO MESSAGE"; echo "Message from $from: $message";
$from = $_POST["name"] ?? "NO NAME"; $message = $_POST["message"] ?? "NO MESSAGE"; echo "Message from $from: $message";
# Reading GET data
Data from a GET request is stored in the superglobal
(opens new window) $_GET in the form of an associative array.
Note that accessing a non-existent array item generates a notice, so existence should always be checked with the isset() or empty() functions, or the null coalesce operator.
Example: (for URL /topics.php?author=alice&topic=php )
$author = isset($_GET["author"]) ? $_GET["author"] : "NO AUTHOR"; $topic = isset($_GET["topic"]) ? $_GET["topic"] : "NO TOPIC"; echo "Showing posts from $author about $topic";
$author = $_GET["author"] ?? "NO AUTHOR"; $topic = $_GET["topic"] ?? "NO TOPIC"; echo "Showing posts from $author about $topic";
# Handling file upload errors
The $_FILES[«FILE_NAME»][‘error’] (where «FILE_NAME» is the value of the name attribute of the file input, present in your form) might contain one of the following values:
- UPLOAD_ERR_OK — There is no error, the file uploaded with success.
- UPLOAD_ERR_INI_SIZE — The uploaded file exceeds the upload_max_filesize directive in php.ini .
- UPLOAD_ERR_PARTIAL — The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
- UPLOAD_ERR_NO_FILE — No file was uploaded.
- UPLOAD_ERR_NO_TMP_DIR — Missing a temporary folder. (From PHP 5.0.3).
- UPLOAD_ERR_CANT_WRITE — Failed to write file to disk. (From PHP 5.1.0).
- UPLOAD_ERR_EXTENSION — A PHP extension stopped the file upload. (From PHP 5.2.0).
An basic way to check for the errors, is as follows:
$fileError = $_FILES["FILE_NAME"]["error"]; // where FILE_NAME is the name attribute of the file input in your form switch($fileError) case UPLOAD_ERR_INI_SIZE: // Exceeds max size in php.ini break; case UPLOAD_ERR_PARTIAL: // Exceeds max size in html form break; case UPLOAD_ERR_NO_FILE: // No file was uploaded break; case UPLOAD_ERR_NO_TMP_DIR: // No /tmp dir to write to break; case UPLOAD_ERR_CANT_WRITE: // Error writing to disk break; default: // No error was faced! Phew! break; >
# Uploading files with HTTP PUT
(opens new window) for the HTTP PUT method used by some clients to store files on a server. PUT requests are much simpler than a file upload using POST requests and they look something like this:
PUT /path/filename.html HTTP/1.1
Into your PHP code you would then do something like this:
/* PUT data comes in on the stdin stream */ $putdata = fopen("php://input", "r"); /* Open a file for writing */ $fp = fopen("putfile.ext", "w"); /* Read the data 1 KB at a time and write to the file */ while ($data = fread($putdata, 1024)) fwrite($fp, $data); /* Close the streams */ fclose($fp); fclose($putdata); ?>
(opens new window) you can read interesting SO question/answers about receiving file via HTTP PUT.
# Passing arrays by POST
Usually, an HTML form element submitted to PHP results in a single value. For example:
pre> print_r($_POST);?> pre> form method="post"> input type="hidden" name="foo" value="bar"/> button type="submit">Submitbutton> form>
This results in the following output:
However, there may be cases where you want to pass an array of values. This can be done by adding a PHP-like suffix to the name of the HTML elements:
pre> print_r($_POST);?> pre> form method="post"> input type="hidden" name="foo[]" value="bar"/> input type="hidden" name="foo[]" value="baz"/> button type="submit">Submitbutton> form>
This results in the following output:
Array ( [foo] => Array ( [0] => bar [1] => baz ) )
You can also specify the array indices, as either numbers or strings:
pre> print_r($_POST);?> pre> form method="post"> input type="hidden" name="foo[42]" value="bar"/> input type="hidden" name="foo[foo]" value="baz"/> button type="submit">Submitbutton> form>
Which returns this output:
Array ( [foo] => Array ( [42] => bar [foo] => baz ) )
This technique can be used to avoid post-processing loops over the $_POST array, making your code leaner and more concise.
# Remarks
# Choosing between GET and POST
GET requests, are best for providing data that’s needed to render the page and may be used multiple times (search queries, data filters. ). They are a part of the URL, meaning that they can be bookmarked and are often reused.
POST requests on the other hand, are meant for submitting data to the server just once (contact forms, login forms. ). Unlike GET, which only accepts ASCII, POST requests also allow binary data, including file uploads
You can find a more detailed explanation of their differences here
# Request Data Vulnerabilities
Retrieving data from the $_GET and $_POST superglobals without any validation is considered bad practice, and opens up methods for users to potentially access or compromise data through code
(opens new window) . Invalid data should be checked for and rejected as to prevent such attacks.
Request data should be escaped depending on how it is being used in code, as noted here
(opens new window) . A few different escape functions for common data use cases can be found in this answer
PHP multipart form data PUT request?
First of all, $_FILES is not populated when handling PUT requests. It is only populated by PHP when handling POST requests.
You need to parse it manually. That goes for «regular» fields as well:
// Fetch content and determine boundary $raw_data = file_get_contents('php://input'); $boundary = substr($raw_data, 0, strpos($raw_data, "\r\n")); // Fetch each part $parts = array_slice(explode($boundary, $raw_data), 1); $data = array(); 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($raw_headers, $body) = explode("\r\n\r\n", $part, 2); // Parse the headers list $raw_headers = explode("\r\n", $raw_headers); $headers = array(); foreach ($raw_headers 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( '/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', $headers['content-disposition'], $matches ); list(, $type, $name) = $matches; isset($matches[4]) and $filename = $matches[4]; // handle your fields here switch ($name) < // this is a file upload case 'userfile': file_put_contents($filename, $body); break; // default for all other files is to populate $data default: $data[$name] = substr($body, 0, strlen($body) - 2); break; >> >
At each iteration, the $data array will be populated with your parameters, and the $headers array will be populated with the headers for each part (e.g.: Content-Type , etc.), and $filename will contain the original filename, if supplied in the request and is applicable to the field.
Take note the above will only work for multipart content types. Make sure to check the request Content-Type header before using the above to parse the body.
Solution 2
Please don’t delete this again, it’s helpful to a majority of people coming here! All previous answers were partial answers that don’t cover the solution as a majority of people asking this question would want.
This takes what has been said above and additionally handles multiple file uploads and places them in $_FILES as someone would expect. To get this to work, you have to add ‘Script PUT /put.php’ to your Virtual Host for the project per Documentation. I also suspect I’ll have to setup a cron to cleanup any ‘.tmp’ files.
private function _parsePut( ) < global $_PUT; /* PUT data comes in on the stdin stream */ $putdata = fopen("php://input", "r"); /* Open a file for writing */ // $fp = fopen("myputfile.ext", "w"); $raw_data = ''; /* Read the data 1 KB at a time and write to the file */ while ($chunk = fread($putdata, 1024)) $raw_data .= $chunk; /* Close the streams */ fclose($putdata); // Fetch content and determine boundary $boundary = substr($raw_data, 0, strpos($raw_data, "\r\n")); if(empty($boundary))< parse_str($raw_data,$data); $GLOBALS[ '_PUT' ] = $data; return; >// Fetch each part $parts = array_slice(explode($boundary, $raw_data), 1); $data = array(); 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($raw_headers, $body) = explode("\r\n\r\n", $part, 2); // Parse the headers list $raw_headers = explode("\r\n", $raw_headers); $headers = array(); foreach ($raw_headers 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; $tmp_name = null; preg_match( '/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', $headers['content-disposition'], $matches ); list(, $type, $name) = $matches; //Parse File if( isset($matches[4]) ) < //if labeled the same as previous, skip if( isset( $_FILES[ $matches[ 2 ] ] ) ) < continue; >//get filename $filename = $matches[4]; //get tmp name $filename_parts = pathinfo( $filename ); $tmp_name = tempnam( ini_get('upload_tmp_dir'), $filename_parts['filename']); //populate $_FILES with information, size may be off in multibyte situation $_FILES[ $matches[ 2 ] ] = array( 'error'=>0, 'name'=>$filename, 'tmp_name'=>$tmp_name, 'size'=>strlen( $body ), 'type'=>$value ); //place in temporary directory file_put_contents($tmp_name, $body); > //Parse Field else < $data[$name] = substr($body, 0, strlen($body) - 2); >> > $GLOBALS[ '_PUT' ] = $data; return; >
Solution 3
For whom using Apiato (Laravel) framework: create new Middleware like file below, then declair this file in your laravel kernel file within the protected $middlewareGroups variable (inside web or api, whatever you want) like this:
protected $middlewareGroups = [ 'web' => [], 'api' => [HandlePutFormData::class], ];
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 = []; $data = []; // 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 = $rawData ? 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 = $this->transformData($files, $fieldName, [ '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 () use ($localFileName) < unlink($localFileName); >); > else < $data = $this->transformData($data, $fieldName, $content); > > > $fields = new ParameterBag($data); return ["inputs" => $fields->all(), "files" => $files]; > private function transformData($data, $name, $value) < $isArray = strpos($name, '[]'); if ($isArray && (($isArray + 2) == strlen($name))) < $name = str_replace('[]', '', $name); $data[$name][]= $value; >else < $data[$name] = $value; >return $data; > >
Pls note: Those codes above not all mine, some from above comment, some modified by me.
Solution 4
Quoting netcoder reply : «Take note the above will only work for multipart content types»
To work with any content type I have added the following lines to Mr. netcoder’s solution :
// Fetch content and determine boundary $raw_data = file_get_contents('php://input'); $boundary = substr($raw_data, 0, strpos($raw_data, "\r\n")); /*. My edit --------- */ if(empty($boundary)) < parse_str($raw_data,$data); return $data; >/* . My edit ends . */ // Fetch each part $parts = array_slice(explode($boundary, $raw_data), 1); $data = array(); . .
Solution 5
I’ve been trying to figure out how to work with this issue without having to break RESTful convention and boy howdie, what a rabbit hole, let me tell you.
I’m adding this anywhere I can find in the hope that it will help somebody out in the future.
I’ve just lost a day of development firstly figuring out that this was an issue, then figuring out where the issue lay.
As mentioned, this isn’t a symfony (or laravel, or any other framework) issue, it’s a limitation of PHP.
After trawling through a good few RFCs for php core, the core development team seem somewhat resistant to implementing anything to do with modernising the handling of HTTP requests. The issue was first reported in 2011, it doesn’t look any closer to having a native solution.
That said, I managed to find this PECL extension called Always Populate Form Data. I’m not really very familiar with pecl, and couldn’t seem to get it working using pear. but I’m using CentOS and Remi PHP which has a yum package.
I ran yum install php-pecl-apfd and it literally fixed the issue straight away (well I had to restart my docker containers but that was a given).
I believe there are other packages in various flavours of linux and I’m sure anybody with more knowledge of pear/pecl/general php extensions could get it running on windows or mac with no issue.