Completed
Push — master ( 1398f2...fda5cf )
by Vitaliy
02:58
created

CsvExport::renderCsv()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 23
rs 9.0856
cc 3
eloc 18
nc 3
nop 0
1
<?php
2
3
namespace ViewComponents\Grids\Component;
4
5
use League\Uri\Schemes\Http as HttpUri;
6
use ViewComponents\ViewComponents\Base\ViewComponentInterface;
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)
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);
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
        $file = fopen('php://output', 'w');
133
        header('Content-Type: text/csv');
134
        header('Content-Disposition: attachment; filename="' . $this->getFileName() . '"');
135
        header('Pragma: no-cache');
136
        set_time_limit(0);
137
        /** @var Grid $grid */
138
        $grid = $this->root;
139
        $provider = $grid->getDataProvider();
140
        $this->renderHeadingRow($file);
141
        $this->removePagination($provider);
0 ignored issues
show
Bug introduced by
It seems like $provider defined by $grid->getDataProvider() on line 139 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...
142
        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...
143
            $output = [];
144
            $grid->setCurrentRow($row);
145
            foreach ($grid->getColumns() as $column) {
146
                $output[] = $this->escapeString($column->getCurrentValueFormatted());
147
            }
148
            fputcsv($file, $output, $this->getCsvDelimiter());
149
        }
150
        fclose($file);
151
        $this->finishApplication();
152
    }
153
154
    protected function escapeString($str)
155
    {
156
        return str_replace('"', '\'', strip_tags(html_entity_decode($str)));
157
    }
158
159
    /**
160
     * @param resource $file
161
     */
162
    protected function renderHeadingRow($file)
163
    {
164
        $output = [];
165
        /** @var Grid $grid */
166
        $grid = $this->root;
167
        foreach ($grid->getColumns() as $column) {
168
            $output[] = $this->escapeString($column->getLabel());
169
        }
170
        fputcsv($file, $output, $this->getCsvDelimiter());
171
    }
172
173
    protected function finishApplication()
174
    {
175
        if (!$this->getExitFunction()) {
176
            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...
177
        }
178
        call_user_func($this->getExitFunction());
179
    }
180
181
    public function getExportUrl()
0 ignored issues
show
Coding Style introduced by
getExportUrl uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
182
    {
183
        $url = HttpUri::createFromServer($_SERVER);
184
        $query = $url->query->merge(http_build_query([$this->inputOption->getKey() => 1]));
185
        return (string)$url->withQuery((string)$query);
186
    }
187
188
    protected function makeDefaultView()
189
    {
190
        $href = $this->getExportUrl();
191
        return new Tag(
192
            'button',
193
            [
194
                // required to avoid emitting 'click' on pressing enter
195
                'type' => 'button',
196
                'onclick' => "window.location='$href'; return false;"
197
            ],
198
            [new DataView('CSV Export')]
199
        );
200
    }
201
}
202