Completed
Pull Request — master (#309)
by ignace nyamagana
04:57
created

Writer::convertField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 12
ccs 7
cts 7
cp 1
crap 2
rs 9.8666
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 Traversable;
20
use TypeError;
21
use const SEEK_CUR;
22
use const STREAM_FILTER_WRITE;
23
use function array_map;
24
use function array_reduce;
25
use function gettype;
26
use function implode;
27
use function is_iterable;
28
use function sprintf;
29
use function str_replace;
30
use function strlen;
31
32
/**
33
 * A class to insert records into a CSV Document.
34
 *
35
 * @package League.csv
36
 * @since   4.0.0
37
 * @author  Ignace Nyamagana Butera <[email protected]>
38
 */
39
class Writer extends AbstractCsv
40
{
41
    const MODE_PHP = 'MODE_PHP';
42
43
    const MODE_RFC4180 = 'MODE_RFC4180';
44
45
    /**
46
     * callable collection to format the record before insertion.
47
     *
48
     * @var callable[]
49
     */
50
    protected $formatters = [];
51
52
    /**
53
     * callable collection to validate the record before insertion.
54
     *
55
     * @var callable[]
56
     */
57
    protected $validators = [];
58
59
    /**
60
     * newline character.
61
     *
62
     * @var string
63
     */
64
    protected $newline = "\n";
65
66
    /**
67
     * Insert records count for flushing.
68
     *
69
     * @var int
70
     */
71
    protected $flush_counter = 0;
72
73
    /**
74
     * Buffer flush threshold.
75
     *
76
     * @var int|null
77
     */
78
    protected $flush_threshold;
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    protected $stream_filter_mode = STREAM_FILTER_WRITE;
84
85
    /**
86
     * Regular expression used to detect if enclosure are necessary or not.
87
     *
88
     * @var string
89
     */
90
    protected $rfc4180_regexp;
91
92
    /**
93
     * Returns the current newline sequence characters.
94
     */
95 3
    public function getNewline(): string
96
    {
97 3
        return $this->newline;
98
    }
99
100
    /**
101
     * Get the flush threshold.
102
     *
103
     * @return int|null
104
     */
105 3
    public function getFlushThreshold()
106
    {
107 3
        return $this->flush_threshold;
108
    }
109
110
    /**
111
     * Adds multiple records to the CSV document.
112
     *
113
     * @see Writer::insertOne
114
     *
115
     * @param Traversable|array $records
116
     */
117 9
    public function insertAll($records, string $mode = self::MODE_PHP): int
118
    {
119 9
        if (!is_iterable($records)) {
120 3
            throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records)));
121
        }
122
123 6
        $bytes = 0;
124 6
        foreach ($records as $record) {
125 6
            $bytes += $this->insertOne($record, $mode);
126
        }
127
128 6
        $this->flush_counter = 0;
129 6
        $this->document->fflush();
130
131 6
        return $bytes;
132
    }
133
134
    /**
135
     * Adds a single record to a CSV document.
136
     *
137
     * A record is an array that can contains scalar types values, NULL values
138
     * or objects implementing the __toString method.
139
     *
140
     * @throws CannotInsertRecord If the record can not be inserted
141
     */
142 48
    public function insertOne(array $record, string $mode = self::MODE_PHP): int
143
    {
144 48
        static $method = [self::MODE_PHP => 'fputcsvPHP', self::MODE_RFC4180 => 'fputcsvRFC4180'];
145 48
        if (!isset($method[$mode])) {
146 3
            throw new Exception(sprintf('Unknown or unsupported writing mode %s', $mode));
147
        }
148
149 45
        $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record);
150 45
        $this->validateRecord($record);
151 42
        $bytes = $this->{$method[$mode]}($record);
152 42
        if (false !== $bytes && 0 !== $bytes) {
153 36
            return $bytes + $this->consolidate();
154
        }
155
156 6
        throw CannotInsertRecord::triggerOnInsertion($record);
157
    }
158
159
    /**
160
     * Adds a single record to a CSV Document using PHP algorithm.
161
     */
162 9
    protected function fputcsvPHP(array $record)
163
    {
164 9
        return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape);
165
    }
166
167
    /**
168
     * Adds a single record to a CSV Document using RFC4180 algorithm.
169
     */
170 18
    protected function fputcsvRFC4180(array $record)
