Failed Conditions
Pull Request — develop_3.0 (#434)
by Hura
03:29
created

WriterAbstract   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 92.78%

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 7
dl 0
loc 395
ccs 90
cts 97
cp 0.9278
rs 8.295
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
openWriter() 0 1 ?
addRowToWriter() 0 1 ?
closeWriter() 0 1 ?
A setDefaultRowStyle() 0 5 1
A openToFile() 0 12 1
B openToBrowser() 0 30 1
A throwIfFilePointerIsNotAvailable() 0 6 2
A throwIfWriterAlreadyOpened() 0 6 2
C addRow() 0 28 8
A withRow() 0 4 1
B addRowWithStyle() 0 17 5
A createRowFromArray() 0 15 3
B addRows() 0 13 5
B addRowsWithStyle() 0 21 5
A applyDefaultRowStyle() 0 9 2
A close() 0 14 3
A closeAndAttemptToCleanupAllFiles() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like WriterAbstract often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WriterAbstract, and based on these observations, apply Extract Interface, too.

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);
0 ignored issues
show
Bug introduced by
It seems like $row defined by parameter $row on line 208 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...
222 72
                    $this->addRowToWriter($row);
0 ignored issues
show
Bug introduced by
It seems like $row defined by parameter $row on line 208 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...
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);
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($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