sunnysideup /
silverstripe-array-to-csv-download
| 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
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 |