/
CsvResponseFormatter.php
134 lines (121 loc) · 4.12 KB
/
CsvResponseFormatter.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php
namespace SamIT\Yii2;
use yii\base\Arrayable;
use yii\base\Component;
use yii\web\Response;
use yii\web\ResponseFormatterInterface;
/**
* CsvResponseFormatter formats the given data into CSV response content.
*
* It is used by [[Response]] to format response data.
*/
class CsvResponseFormatter extends Component implements ResponseFormatterInterface
{
public const FORMAT_CSV = 'csv';
/**
* @var int Maximum number of bytes to use in memory before using a temp file. Defaults to 20MB
*/
public $maxMemory = 20971520;
/**
* @var boolean Whether to include column names as the first line.
*/
public $includeColumnNames = true;
/**
* @var string The delimiter to use (one character only)
* @see fputcsv
*/
public $delimiter = ',';
/**
* @var string The field enclosure to use (one character only)
* @see fputcsv
*/
public $enclosure = '"';
/**
* @var string The escape character to use (one character only)
* @see fputcsv
*/
public $escape = '\'';
/**
* @var bool Whether to check all rows for column names. This means iterating the data twice but it adds support
* for non-uniform data (ie rows with missing columns).
*/
public $checkAllRows = false;
/**
* @var string The value to use for NULL values.
*/
public $nullValue = "(null)";
/**
* @var string The value to use for missing columns (only applicable if `$checkAllRows` is true)
*/
public $missingValue = "(missing)";
/**
* Formats the specified response.
* @param Response $response the response to be formatted.
* @throws \RuntimeException
* @return void
*/
public function format($response): void
{
$response->getHeaders()->set('Content-Type', 'text/csv; charset=UTF-8');
$handle = fopen('php://temp/maxmemory:' . intval($this->maxMemory), 'w+');
$response->stream = $handle;
if (!is_iterable($response->data)) {
throw new \InvalidArgumentException('Response data must be iterable');
}
if ($this->includeColumnNames) {
$columns = $this->getColumnNames($response->data, $this->checkAllRows);
$this->put($handle, $columns);
}
$this->writeData($response, $handle, $columns);
rewind($handle);
}
private function writeData(Response $response, $handle, array $columns)
{
foreach($response->data as $row) {
if ($row instanceof Arrayable) {
$row = $row->toArray();
}
$rowData = [];
// Map columns.
foreach($columns as $column) {
if (array_key_exists($column, $row)) {
$rowData[] = $row[$column] ?? $this->nullValue;
} else {
$rowData[] = $this->missingValue;
}
}
$this->put($handle, $rowData);
}
}
/**
* @param iterable $data The data set
* @return string[] The column names found in the data
*/
protected function getColumnNames(iterable $data, bool $checkAllRows): array
{
$columns = [];
// Use foreach to support arrays and traversable objects.
foreach($data as $row) {
foreach($row as $column => $value) {
if (is_int($column)) {
throw new \RuntimeException('You cannot use $checkAllRows in combination with non-associative rows.');
}
$columns[$column] = true;
}
if (!$checkAllRows) break;
}
return array_keys($columns);
}
/**
* Writes a line of CSV data using configuration from the formatter.
* @param resource $handle The file handle write to
* @param array $data The data to write
* @throws \RuntimeException In case CSV data fails to write.
*/
protected function put($handle, array $data): void
{
if (fputcsv($handle, $data, $this->delimiter, $this->enclosure, $this->escape) === false) {
throw new \RuntimeException("Failed to write CSV data");
}
}
}