Completed
Pull Request — master (#309)
by ignace nyamagana
02:22
created

Writer   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 301
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 0
loc 301
ccs 78
cts 78
cp 1
rs 9.92
c 0
b 0
f 0
wmc 31
lcom 1
cbo 3

14 Methods

Rating   Name   Duplication   Size   Complexity  
A resetProperties() 0 11 1
A getNewline() 0 4 1
A getFlushThreshold() 0 4 1
A insertAll() 0 16 3
A insertOne() 0 16 5
A addRecord() 0 4 1
A addRFC4180CompliantRecord() 0 12 3
A formatRecord() 0 4 1
A validateRecord() 0 8 3
A consolidate() 0 20 4
A addFormatter() 0 6 1
A addValidator() 0 6 1
A setNewline() 0 6 1
A setFlushThreshold() 0 20 5
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 sprintf;
28
use function str_replace;
29
use function strlen;
30
31
/**
32
 * A class to insert records into a CSV Document.
33
 *
34
 * @package League.csv
35
 * @since   4.0.0
36
 * @author  Ignace Nyamagana Butera <[email protected]>
37
 */
38
class Writer extends AbstractCsv
39
{
40
    /**
41
     * callable collection to format the record before insertion.
42
     *
43
     * @var callable[]
44
     */
45
    protected $formatters = [];
46
47
    /**
48
     * callable collection to validate the record before insertion.
49
     *
50
     * @var callable[]
51
     */
52
    protected $validators = [];
53
54
    /**
55
     * newline character.
56
     *
57
     * @var string
58
     */
59
    protected $newline = "\n";
60
61
    /**
62
     * Insert records count for flushing.
63
     *
64
     * @var int
65
     */
66
    protected $flush_counter = 0;
67
68
    /**
69
     * Buffer flush threshold.
70
     *
71
     * @var int|null
72
     */
73
    protected $flush_threshold;
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    protected $stream_filter_mode = STREAM_FILTER_WRITE;
79
80
    /**
81
     * Regular expression used to detect if RFC4180 formatting is necessary.
82
     *
83
     * @var string
84
     */
85
    protected $rfc4180_regexp;
86
87
    /**
88
     * double enclosure for RFC4180 compliance.
89
     *
90
     * @var string
91
     */
92
    protected $rfc4180_enclosure;
93
94
    /**
95
     * {@inheritdoc}
96
     */
97 15
    protected function resetProperties()
98
    {
99 15
        parent::resetProperties();
100 15
        $characters = "\n|\r".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/');
101 15
        $this->rfc4180_regexp = '/
102
            ^(\ +)                # leading whitespaces
103 15
            |(['.$characters.'])  # delimiter, enclosure, line-breaks characters
104
            |(\ +)$               # trailing whitespaces
105
        /x';
106 15
        $this->rfc4180_enclosure = $this->enclosure.$this->enclosure;
107 15
    }
108
109
    /**
110
     * Returns the current newline sequence characters.
111
     */
112 3
    public function getNewline(): string
113
    {
114 3
        return $this->newline;
115
    }
116
117
    /**
118
     * Get the flush threshold.
119
     *
120
     * @return int|null
121
     */
122 3
    public function getFlushThreshold()
123
    {
124 3
        return $this->flush_threshold;
125
    }
126
127
    /**
128
     * Adds multiple records to the CSV document.
129
     *
130
     * @see Writer::insertOne
131
     *
132
     * @param Traversable|array $records
133
     */
134 9
    public function insertAll($records): int
135
    {
136 9
        if (!is_iterable($records)) {
137 3
            throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records)));
138
        }
139
140 6
        $bytes = 0;
141 6
        foreach ($records as $record) {
142 6
            $bytes += $this->insertOne($record);
143
        }
144
145 6
        $this->flush_counter = 0;
146 6
        $this->document->fflush();
147
148 6
        return $bytes;
149
    }
150
151
    /**
152
     * Adds a single record to a CSV document.
153
     *
154
     * A record is an array that can contains scalar types values, NULL values
155
     * or objects implementing the __toString method.
156
     *
157
     * @throws CannotInsertRecord If the record can not be inserted
158
     */
159 54
    public function insertOne(array $record): int
160
    {
161 54
        $method = 'addRecord';
162 54
        if ('' === $this->escape && !static::$has_native_support_for_empty_string_escape_char) {
163 27
            $method = 'addRFC4180CompliantRecord';
164
        }
165
166 54
        $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record);
167 54
        $this->validateRecord($record);
168 51
        $bytes = $this->$method($record);
169 51
        if (false !== $bytes && 0 !== $bytes) {
170 45
            return $bytes + $this->consolidate();
171
        }
172
173 6
        throw CannotInsertRecord::triggerOnInsertion($record);
174
    }
175
176
    /**
177
     * Adds a single record to a CSV Document using PHP algorithm.
178
     *
179
     * @see https://php.net/manual/en/function.fputcsv.php
180
     *
181
     * @return int|bool
182
     */
183 12
    protected function addRecord(array $record)
