Completed
Pull Request — develop_3.0 (#434)
by Hura
04:17
created

WriterAbstract::addRows()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 8
cts 8
cp 1
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 8
nc 3
nop 1
crap 5
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\StyleManager;
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 StyleManager Style manager */
43
    protected $styleManager;
44
45
    /** @var string Content-Type value for the header - to be defined by child class */
46
    protected static $headerContentType;
47
48
    /**
49
     * Opens the streamer and makes it ready to accept data.
50
     *
51
     * @return void
52
     * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
53
     */
54
    abstract protected function openWriter();
55
56
    /**
57
     * Adds a row to the currently opened writer.
58
     *
59
     * @param Row $row The row containing cells and styles
60
     * @return void
61
     */
62
    abstract protected function addRowToWriter(Row $row);
63
64
    /**
65
     * Closes the streamer, preventing any additional writing.
66
     *
67
     * @return void
68
     */
69
    abstract protected function closeWriter();
70
71
    /**
72
     * @param OptionsManagerInterface $optionsManager
73
     * @param StyleManager $styleManager
74
     */
75 112
    public function __construct(OptionsManagerInterface $optionsManager, StyleManager $styleManager)
76
    {
77 112
        $this->optionsManager = $optionsManager;
78 112
        $this->styleManager = $styleManager;
79 112
    }
80
81
    /**
82
     * Sets the default styles for all rows added with "addRow".
83
     * Overriding the default style instead of using "addRowWithStyle" improves performance by 20%.
84
     * @see https://github.com/box/spout/issues/272
85
     *
86
     * @param Style $defaultStyle
87
     * @return WriterAbstract
88
     */
89 2
    public function setDefaultRowStyle($defaultStyle)
90
    {
91 2
        $this->optionsManager->setOption(Options::DEFAULT_ROW_STYLE, $defaultStyle);
92 2
        return $this;
93
    }
94
95
    /**
96
     * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
97
     * @return WriterAbstract
98
     */
99 112
    public function setGlobalFunctionsHelper($globalFunctionsHelper)
100
    {
101 112
        $this->globalFunctionsHelper = $globalFunctionsHelper;
102 112
        return $this;
103
    }
104
105
    /**
106
     * Inits the writer and opens it to accept data.
107
     * By using this method, the data will be written to a file.
108
     *
109
     * @api
110
     * @param  string $outputFilePath Path of the output file that will contain the data
111
     * @return WriterAbstract
112
     * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
113
     */
114 101
    public function openToFile($outputFilePath)
115
    {
116 101
        $this->outputFilePath = $outputFilePath;
117
118 101
        $this->filePointer = $this->globalFunctionsHelper->fopen($this->outputFilePath, 'wb+');
119 101
        $this->throwIfFilePointerIsNotAvailable();
120
121 98
        $this->openWriter();
122 98
        $this->isWriterOpened = true;
123
124 98
        return $this;
125
    }
126
127
    /**
128
     * Inits the writer and opens it to accept data.
129
     * By using this method, the data will be outputted directly to the browser.
130
     *
131
     * @codeCoverageIgnore
132
     *
133
     * @api
134
     * @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
135
     * @return WriterAbstract
136
     * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
137
     */
138
    public function openToBrowser($outputFileName)
139
    {
140
        $this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName);
141
142
        $this->filePointer = $this->globalFunctionsHelper->fopen('php://output', 'w');
143
        $this->throwIfFilePointerIsNotAvailable();
144
145
        // Clear any previous output (otherwise the generated file will be corrupted)
146
        // @see https://github.com/box/spout/issues/241
147
        $this->globalFunctionsHelper->ob_end_clean();
148
149
        // Set headers
150
        $this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType);
151
        $this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"');
152
153
        /*
154
         * When forcing the download of a file over SSL,IE8 and lower browsers fail
155
         * if the Cache-Control and Pragma headers are not set.
156
         *
157
         * @see http://support.microsoft.com/KB/323308
158
         * @see https://github.com/liuggio/ExcelBundle/issues/45
159
         */
160
        $this->globalFunctionsHelper->header('Cache-Control: max-age=0');
161
        $this->globalFunctionsHelper->header('Pragma: public');
162
163
        $this->openWriter();
164
        $this->isWriterOpened = true;
165
166
        return $this;
167
    }
