Writer::consolidate()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 6
nop 0
dl 0
loc 22
ccs 13
cts 13
cp 1
crap 4
rs 9.568
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * League.Csv (https://csv.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Csv;
15
16
use function array_reduce;
17
use function implode;
18
use function preg_match;
19
use function preg_quote;
20
use function str_replace;
21
use function strlen;
22
use const PHP_VERSION_ID;
23
use const SEEK_CUR;
24
use const STREAM_FILTER_WRITE;
25
26
/**
27
 * A class to insert records into a CSV Document.
28
 */
29
class Writer extends AbstractCsv
30
{
31
    /**
32
     * callable collection to format the record before insertion.
33
     *
34
     * @var callable[]
35
     */
36
    protected $formatters = [];
37
38
    /**
39
     * callable collection to validate the record before insertion.
40
     *
41
     * @var callable[]
42
     */
43
    protected $validators = [];
44
45
    /**
46
     * newline character.
47
     *
48
     * @var string
49
     */
50
    protected $newline = "\n";
51
52
    /**
53
     * Insert records count for flushing.
54
     *
55
     * @var int
56
     */
57
    protected $flush_counter = 0;
58
59
    /**
60
     * Buffer flush threshold.
61
     *
62
     * @var int|null
63
     */
64
    protected $flush_threshold;
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    protected $stream_filter_mode = STREAM_FILTER_WRITE;
70
71
    /**
72
     * Regular expression used to detect if RFC4180 formatting is necessary.
73
     *
74
     * @var string
75
     */
76
    protected $rfc4180_regexp;
77
78
    /**
79
     * double enclosure for RFC4180 compliance.
80
     *
81
     * @var string
82
     */
83
    protected $rfc4180_enclosure;
84
85
    /**
86
     * {@inheritdoc}
87
     */
88 15
    protected function resetProperties(): void
89
    {
90 15
        parent::resetProperties();
91 15
        $characters = preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/');
92 15
        $this->rfc4180_regexp = '/[\s|'.$characters.']/x';
93 15
        $this->rfc4180_enclosure = $this->enclosure.$this->enclosure;
94 15
    }
95
96
    /**
97
     * Returns the current newline sequence characters.
98
     */
99 3
    public function getNewline(): string
100
    {
101 3
        return $this->newline;
102
    }
103
104
    /**
105
     * Get the flush threshold.
106
     *
107
     * @return int|null
108
     */
109 3
    public function getFlushThreshold()
110
    {
111 3
        return $this->flush_threshold;
112
    }
113
114
    /**
115
     * Adds multiple records to the CSV document.
116
     *
117
     * @see Writer::insertOne
118
     */
119 6
    public function insertAll(iterable $records): int
120
    {
121 6
        $bytes = 0;
122 6
        foreach ($records as $record) {
123 6
            $bytes += $this->insertOne($record);
124
        }
125
126 6
        $this->flush_counter = 0;
127 6
        $this->document->fflush();
128
129 6
        return $bytes;
130
    }
131
132
    /**
133
     * Adds a single record to a CSV document.
134
     *
135
     * A record is an array that can contains scalar types values, NULL values
136
     * or objects implementing the __toString method.
137
     *
138
     * @throws CannotInsertRecord If the record can not be inserted
139
     */
140 57
    public function insertOne(array $record): int
141
    {
142 57
        $method = 'addRecord';
143 57
        if (70400 > PHP_VERSION_ID && '' === $this->escape) {
144 20
            $method = 'addRFC4180CompliantRecord';
145
        }
146
147 57
        $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record);
148 57
        $this->validateRecord($record);
149 54
        $bytes = $this->$method($record);
150 52
        if (false === $bytes || 0 >= $bytes) {
151 4
            throw CannotInsertRecord::triggerOnInsertion($record);
152
        }
153
154 48
        return $bytes + $this->consolidate();
155
    }
156
157
    /**
158
     * Adds a single record to a CSV Document using PHP algorithm.
159
     *
160
     * @see https://php.net/manual/en/function.fputcsv.php
161
     *
162
     * @return int|false
163
     */
164 22
    protected function addRecord(array $record)
165
    {
166 22
        return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape);
167
    }
