-
-
Notifications
You must be signed in to change notification settings - Fork 340
/
Copy pathWriter.php
317 lines (277 loc) · 7.95 KB
/
Writer.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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
<?php
/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Csv;
use function array_reduce;
use function implode;
use function preg_match;
use function preg_quote;
use function str_replace;
use function strlen;
use const PHP_VERSION_ID;
use const SEEK_CUR;
use const STREAM_FILTER_WRITE;
/**
* A class to insert records into a CSV Document.
*/
class Writer extends AbstractCsv
{
/**
* callable collection to format the record before insertion.
*
* @var callable[]
*/
protected $formatters = [];
/**
* callable collection to validate the record before insertion.
*
* @var callable[]
*/
protected $validators = [];
/**
* newline character.
*
* @var string
*/
protected $newline = "\n";
/**
* Insert records count for flushing.
*
* @var int
*/
protected $flush_counter = 0;
/**
* Buffer flush threshold.
*
* @var int|null
*/
protected $flush_threshold;
/**
* {@inheritdoc}
*/
protected $stream_filter_mode = STREAM_FILTER_WRITE;
/**
* Regular expression used to detect if RFC4180 formatting is necessary.
*
* @var string
*/
protected $rfc4180_regexp;
/**
* double enclosure for RFC4180 compliance.
*
* @var string
*/
protected $rfc4180_enclosure;
/**
* {@inheritdoc}
*/
protected function resetProperties(): void
{
parent::resetProperties();
$characters = preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/');
$this->rfc4180_regexp = '/[\s|'.$characters.']/x';
$this->rfc4180_enclosure = $this->enclosure.$this->enclosure;
}
/**
* Returns the current newline sequence characters.
*/
public function getNewline(): string
{
return $this->newline;
}
/**
* Get the flush threshold.
*
* @return int|null
*/
public function getFlushThreshold()
{
return $this->flush_threshold;
}
/**
* Adds multiple records to the CSV document.
*
* @see Writer::insertOne
*/
public function insertAll(iterable $records): int
{
$bytes = 0;
foreach ($records as $record) {
$bytes += $this->insertOne($record);
}
$this->flush_counter = 0;
$this->document->fflush();
return $bytes;
}
/**
* Adds a single record to a CSV document.
*
* A record is an array that can contains scalar types values, NULL values
* or objects implementing the __toString method.
*
* @throws CannotInsertRecord If the record can not be inserted
*/
public function insertOne(array $record): int
{
$method = 'addRecord';
if (70400 > PHP_VERSION_ID && '' === $this->escape) {
$method = 'addRFC4180CompliantRecord';
}
$record = array_reduce($this->formatters, [$this, 'formatRecord'], $record);
$this->validateRecord($record);
$bytes = $this->$method($record);
if (false === $bytes || 0 >= $bytes) {
throw CannotInsertRecord::triggerOnInsertion($record);
}
return $bytes + $this->consolidate();
}
/**
* Adds a single record to a CSV Document using PHP algorithm.
*
* @see https://php.net/manual/en/function.fputcsv.php
*
* @return int|false
*/
protected function addRecord(array $record)
{
return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape);
}
/**
* Adds a single record to a CSV Document using RFC4180 algorithm.
*
* @see https://php.net/manual/en/function.fputcsv.php
* @see https://php.net/manual/en/function.fwrite.php
* @see https://tools.ietf.org/html/rfc4180
* @see http://edoceo.com/utilitas/csv-file-format
*
* String conversion is done without any check like fputcsv.
*
* - Emits E_NOTICE on Array conversion (returns the 'Array' string)
* - Throws catchable fatal error on objects that can not be converted
* - Returns resource id without notice or error (returns 'Resource id #2')
* - Converts boolean true to '1', boolean false to the empty string
* - Converts null value to the empty string
*
* Fields must be delimited with enclosures if they contains :
*
* - Embedded whitespaces
* - Embedded delimiters
* - Embedded line-breaks
* - Embedded enclosures.
*
* Embedded enclosures must be doubled.
*
* The LF character is added at the end of each record to mimic fputcsv behavior
*
* @return int|false
*/
protected function addRFC4180CompliantRecord(array $record)
{
foreach ($record as &$field) {
$field = (string) $field;
if (1 === preg_match($this->rfc4180_regexp, $field)) {
$field = $this->enclosure.str_replace($this->enclosure, $this->rfc4180_enclosure, $field).$this->enclosure;
}
}
unset($field);
return $this->document->fwrite(implode($this->delimiter, $record)."\n");
}
/**
* Format a record.
*
* The returned array must contain
* - scalar types values,
* - NULL values,
* - or objects implementing the __toString() method.
*/
protected function formatRecord(array $record, callable $formatter): array
{
return $formatter($record);
}
/**
* Validate a record.
*
* @throws CannotInsertRecord If the validation failed
*/
protected function validateRecord(array $record): void
{
foreach ($this->validators as $name => $validator) {
if (true !== $validator($record)) {
throw CannotInsertRecord::triggerOnValidation($name, $record);
}
}
}
/**
* Apply post insertion actions.
*/
protected function consolidate(): int
{
$bytes = 0;
if ("\n" !== $this->newline) {
$this->document->fseek(-1, SEEK_CUR);
/** @var int $newlineBytes */
$newlineBytes = $this->document->fwrite($this->newline, strlen($this->newline));
$bytes = $newlineBytes - 1;
}
if (null === $this->flush_threshold) {
return $bytes;
}
++$this->flush_counter;
if (0 === $this->flush_counter % $this->flush_threshold) {
$this->flush_counter = 0;
$this->document->fflush();
}
return $bytes;
}
/**
* Adds a record formatter.
*/
public function addFormatter(callable $formatter): self
{
$this->formatters[] = $formatter;
return $this;
}
/**
* Adds a record validator.
*/
public function addValidator(callable $validator, string $validator_name): self
{
$this->validators[$validator_name] = $validator;
return $this;
}
/**
* Sets the newline sequence.
*/
public function setNewline(string $newline): self
{
$this->newline = $newline;
return $this;
}
/**
* Set the flush threshold.
*
*
* @param ?int $threshold
* @throws Exception if the threshold is a integer lesser than 1
*/
public function setFlushThreshold(?int $threshold): self
{
if ($threshold === $this->flush_threshold) {
return $this;
}
if (null !== $threshold && 1 > $threshold) {
throw new InvalidArgument(__METHOD__.'() expects 1 Argument to be null or a valid integer greater or equal to 1');
}
$this->flush_threshold = $threshold;
$this->flush_counter = 0;
$this->document->fflush();
return $this;
}
}