Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.40% |
165 / 166 |
|
96.77% |
30 / 31 |
CRAP | |
0.00% |
0 / 1 |
Csv | |
99.40% |
165 / 166 |
|
96.77% |
30 / 31 |
102 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
6 | |||
loadFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
loadString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
loadData | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getDataFromFile | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
writeDataToFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
writeTemplateToFile | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
outputDataToHttp | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
outputTemplateToHttp | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
processOptions | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
7 | |||
serialize | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
unserialize | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setData | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setString | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSerialized | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isUnserialized | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
outputToHttp | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
outputBlankFileToHttp | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
prepareHttp | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
writeToFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
writeBlankFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
4 | |||
appendDataToFile | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
appendRowToFile | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
serializeData | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
15 | |||
unserializeString | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
10 | |||
serializeRow | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
11 | |||
getFieldHeaders | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
isValid | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
__toString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (http://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@nolainteractive.com> |
7 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
8 | * @license http://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Csv; |
15 | |
16 | /** |
17 | * CSV class |
18 | * |
19 | * @category Pop |
20 | * @package Pop\Csv |
21 | * @author Nick Sagona, III <dev@nolainteractive.com> |
22 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
23 | * @license http://www.popphp.org/license New BSD License |
24 | * @version 4.0.0 |
25 | */ |
26 | class Csv |
27 | { |
28 | |
29 | /** |
30 | * CSV data in PHP |
31 | * @var mixed |
32 | */ |
33 | protected mixed $data = null; |
34 | |
35 | /** |
36 | * CSV string |
37 | * @var ?string |
38 | */ |
39 | protected ?string $string = null; |
40 | |
41 | /** |
42 | * Constructor |
43 | * |
44 | * Instantiate the Csv object. |
45 | * |
46 | * @param mixed $data |
47 | */ |
48 | public function __construct(mixed $data = null) |
49 | { |
50 | if ($data !== null) { |
51 | // If data is a file |
52 | if (is_string($data) && (stripos($data, '.csv') !== false) && file_exists($data)) { |
53 | $this->string = file_get_contents($data); |
54 | // Else, if it's just data |
55 | } else if (!is_string($data)) { |
56 | $this->data = $data; |
57 | // Else if it's a string or stream of data |
58 | } else { |
59 | $this->string = $data; |
60 | } |
61 | } |
62 | } |
63 | |
64 | /** |
65 | * Load CSV file |
66 | * |
67 | * @param string $file |
68 | * @param array $options |
69 | * @return Csv |
70 | */ |
71 | public static function loadFile(string $file, array $options = []): Csv |
72 | { |
73 | $csv = new self($file); |
74 | $csv->unserialize($options); |
75 | return $csv; |
76 | } |
77 | |
78 | /** |
79 | * Load CSV string |
80 | * |
81 | * @param string $string |
82 | * @param array $options |
83 | * @return Csv |
84 | */ |
85 | public static function loadString(string $string, array $options = []): Csv |
86 | { |
87 | $csv = new self($string); |
88 | $csv->unserialize($options); |
89 | return $csv; |
90 | } |
91 | |
92 | /** |
93 | * Load CSV data |
94 | * |
95 | * @param array $data |
96 | * @param array $options |
97 | * @return Csv |
98 | */ |
99 | public static function loadData(array $data, array $options = []): Csv |
100 | { |
101 | $csv = new self($data); |
102 | $csv->serialize($options); |
103 | return $csv; |
104 | } |
105 | |
106 | /** |
107 | * Load CSV file and get data |
108 | * |
109 | * @param string $file |
110 | * @param array $options |
111 | * @return array |
112 | */ |
113 | public static function getDataFromFile(string $file, array $options = []): array |
114 | { |
115 | $csv = new self($file); |
116 | return $csv->unserialize($options); |
117 | } |
118 | |
119 | /** |
120 | * Write data to file |
121 | * |
122 | * @param array $data |
123 | * @param string $to |
124 | * @param array $options |
125 | * @return void |
126 | */ |
127 | public static function writeDataToFile(array $data, string $to, array $options = []): void |
128 | { |
129 | $csv = new self($data); |
130 | $csv->serialize($options); |
131 | $csv->writeToFile($to); |
132 | } |
133 | |
134 | /** |
135 | * Write data to file |
136 | * |
137 | * @param array $data |
138 | * @param string $to |
139 | * @param string $delimiter |
140 | * @param array $exclude |
141 | * @param array $include |
142 | * @return void |
143 | *@throws Exception |
144 | */ |
145 | public static function writeTemplateToFile( |
146 | array $data, string $to, string $delimiter = ',', array $exclude = [], array $include = [] |
147 | ): void |
148 | { |
149 | $csv = new self($data); |
150 | $csv->writeBlankFile($to, $delimiter, $exclude, $include); |
151 | } |
152 | |
153 | /** |
154 | * Write data to file |
155 | * |
156 | * @param array $data |
157 | * @param array $options |
158 | * @param string $filename |
159 | * @param bool $forceDownload |
160 | * @param array $headers |
161 | * @return void |
162 | */ |
163 | public static function outputDataToHttp( |
164 | array $data, array $options = [], string $filename = 'pop-data.csv', bool $forceDownload = true, array $headers = [] |
165 | ): void |
166 | { |
167 | $csv = new self($data); |
168 | $csv->serialize($options); |
169 | $csv->outputToHttp($filename, $forceDownload, $headers); |
170 | } |
171 | |
172 | /** |
173 | * Write data to file |
174 | * |
175 | * @param array $data |
176 | * @param string $filename |
177 | * @param bool $forceDownload |
178 | * @param array $headers |
179 | * @param string $delimiter |
180 | * @param array $exclude |
181 | * @throws Exception |
182 | * @return void |
183 | */ |
184 | public static function outputTemplateToHttp( |
185 | array $data, string $filename = 'pop-data-template.csv', bool $forceDownload = true, |
186 | array $headers = [], string $delimiter = ',', array $exclude = [] |
187 | ) |
188 | { |
189 | $csv = new self($data); |
190 | $csv->outputBlankFileToHttp($filename, $forceDownload, $headers, $delimiter, $exclude); |
191 | } |
192 | |
193 | /** |
194 | * Process CSV options |
195 | * |
196 | * @param array $options |
197 | * @return array |
198 | */ |
199 | public static function processOptions(array $options): array |
200 | { |
201 | $options['delimiter'] = (isset($options['delimiter'])) ? $options['delimiter'] : ','; |
202 | $options['enclosure'] = (isset($options['enclosure'])) ? $options['enclosure'] : '"'; |
203 | $options['escape'] = (isset($options['escape'])) ? $options['escape'] : '"'; |
204 | $options['fields'] = (isset($options['fields'])) ? (bool)$options['fields'] : true; |
205 | $options['newline'] = (isset($options['newline'])) ? (bool)$options['newline'] : true; |
206 | $options['limit'] = (isset($options['limit'])) ? (int)$options['limit'] : 0; |
207 | |
208 | return $options; |
209 | } |
210 | |
211 | /** |
212 | * Serialize the data to a CSV string |
213 | * |
214 | * @param array $options |
215 | * @return string |
216 | */ |
217 | public function serialize(array $options = []): string |
218 | { |
219 | $this->string = self::serializeData($this->data, $options); |
220 | return $this->string; |
221 | } |
222 | |
223 | /** |
224 | * Unserialize the string to data |
225 | * |
226 | * @param array $options |
227 | * @return mixed |
228 | */ |
229 | public function unserialize(array $options = []): mixed |
230 | { |
231 | $this->data = self::unserializeString($this->string, $options); |
232 | return $this->data; |
233 | } |
234 | |
235 | /** |
236 | * Set data |
237 | * |
238 | * @param array $data |
239 | * @return Csv |
240 | */ |
241 | public function setData(array $data): Csv |
242 | { |
243 | $this->data = $data; |
244 | return $this; |
245 | } |
246 | |
247 | /** |
248 | * Get data |
249 | * |
250 | * @return array |
251 | */ |
252 | public function getData(): array |
253 | { |
254 | return $this->data; |
255 | } |
256 | |
257 | /** |
258 | * Set string |
259 | * |
260 | * @param string $string |
261 | * @return Csv |
262 | */ |
263 | public function setString(string $string): Csv |
264 | { |
265 | $this->string = $string; |
266 | return $this; |
267 | } |
268 | |
269 | /** |
270 | * Get string |
271 | * |
272 | * @return string |
273 | */ |
274 | public function getString(): string |
275 | { |
276 | return $this->string; |
277 | } |
278 | |
279 | /** |
280 | * Check if data was serialized |
281 | * |
282 | * @return bool |
283 | */ |
284 | public function isSerialized(): bool |
285 | { |
286 | return ($this->string !== null); |
287 | } |
288 | |
289 | /** |
290 | * Check if string was unserialized |
291 | * |
292 | * @return bool |
293 | */ |
294 | public function isUnserialized(): bool |
295 | { |
296 | return ($this->data !== null); |
297 | } |
298 | |
299 | /** |
300 | * Output CSV string data to HTTP |
301 | * |
302 | * @param string $filename |
303 | * @param bool $forceDownload |
304 | * @param array $headers |
305 | * @return void |
306 | */ |
307 | public function outputToHttp(string $filename = 'pop-data.csv', bool $forceDownload = true, array $headers = []): void |
308 | { |
309 | // Attempt to serialize data if it hasn't been done yet |
310 | if (($this->string === null) && ($this->data !== null)) { |
311 | $this->serialize(); |
312 | } |
313 | |
314 | $this->prepareHttp($filename, $forceDownload, $headers); |
315 | |
316 | echo $this->string; |
317 | } |
318 | |
319 | /** |
320 | * Output CSV headers only in a blank file to HTTP |
321 | * |
322 | * @param string $filename |
323 | * @param bool $forceDownload |
324 | * @param array $headers |
325 | * @param string $delimiter |
326 | * @param array $exclude |
327 | * @throws Exception |
328 | * @return void |
329 | */ |
330 | public function outputBlankFileToHttp( |
331 | string $filename = 'pop-data.csv', bool $forceDownload = true, array $headers = [], string $delimiter = ',', array $exclude = [] |
332 | ): void |
333 | { |
334 | // Attempt to serialize data if it hasn't been done yet |
335 | if (($this->string === null) && ($this->data !== null) && isset($this->data[0])) { |
336 | $fieldHeaders = self::getFieldHeaders($this->data[0], $delimiter, $exclude); |
337 | } else { |
338 | throw new Exception('Error: The data has not been set.'); |
339 | } |
340 | |
341 | $this->prepareHttp($filename, $forceDownload, $headers); |
342 | echo $fieldHeaders; |
343 | } |
344 | |
345 | /** |
346 | * Prepare output to HTTP |
347 | * |
348 | * @param string $filename |
349 | * @param bool $forceDownload |
350 | * @param array $headers |
351 | * @return void |
352 | */ |
353 | public function prepareHttp(string $filename = 'pop-data.csv', bool $forceDownload = true, array $headers = []): void |
354 | { |
355 | if (!isset($headers['Content-Type'])) { |
356 | $headers['Content-Type'] = 'text/csv'; |
357 | } |
358 | if (!isset($headers['Content-Disposition'])) { |
359 | $headers['Content-Disposition'] = (($forceDownload) ? 'attachment; ' : null) . 'filename=' . $filename; |
360 | } |
361 | |
362 | // Send the headers and output the file |
363 | if (!headers_sent()) { |
364 | header('HTTP/1.1 200 OK'); |
365 | foreach ($headers as $name => $value) { |
366 | header($name . ': ' . $value); |
367 | } |
368 | } |
369 | } |
370 | |
371 | /** |
372 | * Output CSV data to a file |
373 | * |
374 | * @param string $to |
375 | * @return void |
376 | */ |
377 | public function writeToFile(string $to): void |
378 | { |
379 | // Attempt to serialize data if it hasn't been done yet |
380 | if (($this->string === null) && ($this->data !== null)) { |
381 | $this->serialize(); |
382 | } |
383 | |
384 | file_put_contents($to, $this->string); |
385 | } |
386 | |
387 | /** |
388 | * Output CSV headers only to a blank file |
389 | * |
390 | * @param string $to |
391 | * @param string $delimiter |
392 | * @param array $exclude |
393 | * @param array $include |
394 | * @return void |
395 | *@throws Exception |
396 | */ |
397 | public function writeBlankFile(string $to, string $delimiter = ',', array $exclude = [], array $include = []): void |
398 | { |
399 | // Attempt to get field headers and output file |
400 | if (($this->string === null) && ($this->data !== null) && isset($this->data[0])) { |
401 | file_put_contents($to, self::getFieldHeaders($this->data[0], $delimiter, $exclude, $include)); |
402 | } else { |
403 | throw new Exception('Error: The data has not been set.'); |
404 | } |
405 | } |
406 | |
407 | /** |
408 | * Append additional CSV data to a pre-existing file |
409 | * |
410 | * @param string $file |
411 | * @param array $data |
412 | * @param array $options |
413 | * @param bool $validate |
414 | * @throws Exception |
415 | * @return void |
416 | */ |
417 | public static function appendDataToFile(string $file, array $data, array $options = [], bool $validate = true): void |
418 | { |
419 | if (!file_exists($file)) { |
420 | throw new Exception("Error: The file '" . $file . "' does not exist."); |
421 | } |
422 | |
423 | foreach ($data as $row) { |
424 | self::appendRowToFile($file, $row, $options, $validate); |
425 | } |
426 | } |
427 | |
428 | /** |
429 | * Append additional CSV row of data to a pre-existing file |
430 | * |
431 | * @param string $file |
432 | * @param array $row |
433 | * @param array $options |
434 | * @param bool $validate |
435 | * @throws Exception |
436 | * @return void |
437 | */ |
438 | public static function appendRowToFile(string $file, array $row, array $options = [], bool $validate = true): void |
439 | { |
440 | if (!file_exists($file)) { |
441 | throw new Exception("Error: The file '" . $file . "' does not exist."); |
442 | } |
443 | |
444 | if ($validate) { |
445 | $keys = array_keys($row); |
446 | $headers = array_map( |
447 | function($value) { return str_replace('"', '', $value); }, explode(',', trim(fgets(fopen($file, 'r')))) |
448 | ); |
449 | |
450 | if ($keys != $headers) { |
451 | throw new Exception("Error: The new data's columns do not match the CSV files columns."); |
452 | } |
453 | } |
454 | |
455 | $options = self::processOptions($options); |
456 | $csvRow = self::serializeRow( |
457 | (array)$row, [], [], $options['delimiter'], $options['enclosure'], |
458 | $options['escape'], $options['newline'], $options['limit'] |
459 | ); |
460 | |
461 | file_put_contents($file, $csvRow, FILE_APPEND); |
462 | } |
463 | |
464 | /** |
465 | * Convert the data into CSV format. |
466 | * |
467 | * @param mixed $data |
468 | * @param array $options |
469 | * @return string |
470 | */ |
471 | public static function serializeData(mixed $data, array $options = []): string |
472 | { |
473 | $keys = array_keys($data); |
474 | $isAssoc = false; |
475 | |
476 | foreach ($keys as $key) { |
477 | if (!is_numeric($key)) { |
478 | $isAssoc = true; |
479 | } |
480 | } |
481 | |
482 | if ($isAssoc) { |
483 | $newData = []; |
484 | foreach ($data as $key => $value) { |
485 | $newData = array_merge($newData, $value); |
486 | } |
487 | $data = $newData; |
488 | } |
489 | |
490 | if (isset($options['exclude'])) { |
491 | $exclude = (!is_array($options['exclude'])) ? [$options['exclude']] : $options['exclude']; |
492 | } else { |
493 | $exclude = []; |
494 | } |
495 | |
496 | if (isset($options['include'])) { |
497 | $include = (!is_array($options['include'])) ? [$options['include']] : $options['include']; |
498 | } else { |
499 | $include = []; |
500 | } |
501 | |
502 | $options = self::processOptions($options); |
503 | $csv = ''; |
504 | $firstKey = array_keys($data)[0]; |
505 | |
506 | if (is_array($data) && isset($data[$firstKey]) && |
507 | (is_array($data[$firstKey]) || ($data[$firstKey] instanceof \ArrayObject)) && ($options['fields'])) { |
508 | $csv .= self::getFieldHeaders((array)$data[$firstKey], $options['delimiter'], $exclude, $include); |
509 | } |
510 | |
511 | // Initialize and clean the field values. |
512 | foreach ($data as $value) { |
513 | $csv .= self::serializeRow( |
514 | (array)$value, $exclude, $include, $options['delimiter'], $options['enclosure'], |
515 | $options['escape'], $options['newline'], $options['limit'] |
516 | ); |
517 | } |
518 | |
519 | return $csv; |
520 | } |
521 | |
522 | /** |
523 | * Parse the CSV string into a PHP array |
524 | * |
525 | * @param string $string |
526 | * @param array $options |
527 | * @return array |
528 | */ |
529 | public static function unserializeString(string $string, array $options = []): array |
530 | { |
531 | $options = self::processOptions($options); |
532 | $lines = preg_split("/((\r?\n)|(\r\n?))/", $string); |
533 | $data = []; |
534 | $fieldKeys = []; |
535 | |
536 | $tempFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'pop-csv-tmp-' . time() . '.csv'; |
537 | file_put_contents($tempFile, $string); |
538 | |
539 | if ($options['fields']) { |
540 | $fieldNames = str_getcsv($lines[0], $options['delimiter'], $options['enclosure'], $options['escape']); |
541 | foreach ($fieldNames as $name) { |
542 | $fieldKeys[] = trim($name); |
543 | } |
544 | } |
545 | |
546 | if (($handle = fopen($tempFile, 'r')) !== false) { |
547 | while (($dataFields = fgetcsv($handle, 1000, $options['delimiter'], $options['enclosure'], $options['escape'])) !== false) { |
548 | if (($options['fields']) && (count($dataFields) == count($fieldKeys)) && ($dataFields != $fieldKeys)) { |
549 | $d = []; |
550 | foreach ($dataFields as $i => $value) { |
551 | $d[$fieldKeys[$i]] = $value; |
552 | } |
553 | $data[] = $d; |
554 | } else if ($dataFields != $fieldKeys) { |
555 | $data[] = $dataFields; |
556 | } |
557 | } |
558 | fclose($handle); |
559 | unlink($tempFile); |
560 | } |
561 | |
562 | return $data; |
563 | } |
564 | |
565 | /** |
566 | * Serialize single row of data |
567 | * |
568 | * @param array $value |
569 | * @param array $exclude |
570 | * @param array $include |
571 | * @param string $delimiter |
572 | * @param string $enclosure |
573 | * @param string $escape |
574 | * @param bool $newline |
575 | * @param int $limit |
576 | * @return string |
577 | */ |
578 | public static function serializeRow( |
579 | array $value, array $exclude = [], array $include = [], string $delimiter = ',', string $enclosure = '"', |
580 | string $escape = '"', bool $newline = true, int $limit = 0 |
581 | ): string |
582 | { |
583 | $rowAry = []; |
584 | foreach ($value as $key => $val) { |
585 | if (!in_array($key, $exclude) && (empty($include) || in_array($key, $include))) { |
586 | if (!$newline) { |
587 | $val = str_replace(["\n", "\r"], [" ", " "], $val); |
588 | } |
589 | if ((int)$limit > 0) { |
590 | $val = substr($val, 0, (int)$limit); |
591 | } |
592 | if (str_contains($val, $enclosure)) { |
593 | $val = str_replace($enclosure, $escape . $enclosure, $val); |
594 | } |
595 | if ((str_contains($val, $delimiter)) || (str_contains($val, "\n")) || |
596 | (str_contains($val, $escape . $enclosure))) { |
597 | $val = $enclosure . $val . $enclosure; |
598 | } |
599 | $rowAry[] = $val; |
600 | } |
601 | } |
602 | return implode($delimiter, $rowAry) . "\n"; |
603 | } |
604 | |
605 | /** |
606 | * Get field headers |
607 | * |
608 | * @param mixed $data |
609 | * @param string $delimiter |
610 | * @param array $exclude |
611 | * @param array $include |
612 | * @return string |
613 | */ |
614 | public static function getFieldHeaders(mixed $data, string $delimiter = ',', array $exclude = [], array $include = []): string |
615 | { |
616 | $headers = array_keys($data); |
617 | $headersAry = []; |
618 | foreach ($headers as $header) { |
619 | if (!in_array($header, $exclude) && (empty($include) || in_array($header, $include))) { |
620 | $headersAry[] = $header; |
621 | } |
622 | } |
623 | return implode($delimiter, $headersAry) . PHP_EOL; |
624 | } |
625 | |
626 | /** |
627 | * Determine if the string is valid CSV |
628 | * |
629 | * @param string $string |
630 | * @return bool |
631 | */ |
632 | public static function isValid(string $string): bool |
633 | { |
634 | $lines = preg_split("/((\r?\n)|(\r\n?))/", $string); |
635 | $fields = []; |
636 | if (isset($lines[0])) { |
637 | $fields = str_getcsv($lines[0]); |
638 | } |
639 | return (count($fields) > 0); |
640 | } |
641 | |
642 | /** |
643 | * Render CSV string data to string |
644 | * |
645 | * @return string |
646 | */ |
647 | public function __toString(): string |
648 | { |
649 | // Attempt to serialize data if it hasn't been done yet |
650 | if (($this->string === null) && ($this->data !== null)) { |
651 | $this->serialize(); |
652 | } |
653 | |
654 | return $this->string; |
655 | } |
656 | |
657 | } |