Reputation: 1139
My code looks fine, I get status 200, I get the right headers, ... and yet my CSV file created will not donwload...
There is no error, so I do not understand why it's failing.
Here is my code:
namespace Rac\CaraBundle\Manager;
/* Imports */
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\ValidatorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\HttpFoundation\StreamedResponse;
/* Local Imports */
use Rac\CaraBundle\Entity\Contact;
/**
* Class CSV Contact Importer
*/
class CSVContactImporterManager {
/**
* @var ObjectManager
*/
private $om;
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var ValidatorInterface
*/
private $validator;
/**
* @var ContactManager
*/
private $contactManager;
/**
* @param EventDispatcherInterface $eventDispatcher
* @param ObjectManager $om
* @param Contact $contactManager
*
*/
public function __construct(
EventDispatcherInterface $eventDispatcher, ObjectManager $om, ValidatorInterface $validator, ContactManager $contactManager
) {
$this->eventDispatcher = $eventDispatcher;
$this->om = $om;
$this->validator = $validator;
$this->contactManager = $contactManager;
}
public function getExportToCSVResponse() {
// get the service container to pass to the closure
$contactList = $this->contactManager->findAll();
$response = new StreamedResponse();
$response->setCallback(
function () use ($contactList) {
//Import all contacts
$handle = fopen('php://output', 'r+');
// Add a row with the names of the columns for the CSV file
fputcsv($handle, array('Nom', 'Prénom', 'Société', 'Position', 'Email', 'Adresse', 'Téléphone', 'Téléphone mobile'), "\t");
$header = array();
//print_r($contactList);
foreach ($contactList as $row) {
fputcsv($handle, array(
$row->getFirstName(),
$row->getLastName(),
$row->getCompany(),
$row->getPosition(),
$row->getEmail(),
$row->getAddress(),
$row->getPhone(),
$row->getMobile(),
), "\t");
}
fclose($handle);
}
);
$response->headers->set('Content-Type', 'application/force-download');
$response->headers->set('Content-Disposition', 'attachment; filename="export.csv"');
return $response;
}
And my controller :
use Rac\CaraBundle\Entity\Contact;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use UCS\Bundle\RichUIBundle\Controller\BaseController;
use UCS\Bundle\RichUIBundle\Serializer\AbstractListSerializer;
/**
* Contact BackOffice Environment Controller.
*
*
*
* @Route("/contact_environment")
*/
class ContactEnvironmentController extends BaseController{
/* My code here..*/
/**
* @Route("/export", name="contact_environment_export",options={"expose"=true})
* @Method("GET")
*
* @return type
*/
public function exort(){
$manager = $this->get("cara.csv_contact_importer_manager");
return $manager->getExportToCSVResponse();
}
}
My response headers:
Cache-Control:no-cache, private
Connection:close
Content-Disposition:attachment; filename="export.csv"
Content-Type:application/force-download
Upvotes: 6
Views: 12869
Reputation: 66
if you don't use iterator for database query, then find all data doctrine or other ORM tool by limited or limitless.
If you want stream this big data (suppose that), before waits for it to end this find all query. This may take a long time and may time out.
Solution: query iterator in stream response ;)
Note: I used Symfony Serializer for CSV format
Example:
public function export(): Response
{
$query = $this->getQuery(); // Doctrine query
$serializer = new Serializer([new ObjectNormalizer()], [new CsvEncoder()]);
$response = new StreamedResponse();
$response->setCallback(function () use ($serializer, $query) {
$data = $query->toIterable(); // iterate query, not find all, one by one
$csv = fopen('php://output', 'wb+');
$headTitle = array_keys($data->current()->toArray());
$serializer->encode(
$headTitle,
CsvEncoder::FORMAT
);
fputcsv($csv, $headTitle, ';');
while (null !== $data->current()) {
$line = $data->current()->toArray(); // object to array convert on iterate
$serializer->encode($line, CsvEncoder::FORMAT);
fputcsv($csv, $line, ';');
$data->next();
}
fclose($csv);
});
$response->headers->set('Content-Type', 'text/csv; charset=utf-8; application/octet-stream');
$response->headers->set('Content-Disposition', 'attachment; filename="example.csv"');
return $response;
}
This solution starts the download file directly and streams while downloading. Like this, you can easily download your small or big data.
Upvotes: 1
Reputation: 47329
This is a simple implementation I used more than once, actually using StreamRepsonse
as asked.
It's a new response class that extends StreamResponse
and has a similar signature. Also accepts $separator
and $enclosure
parameters in case one needs for example to use a semi-colon (;
) instead of a comma, etc.
It creates the CSV in php://temp
to try to save memory if one needs to create larger files, and uses stream_get_contents
to retrieve a bit at a time.
class StreamedCsvResponse extends StreamedResponse
{
private string $filename;
public function __construct(
private array $data,
?string $filename = null,
private string $separator = ',',
private string $enclosure = '"',
$status = 200,
$headers = []
) {
if (null === $filename) {
$filename = uniqid() . '.csv';
}
if (!str_ends_with($filename, '.csv')) {
$filename .= '.csv';
}
$this->filename = $filename;
parent::__construct([$this, 'stream'], $status, $headers);
$this->setHeaders();
}
private function setHeaders(): void
{
$this->headers->set(
'Content-disposition',
HeaderUtils::makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $this->filename)
);
if (!$this->headers->has('Content-Type')) {
$this->headers->set('Content-Type', 'text/csv; charset=UTF-8');
}
if (!$this->headers->has('Content-Encoding')) {
$this->headers->set('Content-Encoding', 'UTF-8');
}
}
public function stream(): void
{
$handle = fopen('php://temp', 'r+b');
$this->encode($this->data, $handle);
if (!is_resource($handle)) {
return;
}
rewind($handle);
while ($t = stream_get_contents($handle, 1024)) {
echo $t;
}
fclose($handle);
}
private function encode(array $data, $handle): void
{
if (!is_resource($handle)) {
return;
}
foreach ($data as $row) {
fputcsv($handle, $row, $this->separator, $this->enclosure);
}
}
}
Upvotes: 3
Reputation: 41
Here is a shorter:
/**
* Class CsvResponse
*/
class CsvResponse extends StreamedResponse
{
/**
* CsvResponse constructor.
*
* @param array $rows
* @param string $fileName
*/
public function __construct(array $rows, $fileName)
{
parent::__construct(
function () use ($rows) {
$this->convertArrayToCsv($rows);
},
self::HTTP_OK,
[
'Content-Disposition' => sprintf('attachment; filename="%s"', $fileName),
'Content-Type' => 'text/csv',
]
);
}
/**
* @param array $rows
*
*/
private function convertArrayToCsv(array $rows)
{
$tempFile = fopen('php://output', 'r+b');
foreach ($rows as $row) {
fputcsv($tempFile, $row);
}
fclose($tempFile);
}
}
Upvotes: 1
Reputation: 295
Here is a Streamed Symfony response that works fine. The class creates a file to download with the exported data in it.
class ExportManagerService {
protected $filename;
protected $repdata;
public function publishToCSVReportData(){
$repdata = $this->repdata;
// array check
if (is_array($repdata)){
$response = new StreamedResponse();
$response->setCallback(
function () use ($repdata) {
$handle = fopen('php://output', 'r+');
foreach ($repdata as $row) {
$values = $row['values'];
$position = $row['position'];
$fileData = $this->structureDataInFile($values, $position);
fputcsv($handle, $fileData);
}
fclose($handle);
}
);
} else{
throw new Exception('The report data to be exported should be an array!');
}
$compstring = substr($this->filename,-4);
if($compstring === '.csv'){
// csv file type check
$response->headers->set('Content-Type', 'application/force-download');
$response->headers->set('Content-Disposition', 'attachment; filename='.$this->filename);
} else { throw new Exception('Incorrect file name!');}
return $response;
}
public function structureDataInFile(array $values, $position){
switch ($position){
case 'TopMain':
for ($i = 0; $i < 4; $i++){
array_unshift($values, ' ');
}
return $values;
break;
case 'Top':
$space = array(' ', ' ', ' ');
array_splice($values,1,0,$space);
return $values;
break;
case 'TopFirst':
for ($i = 0; $i < 1; $i++){
array_unshift($values, ' ');
}
$space = array(' ', ' ');
array_splice($values,2,0,$space);
return $values;
break;
case 'TopSecond':
for ($i = 0; $i < 2; $i++){
array_unshift($values, ' ');
}
$space = array(' ');
array_splice($values,3,0,$space);
return $values;
break;
case 'TopThird':
for ($i = 0; $i < 3; $i++){
array_unshift($values, ' ');
}
return $values;
break;
default:
return $values;
}
}
/*
* @var array
*/
public function setRepdata($repdata){
$this->repdata = $repdata;
}
/*
* @var string
*/
public function setFilename($filename){
$this->filename = $filename;
}
}
Upvotes: 2
Reputation: 48865
Here is a Response based solution as requested by the author. In this design, the csv service merely returns the csv text. The Response is generated in the controller.
The csv generator:
class ScheduleGameUtilDumpCSV
{
public function getFileExtension() { return 'csv'; }
public function getContentType() { return 'text/csv'; }
public function dump($games)
{
$fp = fopen('php://temp','r+');
// Header
$row = array(
"Game","Date","DOW","Time","Venue","Field",
"Group","HT Slot","AT Slot",
"Home Team Name",'Away Team Name',
);
fputcsv($fp,$row);
// Games is passed in
foreach($games as $game)
{
// Date/Time
$dt = $game->getDtBeg();
$dow = $dt->format('D');
$date = $dt->format('m/d/Y');
$time = $dt->format('g:i A');
// Build up row
$row = array();
$row[] = $game->getNum();
$row[] = $date;
$row[] = $dow;
$row[] = $time;
$row[] = $game->getVenueName();
$row[] = $game->getFieldName();
$row[] = $game->getGroupKey();
$row[] = $game->getHomeTeam()->getGroupSlot();
$row[] = $game->getAwayTeam()->getGroupSlot();
$row[] = $game->getHomeTeam()->getName();
$row[] = $game->getAwayTeam()->getName();
fputcsv($fp,$row);
}
// Return the content
rewind($fp);
$csv = stream_get_contents($fp);
fclose($fp);
return $csv;
}
The controller:
public function renderResponse(Request $request)
{
// Model is passed via request
$model = $request->attributes->get('model');
$games = $model->loadGames();
// The csv service
$dumper = $this->get('csv_dumper_service');
// Response with content
$response = new Response($dumper->dump($games);
// file prefix was injected
$outFileName = $this->prefix . date('Ymd-Hi') . '.' . $dumper->getFileExtension();
$response->headers->set('Content-Type', $dumper->getContentType());
$response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"',$outFileName));
return $response;
}
Upvotes: 1