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

Writer::resetProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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