168
169
    /**
170
     * Checks if the pointer to the file/stream to write to is available.
171
     * Will throw an exception if not available.
172
     *
173
     * @return void
174
     * @throws \Box\Spout\Common\Exception\IOException If the pointer is not available
175
     */
176 101
    protected function throwIfFilePointerIsNotAvailable()
177
    {
178 101
        if (!$this->filePointer) {
179 3
            throw new IOException('File pointer has not be opened');
180
        }
181 98
    }
182
183
    /**
184
     * Checks if the writer has already been opened, since some actions must be done before it gets opened.
185
     * Throws an exception if already opened.
186
     *
187
     * @param string $message Error message
188
     * @return void
189
     * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be.
190
     */
191 53
    protected function throwIfWriterAlreadyOpened($message)
192
    {
193 53
        if ($this->isWriterOpened) {
194 5
            throw new WriterAlreadyOpenedException($message);
195
        }
196 48
    }
197
198
    /**
199
     * Write given data to the output. New data will be appended to end of stream.
200
     *
201
     * @param array|\Box\Spout\Writer\Common\Entity\Row $row The row to be appended to the stream
202
     * @return WriterInterface
203
     * @internal param array $row Array containing data to be streamed.
204
     *          Example $row= ['data1', 1234, null, '', 'data5'];
205
     * @internal param \Box\Spout\Writer\Common\Entity\Row $row A Row object with cells and styles
206
     *          Example $row = (new Row())->addCell('data1');
207
     *
208
     * @throws SpoutException If anything else goes wrong while writing data
209
     * @throws WriterNotOpenedException If this function is called before opening the writer
210
     *
211
     * @api
212
     */
213 82
    public function addRow($row)
214
    {
215 82
        if (!is_array($row) && !$row instanceof Row) {
216
            throw new InvalidArgumentException('addRow accepts an array with scalar values or a Row object');
217
        }
218
219 82
        if (is_array($row) && !empty($row)) {
220 62
            $row = $this->createRowFromArray($row, null);
221
        }
222
223 82
        if ($this->isWriterOpened) {
224 72
            if (!empty($row)) {
225
                try {
226 72
                    $this->applyDefaultRowStyle($row);
0 ignored issues
show
Bug introduced by
It seems like $row defined by parameter $row on line 213 can also be of type array; however, Box\Spout\Writer\WriterA...:applyDefaultRowStyle() does only seem to accept object<Box\Spout\Writer\Common\Entity\Row>, maybe add an additional type check?

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.

Loading history...
227 72
                    $this->addRowToWriter($row);
0 ignored issues
show
Bug introduced by
It seems like $row defined by parameter $row on line 213 can also be of type array; however, Box\Spout\Writer\WriterAbstract::addRowToWriter() does only seem to accept object<Box\Spout\Writer\Common\Entity\Row>, maybe add an additional type check?

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.

Loading history...
228 5
                } catch (SpoutException $e) {
229
                    // if an exception occurs while writing data,
230
                    // close the writer and remove all files created so far.
231 5
                    $this->closeAndAttemptToCleanupAllFiles();
232
                    // re-throw the exception to alert developers of the error
233 5
                    throw $e;
234
                }
235
            }
236
        } else {
237 10
            throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
238
        }
239 69
        return $this;
240
    }
241
242
    /**
243
     * @inheritdoc
244
     *
245
     * @api
246
     */
247
    public function withRow(\Closure $callback)
248
    {
249
        return $this->addRow($callback(new Row()));
250
    }
251
252
    /**
253
     * Write given data to the output and apply the given style.
254
     * @see addRow
255
     *
256
     * @param array|\Box\Spout\Writer\Common\Entity\Row $row The row to be appended to the stream
257
     * @param Style $style Style to be applied to the row.
258
     * @return WriterInterface
259
     * @internal param array $row Array containing data to be streamed.
260
     *          Example $row= ['data1', 1234, null, '', 'data5'];
261
     * @internal param \Box\Spout\Writer\Common\Entity\Row $row A Row object with cells and styles
262
     *          Example $row = (new Row())->addCell('data1');
263
     * @api
264
     * @throws InvalidArgumentException If the input param is not valid
265
     */
266 19
    public function addRowWithStyle($row, $style)
267
    {
268 19
        if (!is_array($row) && !$row instanceof Row) {
269
            throw new InvalidArgumentException('addRowWithStyle accepts an array with scalar values or a Row object');
270
        }
271
272 19
        if (!$style instanceof Style) {
273 6
            throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.');
274
        }
275
276 13
        if (is_array($row)) {
277 13
            $row = $this->createRowFromArray($row, $style);
278
        }
279
280 13
        $this->addRow($row);
281 9
        return $this;
282
    }
