Completed
Pull Request — master (#255)
by Martin
29:24 queued 14:24
created

Renderer::calculateColumnWidth()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 2
1
<?php
2
/**
3
 * Output as an excel file.
4
 */
5
namespace ZfcDatagrid\Renderer\PHPExcel;
6
7
use PHPExcel;
8
use PHPExcel_Cell;
9
use PHPExcel_Cell_DataType;
10
use PHPExcel_Style_Alignment;
11
use PHPExcel_Style_Border;
12
use PHPExcel_Style_Color;
13
use PHPExcel_Style_Fill;
14
use PHPExcel_Worksheet_PageSetup;
15
use Zend\Http\Headers;
16
use Zend\Http\Response\Stream as ResponseStream;
17
use ZfcDatagrid\Renderer\AbstractExport;
18
19
class Renderer extends AbstractExport
20
{
21
    public function getName()
22
    {
23
        return 'PHPExcel';
24
    }
25
26
    public function isExport()
27
    {
28
        return true;
29
    }
30
31
    public function isHtml()
32
    {
33
        return false;
34
    }
35
36
    public function execute()
37
    {
38
        $options = $this->getOptions();
39
        $optionsExport = $options['settings']['export'];
40
41
        $optionsRenderer = $this->getOptionsRenderer();
42
43
        $phpExcel = new PHPExcel();
44
45
        // Sheet 1
46
        $phpExcel->setActiveSheetIndex(0);
47
        $sheet = $phpExcel->getActiveSheet();
48
        $sheet->setTitle($this->translate($optionsRenderer['sheetName']));
49
50
        if (true === $optionsRenderer['displayTitle']) {
51
            $sheet->setCellValue('A'.$optionsRenderer['rowTitle'], $this->getTitle());
52
            $sheet->getStyle('A'.$optionsRenderer['rowTitle'])
53
                ->getFont()
54
                ->setSize(15);
55
        }
56
57
        /*
58
         * Print settings
59
         */
60
        $this->setPrinting($phpExcel);
61
62
        /*
63
         * Calculate column width
64
         */
65
        $this->calculateColumnWidth($sheet, $this->getColumnsToExport());
66
67
        /*
68
         * Header
69
         */
70
        $xColumn = 0;
71
        $yRow = $optionsRenderer['startRowData'];
72
        foreach ($this->getColumnsToExport() as $col) {
73
            /* @var $column \ZfcDatagrid\Column\AbstractColumn */
74
            $sheet->setCellValueByColumnAndRow($xColumn, $yRow, $this->translate($col->getLabel()));
75
76
            $sheet->getColumnDimension(PHPExcel_Cell::stringFromColumnIndex($xColumn))->setWidth($col->getWidth());
77
78
            ++$xColumn;
79
        }
80
81
        /*
82
         * Data
83
         */
84
        $yRow = $optionsRenderer['startRowData'] + 1;
85
        foreach ($this->getData() as $row) {
86
            $xColumn = 0;
87
            foreach ($this->getColumnsToExport() as $col) {
88
                /* @var $col \ZfcDatagrid\Column\AbstractColumn */
89
90
                $value = $row[$col->getUniqueId()];
91
                if (is_array($value)) {
92
                    $value = implode(PHP_EOL, $value);
93
                }
94
95
                /* @var $column \ZfcDatagrid\Column\AbstractColumn */
96
                $currentColumn = PHPExcel_Cell::stringFromColumnIndex($xColumn);
97
                $cell = $sheet->getCell($currentColumn.$yRow);
98
99
                switch (get_class($col->getType())) {
100
101
                    case 'ZfcDatagrid\Column\Type\Number':
102
                        $cell->setValueExplicit($value, PHPExcel_Cell_DataType::TYPE_NUMERIC);
103
                        break;
104
105
                    case 'ZfcDatagrid\Column\Type\DateTime':
106
                        /* @var $dateType \ZfcDatagrid\Column\Type\DateTime */
107
                        $dateType = $col->getType();
108
                        
109
                        if (! $value instanceof \DateTime && is_scalar($value)) {
110
                            $value = \DateTime::createFromFormat($dateType->getSourceDateTimeFormat(), $value);
111
                            $value->setTimezone(new \DateTimeZone($dateType->getSourceTimezone()));
112
                        }
113
                        
114
                        $value->setTimezone(new \DateTimeZone($dateType->getOutputTimezone()));
115
                        $cell->setValue(\PHPExcel_Shared_Date::PHPToExcel($value));
116
                        
117
                        if ($dateType->getOutputPattern()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dateType->getOutputPattern() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
118
                            $outputPattern = $dateType->getOutputPattern();
119
                        } else {
120
                            $outputPattern = \PHPExcel_Style_NumberFormat::FORMAT_DATE_DATETIME;
121
                        }
122
                        
123
                        $cell->$cell->getStyle()
124
                            ->getNumberFormat()
125
                            ->setFormatCode($outputPattern);
126
                        break;
127
128
                    default:
129
                        $cell->setValueExplicit($value, PHPExcel_Cell_DataType::TYPE_STRING);
130
                        break;
131
                }
132
133
                $columnStyle = $sheet->getStyle($currentColumn.$yRow);
134
                $columnStyle->getAlignment()->setWrapText(true);
135
136
                /*
137
                 * Styles
138
                 */
139
                $styles = array_merge($this->getRowStyles(), $col->getStyles());
140
                foreach ($styles as $style) {
141
                    /* @var $style \ZfcDatagrid\Column\Style\AbstractStyle */
142
                    if ($style->isApply($row) === true) {
143
                        switch (get_class($style)) {
144
145
                            case 'ZfcDatagrid\Column\Style\Bold':
146
                                $columnStyle->getFont()->setBold(true);
147
                                break;
148
149
                            case 'ZfcDatagrid\Column\Style\Italic':
150
                                $columnStyle->getFont()->setItalic(true);
151
                                break;
152
153
                            case 'ZfcDatagrid\Column\Style\Color':
154
                                $columnStyle->getFont()
155
                                    ->getColor()
156
                                    ->setRGB($style->getRgbHexString());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ZfcDatagrid\Column\Style\AbstractStyle as the method getRgbHexString() does only exist in the following sub-classes of ZfcDatagrid\Column\Style\AbstractStyle: ZfcDatagrid\Column\Style\AbstractColor, ZfcDatagrid\Column\Style\BackgroundColor, ZfcDatagrid\Column\Style\Color. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
157
                                break;
158
159
                            case 'ZfcDatagrid\Column\Style\BackgroundColor':
160
                                $columnStyle->getFill()->applyFromArray([
161
                                    'type' => \PHPExcel_Style_Fill::FILL_SOLID,
162
                                    'color' => [
163
                                        'rgb' => $style->getRgbHexString(),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ZfcDatagrid\Column\Style\AbstractStyle as the method getRgbHexString() does only exist in the following sub-classes of ZfcDatagrid\Column\Style\AbstractStyle: ZfcDatagrid\Column\Style\AbstractColor, ZfcDatagrid\Column\Style\BackgroundColor, ZfcDatagrid\Column\Style\Color. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
164
                                    ],
165
                                ]);
166
                                break;
167
168
                            case 'ZfcDatagrid\Column\Style\Align':
169
                                switch ($style->getAlignment()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ZfcDatagrid\Column\Style\AbstractStyle as the method getAlignment() does only exist in the following sub-classes of ZfcDatagrid\Column\Style\AbstractStyle: ZfcDatagrid\Column\Style\Align. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
170
                                    case \ZfcDatagrid\Column\Style\Align::$RIGHT:
171
                                        $columnStyle->getAlignment()->setHorizontal(PHPExcel_Style_Alignment::HORIZONTAL_RIGHT);
172
                                        break;
173
                                    case \ZfcDatagrid\Column\Style\Align::$LEFT:
174
                                        $columnStyle->getAlignment()->setHorizontal(PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
175
                                        break;
176
                                    case \ZfcDatagrid\Column\Style\Align::$CENTER:
177
                                        $columnStyle->getAlignment()->setHorizontal(PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
178
                                        break;
179
                                    case \ZfcDatagrid\Column\Style\Align::$JUSTIFY:
180
                                        $columnStyle->getAlignment()->setHorizontal(PHPExcel_Style_Alignment::HORIZONTAL_JUSTIFY);
181
                                        break;
182
                                    default:
183
                                        //throw new \Exception('Not defined yet: "'.get_class($style->getAlignment()).'"');
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
184
                                        break;
185
                                }
186
187
                                break;
188
189
                            case 'ZfcDatagrid\Column\Style\Strikethrough':
190
                                $columnStyle->getFont()->setStrikethrough(true);
191
                                break;
192
193
                            case 'ZfcDatagrid\Column\Style\Html':
194
                                // @todo strip the html?
195
                                break;
196
197
                            default:
198
                                throw new \Exception('Not defined yet: "'.get_class($style).'"');
199
                                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
200
                        }
201
                    }
202
                }
203
204
                ++$xColumn;
205
            }
206
207
            ++$yRow;
208
        }
209
210
        /*
211
         * Autofilter, freezing, ...
212
         */
213
        $highest = $sheet->getHighestRowAndColumn();
214
215
        // Letzte Zeile merken
216
217
        // Autofilter + Freeze
218
        $sheet->setAutoFilter('A'.$optionsRenderer['startRowData'].':'.$highest['column'].$highest['row']);
219
        $freezeRow = $optionsRenderer['startRowData'] + 1;
220
        $sheet->freezePane('A'.$freezeRow);
221
222
        // repeat the data header for each page!
223
        $sheet->getPageSetup()->setRowsToRepeatAtTopByStartAndEnd($optionsRenderer['startRowData'], $optionsRenderer['startRowData']);
224
225
        // highlight header line
226
        $style = [
227
            'font' => [
228
                'bold' => true,
229
            ],
230
231
            'borders' => [
232
                'allborders' => [
233
                    'style' => PHPExcel_Style_Border::BORDER_MEDIUM,
234
                    'color' => [
235
                        'argb' => PHPExcel_Style_Color::COLOR_BLACK,
236
                    ],
237
                ],
238
            ],
239
            'fill' => [
240
                'type' => PHPExcel_Style_Fill::FILL_SOLID,
241
                'startcolor' => [
242
                    'argb' => PHPExcel_Style_Color::COLOR_YELLOW,
243
                ],
244
            ],
245
        ];
246
        $range = 'A'.$optionsRenderer['startRowData'].':'.$highest['column'].$optionsRenderer['startRowData'];
247
        $sheet->getStyle($range)->applyFromArray($style);
248
249
        // print borders
250
        $range = 'A'.$freezeRow.':'.$highest['column'].$highest['row'];
251
        $sheet->getStyle($range)->applyFromArray([
252
            'borders' => [
253
                'allborders' => [
254
                    'style' => PHPExcel_Style_Border::BORDER_THIN,
255
                ],
256
            ],
257
        ]);
258
259
        /*
260
         * Save the file
261
         */
262
        $path = $optionsExport['path'];
263
        $saveFilename = date('Y-m-d_H-i-s').$this->getCacheId().'.xlsx';
264
265
        $excelWriter = new \PHPExcel_Writer_Excel2007($phpExcel);
266
        $excelWriter->setPreCalculateFormulas(false);
267
        $excelWriter->save($path.'/'.$saveFilename);
268
269
        /*
270
         * Send the response stream
271
         */
272
        $response = new ResponseStream();
273
        $response->setStream(fopen($path.'/'.$saveFilename, 'r'));
274
275
        $headers = new Headers();
276
        $headers->addHeaders([
277
            'Content-Type' => [
278
                'application/force-download',
279
                'application/octet-stream',
280
                'application/download',
281
            ],
282
            'Content-Length' => filesize($path.'/'.$saveFilename),
283
            'Content-Disposition' => 'attachment;filename='.$this->getFilename().'.xlsx',
284
            'Cache-Control' => 'must-revalidate',
285
            'Pragma' => 'no-cache',
286
            'Expires' => 'Thu, 1 Jan 1970 00:00:00 GMT',
287
        ]);
288
289
        $response->setHeaders($headers);
290
291
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $response; (Zend\Http\Response\Stream) is incompatible with the return type declared by the interface ZfcDatagrid\Renderer\RendererInterface::execute of type Zend\View\Model\ViewModel.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
292
    }
293
294
    /**
295
     * Calculates the column width, based on the papersize and orientation.
296
     *
297
     * @param \PHPExcel_Worksheet $sheet
298
     * @param array               $columns
299
     */
300
    protected function calculateColumnWidth(\PHPExcel_Worksheet $sheet, array $columns)
301
    {
302
        // First make sure the columns width is 100 "percent"
303
        $this->calculateColumnWidthPercent($columns);
304
305
        // width is in mm
306
        $paperWidth = $this->getPaperWidth();
307
308
        // remove margins (they are in inches!)
309
        $paperWidth -= $sheet->getPageMargins()->getLeft() / 0.0393700787402;
310
        $paperWidth -= $sheet->getPageMargins()->getRight() / 0.0393700787402;
311
312
        $paperWidth /= 2;
313
314
        $factor = $paperWidth / 100;
315
        foreach ($columns as $column) {
316
            /* @var $column \ZfcDatagrid\Column\AbstractColumn */
317
            $column->setWidth($column->getWidth() * $factor);
318
        }
319
    }
320
321
    /**
322
     * Set the printing options.
323
     *
324
     * @param PHPExcel $phpExcel
325
     */
326
    protected function setPrinting(PHPExcel $phpExcel)
327
    {
328
        $optionsRenderer = $this->getOptionsRenderer();
329
330
        $phpExcel->getProperties()
331
            ->setCreator('https://github.com/ThaDafinser/ZfcDatagrid')
332
            ->setTitle($this->getTitle());
333
334
        /*
335
         * Printing setup
336
         */
337
        $papersize = $optionsRenderer['papersize'];
338
        $orientation = $optionsRenderer['orientation'];
339
        foreach ($phpExcel->getAllSheets() as $sheet) {
340
            /* @var $sheet \PHPExcel_Worksheet */
341
            if ('landscape' == $orientation) {
342
                $sheet->getPageSetup()->setOrientation(PHPExcel_Worksheet_PageSetup::ORIENTATION_LANDSCAPE);
343
            } else {
344
                $sheet->getPageSetup()->setOrientation(PHPExcel_Worksheet_PageSetup::ORIENTATION_PORTRAIT);
345
            }
346
347
            switch ($papersize) {
348
349
                case 'A5':
350
                    $sheet->getPageSetup()->setPaperSize(PHPExcel_Worksheet_PageSetup::PAPERSIZE_A5);
351
                    break;
352
353
                case 'A4':
354
                    $sheet->getPageSetup()->setPaperSize(PHPExcel_Worksheet_PageSetup::PAPERSIZE_A4);
355
                    break;
356
357
                case 'A3':
358
                    $sheet->getPageSetup()->setPaperSize(PHPExcel_Worksheet_PageSetup::PAPERSIZE_A3);
359
                    break;
360
361
                case 'A2':
362
                    $sheet->getPageSetup()->setPaperSize(PHPExcel_Worksheet_PageSetup::PAPERSIZE_A2);
363
                    break;
364
            }
365
366
            // Margins
367
            $sheet->getPageMargins()->setTop(0.8);
368
            $sheet->getPageMargins()->setBottom(0.5);
369
            $sheet->getPageMargins()->setLeft(0.5);
370
            $sheet->getPageMargins()->setRight(0.5);
371
372
            $this->setHeaderFooter($sheet);
373
        }
374
375
        $phpExcel->setActiveSheetIndex(0);
376
    }
377
378
    /**
379
     * @param \PHPExcel_Worksheet $sheet
380
     */
381
    protected function setHeaderFooter(\PHPExcel_Worksheet $sheet)
382
    {
383
        $textRight = $this->translate('Page').' &P / &N';
384
385
        $sheet->getHeaderFooter()->setOddHeader('&L&16&G '.$this->translate($this->getTitle()));
386
        $sheet->getHeaderFooter()->setOddFooter('&R'.$textRight);
387
    }
388
}
389