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

Writer::addRFC4180CompliantRecord()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 12
ccs 7
cts 7
cp 1
crap 3
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_reduce;
24
use function gettype;
25
use function implode;
26
use function is_iterable;
27
use function preg_match;
28
use function preg_quote;
29
use function sprintf;
30
use function str_replace;
31
use function strlen;
32
33
/**
34
 * A class to insert records into a CSV Document.
35
 *
36
 * @package League.csv
37
 * @since   4.0.0
38
 * @author  Ignace Nyamagana Butera <[email protected]>
39
 */
40
class Writer extends AbstractCsv
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 RFC4180 formatting is necessary.
84
     *
85
     * @var string
86
     */
87
    protected $rfc4180_regexp;
88
89
    /**
90
     * double enclosure for RFC4180 compliance.
91
     *
92
     * @var string
93
     */
94
    protected $rfc4180_enclosure;
95
96
    /**
97
     * {@inheritdoc}
98
     */
99 15
    protected function resetProperties()
100
    {
101 15
        parent::resetProperties();
102 15
        $characters = preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/');
103 15
        $this->rfc4180_regexp = '/[\s|'.$characters.']/x';
104 15
        $this->rfc4180_enclosure = $this->enclosure.$this->enclosure;
105 15
    }
106
107
    /**
108
     * Returns the current newline sequence characters.
109
     */
110 3
    public function getNewline(): string
111
    {
112 3
        return $this->newline;
113
    }
114
115
    /**
116
     * Get the flush threshold.
117
     *
118
     * @return int|null
119
     */
120 3
    public function getFlushThreshold()
121
    {
122 3
        return $this->flush_threshold;
123
    }
124
125
    /**
126
     * Adds multiple records to the CSV document.
127
     *
128
     * @see Writer::insertOne
129
     *
130
     * @param Traversable|array $records
131
     */
132 9
    public function insertAll($records): int
133
    {
134 9
        if (!is_iterable($records)) {
135 3
            throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records)));
136
        }
137
138 6
        $bytes = 0;
139 6
        foreach ($records as $record) {
140 6
            $bytes += $this->insertOne($record);
141
        }
142
143 6
        $this->flush_counter = 0;
144 6
        $this->document->fflush();
145
146 6
        return $bytes;
147
    }
148
149
    /**
150
     * Adds a single record to a CSV document.
151
     *
152
     * A record is an array that can contains scalar types values, NULL values
153
     * or objects implementing the __toString method.
154
     *
155
     * @throws CannotInsertRecord If the record can not be inserted
156
     */
157 54
    public function insertOne(array $record): int
158
    {
159 54
        $method = 'addRecord';
160 54
        if ('' === $this->escape && PHP_VERSION_ID < 70400) {
161 27
            $method = 'addRFC4180CompliantRecord';
162
        }
163
164 54
        $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record);
165 54
        $this->validateRecord($record);
166 51
        $bytes = $this->$method($record);
167 51
        if (false !== $bytes && 0 !== $bytes) {
168 45
            return $bytes + $this->consolidate();
169
        }
170
171 6
        throw CannotInsertRecord::triggerOnInsertion($record);
172
    }
173
174
    /**
175
     * Adds a single record to a CSV Document using PHP algorithm.
176
     *
177
     * @see https://php.net/manual/en/function.fputcsv.php
178
     *
179
     * @return int|bool
180
     */
181 12
    protected function addRecord(array $record)
182
    {
183 12
        return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape);
184
    }
185
186
    /**
187
     * Adds a single record to a CSV Document using RFC4180 algorithm.
188
     *
189
     * @see https://php.net/manual/en/function.fputcsv.php
190
     * @see https://php.net/manual/en/function.fwrite.php
191
     * @see https://tools.ietf.org/html/rfc4180
192
     * @see http://edoceo.com/utilitas/csv-file-format
193
     *
194
     * String conversion is done without any check like fputcsv.
195
     *
196
     *     - Emits E_NOTICE on Array conversion (returns the 'Array' string)
197
     *     - Throws catchable fatal error on objects that can not be converted
198
     *     - Returns resource id without notice or error (returns 'Resource id #2')
199
     *     - Converts boolean true to '1', boolean false to the empty string
200
     *     - Converts null value to the empty string
201
     *
202
     * Fields must be delimited with enclosures if they contains :
203
     *
204
     *     - Embedded whitespaces
205
     *     - Embedded delimiters
206
     *     - Embedded line-breaks
207
     *     - Embedded enclosures.
208
     *
209
     * Embedded enclosures must be doubled.
210
     *
211
     * The LF character is added at the end of each record to mimic fputcsv behavior
212
     *
213
     * @return int|bool
214
     */
215 27
    protected function addRFC4180CompliantRecord(array $record)
216
    {
217 27
        foreach ($record as &$field) {
218 27
            $field = (string) $field;
219 27
            if (preg_match($this->rfc4180_regexp, $field)) {
220 27
                $field = $this->enclosure.str_replace($this->enclosure, $this->rfc4180_enclosure, $field).$this->enclosure;
221
            }
222
        }
223 27
        unset($field);
224
225 27
        return $this->document->fwrite(implode($this->delimiter, $record)."\n");
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
     * Set the flush threshold.
311
     *
312
     * @param int|null $threshold
313
     *
314
     * @throws Exception if the threshold is a integer lesser than 1
315
     */
316 12
    public function setFlushThreshold($threshold): self
317
    {
318 12
        if ($threshold === $this->flush_threshold) {
319 3
            return $this;
320
        }
321
322 12
        if (!is_nullable_int($threshold)) {
323 3
            throw new TypeError(sprintf(__METHOD__.'() expects 1 Argument to be null or an integer %s given', gettype($threshold)));
324
        }
325
326 9
        if (null !== $threshold && 1 > $threshold) {
327 3
            throw new Exception(__METHOD__.'() expects 1 Argument to be null or a valid integer greater or equal to 1');
328
        }
329
330 9
        $this->flush_threshold = $threshold;
331 9
        $this->flush_counter = 0;
332 9
        $this->document->fflush();
333
334 9
        return $this;
335
    }
336
}
337