CharsetConverter::addTo()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 3
dl 0
loc 6
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * League.Csv (https://csv.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Csv;
15
16
use OutOfRangeException;
17
use php_user_filter;
18
use function array_combine;
19
use function array_map;
20
use function array_walk;
21
use function in_array;
22
use function is_numeric;
23
use function mb_convert_encoding;
24
use function mb_list_encodings;
25
use function preg_match;
26
use function sprintf;
27
use function stream_bucket_append;
28
use function stream_bucket_make_writeable;
29
use function stream_filter_register;
30
use function stream_get_filters;
31
use function strpos;
32
use function strtolower;
33
use function substr;
34
35
/**
36
 * Converts resource stream or tabular data content charset.
37
 */
38
class CharsetConverter extends php_user_filter
39
{
40
    const FILTERNAME = 'convert.league.csv';
41
42
    /**
43
     * the filter name used to instantiate the class with.
44
     *
45
     * @var string
46
     */
47
    public $filtername;
48
49
    /**
50
     * @var mixed value passed to passed to stream_filter_append or stream_filter_prepend functions.
51
     */
52
    public $params;
53
54
    /**
55
     * The records input encoding charset.
56
     *
57
     * @var string
58
     */
59
    protected $input_encoding = 'UTF-8';
60
61
    /**
62
     * The records output encoding charset.
63
     *
64
     * @var string
65
     */
66
    protected $output_encoding = 'UTF-8';
67
68
    /**
69
     * Static method to add the stream filter to a {@link AbstractCsv} object.
70
     */
71 3
    public static function addTo(AbstractCsv $csv, string $input_encoding, string $output_encoding): AbstractCsv
72
    {
73 3
        self::register();
74
75 3
        return $csv->addStreamFilter(self::getFiltername($input_encoding, $output_encoding));
76
    }
77
78
    /**
79
     * Static method to register the class as a stream filter.
80
     */
81 3
    public static function register(): void
82
    {
83 3
        $filtername = self::FILTERNAME.'.*';
84 3
        if (!in_array($filtername, stream_get_filters(), true)) {
85 3
            stream_filter_register($filtername, self::class);
86
        }
87 3
    }
88
89
    /**
90
     * Static method to return the stream filter filtername.
91
     */
92 3
    public static function getFiltername(string $input_encoding, string $output_encoding): string
93
    {
94 3
        return sprintf(
95 3
            '%s.%s/%s',
96 3
            self::FILTERNAME,
97 3
            self::filterEncoding($input_encoding),
98 3
            self::filterEncoding($output_encoding)
99
        );
100
    }
101
102
    /**
103
     * Filter encoding charset.
104
     *
105
     * @throws OutOfRangeException if the charset is malformed or unsupported
106
     */
107 6
    protected static function filterEncoding(string $encoding): string
108
    {
109 6
        static $encoding_list;
110 6
        if (null === $encoding_list) {
111 3
            $list = mb_list_encodings();
112 3
            $encoding_list = array_combine(array_map('strtolower', $list), $list);
113
        }
114
115 6
        $key = strtolower($encoding);
116 6
        if (isset($encoding_list[$key])) {
117 3
            return $encoding_list[$key];
118
        }
119
120 3
        throw new OutOfRangeException(sprintf('The submitted charset %s is not supported by the mbstring extension', $encoding));
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 12
    public function onCreate(): bool
127
    {
128 12
        $prefix = self::FILTERNAME.'.';
129 12
        if (0 !== strpos($this->filtername, $prefix)) {
130 3
            return false;
131
        }
132
133 9
        $encodings = substr($this->filtername, strlen($prefix));
134 9
        if (1 !== preg_match(',^(?<input>[-\w]+)\/(?<output>[-\w]+)$,', $encodings, $matches)) {
135 3
            return false;
136
        }
137
138
        try {
139 6
            $this->input_encoding = self::filterEncoding($matches['input']);
140 3
            $this->output_encoding = self::filterEncoding($matches['output']);
141 3
        } catch (OutOfRangeException $e) {
142 3
            return false;
143
        }
144
145 3
        return true;
146
    }
147
148
    /**
149
     * @param resource $in
150
     * @param resource $out
151
     * @param int      $consumed
152
     * @param bool     $closing
153
     */
154 3
    public function filter($in, $out, &$consumed, $closing): int
155
    {
156 3
        while ($res = stream_bucket_make_writeable($in)) {
157 3
            $res->data = @mb_convert_encoding($res->data, $this->output_encoding, $this->input_encoding);
158 3
            $consumed += $res->datalen;
159 3
            stream_bucket_append($out, $res);
160
        }
161
162 3
        return PSFS_PASS_ON;
163
    }
164
165
    /**
166
     * Convert Csv records collection into UTF-8.
167
     */
168 21
    public function convert(iterable $records): iterable
169
    {
170 21
        if ($this->output_encoding === $this->input_encoding) {
171 3
            return $records;
172
        }
173
174 18
        if (is_array($records)) {
175 15
            return array_map($this, $records);
176
        }
177
178 3
        return new MapIterator($records, $this);
179
    }
180
181
    /**
182
     * Enable using the class as a formatter for the {@link Writer}.
183
     */
184 3
    public function __invoke(array $record): array
185
    {
186 3
        array_walk($record, [$this, 'encodeField']);
187
188 3
        return $record;
189
    }
190
191
    /**
192
     * Walker method to convert the offset and the value of a CSV record field.
193
     *
194
     * @param mixed $value  can be a scalar type or null
195
     * @param mixed $offset can be a string or an int
196
     */
197 15
    protected function encodeField(&$value, &$offset): void
198
    {
199 15
        if (null !== $value && !is_numeric($value)) {
200 3
            $value = mb_convert_encoding((string) $value, $this->output_encoding, $this->input_encoding);
201
        }
202
203 15
        if (!is_numeric($offset)) {
204 3
            $offset = mb_convert_encoding((string) $offset, $this->output_encoding, $this->input_encoding);
205
        }
206 15
    }
207
208
    /**
209
     * Sets the records input encoding charset.
210
     */
211 12
    public function inputEncoding(string $encoding): self
212
    {
213 12
        $encoding = self::filterEncoding($encoding);
214 9
        if ($encoding === $this->input_encoding) {
215 6
            return $this;
216
        }
217
218 6
        $clone = clone $this;
219 6
        $clone->input_encoding = $encoding;
220
221 6
        return $clone;
222
    }
223
224
    /**
225
     * Sets the records output encoding charset.
226
     */
227 3
    public function outputEncoding(string $encoding): self
228
    {
229 3
        $encoding = self::filterEncoding($encoding);
230 3
        if ($encoding === $this->output_encoding) {
231 3
            return $this;
232
        }
233
234 3
        $clone = clone $this;
235 3
        $clone->output_encoding = $encoding;
236
237 3
        return $clone;
238
    }
239
}
240