ArrayToCSV::flattenArray()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 19
rs 9.8333
cc 4
nc 5
nop 2
1
<?php
2
3
namespace Sunnysideup\ArrayToCsvDownload\Api;
4
5
use Exception;
6
use SilverStripe\Assets\Filesystem;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Control\Director;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\ORM\SS_List;
12
use SilverStripe\View\ViewableData;
13
use Soundasleep\Html2Text;
14
15
class ArrayToCSV extends ViewableData
16
{
17
    /**
18
     * can the csv file be accessed directly or only via controller?
19
     *
20
     * @var bool
21
     */
22
    protected $hiddenFile = false;
23
24
    /**
25
     * "parent" controller.
26
     *
27
     * @var null|Controller
28
     */
29
    protected $controller;
30
31
    /**
32
     * name of the file - e.g. hello.csv OR hello/foo/bar.csv OR assets/uploads/tmp.csv.
33
     *
34
     * @var string
35
     */
36
    protected $fileName = '';
37
38
    /**
39
     * any array up to two levels deep.
40
     *
41
     * @var array
42
     */
43
    protected $array = [];
44
45
    /**
46
     * headers for CSV
47
     * formatted like this:
48
     * "Key" => "Label".
49
     *
50
     * @var array
51
     */
52
    protected $headers = [];
53
54
    /**
55
     * how many seconds before the file is stale?
56
     *
57
     * @var int
58
     */
59
    protected $maxAgeInSeconds = 86400;
60
61
    /**
62
     * how to glue multi-dimensional values
63
     *
64
     * @var string
65
     */
66
    protected $concatenator = ' | ';
67
68
69
    private static $hidden_download_dir = '_csv_downloads';
70
71
    private static $public_download_dir = 'csv-downloads';
72
73
    /**
74
     * internal.
75
     *
76
     * @var bool
77
     */
78
    private $infiniteLoopEscape = false;
79
80
    /**
81
     * typical array is like this:
82
     * ```php
83
     *     [
84
     *         [
85
     *             "Key1" => "Value1"
86
     *             "Key2" => "Value2"
87
     *             "Key3" => "Value3"
88
     *         ].
89
     *
90
     *         [
91
     *             "Key1" => "Value1"
92
     *             "Key2" => "Value2"
93
     *             "Key3" => "Value3"
94
     *         ].
95
     *
96
     *         [
97
     *             "Key1" => "Value1"
98
     *             "Key2" => "Value2"
99
     *             "Key3" => "Value3"
100
     *         ].
101
     *     ]
102
     * ```
103
     *
104
     * @param string $fileName        name of the file - e.g. hello.csv OR hello/foo/bar.csv OR assets/uploads/tmp.csv
105
     * @param array  $array           any array
106
     * @param int    $maxAgeInSeconds how long before the file is stale
107
     */
108
    public function __construct(string $fileName, array $array, ?int $maxAgeInSeconds = 86400)
109
    {
110
        $this->fileName = $fileName;
111
        $this->array = $array;
112
        $this->maxAgeInSeconds = $maxAgeInSeconds;
113
    }
114
115
    /**
116
     * ensures the file itself can not be downloaded directly.
117
     *
118
     * @param bool $bool
119
     */
120
    public function setHiddenFile(?bool $bool = true): self
121
    {
122
        $this->hiddenFile = $bool;
123
124
        return $this;
125
    }
126
127
    /**
128
     * e.g.
129
     * [
130
     *     "Key1" => "Label1"
131
     *     "Key2" => "Label2"
132
     *     "Key3" => "Label3"
133
     * ].
134
     */
135
    public function setHeaders(array $array): self
136
    {
137
        $this->headers = $array;
138
139
        return $this;
140
    }
141
142
    public function setHeadersFromClassName(string $className): self
143
    {
144
        $this->headers = Injector::inst()->get($className)->fieldLabels();
145
146
        return $this;
147
    }
148
149
    /**
150
     * @param SS_List $list any type of list - e.g. DataList
151
     */
152
    public function setList(SS_List $list): self
153
    {
154
        $this->array = $list->toNestedArray();
155
156
        return $this;
157
    }
158
159
160
    /**
161
     * @param SS_List $list any type of list - e.g. DataList
162
     */
163
    public function setConcatenator(string $c): self
164
    {
165
        $this->concatenator = $c;
166
167
        return $this;
168
    }
169
170
    protected function flattenArray(array $array, ?string $prefix = ''): array
171
    {
172
        $result = [];
173
        foreach ($array as $key => $value) {
174
            $newKey = $prefix . (empty($prefix) ? '' : '.') . $key;
175
            if (is_array($value)) {
176
                $result[$newKey] = http_build_query(
177
                    array_merge(
178
                        $result,
179
                        $this->flattenArray($value, $newKey)
180
                    ),
181
                    '',
182
                    $this->concatenator
183
                );
184
            } else {
185
                $result[$newKey] = $value;
186
            }
187
        }
188
        return $result;
189
    }
190
191
    public function createFile()
192
    {
193
        $path = $this->getFilePath();
194
        if (file_exists($path)) {
195
            unlink($path);
196
        }
197
198
        // make sure there is no recursion in array...
199
        foreach ($this->array as $index => $row) {
200
            $this->array[$index] = $this->flattenArray($row);
201
        }
202
203
        $file = fopen($path, 'w');
204
        if ($this->isAssoc()) {
205
            $row = $this->array[0];
206
            if (empty($this->headers)) {
207
                $keys = array_keys($row);
208
                $this->headers = array_combine($keys, $keys);
209
            }
210
211
            fputcsv($file, $this->headers);
212
        }
213
214
        foreach ($this->array as $row) {
215
            $count = count($row);
0 ignored issues
show
Unused Code introduced by
The assignment to $count is dead and can be removed.
Loading history...
216
            $newRow = [];
217
            foreach ($this->headers as $key => $label) {
218
                try {
219
                    $newRow[$key] = Html2Text::convert(($row[$key] ?? ''), ['ignore_errors' => true]);
220
                } catch (Exception $exception) {
221
                    $newRow[$key] = 'error';
222
                }
223
            }
224
225
            fputcsv($file, $newRow);
226
        }
227
228
        fclose($file);
229
    }
230
231
    public function redirectToFile(?Controller $controller = null, ?bool $returnLinkOnly = false)
232
    {
233
        $this->controller = $controller ?: Controller::curr();
234
        $maxCacheAge = strtotime('Now') - ($this->maxAgeInSeconds);
235
        $path = $this->getFilePath();
236
        $timeChange = 0;
237
        if (file_exists($path)) {
238
            $timeChange = filemtime($path);
239
        }
240
        if ($timeChange < $maxCacheAge) {
241
            $this->createFile();
242
        }
243
        if ($this->hiddenFile) {
244
            if ($returnLinkOnly) {
245
                return '/downloadcsv/download/'.$this->fileName;
246
            } else {
247
                return HTTPRequest::send_file(file_get_contents($path), $this->fileName, 'text/csv');
248
            }
249
        } else {
250
            if ($returnLinkOnly) {
251
                return $this->getFileUrl();
252
            } else {
253
                return $this->controller->redirect($this->getFileUrl());
254
            }
255
        }
256
    }
257
258
    protected function getFileUrl(): string
259
    {
260
        $path = $this->getFilePath();
261
        $remove = Controller::join_links(Director::baseFolder(), PUBLIC_DIR);
262
        $cleaned = str_replace($remove, '', $path);
263
        return Director::absoluteURL($cleaned);
264
    }
265
266
    protected function getFilePath(): string
267
    {
268
        if ($this->hiddenFile) {
269
            $hiddenDownloadDir = $this->Config()->get('hidden_download_dir') ?: '_csv_download_dir';
270
            $dir = Controller::join_links(Director::baseFolder(), $hiddenDownloadDir);
271
        } else {
272
            $publicDownloadDir = $this->Config()->get('public_download_dir') ?: 'csvs';
273
            $dir = Controller::join_links(ASSETS_PATH, $publicDownloadDir);
274
        }
275
276
        Filesystem::makeFolder($dir);
277
        $path = Controller::join_links($dir, $this->fileName);
278
279
        return (string) $path;
280
    }
281
282
    protected function isAssoc(): bool
283
    {
284
        reset($this->array);
285
        $row = $this->array[0] ?? [];
286
        if (empty($row)) {
287
            return false;
288
        }
289
290
        return array_keys($row) !== range(0, count($row) - 1);
291
    }
292
}
293