Completed
Pull Request — master (#233)
by ignace nyamagana
02:19
created

CharsetConverter::addTo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

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