Build csv and upload to google drive in php (laravel 5.8) - php

How do I send a response()->stream() to google drive ? I'm not getting it because this method returns a class and not a file. My question is if I will need to save locally using file_put_contents() so that I can then send it to google drive
public function buildCsv($columns, $content): \Closure
{
return function () use ($columns, $content){
$file = fopen('php://output', 'w');
fputcsv($file, $columns);
foreach ($content as $item) {
fputcsv($file, $item);
}
fclose($file);
};
}
$cb = $this->buildCsv($this->CSVColumns, $csvData);
\Storage::disk('google')->put("csv-test", response()->stream($cb, 200, $headers));
On google drive my file looks like this:

Try this
public function buildCsvFile($columns, $content): string
{
$file = tmpfile();
fputcsv($file, $columns);
foreach ($content as $item) {
fputcsv($file, $item);
}
$metaDatas = stream_get_meta_data($file);
return file_get_contents($metaDatas['uri']);
}
$cb = $this->buildCsv($this->CSVColumns, $csvData);
\Storage::disk('google')->put("csv-test.csv", $cb);

Related

CSV import thousands of rows

I have multiple csv feeds to read with php 7.3.
When i have less than 500 lines (perhaps 1000) it works fine but with more i have
'PHP message: PHP Fatal error: Allowed memory size of 134217728 bytes
exhausted (tried to allocate 8192 bytes)
I read a lot of things about this common error
In php.ini, if I added
memory_limit=1024M
It is similar if I change it to -1.
Data comes from an api
You could see below the 4 main functions called
The error provides from the last (function parse - $lines[$index] = str_getcsv($line);)
Instead of getting the full file into a variable, i parse it for newlines and then do str_getcsv on each array element.
public function getProducts(Advertiser $advertiser): ProductCollection
{
$response = $this->getResponse($advertiser->getFeedUrl());
$content = gzdecode($response);
$products = $this->csvParser->parseProducts($content);
return $products;
}
private function getResponse(string $url): string
{
$cacheKey = sprintf('%s-%s',
static::CACHE_PREFIX,
md5($url)
);
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
$response = $this->client->request(static::REQUEST_METHOD, $url);
$body = (string) $response->getBody();
$this->cache->set($cacheKey, $body);
return $body;
}
public function parseProducts(string $csvContent): ProductCollection
{
$lines = $this->parse($csvContent);
$keys = array_shift($lines);
return new ProductCollection($keys, $lines);
}
private function parse(string $content): array
{
$lines = str_getcsv($content, PHP_EOL);
foreach ($lines as $index => $line) {
$lines[$index] = str_getcsv($line);
}
return $lines;
}
So, i think it's due to parse functon but i don't know what to do

PHP: Parse Web API response in Laravel

I have to consume an old Web API in Laravel.
Response body looks like this:
TRANSACTION_ID: KJASDFYDSF^SDFHJSD/2236
STATUS: OK
DATE: 01/03/18
How can I convert the response into Array using Guzzle 6?
Here is the solution with parsing response:
private function parseResponse(\GuzzleHttp\Psr7\Response $response) {
$body = $response->getBody();
$body->rewind();
$content = (string) $body->getContents();
$lines = explode(PHP_EOL, $content);
$result = [];
foreach ($lines as $line) {
$chunks = explode(':', $line);
$result[trim($chunks[0])] = trim($chunks[1]);
}
return $result;
}

json_decode() - What am I doing wrong?

