Completed
Push — master ( ecd0f8...3ad52b )
by ignace nyamagana
15:08
created

CharsetConverter::encodeField()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

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