Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.91% covered (success)
90.91%
20 / 22
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
CsvTabularStreamWriter
90.48% covered (success)
90.48%
19 / 21
71.43% covered (warning)
71.43%
5 / 7
13.15
0.00% covered (danger)
0.00%
0 / 1
 __construct
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 writeHeaders
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 writeRow
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 close
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getMimeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFileExtension
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sanitizeCell
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\DataExchange\Infrastructure\Writer;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\DataExchange\Domain\Exception\DataExchangeException;
12
13/**
14 * Streaming CSV writer featuring Excel UTF-8 BOM and CSV Formula Injection mitigation.
15 *
16 * @package App\Core\DataExchange\Infrastructure\Writer
17 */
18final class CsvTabularStreamWriter implements TabularStreamWriterInterface
19{
20    /** @var resource */
21    private $stream;
22
23    /**
24     * CsvTabularStreamWriter constructor.
25     *
26     * @param string $outputPath Destination file path or stream URI.
27     * @param string $delimiter  Cell delimiter character.
28     * @param bool   $includeBom Prepend UTF-8 BOM for MS Excel compatibility.
29     */
30    public function __construct(
31        private readonly string $outputPath,
32        private readonly string $delimiter = ',',
33        private readonly bool $includeBom = true
34    ) {
35        $handle = fopen($this->outputPath, 'w');
36        if ($handle === false) {
37            throw new DataExchangeException("Failed to open output stream: {$this->outputPath}");
38        }
39        $this->stream = $handle;
40
41        if ($this->includeBom) {
42            fwrite($this->stream, "\xEF\xBB\xBF");
43        }
44    }
45
46    /** {@inheritdoc} */
47    public function writeHeaders(array $headers): void
48    {
49        $sanitized = array_map([$this, 'sanitizeCell'], $headers);
50        fputcsv($this->stream, $sanitized, $this->delimiter, '"', '\\');
51    }
52
53    /** {@inheritdoc} */
54    public function writeRow(array $row): void
55    {
56        $sanitized = array_map([$this, 'sanitizeCell'], $row);
57        fputcsv($this->stream, $sanitized, $this->delimiter, '"', '\\');
58    }
59
60    /** {@inheritdoc} */
61    public function close(): void
62    {
63        if (is_resource($this->stream)) {
64            fflush($this->stream);
65            fclose($this->stream);
66        }
67    }
68
69    /** {@inheritdoc} */
70    public function getMimeType(): string
71    {
72        return 'text/csv; charset=UTF-8';
73    }
74
75    /** {@inheritdoc} */
76    public function getFileExtension(): string
77    {
78        return 'csv';
79    }
80
81    /**
82     * Mitigates CSV formula injection attacks by prefixing vulnerable formula characters.
83     */
84    private function sanitizeCell(mixed $value): string
85    {
86        if ($value === null) {
87            return '';
88        }
89
90        $str = (string) $value;
91        if ($str !== '' && in_array($str[0], ['=', '+', '-', '@', "\t", "\r"], true)) {
92            return "'" . $str;
93        }
94
95        return $str;
96    }
97}