184
    {
185 12
        return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape);
186
    }
187
188
    /**
189
     * Adds a single record to a CSV Document using RFC4180 algorithm.
190
     *
191
     * @see https://php.net/manual/en/function.fputcsv.php
192
     * @see https://php.net/manual/en/function.fwrite.php
193
     * @see https://tools.ietf.org/html/rfc4180
194
     * @see http://edoceo.com/utilitas/csv-file-format
195
     *
196
     * String conversion is done without any check like fputcsv.
197
     *
198
     *     - Emits E_NOTICE on Array conversion (returns the 'Array' string)
199
     *     - Throws catchable fatal error on objects that can not be converted
200
     *     - Returns resource id without notice or error (returns 'Resource id #2')
201
     *     - Converts boolean true to '1', boolean false to the empty string
202
     *     - Converts null value to the empty string
203
     *
204
     * Fields must be delimited with enclosures if they contains :
205
     *
206
     *     - Leading or trailing whitespaces
207
     *     - Embedded delimiters
208
     *     - Embedded line-breaks
209
     *     - Embedded enclosures.
210
     *
211
     * Embedded enclosures must be doubled.
212
     *
213
     * The LF character is added at the end of each record to mimic fputcsv behavior
214
     *
215
     * @return int|bool
216
     */
217 27
    protected function addRFC4180CompliantRecord(array $record)
218
    {
219 27
        foreach ($record as &$field) {
220 27
            $field = (string) $field;
221 27
            if (preg_match($this->rfc4180_regexp, $field)) {
222 27
                $field = $this->enclosure.str_replace($this->enclosure, $this->rfc4180_enclosure, $field).$this->enclosure;
223
            }
224
        }
225 27
        unset($field);
226
227 27
        return $this->document->fwrite(implode($this->delimiter, $record)."\n");
228
    }
229
230
    /**
231
     * Format a record.
232
     *
233
     * The returned array must contain
234
     *   - scalar types values,
235
     *   - NULL values,
236
     *   - or objects implementing the __toString() method.
237
     */
238 3
    protected function formatRecord(array $record, callable $formatter): array
239
    {
240 3
        return $formatter($record);
241
    }
242
243
    /**
244
     * Validate a record.
245
     *
246
     * @throws CannotInsertRecord If the validation failed
247
     */
248 12
    protected function validateRecord(array $record)
249
    {
250 12
        foreach ($this->validators as $name => $validator) {
251 3
            if (true !== $validator($record)) {
252 3
                throw CannotInsertRecord::triggerOnValidation($name, $record);
253
            }
254
        }
255 9
    }
256
257
    /**
258
     * Apply post insertion actions.
259
     */
260 12
    protected function consolidate(): int
261
    {
262 12
        $bytes = 0;
263 12
        if ("\n" !== $this->newline) {
264 3
            $this->document->fseek(-1, SEEK_CUR);
265 3
            $bytes = $this->document->fwrite($this->newline, strlen($this->newline)) - 1;
266
        }
267
268 12
        if (null === $this->flush_threshold) {
269 9
            return $bytes;
270
        }
271
272 3
        ++$this->flush_counter;
273 3
        if (0 === $this->flush_counter % $this->flush_threshold) {
274 3
            $this->flush_counter = 0;
275 3
            $this->document->fflush();
276
        }
277
278 3
        return $bytes;
279
    }
280
281
    /**
282
     * Adds a record formatter.
283
     */
284 3
    public function addFormatter(callable $formatter): self
285
    {
286 3
        $this->formatters[] = $formatter;
287
288 3
        return $this;
289
    }
290
291
    /**
292
     * Adds a record validator.
293
     */
294 3
    public function addValidator(callable $validator, string $validator_name): self
295
    {
296 3
        $this->validators[$validator_name] = $validator;
297
298 3
        return $this;
299
    }
300
301
    /**
302
     * Sets the newline sequence.
303
     */
304 3
    public function setNewline(string $newline): self
305
    {
306 3
        $this->newline = $newline;
307
308 3
        return $this;
309
    }
310
311
    /**
312
     * Set the flush threshold.
313
     *
314
     * @param int|null $threshold
315
     *
316
     * @throws Exception if the threshold is a integer lesser than 1
317
     */
318 12
    public function setFlushThreshold($threshold): self
319
    {
320 12
        if ($threshold === $this->flush_threshold) {
321 3
            return $this;
322
        }
323
324 12
        if (!is_nullable_int($threshold)) {
325 3
            throw new TypeError(sprintf(__METHOD__.'() expects 1 Argument to be null or an integer %s given', gettype($threshold)));
326
        }
327
328 9
        if (null !== $threshold && 1 > $threshold) {
329 3
            throw new Exception(__METHOD__.'() expects 1 Argument to be null or a valid integer greater or equal to 1');
330
        }
331
332 9
        $this->flush_threshold = $threshold;
333 9
        $this->flush_counter = 0;
334 9
        $this->document->fflush();
335
336 9
        return $this;
337
    }
338
}
339