Completed
Push — master ( 4019f1...d4bc74 )
by ignace nyamagana
07:21 queued 04:41
created

CharsetConverter::register()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
ccs 5
cts 5
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 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 return the stream filter filtername
53
     *
54
     * @param  string $input_encoding
55
     * @param  string $output_encoding
56
     * @return string
57
     */
58 2
    public static function getFiltername(string $input_encoding, string $output_encoding): string
59
    {
60 2
        return sprintf(
61 2
            '%s.%s/%s',
62 2
            self::FILTERNAME,
63 2
            self::filterEncoding($input_encoding),
64 2
            self::filterEncoding($output_encoding)
65
        );
66
    }
67
68
    /**
69
     * Static method to register the class as a stream filter
70
     */
71 2
    public static function register()
72
    {
73 2
        $filtername = self::FILTERNAME.'.*';
74 2
        if (!in_array($filtername, stream_get_filters())) {
75 2
            stream_filter_register($filtername, __CLASS__);
76
        }
77 2
    }
78
79
    /**
80
     * Static method to add the stream filter to a {@link AbstractCsv} object
81
     *
82
     * @param AbstractCsv $csv
83
     * @param string      $input_encoding
84
     * @param string      $output_encoding
85
     *
86
     * @return AbstractCsv
87
     */
88 2
    public static function addTo(AbstractCsv $csv, string $input_encoding, string $output_encoding): AbstractCsv
89
    {
90 2
        self::register();
91
92 2
        return $csv->addStreamFilter(self::getFiltername($input_encoding, $output_encoding));
93
    }
94
95
    /**
96
     * Filter encoding charset
97
     *
98
     * @param string $encoding
99
     *
100
     * @throws OutOfRangeException if the charset is malformed or unsupported
101
     *
102
     * @return string
103
     */
104 4
    protected static function filterEncoding(string $encoding): string
105
    {
106 4
        static $encoding_list;
107 4
        if (null === $encoding_list) {
108 2
            $list = mb_list_encodings();
109 2
            $encoding_list = array_combine(array_map('strtolower', $list), $list);
110
        }
111
112 4
        $key = strtolower($encoding);
113 4
        if (isset($encoding_list[$key])) {
114 2
            return $encoding_list[$key];
115
        }
116
117 2
        throw new OutOfRangeException(sprintf('The submitted charset %s is not supported by the mbstring extension', $encoding));
118
    }
119
120
    /**
121
     * @inheritdoc
122
     */
123 2
    public function filter($in, $out, &$consumed, $closing)
124
    {
125 2
        while ($res = stream_bucket_make_writeable($in)) {
126 2
            $res->data = @mb_convert_encoding($res->data, $this->output_encoding, $this->input_encoding);
127 2
            $consumed += $res->datalen;
128 2
            stream_bucket_append($out, $res);
129
        }
130
131 2
        return PSFS_PASS_ON;
132
    }
133
134
    /**
135
     * @inheritdoc
136
     */
137 8
    public function onCreate()
138
    {
139 8
        $prefix = self::FILTERNAME.'.';
140 8
        if (0 !== strpos($this->filtername, $prefix)) {
141 2
            return false;
142
        }
143
144 6
        $encodings = substr($this->filtername, strlen($prefix));
145 6
        if (!preg_match(',^(?<input>[-\w]+)\/(?<output>[-\w]+)$,', $encodings, $matches)) {
146 2
            return false;
147
        }
148
149
        try {
150 4
            $this->input_encoding = $this->filterEncoding($matches['input']);
151 2
            $this->output_encoding = $this->filterEncoding($matches['output']);
152 2
            return true;
153 2
        } catch (Throwable $e) {
154 2
            return false;
155
        }
156
    }
157
158
    /**
159
     * Enable using the class as a formatter for the {@link Writer}
160
     *
161
     * @param array $record CSV record
162
     *
163
     * @return string[]
164
     */
165 2
    public function __invoke(array $record): array
166
    {
167 2
        if ($this->output_encoding !== $this->input_encoding) {
168 2
            array_walk($record, [$this, 'encodeField']);
169
        }
170
171 2
        return $record;
172
    }
173
174
    /**
175
     * Walker method to convert the offset and the value of a CSV record field
176
     *
177
     * @param string|null &$value
178
     * @param string|int  &$offset
179
     */
180 2
    protected function encodeField(&$value, &$offset)
181
    {
182 2
        if (null !== $value) {
183 2
            $value = mb_convert_encoding((string) $value, $this->output_encoding, $this->input_encoding);
184
        }
185
186 2
        if (!is_int($offset)) {
187 2
            $offset = mb_convert_encoding((string) $offset, $this->output_encoding, $this->input_encoding);
188
        }
189 2
    }
190
191
    /**
192
     * Convert Csv file into UTF-8
193
     *
194
     * @param array|Traversable $records the CSV records collection
195
     *
196
     * @return Iterator
197
     */
198 4
    public function convert($records): Iterator
199
    {
200 4
        $records = $this->filterIterable($records, __METHOD__);
201 4
        if (is_array($records)) {
202 2
            $records = new ArrayIterator($records);
203
        }
204
205 4
        if ($this->output_encoding === $this->input_encoding) {
206 2
            return $records;
207
        }
208
209 2
        $convert = function (array $record): array {
210 2
            array_walk($record, [$this, 'encodeField']);
211 2
            return $record;
212 1
        };
213
214 2
        return new MapIterator($records, $convert);
215
    }
216
217
    /**
218
     * Sets the records input encoding charset
219
     *
220
     * @param string $encoding
221
     *
222
     * @return self
223
     */
224 6
    public function inputEncoding(string $encoding): self
225
    {
226 6
        $encoding = $this->filterEncoding($encoding);
227 4
        if ($encoding === $this->input_encoding) {
228 4
            return $this;
229
        }
230
231 2
        $clone = clone $this;
232 2
        $clone->input_encoding = $encoding;
233
234 2
        return $clone;
235
    }
236
237
    /**
238
     * Sets the records output encoding charset
239
     *
240
     * @param string $encoding
241
     *
242
     * @return self
243
     */
244 2
    public function outputEncoding(string $encoding): self
245
    {
246 2
        $encoding = $this->filterEncoding($encoding);
247 2
        if ($encoding === $this->output_encoding) {
248 2
            return $this;
249
        }
250
251 2
        $clone = clone $this;
252 2
        $clone->output_encoding = $encoding;
253
254 2
        return $clone;
255
    }
256
}
257