283
284
    /**
285
     * @param array $dataRows
286
     * @param Style|null $style
287
     * @return Row
288
     */
289 82
    protected function createRowFromArray(array $dataRows, Style $style = null)
290
    {
291
        $row = (new Row())->setCells(array_map(function ($value) {
292 82
            if ($value instanceof Cell) {
293 5
                return $value;
294
            }
295 77
            return new Cell($value);
296 82
        }, $dataRows));
297
298 82
        if ($style !== null) {
299 23
            $row->setStyle($style);
300
        }
301
302 82
        return $row;
303
    }
304
305
    /**
306
     * Write given data to the output. New data will be appended to end of stream.
307
     *
308
     * @api
309
     * @param  array $dataRows Array of array containing data to be streamed.
310
     *                         If a row is empty, it won't be added (i.e. not even as a blank row)
311
     *                         Example: $dataRows = [
312
     *                             ['data11', 12, , '', 'data13'],
313
     *                             ['data21', 'data22', null, false],
314
     *                         ];
315
     * @return WriterAbstract
316
     * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
317
     * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
318
     * @throws \Box\Spout\Common\Exception\IOException If unable to write data
319
     */
320 61
    public function addRows(array $dataRows)
321
    {
322 61
        if (!empty($dataRows)) {
323 61
            $firstRow = reset($dataRows);
324 61
            if (!is_array($firstRow) && !$firstRow instanceof Row) {
325 1
                throw new InvalidArgumentException('The input should be an array of arrays or row objects');
326
            }
327 60
            foreach ($dataRows as $dataRow) {
328 60
                $this->addRow($dataRow);
329
            }
330
        }
331 52
        return $this;
332
    }
333
334
    /**
335
     * Write given data to the output and apply the given style.
336
     * @see addRows
337
     *
338
     * @api
339
     * @param array $dataRows Array of array containing data to be streamed.
340
     * @param Style $style Style to be applied to the rows.
341
     * @return WriterAbstract
342
     * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
343
     * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
344
     * @throws \Box\Spout\Common\Exception\IOException If unable to write data
345
     */
346 16
    public function addRowsWithStyle(array $dataRows, $style)
347
    {
348 16
        if (!$style instanceof Style) {
349 6
            throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.');
350
        }
351
352 10
        $this->addRows(array_map(function ($row) use ($style) {
353 10
            if (is_array($row)) {
354 10
                return $this->createRowFromArray($row, $style);
355
            } elseif ($row instanceof Row) {
356
                return $row;
357
            } else {
358
                throw new InvalidArgumentException();
359
            }
360 10
        }, $dataRows));
361
362 10
        return $this;
363
    }
364
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
        $merged = $this->styleManager->merge($row->getStyle(), $defaultRowStyle);
0 ignored issues
show
Bug introduced by
It seems like $row->getStyle() can be null; however, merge() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
376 64
        $row->setStyle($merged);
377 64
        return $this;
378
    }
379
380
    /**
381
     * Closes the writer. This will close the streamer as well, preventing new data
382
     * to be written to the file.
383
     *
384
     * @api
385
     * @return void
386
     */
387 79
    public function close()
388
    {
389 79
        if (!$this->isWriterOpened) {
390 3
            return;
391
        }
392
393 79
        $this->closeWriter();
394
395 79
        if (is_resource($this->filePointer)) {
396 79
            $this->globalFunctionsHelper->fclose($this->filePointer);
397
        }
398
399 79
        $this->isWriterOpened = false;
400 79
    }
401
402
    /**
403
     * Closes the writer and attempts to cleanup all files that were
404
     * created during the writing process (temp files & final file).
405
     *
406
     * @return void
407
     */
408 5
    private function closeAndAttemptToCleanupAllFiles()
409
    {
410
        // close the writer, which should remove all temp files
411 5
        $this->close();
412
413
        // remove output file if it was created
414 5
        if ($this->globalFunctionsHelper->file_exists($this->outputFilePath)) {
415 5
            $outputFolderPath = dirname($this->outputFilePath);
416 5
            $fileSystemHelper = new FileSystemHelper($outputFolderPath);
417 5
            $fileSystemHelper->deleteFile($this->outputFilePath);
418
        }
419 5
    }
420
}
421