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