I would like to output a csv file with a drupal 6 module. This is the code I have, but it's hacked together with some of it in my custom theme and some of it in my module. Is there anyway I can move all of it into my module?
///////////////////////////
// csv.module <- a custom module
///////////////////////////
function csv_menu() {
$items = array();
$items['csv/results'] = array (
'page callback' => 'csv_results_page',
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
return $items;
}
function csv_theme() {
$items = array();
$items['csv_results'] = array(
'arguments' => array(),
);
return $items;
}
function csv_results_page() {
return generate_csv_results();
}
function theme_csv_results() {
return generate_csv_results();
}
function generate_csv_results() {
return "header1,header2,header3,header4\nval1,val2,val3,val4\n";
}
//////////////////////////////
// page-csv-results.tpl.php <- in my theme. I would like it all contained in the module.
//////////////////////////////
<?php
//!TODO: Change Content Type Header
print theme('csv_results');
EDIT
Below is an updated version for anyone with a similar question. Thanks goes to chx!
///////////////////////////
// csv.module <- a custom module
///////////////////////////
function csv_menu() {
$items = array();
$items['csv/results'] = array (
'page callback' => 'csv_results_page',
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
return $items;
}
function csv_results_page() {
//Take a look at the Nikit's response on header stuff. This might be incorrect.
header('Content-Type: text/x-comma-separated-values');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private',false); // required for certain browser
print generate_csv_results();
}
function generate_csv_results() {
return "header1,header2,header3,header4\nval1,val2,val3,val4\n";
}
This is my simplified version of exporting csv file, add this code into csv_results_page(). Here used query, but can be other thing (like arrays, file listing, etc):
/**
* Export to cvs
*/
function _YOURMODULENAME_csv_export() {
$delimiter = "\t"; // This is tab delimiter, can be other
$temp = file_directory_temp();
$file = tempnam(realpath($temp), 'csv');
if (!$fp = fopen($file, 'a')) {
drupal_set_message(t('The file for exported could not be created. Refer to the site administrator'), 'error');
return;
}
$title = "Some title for csv\n"; // you can remove it, if don't want title in csv
$title .= implode($delimiter, array('Column name 1','Column name 2')). "\n"; // Add all columns titles here, it should mutch query result fields below
fwrite($fp, $title);
$query = 'WRITE HERE SOME CODE THAT SHOULD RESULT QUERY AND RETURN fields in order described in $title above';
$result = db_query($query);
while ($data = db_fetch_array($result)) {
$rows = implode($delimiter, $data);
fwrite($fp, $rows."\n");
}
fclose($fp);
$header = array();
$header[] = 'Pragma: no-cache';
$header[] = 'Cache-Control: no-cache, must-revalidate';
$header[] = 'Content-Disposition: attachment; filename="exort_'.date('Ymd',time()).'.csv";'; // This is file name
$header[] = 'Content-Type: text/x-comma-separated-values';
$header[] = 'Content-Length: '.filesize($file);
$header[] = 'Connection: close';
file_transfer($file, $header);
file_delete($file);
return;
}
Outputting CSV should be done by just printing stuff in the page callback, if you return as you do it will be in the current theme, surrounded by HTML and you dont need that.
Related
When I generate an excel sheet from the database it shows an error that "excel file does not match format. Do you want to open it anyway?" and it says to click on ok button to upgrade format of excel file. When I click on ok it works fine... but in mobile its not open.
i want to generate microsoft excel file with no error.
//generating Excel File
$setSql = "SELECT * from demo table";
$setRec = mysqli_query($db, $setSql);
$header_name="DATA LIST IN EXCEL";
$Event= "This is a demo";
date_default_timezone_set("Asia/Kolkata");
$date='Export Date:-'.date("l, jS \of F Y h:i:s A");
$columnHeader = '';
$columnHeader = "Name" . "\t" . "Date" . "\t". "Mode No" . "\t". "Address" . "\t". "eduction"."\t"."Organisation" . "\t". "Paid Status" . "\t";
$setData = '';
while ($rec = mysqli_fetch_row($setRec)) {
$rowData = '';
foreach ($rec as $value) {
$value = '"' . $value . '"' . "\t";
$rowData .= $value;
}
$setData .= trim($rowData) . "\n";
}
header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename= file_name.xls");
header("Pragma: no-cache");
header("Expires: 0");
echo $header_name ."\t\t\t\t\t\t\t\n". $Event ."\t\t\t". $date ."\n". ucwords($columnHeader) . "\n" . $setData . "\n";
Here you have our code.
Notice
ob_clean before download
This is the tricky point. If you have some content in the output buffer (maybe for an incorrect include file) it is sent with the file. So, you have to clean it before any download command
Added BOM header to CSV file
And, if you plan to open the file with Excel, and the file is UTF8, you have to add BOM header
public function reportAsset($budgetPeriod_id)
{
$timeProcess = round(microtime(true) / 1000);
if ($budgetPeriod_id) {
$budget = \App\Models\Invoicing\BudgetPeriod::select(['description'])->find((int) $budgetPeriod_id);
$client = \App\Models\Structure\Client::select(['description'])->find($this->client_id);
$filename = date('Ymd').'_'.$budget->description . '_' . $client->description . '.csv';
$headers = [
'Content-type' => 'text/csv;charset=UTF-8',
'Content-Disposition' => 'attachment; filename=' . $filename,
'Pragma' => 'no-cache',
'Expires' => '0',
];
$output = fopen($filename, "w");
// JMA: Add BOM header
fputs($output, $bom =( chr(0xEF) . chr(0xBB) . chr(0xBF) ));
fputcsv($output, [_i('Asset_id'), _i('Asset'), _i('Client Category'), _i('Budget'), _i('Spent'), _i('Available'),_i('Spent') .' %', _i('# Invoices')], ';');
$query = \App\Models\Invoicing\AssetBudget::query();
$query->has('asset')
->has('clientCategory')
->with('asset:PROCESS_ID,Identificador_Processo', 'clientCategory:id,description')
->orderBy('asset_id', 'clientCategory_id', 'clientCategory.id')
->selectRaw('amount as total, asset_id, budgetPeriod_id, clientCategory_id')
->where('budgetPeriod_id', $budgetPeriod_id)
->chunk($this::BUDGET_CHUNK, function ($chunk_query) use ($budgetPeriod_id, $output, $timeProcess) {
foreach ((array) $chunk_query as $report) {
foreach ($report as $rep) {
$row = [];
// JMA: The amount has to be the individual amount per asset
// So. we read asset_invoices where the invoice is in the budget period and category
// TODO: Replace this piece of code with consumedBudgetByRequest function in Invoicing BudgetController
// TODO: Try with calculateBudget but is not the same structure
//$invoices = \App\Library\Invoicing\BudgetCalculator::calculateBudget($rep->budgetPeriod_id, $rep->clientCategory_id, (array)$rep->asset_id);
$invoices=AssetInvoice::whereHas('invoice' , function ($invoice) use ($rep) {
$invoice->where('budgetPeriod_id',$rep->budgetPeriod_id)
->where('clientCategory_id',$rep->clientCategory_id);
}
)
->selectRaw('count(asset_id) as nInvoices, sum(amount) as spent')
->where('asset_id',$rep->asset_id)
->first();
// Log::debug('BudgetController->reportAsset: Invoices found='.$invoices->nInvoices.' spent='.$invoices->spent);
$row['asset_id'] = $rep->asset->PROCESS_ID;
$row['Identificador_Processo'] = $rep->asset->Identificador_Processo;
$row['clientCategory'] = $rep->clientCategory->description;
$row["budget"] = floatval($rep->total);
$row["spent"] = floatval($invoices->spent);
$row["available"] = $row["budget"] - $row["spent"];
if(floatval($rep->total)==0 ){
$row["percentaje"] = '';
}else{
$row["percentaje"] = number_format((float)(floatval($invoices->spent)*100)/ floatval($rep->total), 2, '.', '');
}
$row["nInvoices"] = floatval($invoices->nInvoices);
// Uncomment this line to monitor time consumption
// $row["times"] = $timeProcess - round(microtime(true) / 1000);
fputcsv($output, $row, ';');
}
}
});
fclose($output);
// CHECK THIS: Clean output buffer before sending files (avoid initial whitespaces)
if (ob_get_contents()) {
ob_clean();
}
// Send csv file as response
return response()->download($filename, $filename, $headers)->deleteFileAfterSend(true);
}
Please help, I'm new in Yii. I want to generate and export CSV file from checked rows in CGridView. When I use static SQL query it works normally, but when I use implode function in WHERE clause - controller returns an error.
My button that called controller action:
$this->widget('bootstrap.widgets.TbButtonGroup', array(
'type' => 'primary',
'size'=>'mini',
'buttons' => array(
array(
'label' => 'Export',
'type' => 'success',
'buttonType'=>'ajaxLink',
'encodeLabel'=>true,
'icon'=> 'th white',
'url'=>Yii::app()->createUrl('/propertyPurchaseSale/ExportChecked'),
'ajaxOptions'=>array(
"type" => "post",
"data" => "js:{ids:$.fn.yiiGridView.getSelection('property-purchase-sale-grid')}",
"update" => '#', 'success'=>"js:function(data) {window.location.assign('/propertyPurchaseSale/ExportChecked');}"),
array( //htmlOptions
)
),
array(
...
),
),
));
My controller action:
public function actionExportchecked() {
header('Content-type: text/csv');
header('Content-type: multipart/form-data');
header('Content-Disposition: attachment; filename="Export_(' . date('H-i_d.m.Y') .').csv"');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Description: File Transfer');
$fp = fopen('php://output', 'w');
if(Yii::app()->request->isAjaxRequest)
{
if(isset($_POST['ids']))
{
$idx = $_POST['ids'];
$count=Yii::app()->db->createCommand('SELECT COUNT(*) FROM property')->queryScalar();
// $sql='SELECT * FROM property WHERE id IN (981, 982, 985)'; --> when I use static values - all work
$sql="SELECT * FROM property WHERE id IN('".implode("', '",$idx)."')"; // --> when I use join or implode function - data exist in firebug, but page return error 500
$dataProvider=new CSqlDataProvider($sql, array(
'totalItemCount'=>$count,
'sort'=>array(
'attributes'=>array(
'fullname', 'address', 'phone', 'db_number', 'created_date'
),
),
'pagination'=>false,
));
fputs($fp, $bom =( chr(0xEF) . chr(0xBB) . chr(0xBF) ));
if ($fp)
{
echo PropertyPurchaseSale::model()->getAttributeLabel("id").";".
PropertyPurchaseSale::model()->getAttributeLabel("fullname").";".
PropertyPurchaseSale::model()->getAttributeLabel("address").";".
PropertyPurchaseSale::model()->getAttributeLabel("phone").";".
PropertyPurchaseSale::model()->getAttributeLabel("db_number").";".
PropertyPurchaseSale::model()->getAttributeLabel("created_date").
" \r\n";
foreach ($dataProvider->getData() as $data) {
echo $data['id'] . '; ' . $data['fullname'] . '; ' . $data['address'] . '; ' . $data['phone'] . '; ' . $data['db_number'] . '; ' . $data['created_date'] . '; ' . "\r\n";
}
}
exit;
}
}}
Please, help, what am I doing incorrectly?
My knee-jerk reaction is that there is nothing confirming that $_POST['ids'] is an array. If it is just a string, then implode will fail, the SQL will be improperly formatted, and that would lead to a 500 level error.
Perhaps this might work:
// check for empty, that way invalid entry won't go through
if(!empty($_POST['ids'])) {
// Check if it is an array
$input_idx = is_array($_POST['ids'])?
// if so, then use it as an array
$_POST['ids']:
// If not, you need to turn it into an array. I'm only guessing that this should
// be a ','. It could be a " " or some other character(s)
explode(',',$_POST['ids']);
// Remove all non-numeric items in the array.
$idx = array_filter($input_idx, 'is_numeric');
if(!$idx) {
// Do something with bad data.
}
// continue with the line $count = ...
You should also look to making sure that the data is sanitary as those IDs could include SQL injection. (Perhaps this answer)
I am writing a PHP export script for my copy of magento. For some reason, the following code gives me a "Headers already sent" error:
//Load magento and set to match frontend
require_once '../../app/Mage.php';
umask(0);
Mage::app();
Mage::app()->loadArea(Mage_Core_Model_App_Area::AREA_FRONTEND);
//Send headers to browser to prep for csv file download
header('Content-Type: text/csv');
header('Content-Disposition: attachment;filename=exportSKUs.csv');
//Load only product collection details that we need.
$product = Mage::getModel('catalog/product');
$product->getCollection()->getSelect()->limit(5);
$products = $product->getCollection()
->addFieldToFilter('status','1')
->addAttributeToSelect('name')
->addAttributeToSelect('sku')
->addAttributeToSelect('upc')
->addAttributeToSelect('status')
->addAttributeToSelect('price')
->addAttributeToSelect('special_price')
->addAttributeToSelect('description')
->addAttributeToSelect('category_ids')
->addAttributeToSelect('short_description');
//Open current output to fputcsv
$fp = fopen('php://output', 'w');
//CSV headers
$headerRow = array(
'name',
'sku',
'upc',
'status',
'price',
'special_price',
'description',
'category_ids',
'short_description'
);
fputcsv($fp, $headerRow);
$count = 0;
//CSV Rows
foreach($products as &$product){
$categoryIds = implode(',', $product->getCategoryIds());
$row = array(
$product->getName(),
$product->getSku(),
$product->getUpc(),
$product->getStatus(),
$product->getPrice(),
$product->getSpecialPrice(),
$product->getDescription(),
$categoryIds,
$product->getShortDescription()
);
fputcsv($fp, $row);
$count++;
if($count>5){
//Close current output (save csv)
fclose($fp);
exit;
}
}
The line of code here that is causing me problems is this: fputcsv($fp, $headerRow);
For some reason, when this line is commented out the script runs fine. However, when this line is run with the script, it shoots the header already sent error. I don't understand why I am able to run fputcsv INSIDE my foreach loop any number of times (fputcsv($fp, $row);)but I can not run it before the foreach loop at all.
I have ways around this issue, so it's not super critical, but I really wish I could understand what was going on here to cause this.
Thanks for your time!
i have change your code ... check this.I have using magento process to export...
<?php
//Load magento and set to match frontend
require_once '../../app/Mage.php';
umask(0);
Mage::app();
Mage::app()->loadArea(Mage_Core_Model_App_Area::AREA_FRONTEND);
//Send headers to browser to prep for csv file download
//header('Content-Type: text/csv');
//header('Content-Disposition: attachment;filename=exportSKUs.csv');
//
$filename="exportSKUs.csv";
//Load only product collection details that we need.
$product = Mage::getModel('catalog/product');
$product->getCollection()->getSelect()->limit(5);
$products = $product->getCollection()
->addFieldToFilter('status','1')
->addAttributeToSelect('name')
->addAttributeToSelect('sku')
->addAttributeToSelect('upc')
->addAttributeToSelect('status')
->addAttributeToSelect('price')
->addAttributeToSelect('special_price')
->addAttributeToSelect('description')
->addAttributeToSelect('category_ids')
->addAttributeToSelect('short_description');
$io = new Varien_Io_File();
$path = Mage::getBaseDir('var') . DS . 'export' . DS;
$name = md5(microtime());
$file = $path . DS . $name . '.csv';
$io->setAllowCreateFolders(true);
$io->open(array('path' => $path));
$io->streamOpen($filename, 'w+');
$io->streamLock(true);
$headerRow = array(
'name',
'sku',
'upc',
'status',
'price',
'special_price',
'description',
'category_ids',
'short_description'
);
$io->streamWriteCsv($headerRow);
foreach($products as &$product){
$categoryIds = implode(',', $product->getCategoryIds());
$row = array(
$product->getName(),
$product->getSku(),
$product->getUpc(),
$product->getStatus(),
$product->getPrice(),
$product->getSpecialPrice(),
$product->getDescription(),
$categoryIds,
$product->getShortDescription()
);
$io->streamWriteCsv($row);
}
?>
There is a difference between a PUT and POST request I send through a REST CLIENT in my API. It is implemented in CodeIgniter with Phil Sturgeon's REST server.
function station_put(){
$data = array(
'name' => $this->input->post('name'),
'number' => $this->input->post('number'),
'longitude' => $this->input->post('longitude'),
'lat' => $this->input->post('latitude'),
'typecode' => $this->input->post('typecode'),
'description' => $this->input->post('description'),
'height' => $this->input->post('height'),
'mult' => $this->input->post('mult'),
'exp' => $this->input->post('exp'),
'elevation' => $this->input->post('elevation')
);
$id_returned = $this->station_model->add_station($data);
$this->response(array('id'=>$id_returned,'message'=>"Successfully created."),201);
}
this request successfully inserts data into the server BUT - it renders the rest of the values NULL except for the id.
But if you change the function name into station_post, it inserts the data correctly.
Would somebody please point out why the PUT request does not work? I am using the latest version of google chrome.
Btw this API will be integrated to a BackBone handled app. Do I really need to use PUT? Or is there another workaround with the model saving function in backbone when using post?
Finally answered. Instead of $this->input->post or $this->input->put, it must be $this->put or $this->post because the data is not coming from a form.
Codeigniter put_stream also failing to fetch put data, therefore I had to handle php PUT request and I found this function useful enough to save someone's time:
function parsePutRequest()
{
// 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;
}
}
}
return $data;
}
I'm writing a RESTful API. I'm having trouble with uploading images using the different verbs.
Consider:
I have an object which can be created/modified/deleted/viewed via a post/put/delete/get request to a URL. The request is multi part form when there is a file to upload, or application/xml when there's just text to process.
To handle the image uploads which are associated with the object I am doing something like:
if(isset($_FILES['userfile'])) {
$data = $this->image_model->upload_image();
if($data['error']){
$this->response(array('error' => $error['error']));
}
$xml_data = (array)simplexml_load_string( urldecode($_POST['xml']) );
$object = (array)$xml_data['object'];
} else {
$object = $this->body('object');
}
The major problem here is when trying to handle a put request, obviously $_POST doesn't contain the put data (as far as I can tell!).
For reference this is how I'm building the requests:
curl -F userfile=#./image.png -F xml="<xml><object>stuff to edit</object></xml>"
http://example.com/object -X PUT
Does anyone have any ideas how I can access the xml variable in my 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.
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;
}
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],
];
<?php
namespace App\Ship\Middlewares\Http;
use Closure;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
* #author Quang Pham
*/
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 = [];
$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.
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();
............
...............
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.
I know this article is old.
But unfortunately, PHP still does not pay attention to form-data other than the Post method.
Thanks to friends (#netcoder, #greendot, #pham-quang) who suggested solutions above.
Using those solutions I wrote a library for this purpose:
composer require alireaza/php-form-data
You can also use composer require alireaza/laravel-form-data in Laravel.