168
169
    /**
170
     * Adds a single record to a CSV Document using RFC4180 algorithm.
171
     *
172
     * @see https://php.net/manual/en/function.fputcsv.php
173
     * @see https://php.net/manual/en/function.fwrite.php
174
     * @see https://tools.ietf.org/html/rfc4180
175
     * @see http://edoceo.com/utilitas/csv-file-format
176
     *
177
     * String conversion is done without any check like fputcsv.
178
     *
179
     *     - Emits E_NOTICE on Array conversion (returns the 'Array' string)
180
     *     - Throws catchable fatal error on objects that can not be converted
181
     *     - Returns resource id without notice or error (returns 'Resource id #2')
182
     *     - Converts boolean true to '1', boolean false to the empty string
183
     *     - Converts null value to the empty string
184
     *
185
     * Fields must be delimited with enclosures if they contains :
186
     *
187
     *     - Embedded whitespaces
188
     *     - Embedded delimiters
189
     *     - Embedded line-breaks
190
     *     - Embedded enclosures.
191
     *
192
     * Embedded enclosures must be doubled.
193
     *
194
     * The LF character is added at the end of each record to mimic fputcsv behavior
195
     *
196
     * @return int|false
197
     */
198 20
    protected function addRFC4180CompliantRecord(array $record)
199
    {
200 20
        foreach ($record as &$field) {
201 20
            $field = (string) $field;
202 20
            if (1 === preg_match($this->rfc4180_regexp, $field)) {
203 20
                $field = $this->enclosure.str_replace($this->enclosure, $this->rfc4180_enclosure, $field).$this->enclosure;
204
            }
205
        }
206 20
        unset($field);
207
208 20
        return $this->document->fwrite(implode($this->delimiter, $record)."\n");
209
    }
210
211
    /**
212
     * Format a record.
213
     *
214
     * The returned array must contain
215
     *   - scalar types values,
216
     *   - NULL values,
217
     *   - or objects implementing the __toString() method.
218
     */
219 3
    protected function formatRecord(array $record, callable $formatter): array
220
    {
221 3
        return $formatter($record);
222
    }
223
224
    /**
225
     * Validate a record.
226
     *
227
     * @throws CannotInsertRecord If the validation failed
228
     */
229 12
    protected function validateRecord(array $record): void
230
    {
231 12
        foreach ($this->validators as $name => $validator) {
232 3
            if (true !== $validator($record)) {
233 3
                throw CannotInsertRecord::triggerOnValidation($name, $record);
234
            }
235
        }
236 9
    }
237
238
    /**
239
     * Apply post insertion actions.
240
     */
241 12
    protected function consolidate(): int
242
    {
243 12
        $bytes = 0;
244 12
        if ("\n" !== $this->newline) {
245 3
            $this->document->fseek(-1, SEEK_CUR);
246
            /** @var int $newlineBytes */
247 3
            $newlineBytes = $this->document->fwrite($this->newline, strlen($this->newline));
248 3
            $bytes =  $newlineBytes - 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
     * Set the flush threshold.
296
     *
297
     *
298
     * @param  ?int      $threshold
0 ignored issues
show
Documentation introduced by
The doc-type ?int could not be parsed: Unknown type name "?int" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
299
     * @throws Exception if the threshold is a integer lesser than 1
300
     */
301 9
    public function setFlushThreshold(?int $threshold): self
302
    {
303 9
        if ($threshold === $this->flush_threshold) {
304 3
            return $this;
305
        }
306
307 9
        if (null !== $threshold && 1 > $threshold) {
308 3
            throw new InvalidArgument(__METHOD__.'() expects 1 Argument to be null or a valid integer greater or equal to 1');
309
        }
310
311 9
        $this->flush_threshold = $threshold;
312 9
        $this->flush_counter = 0;
313 9
        $this->document->fflush();
314
315 9
        return $this;
316
    }
317
}
318