Completed
Pull Request — master (#309)
by ignace nyamagana
01:34
created

Writer::getFlushThreshold()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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