Completed
Push — master ( 58b3d6...a4bd6c )
by Vitaliy
03:59
created

CsvExport::renderCsv()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 3
Bugs 2 Features 0
Metric Value
c 3
b 2
f 0
dl 0
loc 24
ccs 0
cts 22
cp 0
rs 8.9713
cc 3
eloc 19
nc 3
nop 0
crap 12
1
<?php
2
3
namespace ViewComponents\Grids\Component;
4
5
use ViewComponents\ViewComponents\Base\ViewComponentInterface;
6
use ViewComponents\ViewComponents\Common\UriFunctions;
7
use ViewComponents\ViewComponents\Component\Compound;
8
use ViewComponents\ViewComponents\Component\DataView;
9
use ViewComponents\ViewComponents\Component\Html\Tag;
10
use ViewComponents\ViewComponents\Component\Part;
11
use ViewComponents\ViewComponents\Data\DataProviderInterface;
12
use ViewComponents\ViewComponents\Data\Operation\PaginateOperation;
13
use ViewComponents\ViewComponents\Input\InputOption;
14
use ViewComponents\Grids\Grid;
15
16
class CsvExport extends Part
17
{
18
19
    private $fileName = 'data.csv';
20
    private $csvDelimiter = ';';
21
    private $exitFunction;
22
23
    /**
24
     * @var InputOption
25
     */
26
    private $inputOption;
27
28
    public function __construct(InputOption $inputOption = null, ViewComponentInterface $controlView = null)
29
    {
30
        $this->inputOption = $inputOption;
31
        parent::__construct($controlView ?: $this->makeDefaultView(), 'csv_export', 'control_container');
32
    }
33
34
    /**
35
     * @param InputOption $inputOption
36
     * @return CsvExport
37
     */
38
    public function setInputOption($inputOption)
39
    {
40
        $this->inputOption = $inputOption;
41
        return $this;
42
    }
43
44
    /**
45
     * @return InputOption
46
     */
47
    public function getInputOption()
48
    {
49
        return $this->inputOption;
50
    }
51
52
    /**
53
     * Sets exit function.
54
     *
55
     * You may specify custom exit function to finish application cleanly.
56
     * 'exit' will be called after rendering CSV if no exit function specified.
57
     *
58
     * @param callable|null $exitFunction
59
     * @return $this
60
     */
61
    public function setExitFunction($exitFunction)
62
    {
63
        $this->exitFunction = $exitFunction;
64
        return $this;
65
    }
66
67
    /**
68
     * @return callable|null
69
     */
70
    public function getExitFunction()
71
    {
72
        return $this->exitFunction;
73
    }
74
75
    /**
76
     * @return string
77
     */
78
    public function getCsvDelimiter()
79
    {
80
        return $this->csvDelimiter;
81
    }
82
83
    /**
84
     * @param string $csvDelimiter
85
     * @return $this
86
     */
87
    public function setCsvDelimiter($csvDelimiter)
88
    {
89
        $this->csvDelimiter = $csvDelimiter;
90
        return $this;
91
    }
92
93
    /**
94
     * @param string $name
95
     * @return $this
96
     */
97
    public function setFileName($name)
98
    {
99
        $this->fileName = $name;
100
        return $this;
101
    }
102
103
    /**
104
     * @return string
105
     */
106
    public function getFileName()
107
    {
108
        return $this->fileName;
109
    }
110
111
    public function attachToCompound(Compound $root, $prepend = false)
112
    {
113
        // prepend component that will render results
114
        $root->children()->add(new DataView(function () {
0 ignored issues
show
Bug introduced by
The method add() does not seem to exist on object<Nayjest\Collectio...adonlyObjectCollection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
115
            if ($this->inputOption->hasValue()) {
116
                $this->renderCsv();
117
            }
118
        }), true);
119
        parent::attachToCompound($root, $prepend);
120
    }
121
122
    protected function removePagination(DataProviderInterface $provider)
123
    {
124
        $pagination = $provider->operations()->findByType(PaginateOperation::class);
125
        if ($pagination) {
126
            $provider->operations()->remove($pagination);
127
        }
128
    }
129
130
    protected function renderCsv()
131
    {
132
        ob_clean(); // removes previous output from buffer if grid was rendered inside view
133
        $file = fopen('php://output', 'w');
134
        header('Content-Type: text/csv');
135
        header('Content-Disposition: attachment; filename="' . $this->getFileName() . '"');
136
        header('Pragma: no-cache');
137
        set_time_limit(0);
138
        /** @var Grid $grid */
139
        $grid = $this->root;
140
        $provider = $grid->getDataProvider();
141
        $this->renderHeadingRow($file);
142
        $this->removePagination($provider);
0 ignored issues
show
Bug introduced by
It seems like $provider defined by $grid->getDataProvider() on line 140 can be null; however, ViewComponents\Grids\Com...ort::removePagination() 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...
143
        foreach ($provider as $row) {
0 ignored issues
show
Bug introduced by
The expression $provider of type object<ViewComponents\Vi...ProviderInterface>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
144
            $output = [];
145
            $grid->setCurrentRow($row);
146
            foreach ($grid->getColumns() as $column) {
147
                $output[] = $this->escapeString($column->getCurrentValueFormatted());
148
            }
149
            fputcsv($file, $output, $this->getCsvDelimiter());
150
        }
151
        fclose($file);
152
        $this->finishApplication();
153
    }
154
155
    protected function escapeString($str)
156
    {
157
        return str_replace('"', '\'', strip_tags(html_entity_decode($str)));
158
    }
159
160
    /**
161
     * @param resource $file
162
     */
163
    protected function renderHeadingRow($file)
164
    {
165
        $output = [];
166
        /** @var Grid $grid */
167
        $grid = $this->root;
168
        foreach ($grid->getColumns() as $column) {
169
            $output[] = $this->escapeString($column->getLabel());
170
        }
171
        fputcsv($file, $output, $this->getCsvDelimiter());
172
    }
173
174
    protected function finishApplication()
175
    {
176
        if (!$this->getExitFunction()) {
177
            exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method finishApplication() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
178
        }
179
        call_user_func($this->getExitFunction());
180
    }
181
182
    public function getExportUrl()
183
    {
184
        return UriFunctions::modifyQuery(null, [$this->inputOption->getKey() => 1]);
185
    }
186
187
    protected function makeDefaultView()
188
    {
189
        $href = $this->getExportUrl();
190
        return new Tag(
191
            'button',
192
            [
193
                // required to avoid emitting 'click' on pressing enter
194
                'type' => 'button',
195
                'onclick' => "window.location='$href'; return false;",
196
                'style' => 'margin:2px;'
197
            ],
198
            [new DataView('CSV Export')]
199
        );
200
    }
201
}
202