1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Box\Spout\Writer; |
4
|
|
|
|
5
|
|
|
use Box\Spout\Common\Exception\InvalidArgumentException; |
6
|
|
|
use Box\Spout\Common\Exception\IOException; |
7
|
|
|
use Box\Spout\Common\Exception\SpoutException; |
8
|
|
|
use Box\Spout\Common\Helper\FileSystemHelper; |
9
|
|
|
use Box\Spout\Common\Helper\GlobalFunctionsHelper; |
10
|
|
|
use Box\Spout\Writer\Common\Entity\Cell; |
11
|
|
|
use Box\Spout\Writer\Common\Entity\Options; |
12
|
|
|
use Box\Spout\Writer\Common\Entity\Row; |
13
|
|
|
use Box\Spout\Writer\Common\Entity\Style\Style; |
14
|
|
|
use Box\Spout\Writer\Common\Manager\OptionsManagerInterface; |
15
|
|
|
use Box\Spout\Writer\Common\Manager\Style\StyleMerger; |
16
|
|
|
use Box\Spout\Writer\Exception\WriterAlreadyOpenedException; |
17
|
|
|
use Box\Spout\Writer\Exception\WriterNotOpenedException; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Class WriterAbstract |
21
|
|
|
* |
22
|
|
|
* @package Box\Spout\Writer |
23
|
|
|
* @abstract |
24
|
|
|
*/ |
25
|
|
|
abstract class WriterAbstract implements WriterInterface |
26
|
|
|
{ |
27
|
|
|
/** @var string Path to the output file */ |
28
|
|
|
protected $outputFilePath; |
29
|
|
|
|
30
|
|
|
/** @var resource Pointer to the file/stream we will write to */ |
31
|
|
|
protected $filePointer; |
32
|
|
|
|
33
|
|
|
/** @var bool Indicates whether the writer has been opened or not */ |
34
|
|
|
protected $isWriterOpened = false; |
35
|
|
|
|
36
|
|
|
/** @var GlobalFunctionsHelper Helper to work with global functions */ |
37
|
|
|
protected $globalFunctionsHelper; |
38
|
|
|
|
39
|
|
|
/** @var OptionsManagerInterface Writer options manager */ |
40
|
|
|
protected $optionsManager; |
41
|
|
|
|
42
|
|
|
/** @var StyleMerger Helps merge styles together */ |
43
|
|
|
protected $styleMerger; |
44
|
|
|
|
45
|
|
|
/** @var string Content-Type value for the header - to be defined by child class */ |
46
|
|
|
protected static $headerContentType; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @param OptionsManagerInterface $optionsManager |
50
|
|
|
* @param StyleMerger $styleMerger |
51
|
|
|
* @param GlobalFunctionsHelper $globalFunctionsHelper |
52
|
|
|
*/ |
53
|
112 |
|
public function __construct( |
54
|
|
|
OptionsManagerInterface $optionsManager, |
55
|
|
|
StyleMerger $styleMerger, |
56
|
|
|
GlobalFunctionsHelper $globalFunctionsHelper) |
57
|
|
|
{ |
58
|
112 |
|
$this->optionsManager = $optionsManager; |
59
|
112 |
|
$this->styleMerger = $styleMerger; |
60
|
112 |
|
$this->globalFunctionsHelper = $globalFunctionsHelper; |
61
|
112 |
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Opens the streamer and makes it ready to accept data. |
65
|
|
|
* |
66
|
|
|
* @return void |
67
|
|
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened |
68
|
|
|
*/ |
69
|
|
|
abstract protected function openWriter(); |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Adds a row to the currently opened writer. |
73
|
|
|
* |
74
|
|
|
* @param Row $row The row containing cells and styles |
75
|
|
|
* @return void |
76
|
|
|
*/ |
77
|
|
|
abstract protected function addRowToWriter(Row $row); |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Closes the streamer, preventing any additional writing. |
81
|
|
|
* |
82
|
|
|
* @return void |
83
|
|
|
*/ |
84
|
|
|
abstract protected function closeWriter(); |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Sets the default styles for all rows added with "addRow". |
88
|
|
|
* Overriding the default style instead of using "addRowWithStyle" improves performance by 20%. |
89
|
|
|
* @see https://github.com/box/spout/issues/272 |
90
|
|
|
* |
91
|
|
|
* @param Style $defaultStyle |
92
|
|
|
* @return WriterAbstract |
93
|
|
|
*/ |
94
|
2 |
|
public function setDefaultRowStyle($defaultStyle) |
95
|
|
|
{ |
96
|
2 |
|
$this->optionsManager->setOption(Options::DEFAULT_ROW_STYLE, $defaultStyle); |
97
|
2 |
|
return $this; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Inits the writer and opens it to accept data. |
102
|
|
|
* By using this method, the data will be written to a file. |
103
|
|
|
* |
104
|
|
|
* @api |
105
|
|
|
* @param string $outputFilePath Path of the output file that will contain the data |
106
|
|
|
* @return WriterAbstract |
107
|
|
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable |
108
|
|
|
*/ |
109
|
101 |
|
public function openToFile($outputFilePath) |
110
|
|
|
{ |
111
|
101 |
|
$this->outputFilePath = $outputFilePath; |
112
|
|
|
|
113
|
101 |
|
$this->filePointer = $this->globalFunctionsHelper->fopen($this->outputFilePath, 'wb+'); |
114
|
101 |
|
$this->throwIfFilePointerIsNotAvailable(); |
115
|
|
|
|
116
|
98 |
|
$this->openWriter(); |
117
|
98 |
|
$this->isWriterOpened = true; |
118
|
|
|
|
119
|
98 |
|
return $this; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Inits the writer and opens it to accept data. |
124
|
|
|
* By using this method, the data will be outputted directly to the browser. |
125
|
|
|
* |
126
|
|
|
* @codeCoverageIgnore |
127
|
|
|
* |
128
|
|
|
* @api |
129
|
|
|
* @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept |
130
|
|
|
* @return WriterAbstract |
131
|
|
|
* @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened |
132
|
|
|
*/ |
133
|
|
|
public function openToBrowser($outputFileName) |
134
|
|
|
{ |
135
|
|
|
$this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName); |
136
|
|
|
|
137
|
|
|
$this->filePointer = $this->globalFunctionsHelper->fopen('php://output', 'w'); |
138
|
|
|
$this->throwIfFilePointerIsNotAvailable(); |
139
|
|
|
|
140
|
|
|
// Clear any previous output (otherwise the generated file will be corrupted) |
141
|
|
|
// @see https://github.com/box/spout/issues/241 |
142
|
|
|
$this->globalFunctionsHelper->ob_end_clean(); |
143
|
|
|
|
144
|
|
|
// Set headers |
145
|
|
|
$this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType); |
146
|
|
|
$this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"'); |
147
|
|
|
|
148
|
|
|
/* |
149
|
|
|
* When forcing the download of a file over SSL,IE8 and lower browsers fail |
150
|
|
|
* if the Cache-Control and Pragma headers are not set. |
151
|
|
|
* |
152
|
|
|
* @see http://support.microsoft.com/KB/323308 |
153
|
|
|
* @see https://github.com/liuggio/ExcelBundle/issues/45 |
154
|
|
|
*/ |
155
|
|
|
$this->globalFunctionsHelper->header('Cache-Control: max-age=0'); |
156
|
|
|
$this->globalFunctionsHelper->header('Pragma: public'); |
157
|
|
|
|
158
|
|
|
$this->openWriter(); |
159
|
|
|
$this->isWriterOpened = true; |
160
|
|
|
|
161
|
|
|
return $this; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Checks if the pointer to the file/stream to write to is available. |
166
|
|
|
* Will throw an exception if not available. |
167
|
|
|
* |
168
|
|
|
* @return void |
169
|
|
|
* @throws \Box\Spout\Common\Exception\IOException If the pointer is not available |
170
|
|
|
*/ |
171
|
101 |
|
protected function throwIfFilePointerIsNotAvailable() |
172
|
|
|
{ |
173
|
101 |
|
if (!$this->filePointer) { |
174
|
3 |
|
throw new IOException('File pointer has not be opened'); |
175
|
|
|
} |
176
|
98 |
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Checks if the writer has already been opened, since some actions must be done before it gets opened. |
180
|
|
|
* Throws an exception if already opened. |
181
|
|
|
* |
182
|
|
|
* @param string $message Error message |
183
|
|
|
* @return void |
184
|
|
|
* @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be. |
185
|
|
|
*/ |
186
|
53 |
|
protected function throwIfWriterAlreadyOpened($message) |
187
|
|
|
{ |
188
|
53 |
|
if ($this->isWriterOpened) { |
189
|
5 |
|
throw new WriterAlreadyOpenedException($message); |
190
|
|
|
} |
191
|
48 |
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Write given data to the output. New data will be appended to end of stream. |
195
|
|
|
* |
196
|
|
|
* @param array|\Box\Spout\Writer\Common\Entity\Row $row The row to be appended to the stream |
197
|
|
|
* @return WriterInterface |
198
|
|
|
* @internal param array $row Array containing data to be streamed. |
199
|
|
|
* Example $row= ['data1', 1234, null, '', 'data5']; |
200
|
|
|
* @internal param \Box\Spout\Writer\Common\Entity\Row $row A Row object with cells and styles |
201
|
|
|
* Example $row = (new Row())->addCell('data1'); |
202
|
|
|
* |
203
|
|
|
* @throws SpoutException If anything else goes wrong while writing data |
204
|
|
|
* @throws WriterNotOpenedException If this function is called before opening the writer |
205
|
|
|
* |
206
|
|
|
* @api |
207
|
|
|
*/ |
208
|
82 |
|
public function addRow($row) |
209
|
|
|
{ |
210
|
82 |
|
if (!is_array($row) && !$row instanceof Row) { |
211
|
|
|
throw new InvalidArgumentException('addRow accepts an array with scalar values or a Row object'); |
212
|
|
|
} |
213
|
|
|
|
214
|
82 |
|
if (is_array($row) && !empty($row)) { |
215
|
62 |
|
$row = $this->createRowFromArray($row, null); |
216
|
|
|
} |
217
|
|
|
|
218
|
82 |
|
if ($this->isWriterOpened) { |
219
|
72 |
|
if (!empty($row)) { |
220
|
|
|
try { |
221
|
72 |
|
$this->applyDefaultRowStyle($row); |
|
|
|
|
222
|
72 |
|
$this->addRowToWriter($row); |
|
|
|
|
223
|
5 |
|
} catch (SpoutException $e) { |
224
|
|
|
// if an exception occurs while writing data, |
225
|
|
|
// close the writer and remove all files created so far. |
226
|
5 |
|
$this->closeAndAttemptToCleanupAllFiles(); |
227
|
|
|
// re-throw the exception to alert developers of the error |
228
|
5 |
|
throw $e; |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
} else { |
232
|
10 |
|
throw new WriterNotOpenedException('The writer needs to be opened before adding row.'); |
233
|
|
|
} |
234
|
69 |
|
return $this; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @inheritdoc |
239
|
|
|
* |
240
|
|
|
* @api |
241
|
|
|
*/ |
242
|
|
|
public function withRow(\Closure $callback) |
243
|
|
|
{ |
244
|
|
|
return $this->addRow($callback(new Row())); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* Write given data to the output and apply the given style. |
249
|
|
|
* @see addRow |
250
|
|
|
* |
251
|
|
|
* @param array|\Box\Spout\Writer\Common\Entity\Row $row The row to be appended to the stream |
252
|
|
|
* @param Style $style Style to be applied to the row. |
253
|
|
|
* @return WriterInterface |
254
|
|
|
* @internal param array $row Array containing data to be streamed. |
255
|
|
|
* Example $row= ['data1', 1234, null, '', 'data5']; |
256
|
|
|
* @internal param \Box\Spout\Writer\Common\Entity\Row $row A Row object with cells and styles |
257
|
|
|
* Example $row = (new Row())->addCell('data1'); |
258
|
|
|
* @api |
259
|
|
|
* @throws InvalidArgumentException If the input param is not valid |
260
|
|
|
*/ |
261
|
19 |
|
public function addRowWithStyle($row, $style) |
262
|
|
|
{ |
263
|
19 |
|
if (!is_array($row) && !$row instanceof Row) { |
264
|
|
|
throw new InvalidArgumentException('addRowWithStyle accepts an array with scalar values or a Row object'); |
265
|
|
|
} |
266
|
|
|
|
267
|
19 |
|
if (!$style instanceof Style) { |
268
|
6 |
|
throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.'); |
269
|
|
|
} |
270
|
|
|
|
271
|
13 |
|
if (is_array($row)) { |
272
|
13 |
|
$row = $this->createRowFromArray($row, $style); |
273
|
|
|
} |
274
|
|
|
|
275
|
13 |
|
$this->addRow($row); |
276
|
9 |
|
return $this; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* @param array $dataRows |
281
|
|
|
* @param Style|null $style |
282
|
|
|
* @return Row |
283
|
|
|
*/ |
284
|
|
|
protected function createRowFromArray(array $dataRows, Style $style = null) |
285
|
|
|
{ |
286
|
82 |
|
$row = (new Row())->setCells(array_map(function ($value) { |
287
|
82 |
|
if ($value instanceof Cell) { |
288
|
5 |
|
return $value; |
289
|
|
|
} |
290
|
77 |
|
return new Cell($value); |
291
|
82 |
|
}, $dataRows)); |
292
|
|
|
|
293
|
82 |
|
if ($style !== null) { |
294
|
23 |
|
$row->setStyle($style); |
295
|
|
|
} |
296
|
|
|
|
297
|
82 |
|
return $row; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Write given data to the output. New data will be appended to end of stream. |
302
|
|
|
* |
303
|
|
|
* @api |
304
|
|
|
* @param array $dataRows Array of array containing data to be streamed. |
305
|
|
|
* If a row is empty, it won't be added (i.e. not even as a blank row) |
306
|
|
|
* Example: $dataRows = [ |
307
|
|
|
* ['data11', 12, , '', 'data13'], |
308
|
|
|
* ['data21', 'data22', null, false], |
309
|
|
|
* ]; |
310
|
|
|
* @return WriterAbstract |
311
|
|
|
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid |
312
|
|
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer |
313
|
|
|
* @throws \Box\Spout\Common\Exception\IOException If unable to write data |
314
|
|
|
*/ |
315
|
51 |
|
public function addRows(array $dataRows) |
316
|
|
|
{ |
317
|
51 |
|
if (!empty($dataRows)) { |
318
|
51 |
|
$firstRow = reset($dataRows); |
319
|
51 |
|
if (!is_array($firstRow) && !$firstRow instanceof Row) { |
320
|
1 |
|
throw new InvalidArgumentException('The input should be an array of arrays or row objects'); |
321
|
|
|
} |
322
|
50 |
|
foreach ($dataRows as $dataRow) { |
323
|
50 |
|
$this->addRow($dataRow); |
324
|
|
|
} |
325
|
|
|
} |
326
|
42 |
|
return $this; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* Write given data to the output and apply the given style. |
331
|
|
|
* @see addRows |
332
|
|
|
* |
333
|
|
|
* @api |
334
|
|
|
* @param array $dataRows Array of array containing data to be streamed. |
335
|
|
|
* @param Style $style Style to be applied to the rows. |
336
|
|
|
* @return WriterAbstract |
337
|
|
|
* @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid |
338
|
|
|
* @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer |
339
|
|
|
* @throws \Box\Spout\Common\Exception\IOException If unable to write data |
340
|
|
|
*/ |
341
|
16 |
|
public function addRowsWithStyle(array $dataRows, $style) |
342
|
|
|
{ |
343
|
16 |
|
if (!$style instanceof Style) { |
344
|
6 |
|
throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.'); |
345
|
|
|
} |
346
|
|
|
|
347
|
10 |
|
foreach($dataRows as $row) { |
348
|
|
|
|
349
|
10 |
|
if (is_array($row)) { |
350
|
10 |
|
$row = $this->createRowFromArray($row, $style); |
351
|
|
|
} elseif ($row instanceof Row) { |
352
|
|
|
$row->setStyle($style); |
353
|
|
|
} else { |
354
|
|
|
throw new InvalidArgumentException(); |
355
|
|
|
} |
356
|
|
|
|
357
|
10 |
|
$this->addRow($row); |
358
|
|
|
} |
359
|
|
|
|
360
|
10 |
|
return $this; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* @TODO: Move this into styleMerger |
365
|
|
|
* |
366
|
|
|
* @param Row $row |
367
|
|
|
* @return $this |
368
|
|
|
*/ |
369
|
72 |
|
private function applyDefaultRowStyle(Row $row) |
370
|
|
|
{ |
371
|
72 |
|
$defaultRowStyle = $this->optionsManager->getOption(Options::DEFAULT_ROW_STYLE); |
372
|
72 |
|
if (null === $defaultRowStyle) { |
373
|
8 |
|
return $this; |
374
|
|
|
} |
375
|
64 |
|
$mergedStyle = $this->styleMerger->merge($row->getStyle(), $defaultRowStyle); |
|
|
|
|
376
|
64 |
|
$row->setStyle($mergedStyle); |
377
|
64 |
|
} |
378
|
|
|
|
379
|
|
|
/** |
380
|
|
|
* Closes the writer. This will close the streamer as well, preventing new data |
381
|
|
|
* to be written to the file. |
382
|
|
|
* |
383
|
|
|
* @api |
384
|
|
|
* @return void |
385
|
|
|
*/ |
386
|
79 |
|
public function close() |
387
|
|
|
{ |
388
|
79 |
|
if (!$this->isWriterOpened) { |
389
|
3 |
|
return; |
390
|
|
|
} |
391
|
|
|
|
392
|
79 |
|
$this->closeWriter(); |
393
|
|
|
|
394
|
79 |
|
if (is_resource($this->filePointer)) { |
395
|
79 |
|
$this->globalFunctionsHelper->fclose($this->filePointer); |
396
|
|
|
} |
397
|
|
|
|
398
|
79 |
|
$this->isWriterOpened = false; |
399
|
79 |
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* Closes the writer and attempts to cleanup all files that were |
403
|
|
|
* created during the writing process (temp files & final file). |
404
|
|
|
* |
405
|
|
|
* @return void |
406
|
|
|
*/ |
407
|
5 |
|
private function closeAndAttemptToCleanupAllFiles() |
408
|
|
|
{ |
409
|
|
|
// close the writer, which should remove all temp files |
410
|
5 |
|
$this->close(); |
411
|
|
|
|
412
|
|
|
// remove output file if it was created |
413
|
5 |
|
if ($this->globalFunctionsHelper->file_exists($this->outputFilePath)) { |
414
|
5 |
|
$outputFolderPath = dirname($this->outputFilePath); |
415
|
5 |
|
$fileSystemHelper = new FileSystemHelper($outputFolderPath); |
416
|
5 |
|
$fileSystemHelper->deleteFile($this->outputFilePath); |
417
|
|
|
} |
418
|
5 |
|
} |
419
|
|
|
} |
420
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.