ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.ilExcel.php
Go to the documentation of this file.
1 <?php
2 
29 
30 /*
31  * Wrapper for Microsoft Excel Import/Export (based on PHPSpreadsheet, formerPHPExcel which is deprecated)
32  * @author Jörg Lützenkirchen <luetzenkirchen@leifos.com>
33  */
34 class ilExcel
35 {
36  public const FORMAT_XML = 'Xlsx';
37  public const FORMAT_BIFF = 'Xls';
38  protected string $format;
39 
40  private array $noncharacters = [
41  '\x{FFFE}-\x{FFFF}',
42  '\x{1FFFE}-\x{1FFFF}',
43  '\x{2FFFE}-\x{2FFFF}',
44  '\x{3FFFE}-\x{3FFFF}',
45  '\x{4FFFE}-\x{4FFFF}',
46  '\x{5FFFE}-\x{5FFFF}',
47  '\x{6FFFE}-\x{6FFFF}',
48  '\x{7FFFE}-\x{7FFFF}',
49  '\x{8FFFE}-\x{8FFFF}',
50  '\x{9FFFE}-\x{9FFFF}',
51  '\x{AFFFE}-\x{AFFFF}',
52  '\x{BFFFE}-\x{BFFFF}',
53  '\x{CFFFE}-\x{CFFFF}',
54  '\x{DFFFE}-\x{DFFFF}',
55  '\x{EFFFE}-\x{EFFFF}',
56  '\x{FFFFE}-\x{FFFFF}',
57  '\x{10FFFE}-\x{10FFFF}',
58  '\x{FDD0}-\x{FDEF}'
59  ];
60 
61  protected ilLanguage $lng;
62  protected Spreadsheet $workbook;
63  protected string $type;
64 
68  public function __construct()
69  {
70  global $DIC;
71 
72  $this->lng = $DIC->language();
73  $this->setFormat(self::FORMAT_XML);
74  $this->workbook = new Spreadsheet();
75  $this->workbook->removeSheetByIndex(0);
76  }
77 
78  //
79  // loading files
80  //
81 
85  public function loadFromFile(string $filename): void
86  {
87  $this->workbook = IOFactory::load($filename);
88  }
89 
90  //
91  // type/format
92  //
93 
97  public function getValidFormats(): array
98  {
99  return [self::FORMAT_XML, self::FORMAT_BIFF];
100  }
101 
105  public function setFormat(string $a_format): void
106  {
107  if (in_array($a_format, $this->getValidFormats())) {
108  $this->format = $a_format;
109  }
110  }
111 
112 
113  //
114  // sheets
115  //
116 
122  public function addSheet(
123  string $a_name,
124  bool $a_activate = true
125  ): int {
126  #20749
127  // see Worksheet::$_invalidCharacters;
128  $invalid = ['*', ':', '/', '\\', '?', '[', ']', '\'-', '\''];
129 
130  $a_name = str_replace($invalid, '', $a_name);
131 
132  // #19056 - phpExcel only allows 31 chars
133  // see https://github.com/PHPOffice/PHPExcel/issues/79
134  $a_name = ilStr::shortenTextExtended($a_name, 31);
135 
136  $sheet = new Worksheet($this->workbook, $a_name);
137  $this->workbook->addSheet($sheet);
138  $new_index = $this->workbook->getSheetCount() - 1;
139 
140  if ($a_activate) {
141  $this->setActiveSheet($new_index);
142  }
143 
144  return $new_index;
145  }
146 
150  public function setActiveSheet(int $a_index): void
151  {
152  $this->workbook->setActiveSheetIndex($a_index);
153  }
154 
155 
159  public function getSheetCount(): int
160  {
161  return $this->workbook->getSheetCount();
162  }
163 
164 
168  public function getSheetTitle(): string
169  {
170  return $this->workbook->getActiveSheet()->getTitle();
171  }
172 
173 
174  //
175  // cells
176  //
177 
184  protected function prepareValue($a_value)
185  {
186  if (is_bool($a_value)) {
187  return $this->prepareBooleanValue($a_value);
188  }
189 
190  if ($a_value instanceof ilDateTime) {
191  return $this->prepareDateValue($a_value);
192  }
193 
194  if (is_string($a_value)) {
195  if (!mb_check_encoding($a_value, 'UTF-8')) {
196  throw new InvalidArgumentException('Invalid UTF-8 passed.');
197  }
198  return $this->prepareString($a_value);
199  }
200 
201  return $a_value;
202  }
203 
208  protected function prepareDateValue(ilDateTime $a_value)
209  {
210  switch (true) {
211  case $a_value instanceof ilDate:
212  $a_value = Date::stringToExcel($a_value->get(IL_CAL_DATE));
213  break;
214 
215  default:
216  $a_value = Date::stringToExcel($a_value->get(IL_CAL_DATETIME));
217  break;
218  }
219 
220  return $a_value;
221  }
222 
223  protected function prepareBooleanValue(bool $a_value): string
224  {
225  $lng = $this->lng;
226 
227  return $a_value ? $lng->txt('yes') : $lng->txt('no');
228  }
229 
230  protected function prepareString(string $a_value): string
231  {
232  return $this->cleanupNonCharachters(
233  strip_tags($a_value)
234  ); // #14542
235  }
236 
243  protected function setDateFormat(Cell $a_cell, $a_value): void
244  {
245  if ($a_value instanceof ilDate) {
246  $a_cell->getStyle()->getNumberFormat()->setFormatCode('dd.mm.yyyy');
247  } elseif ($a_value instanceof ilDateTime) {
248  $a_cell->getStyle()->getNumberFormat()->setFormatCode('dd.mm.yyyy hh:mm:ss');
249  }
250  }
251 
257  public function setCellByCoordinates($a_coords, $a_value): void
258  {
259  if ($a_value instanceof ilDateTime) {
260  $wb = $this->workbook->getActiveSheet()->setCellValue(
261  $a_coords,
262  $this->prepareValue($a_value)
263  );
264  $cell = $wb->getCell($a_coords);
265  $this->setDateFormat($cell, $a_value);
266  } elseif (is_numeric($a_value)) {
267  $this->workbook->getActiveSheet()->setCellValueExplicit(
268  $a_coords,
269  $this->prepareValue($a_value),
270  DataType::TYPE_NUMERIC
271  );
272  } else {
273  $this->workbook->getActiveSheet()->setCellValueExplicit(
274  $a_coords,
275  $this->prepareValue($a_value),
276  DataType::TYPE_STRING
277  );
278  }
279  }
280 
288  public function setCell(
289  int $a_row,
290  int $a_col,
291  $a_value,
292  ?string $a_datatype = null
293  ): void {
294  $coordinate = $this->getCoordByColumnAndRow($a_col, $a_row);
295 
296  if ($a_datatype !== null) {
297  $this->workbook->getActiveSheet()->setCellValueExplicit(
298  $coordinate,
299  $this->prepareValue($a_value),
300  $a_datatype
301  );
302  } elseif ($a_value instanceof ilDateTime) {
303  $wb = $this->workbook->getActiveSheet()->setCellValue(
304  $coordinate,
305  $this->prepareValue($a_value)
306  );
307  $this->setDateFormat($wb->getCell($coordinate), $a_value);
308  } elseif (is_numeric($a_value)) {
309  $this->workbook->getActiveSheet()->setCellValueExplicit(
310  $coordinate,
311  $this->prepareValue($a_value),
312  DataType::TYPE_NUMERIC
313  );
314  } else {
315  $this->workbook->getActiveSheet()->setCellValueExplicit(
316  $coordinate,
317  $this->prepareValue($a_value),
318  DataType::TYPE_STRING
319  );
320  }
321  }
322 
330  public function setCellArray(
331  array $a_values,
332  string $a_top_left = 'A1',
333  $a_null_value = null
334  ): void {
335  foreach ($a_values as $row_idx => $cols) {
336  if (is_array($cols)) {
337  foreach ($cols as $col_idx => $col_value) {
338  $a_values[$row_idx][$col_idx] = $this->prepareValue($col_value);
339  }
340  } else {
341  $a_values[$row_idx] = $this->prepareValue($cols);
342  }
343  }
344 
345  $this->workbook->getActiveSheet()->fromArray($a_values, $a_null_value, $a_top_left);
346  }
347 
348 
353  public function getCell(
354  int $a_row,
355  int $a_col
356  ) {
357  $coordinate = $this->getCoordByColumnAndRow($a_col, $a_row);
358  return $this->workbook->getActiveSheet()->getCell($coordinate)->getValue();
359  }
360 
364  public function getSheetAsArray(): array
365  {
366  return $this->workbook->getActiveSheet()->toArray();
367  }
368 
372  public function getColumnCount(): int
373  {
374  return Coordinate::columnIndexFromString($this->workbook->getActiveSheet()->getHighestDataColumn());
375  }
376 
380  public function getColumnCoord(int $a_col): string
381  {
382  $col = $this->columnIndexAdjustment($a_col);
383  return Coordinate::stringFromColumnIndex($col);
384  }
385 
389  protected function setGlobalAutoSize(): void
390  {
391  // this may change the active sheet
392  foreach ($this->workbook->getWorksheetIterator() as $worksheet) {
393  $this->workbook->setActiveSheetIndex($this->workbook->getIndex($worksheet));
394  $sheet = $this->workbook->getActiveSheet();
395  $cellIterator = $sheet->getRowIterator()->current()->getCellIterator();
396  $cellIterator->setIterateOnlyExistingCells(true);
397  foreach ($cellIterator as $cell) {
398  $sheet->getColumnDimension($cell->getColumn())->setAutoSize(true);
399  }
400  }
401  }
402 
403  //
404  // deliver/save
405  //
406 
410  protected function prepareStorage(string $a_file_name): string
411  {
412  $this->setGlobalAutoSize();
413  $this->workbook->setActiveSheetIndex(0);
414 
415  switch ($this->format) {
416  case self::FORMAT_BIFF:
417  if (stripos($a_file_name, '.xls') === false) {
418  $a_file_name .= '.xls';
419  }
420  break;
421 
422  case self::FORMAT_XML:
423  if (stripos($a_file_name, '.xlsx') === false) {
424  $a_file_name .= '.xlsx';
425  }
426  break;
427  }
428 
429  return $a_file_name;
430  }
431 
436  public function sendToClient(string $a_file_name): void
437  {
438  $a_file_name = $this->prepareStorage($a_file_name);
439  switch ($this->format) {
440  case self::FORMAT_BIFF:
441  $a_mime_type = MimeType::APPLICATION__VND_MS_EXCEL;
442  break;
443 
444  case self::FORMAT_XML:
445  $a_mime_type = MimeType::APPLICATION__VND_OPENXMLFORMATS_OFFICEDOCUMENT_SPREADSHEETML_SHEET;
446  break;
447  default:
448  $a_mime_type = MimeType::APPLICATION__OCTET_STREAM;
449  break;
450  }
451  $tmp_name = ilFileUtils::ilTempnam();
452 
453  $writer = IOFactory::createWriter($this->workbook, $this->format);
454  $writer->save($tmp_name);
455 
456  ilFileDelivery::deliverFileAttached($tmp_name, $a_file_name, $a_mime_type, true);
457  }
458 
464  public function writeToFile(string $a_file): void
465  {
466  $a_file = $this->prepareStorage($a_file);
467 
468  $writer = IOFactory::createWriter($this->workbook, $this->format);
469  $writer->save($a_file);
470  }
471 
475  public function writeToTmpFile(): string
476  {
477  $writer = IOFactory::createWriter($this->workbook, $this->format);
479  $writer->save($filename);
480 
481  return $filename;
482  }
483 
487  public function setBold(string $a_coords): void
488  {
489  $this->workbook->getActiveSheet()->getStyle($a_coords)->getFont()->setBold(true);
490  }
491 
495  public function setColors(
496  string $a_coords,
497  string $a_background,
498  string $a_font = null
499  ): void {
500  $opts = [
501  'fill' => [
502  'fillType' => Fill::FILL_SOLID,
503  'color' => ['rgb' => $a_background]
504  ]
505  ];
506 
507  if ($a_font) {
508  $opts['font'] = [
509  'color' => ['rgb' => $a_font]
510  ];
511  }
512 
513  $this->workbook->getActiveSheet()->getStyle($a_coords)->applyFromArray($opts);
514  }
515 
519  public function setBorders(
520  string $a_coords,
521  bool $a_top,
522  bool $a_right = false,
523  bool $a_bottom = false,
524  bool $a_left = false
525  ): void {
526  $style = $this->workbook->getActiveSheet()->getStyle($a_coords);
527 
528  // :TODO: border styles?
529  if ($a_top) {
530  $style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
531  }
532  if ($a_right) {
533  $style->getBorders()->getRight()->setBorderStyle(Border::BORDER_THIN);
534  }
535  if ($a_bottom) {
536  $style->getBorders()->getBottom()->setBorderStyle(Border::BORDER_THIN);
537  }
538  if ($a_left) {
539  $style->getBorders()->getLeft()->setBorderStyle(Border::BORDER_THIN);
540  }
541  }
542 
546  public function getCoordByColumnAndRow(
547  int $pColumn = 1,
548  int $pRow = 1
549  ): string {
550  $col = $this->columnIndexAdjustment($pColumn);
551  $columnLetter = Coordinate::stringFromColumnIndex($col);
552 
553  return $columnLetter . $pRow;
554  }
555 
559  public function addLink(
560  int $a_row,
561  int $a_column,
562  string $a_path
563  ): void {
564  $coordinate = $this->getCoordByColumnAndRow($a_column, $a_row);
565  $this->workbook->getActiveSheet()->getCell($coordinate)->getHyperlink()->setUrl($a_path);
566  }
567 
572  public function columnIndexAdjustment(int $column): int
573  {
574  return ++$column;
575  }
576 
581  public function mergeCells(string $coordinatesRange): void
582  {
583  $this->workbook->getActiveSheet()->mergeCells($coordinatesRange);
584  }
585 
586  private function cleanupNonCharachters(string $string): string
587  {
588  return mb_ereg_replace('[' . implode('', $this->noncharacters) . ']', '', $string);
589  }
590 }
get(int $a_format, string $a_format_str='', string $a_tz='')
get formatted date
prepareBooleanValue(bool $a_value)
prepareStorage(string $a_file_name)
Prepare workbook for storage/delivery.
getSheetAsArray()
Returns the active sheet as an array.
const FORMAT_BIFF
const IL_CAL_DATETIME
txt(string $a_topic, string $a_default_lang_fallback_mod="")
gets the text for a given topic if the topic is not in the list, the topic itself with "-" will be re...
setDateFormat(Cell $a_cell, $a_value)
Set date format of cell.
getColumnCoord(int $a_col)
Get column "name" from number.
setBorders(string $a_coords, bool $a_top, bool $a_right=false, bool $a_bottom=false, bool $a_left=false)
Toggle cell(s) borders.
getColumnCount()
Returns the number of columns the sheet contains.
addLink(int $a_row, int $a_column, string $a_path)
setCell(int $a_row, int $a_col, $a_value, ?string $a_datatype=null)
Set cell value.
cleanupNonCharachters(string $string)
setFormat(string $a_format)
Set file format.
setGlobalAutoSize()
Set all existing columns on all sheets to autosize.
global $DIC
Definition: feed.php:28
ilLanguage $lng
setBold(string $a_coords)
Set cell(s) to bold.
getValidFormats()
static deliverFileAttached(string $path_to_file, ?string $download_file_name=null, ?string $mime_type=null, bool $delete_file=false)
getSheetTitle()
Return the current sheet title.
getCoordByColumnAndRow(int $pColumn=1, int $pRow=1)
Get cell coordinate (e.g.
getSheetCount()
Returns number of sheets.
writeToFile(string $a_file)
Save workbook to file.
array $noncharacters
addSheet(string $a_name, bool $a_activate=true)
Add sheet.
getCell(int $a_row, int $a_col)
Returns the value of a cell.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setCellByCoordinates($a_coords, $a_value)
Set cell value by coordinates.
$filename
Definition: buildRTE.php:78
columnIndexAdjustment(int $column)
Adjustment needed because of migration PHPExcel to PhpSpreadsheet.
static ilTempnam(?string $a_temp_path=null)
Returns a unique and non existing Path for e temporary file or directory.
setCellArray(array $a_values, string $a_top_left='A1', $a_null_value=null)
Set cell values from array.
const IL_CAL_DATE
mergeCells(string $coordinatesRange)
prepareDateValue(ilDateTime $a_value)
setActiveSheet(int $a_index)
const FORMAT_XML
Spreadsheet $workbook
static shortenTextExtended(string $a_str, int $a_len, bool $a_dots=false, bool $a_next_blank=false, bool $a_keep_extension=false)
loadFromFile(string $filename)
Loads a spreadsheet from file.
prepareString(string $a_value)
string $format
setColors(string $a_coords, string $a_background, string $a_font=null)
Set cell(s) colors.
string $type
sendToClient(string $a_file_name)
Send workbook to client.
prepareValue($a_value)
Prepare value for cell.