1 | <?php |
||
29 | class Writer extends AbstractCsv |
||
30 | { |
||
31 | /** |
||
32 | * callable collection to format the record before insertion. |
||
33 | * |
||
34 | * @var callable[] |
||
35 | */ |
||
36 | protected $formatters = []; |
||
37 | |||
38 | /** |
||
39 | * callable collection to validate the record before insertion. |
||
40 | * |
||
41 | * @var callable[] |
||
42 | */ |
||
43 | protected $validators = []; |
||
44 | |||
45 | /** |
||
46 | * newline character. |
||
47 | * |
||
48 | * @var string |
||
49 | */ |
||
50 | protected $newline = "\n"; |
||
51 | |||
52 | /** |
||
53 | * Insert records count for flushing. |
||
54 | * |
||
55 | * @var int |
||
56 | */ |
||
57 | protected $flush_counter = 0; |
||
58 | |||
59 | /** |
||
60 | * Buffer flush threshold. |
||
61 | * |
||
62 | * @var int|null |
||
63 | */ |
||
64 | protected $flush_threshold; |
||
65 | |||
66 | /** |
||
67 | * {@inheritdoc} |
||
68 | */ |
||
69 | protected $stream_filter_mode = STREAM_FILTER_WRITE; |
||
70 | |||
71 | /** |
||
72 | * Regular expression used to detect if RFC4180 formatting is necessary. |
||
73 | * |
||
74 | * @var string |
||
75 | */ |
||
76 | protected $rfc4180_regexp; |
||
77 | |||
78 | /** |
||
79 | * double enclosure for RFC4180 compliance. |
||
80 | * |
||
81 | * @var string |
||
82 | */ |
||
83 | protected $rfc4180_enclosure; |
||
84 | |||
85 | /** |
||
86 | * {@inheritdoc} |
||
87 | */ |
||
88 | 15 | protected function resetProperties(): void |
|
89 | { |
||
90 | 15 | parent::resetProperties(); |
|
91 | 15 | $characters = preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); |
|
92 | 15 | $this->rfc4180_regexp = '/[\s|'.$characters.']/x'; |
|
93 | 15 | $this->rfc4180_enclosure = $this->enclosure.$this->enclosure; |
|
94 | 15 | } |
|
95 | |||
96 | /** |
||
97 | * Returns the current newline sequence characters. |
||
98 | */ |
||
99 | 3 | public function getNewline(): string |
|
100 | { |
||
101 | 3 | return $this->newline; |
|
102 | } |
||
103 | |||
104 | /** |
||
105 | * Get the flush threshold. |
||
106 | * |
||
107 | * @return int|null |
||
108 | */ |
||
109 | 3 | public function getFlushThreshold() |
|
110 | { |
||
111 | 3 | return $this->flush_threshold; |
|
112 | } |
||
113 | |||
114 | /** |
||
115 | * Adds multiple records to the CSV document. |
||
116 | * |
||
117 | * @see Writer::insertOne |
||
118 | */ |
||
119 | 6 | public function insertAll(iterable $records): int |
|
120 | { |
||
121 | 6 | $bytes = 0; |
|
122 | 6 | foreach ($records as $record) { |
|
123 | 6 | $bytes += $this->insertOne($record); |
|
124 | } |
||
125 | |||
126 | 6 | $this->flush_counter = 0; |
|
127 | 6 | $this->document->fflush(); |
|
128 | |||
129 | 6 | return $bytes; |
|
130 | } |
||
131 | |||
132 | /** |
||
133 | * Adds a single record to a CSV document. |
||
134 | * |
||
135 | * A record is an array that can contains scalar types values, NULL values |
||
136 | * or objects implementing the __toString method. |
||
137 | * |
||
138 | * @throws CannotInsertRecord If the record can not be inserted |
||
139 | */ |
||
140 | 57 | public function insertOne(array $record): int |
|
141 | { |
||
142 | 57 | $method = 'addRecord'; |
|
143 | 57 | if (70400 > PHP_VERSION_ID && '' === $this->escape) { |
|
144 | 20 | $method = 'addRFC4180CompliantRecord'; |
|
145 | } |
||
146 | |||
147 | 57 | $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record); |
|
148 | 57 | $this->validateRecord($record); |
|
149 | 54 | $bytes = $this->$method($record); |
|
150 | 52 | if (false === $bytes || 0 >= $bytes) { |
|
151 | 4 | throw CannotInsertRecord::triggerOnInsertion($record); |
|
152 | } |
||
153 | |||
154 | 48 | return $bytes + $this->consolidate(); |
|
155 | } |
||
156 | |||
157 | /** |
||
158 | * Adds a single record to a CSV Document using PHP algorithm. |
||
159 | * |
||
160 | * @see https://php.net/manual/en/function.fputcsv.php |
||
161 | * |
||
162 | * @return int|false |
||
163 | */ |
||
164 | 22 | protected function addRecord(array $record) |
|
165 | { |
||
166 | 22 | return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape); |
|
167 | } |
||
168 | |||
169 | /** |
||
170 | * Adds a single record to a CSV Document using RFC4180 algorithm. |
||
171 | * |
||
172 | * @see https://php.net/manual/en/function.fputcsv.php |
||
173 | * @see https://php.net/manual/en/function.fwrite.php |
||
174 | * @see https://tools.ietf.org/html/rfc4180 |
||
175 | * @see http://edoceo.com/utilitas/csv-file-format |
||
176 | * |
||
177 | * String conversion is done without any check like fputcsv. |
||
178 | * |
||
179 | * - Emits E_NOTICE on Array conversion (returns the 'Array' string) |
||
180 | * - Throws catchable fatal error on objects that can not be converted |
||
181 | * - Returns resource id without notice or error (returns 'Resource id #2') |
||
182 | * - Converts boolean true to '1', boolean false to the empty string |
||
183 | * - Converts null value to the empty string |
||
184 | * |
||
185 | * Fields must be delimited with enclosures if they contains : |
||
186 | * |
||
187 | * - Embedded whitespaces |
||
188 | * - Embedded delimiters |
||
189 | * - Embedded line-breaks |
||
190 | * - Embedded enclosures. |
||
191 | * |
||
192 | * Embedded enclosures must be doubled. |
||
193 | * |
||
194 | * The LF character is added at the end of each record to mimic fputcsv behavior |
||
195 | * |
||
196 | * @return int|false |
||
197 | */ |
||
198 | 20 | protected function addRFC4180CompliantRecord(array $record) |
|
199 | { |
||
200 | 20 | foreach ($record as &$field) { |
|
201 | 20 | $field = (string) $field; |
|
202 | 20 | if (1 === preg_match($this->rfc4180_regexp, $field)) { |
|
203 | 20 | $field = $this->enclosure.str_replace($this->enclosure, $this->rfc4180_enclosure, $field).$this->enclosure; |
|
204 | } |
||
205 | } |
||
206 | 20 | unset($field); |
|
207 | |||
208 | 20 | return $this->document->fwrite(implode($this->delimiter, $record)."\n"); |
|
209 | } |
||
210 | |||
211 | /** |
||
212 | * Format a record. |
||
213 | * |
||
214 | * The returned array must contain |
||
215 | * - scalar types values, |
||
216 | * - NULL values, |
||
217 | * - or objects implementing the __toString() method. |
||
218 | */ |
||
219 | 3 | protected function formatRecord(array $record, callable $formatter): array |
|
220 | { |
||
221 | 3 | return $formatter($record); |
|
222 | } |
||
223 | |||
224 | /** |
||
225 | * Validate a record. |
||
226 | * |
||
227 | * @throws CannotInsertRecord If the validation failed |
||
228 | */ |
||
229 | 12 | protected function validateRecord(array $record): void |
|
230 | { |
||
231 | 12 | foreach ($this->validators as $name => $validator) { |
|
232 | 3 | if (true !== $validator($record)) { |
|
233 | 3 | throw CannotInsertRecord::triggerOnValidation($name, $record); |
|
234 | } |
||
235 | } |
||
236 | 9 | } |
|
237 | |||
238 | /** |
||
239 | * Apply post insertion actions. |
||
240 | */ |
||
241 | 12 | protected function consolidate(): int |
|
242 | { |
||
243 | 12 | $bytes = 0; |
|
244 | 12 | if ("\n" !== $this->newline) { |
|
245 | 3 | $this->document->fseek(-1, SEEK_CUR); |
|
246 | /** @var int $newlineBytes */ |
||
247 | 3 | $newlineBytes = $this->document->fwrite($this->newline, strlen($this->newline)); |
|
248 | 3 | $bytes = $newlineBytes - 1; |
|
249 | } |
||
250 | |||
251 | 12 | if (null === $this->flush_threshold) { |
|
252 | 9 | return $bytes; |
|
253 | } |
||
254 | |||
255 | 3 | ++$this->flush_counter; |
|
256 | 3 | if (0 === $this->flush_counter % $this->flush_threshold) { |
|
257 | 3 | $this->flush_counter = 0; |
|
258 | 3 | $this->document->fflush(); |
|
259 | } |
||
260 | |||
261 | 3 | return $bytes; |
|
262 | } |
||
263 | |||
264 | /** |
||
265 | * Adds a record formatter. |
||
266 | */ |
||
267 | 3 | public function addFormatter(callable $formatter): self |
|
273 | |||
274 | /** |
||
275 | * Adds a record validator. |
||
276 | */ |
||
277 | 3 | public function addValidator(callable $validator, string $validator_name): self |
|
283 | |||
284 | /** |
||
285 | * Sets the newline sequence. |
||
286 | */ |
||
287 | 3 | public function setNewline(string $newline): self |
|
293 | |||
294 | /** |
||
295 | * Set the flush threshold. |
||
296 | * |
||
297 | * |
||
298 | * @param ?int $threshold |
||
|
|||
299 | * @throws Exception if the threshold is a integer lesser than 1 |
||
300 | */ |
||
301 | 9 | public function setFlushThreshold(?int $threshold): self |
|
302 | { |
||
303 | 9 | if ($threshold === $this->flush_threshold) { |
|
317 | } |
||
318 |
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.