So I decided to make my own helper in Codeigniter to get JSON files and save PokeAPI calls as JSON.
The save JSON method I created works fine:
if ( ! function_exists('saveJson')) {
function saveJson($file, $data) {
$fp = fopen($file, 'w');
fwrite($fp, json_encode($data));
fclose($fp);
}
}
However the getJSON function works very randomly. It works for fetching certain files but others it throws this error: Message: json_decode() expects parameter 1 to be string, array given. (all json files are the same format)
getJSON function:
if ( ! function_exists('getJson')) {
function getJson($file) {
$json = file_get_contents($file);
$data = json_decode($json, true);
$pkm = json_decode($data, true);
return $pkm;
}
}
Its odd, I have to decode the JSON twice or can I cannot access the array in my views.
My model and controller for further depth on the issue:
Model function example:
function getPokemonById($id) {
$filepath = './assets/jsonsaves/pokemoncalls/'. $id. '.json';
if(file_exists($filepath)) {
$pokemonByIdData = getJson($filepath);
} else {
$url = $this->pokemonApiAddress.$id.'/';
$response = Requests::get($url);
saveJson($filepath, $response);
$pokemonByIdData = json_decode($response->body, true);
}
return $pokemonByIdData;
}
Controller function example:
public function viewPokemon($id) {
$singlePokemon['pokemon'] = $this->pokemon_model->getPokemonById($id);
$singlePokemon['species'] = $this->pokemon_model->getPokemonSpecies($id);
$data['thepokemon'] = $this->pokemon_model->getAllPokemon();
$this->load->view('template/header', $data);
$this->load->view('pokemonpage', $singlePokemon);
$this->load->view('template/footer');
}
So there is some variation in my JSON file. In one JSON file that does not work it has, at the beginning:
{"body":"{\"forms\":[{\"url\":\"https:\\\/\\\/pokeapi.co\\\/api\\\/v2\\\/pokemon-form\\\/142\\\/\",\"name\":\"aerodactyl\"}],...
However this one works:
"{\"forms\":[{\"url\":\"https:\\\/\\\/pokeapi.co\\\/api\\\/v2\\\/pokemon-form\\\/6\\\/\",\"name\":\"charizard\"}],...
I fixed the issue thanks to #ccKep.
I removed the JSON encode from my saveJSON function like so:
if ( ! function_exists('saveJson')) {
function saveJson($file, $data) {
$fp = fopen($file, 'w');
fwrite($fp, $data);
fclose($fp);
}
}
And then removed the second json_decode from my getJSON function:
if ( ! function_exists('getJson')) {
function getJson($file) {
$json = file_get_contents($file);
$data = json_decode($json, true);
return $data;
}
}
This fixed the errors I was receiving.

Convert JSON to CSV and save from browser to computer

I have this JSON:
{"data":[{"ID":1,"br":"1-2015","kupac":"ADAkolor","datum":"2015-05-19","rok":"2015-05-21","status":"placeno"},{"ID":2,"br":"2-2015","kupac":"Milenk","datum":"2015-05-27","rok":"2015-05-28","status":""}]}
How to convert this to CSV file or Exel XLS using php pdo?
Also in this example:
if (empty($argv[1])) die("The json file name or URL is missed\n");
$jsonFilename = $argv[1];
$json = file_get_contents($jsonFilename);
$array = json_decode($json, true);
$f = fopen('php://output', 'w');
$firstLineKeys = false;
foreach ($array as $line)
{
if (empty($firstLineKeys))
{
$firstLineKeys = array_keys($line);
fputcsv($f, $firstLineKeys);
$firstLineKeys = array_flip($firstLineKeys);
}
// Using array_merge is important to maintain the order of keys acording to the first element
fputcsv($f, array_merge($firstLineKeys, $line));
}
where I need to put my $jsonTable ?
Use JSON2CSV
A simple PHP script to convert JSON data to CSV
example usage:
php json2csv.php --file=/path/to/source/file.json --dest=/path/to/destination/file.csv
OR You can have it dump the CSV file relative to the PHP script:
php json2csv.php --file=/path/to/source/file.json
I am not sure about the pdo part, but to write it to a file you would do something like this
$json = '{"data":[{"ID":1,"br":"1-2015","kupac":"ADAkolor","datum":"2015-05-19","rok":"2015-05-21","status":"placeno"},{"ID":2,"br":"2-2015","kupac":"Milenk","datum":"2015-05-27","rok":"2015-05-28","status":""}]}';
$out = fopen('file.csv', 'w');
foreach(json_decode($json, true)['data'] as $key => $value) {
fputcsv($out, $value);
}
fclose($out);

PHP multipart form data PUT request?

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.

Categories