171
    {
172 18
        return $this->document->fwrite(implode($this->delimiter, array_map([$this, 'convertField'], $record))."\n");
173
    }
174
175
    /**
176
     * Converts and Format a record field to be inserted into a CSV Document.
177
     *
178
     * @see https://tools.ietf.org/html/rfc4180
179
     */
180 18
    protected function convertField($field): string
181
    {
182 18
        $field = (string) $field;
183 18
        if (!preg_match($this->rfc4180_regexp, $field)) {
184 15
            return $field;
185
        }
186
187 15
        return $this->enclosure
188 15
            .str_replace($this->enclosure, $this->enclosure.$this->enclosure, $field)
189 15
            .$this->enclosure
190
        ;
191
    }
192
193
    /**
194
     * Format a record.
195
     *
196
     * The returned array must contain
197
     *   - scalar types values,
198
     *   - NULL values,
199
     *   - or objects implementing the __toString() method.
200
     */
201 3
    protected function formatRecord(array $record, callable $formatter): array
202
    {
203 3
        return $formatter($record);
204
    }
205
206
    /**
207
     * Validate a record.
208
     *
209
     * @throws CannotInsertRecord If the validation failed
210
     */
211 12
    protected function validateRecord(array $record)
212
    {
213 12
        foreach ($this->validators as $name => $validator) {
214 3
            if (true !== $validator($record)) {
215 3
                throw CannotInsertRecord::triggerOnValidation($name, $record);
216
            }
217
        }
218 9
    }
219
220
    /**
221
     * Apply post insertion actions.
222
     */
223 12
    protected function consolidate(): int
224
    {
225 12
        $bytes = 0;
226 12
        if ("\n" !== $this->newline) {
227 3
            $this->document->fseek(-1, SEEK_CUR);
228 3
            $bytes = $this->document->fwrite($this->newline, strlen($this->newline)) - 1;
229
        }
230
231 12
        if (null === $this->flush_threshold) {
232 9
            return $bytes;
233
        }
234
235 3
        ++$this->flush_counter;
236 3
        if (0 === $this->flush_counter % $this->flush_threshold) {
237 3
            $this->flush_counter = 0;
238 3
            $this->document->fflush();
239
        }
240
241 3
        return $bytes;
242
    }
243
244
    /**
245
     * Adds a record formatter.
246
     */
247 3
    public function addFormatter(callable $formatter): self
248
    {
249 3
        $this->formatters[] = $formatter;
250
251 3
        return $this;
252
    }
253
254
    /**
255
     * Adds a record validator.
256
     */
257 3
    public function addValidator(callable $validator, string $validator_name): self
258
    {
259 3
        $this->validators[$validator_name] = $validator;
260
261 3
        return $this;
262
    }
263
264
    /**
265
     * Sets the newline sequence.
266
     */
267 3
    public function setNewline(string $newline): self
268
    {
269 3
        $this->newline = $newline;
270
271 3
        return $this;
272
    }
273
274
    /**
275
     * Reset dynamic object properties to improve performance.
276
     */
277 15
    protected function resetProperties()
278
    {
279 15
        $this->rfc4180_regexp = "/[\n|\r"
280 15
            .preg_quote($this->delimiter, '/')
281 15
            .'|'
282 15
            .preg_quote($this->enclosure, '/')
283 15
        .']/';
284 15
    }
285
286
    /**
287
     * Set the flush threshold.
288
     *
289
     * @param int|null $threshold
290
     *
291
     * @throws Exception if the threshold is a integer lesser than 1
292
     */
293 12
    public function setFlushThreshold($threshold): self
294
    {
295 12
        if ($threshold === $this->flush_threshold) {
296 3
            return $this;
297
        }
298
299 12
        if (!is_nullable_int($threshold)) {
300 3
            throw new TypeError(sprintf(__METHOD__.'() expects 1 Argument to be null or an integer %s given', gettype($threshold)));
301
        }
302
303 9
        if (null !== $threshold && 1 > $threshold) {
304 3
            throw new Exception(__METHOD__.'() expects 1 Argument to be null or a valid integer greater or equal to 1');
305
        }
306
307 9
        $this->flush_threshold = $threshold;
308 9
        $this->flush_counter = 0;
309 9
        $this->document->fflush();
310
311 9
        return $this;
312
    }
313
}
314