ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
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($value)
185  {
186  if (is_bool($value)) {
187  return $this->prepareBooleanValue($value);
188  }
189 
190  if ($value instanceof ilDateTime) {
191  return $this->prepareDateValue($value);
192  }
193 
194  if (is_string($value)) {
195  return $this->prepareString($value);
196  }
197 
198  return $value;
199  }
200 
205  protected function prepareDateValue(ilDateTime $a_value)
206  {
207  switch (true) {
208  case $a_value instanceof ilDate:
209  $a_value = Date::stringToExcel($a_value->get(IL_CAL_DATE));
210  break;
211 
212  default:
213  $a_value = Date::stringToExcel($a_value->get(IL_CAL_DATETIME));
214  break;
215  }
216 
217  return $a_value;
218  }
219 
220  protected function prepareBooleanValue(bool $a_value): string
221  {
222  $lng = $this->lng;
223 
224  return $a_value ? $lng->txt('yes') : $lng->txt('no');
225  }
226 
227  protected function prepareString(
228  string $value,
229  bool $disable_strip_tags = false
230  ): string {
231  if (!mb_check_encoding($value, 'UTF-8')) {
232  throw new InvalidArgumentException('Invalid UTF-8 passed.');
233  }
234 
235  return $this->cleanupNonCharachters(
236  $disable_strip_tags ? $value : strip_tags($value)
237  ); // #14542
238  }
239 
246  protected function setDateFormat(Cell $a_cell, $a_value): void
247  {
248  if ($a_value instanceof ilDate) {
249  $a_cell->getStyle()->getNumberFormat()->setFormatCode('dd.mm.yyyy');
250  } elseif ($a_value instanceof ilDateTime) {
251  $a_cell->getStyle()->getNumberFormat()->setFormatCode('dd.mm.yyyy hh:mm:ss');
252  }
253  }
254 
260  public function setCellByCoordinates($a_coords, $a_value): void
261  {
262  if ($a_value instanceof ilDateTime) {
263  $wb = $this->workbook->getActiveSheet()->setCellValue(
264  $a_coords,
265  $this->prepareValue($a_value)
266  );
267  $cell = $wb->getCell($a_coords);
268  $this->setDateFormat($cell, $a_value);
269  } elseif (is_numeric($a_value)) {
270  $this->workbook->getActiveSheet()->setCellValueExplicit(
271  $a_coords,
272  $this->prepareValue($a_value),
273  DataType::TYPE_NUMERIC
274  );
275  } else {
276  $this->workbook->getActiveSheet()->setCellValueExplicit(
277  $a_coords,
278  $this->prepareValue($a_value),
279  DataType::TYPE_STRING
280  );
281  }
282  }
283 
291  public function setCell(
292  int $a_row,
293  int $col,
294  $value,
295  ?string $datatype = null,
296  bool $disable_strip_tags_for_strings = false
297  ): void {
298  $coordinate = $this->getCoordByColumnAndRow($col, $a_row);
299 
300  if ($datatype === DataType::TYPE_STRING) {
301  $this->workbook->getActiveSheet()->setCellValueExplicit(
302  $coordinate,
303  $this->prepareString($value, $disable_strip_tags_for_strings),
304  $datatype
305  );
306  } elseif ($datatype !== null) {
307  $this->workbook->getActiveSheet()->setCellValueExplicit(
308  $coordinate,
309  $this->prepareValue($value),
310  $datatype
311  );
312  } elseif ($value instanceof ilDateTime) {
313  $wb = $this->workbook->getActiveSheet()->setCellValue(
314  $coordinate,
315  $this->prepareValue($value)
316  );
317  $this->setDateFormat($wb->getCell($coordinate), $value);
318  } elseif (is_numeric($value)) {
319  $this->workbook->getActiveSheet()->setCellValueExplicit(
320  $coordinate,
321  $this->prepareValue($value),
322  DataType::TYPE_NUMERIC
323  );
324  } else {
325  $this->workbook->getActiveSheet()->setCellValueExplicit(
326  $coordinate,
327  $this->prepareValue($value),
328  DataType::TYPE_STRING
329  );
330  }
331  }
332 
340  public function setCellArray(
341  array $a_values,
342  string $a_top_left = 'A1',
343  $a_null_value = null
344  ): void {
345  foreach ($a_values as $row_idx => $cols) {
346  if (is_array($cols)) {
347  foreach ($cols as $col_idx => $col_value) {
348  $a_values[$row_idx][$col_idx] = $this->prepareValue($col_value);
349  }
350  } else {
351  $a_values[$row_idx] = $this->prepareValue($cols);
352  }
353  }
354 
355  $this->workbook->getActiveSheet()->fromArray($a_values, $a_null_value, $a_top_left);
356  }
357 
358 
363  public function getCell(
364  int $a_row,
365  int $a_col
366  ) {
367  $coordinate = $this->getCoordByColumnAndRow($a_col, $a_row);
368  return $this->workbook->getActiveSheet()->getCell($coordinate)->getValue();
369  }
370 
374  public function getSheetAsArray(): array
375  {
376  return $this->workbook->getActiveSheet()->toArray();
377  }
378 
382  public function getColumnCount(): int
383  {
384  return Coordinate::columnIndexFromString($this->workbook->getActiveSheet()->getHighestDataColumn());
385  }
386 
390  public function getColumnCoord(int $a_col): string
391  {
392  $col = $this->columnIndexAdjustment($a_col);
393  return Coordinate::stringFromColumnIndex($col);
394  }
395 
399  protected function setGlobalAutoSize(): void
400  {
401  // this may change the active sheet
402  foreach ($this->workbook->getWorksheetIterator() as $worksheet) {
403  $this->workbook->setActiveSheetIndex($this->workbook->getIndex($worksheet));
404  $sheet = $this->workbook->getActiveSheet();
405  $cellIterator = $sheet->getRowIterator()->current()->getCellIterator();
406  $cellIterator->setIterateOnlyExistingCells(true);
407  foreach ($cellIterator as $cell) {
408  $sheet->getColumnDimension($cell->getColumn())->setAutoSize(true);
409  }
410  }
411  }
412 
413  //
414  // deliver/save
415  //
416 
420  protected function prepareStorage(string $a_file_name): string
421  {
422  $this->setGlobalAutoSize();
423  $this->workbook->setActiveSheetIndex(0);
424 
425  switch ($this->format) {
426  case self::FORMAT_BIFF:
427  if (stripos($a_file_name, '.xls') === false) {
428  $a_file_name .= '.xls';
429  }
430  break;
431 
432  case self::FORMAT_XML:
433  if (stripos($a_file_name, '.xlsx') === false) {
434  $a_file_name .= '.xlsx';
435  }
436  break;
437  }
438 
439  return $a_file_name;
440  }
441 
446  public function sendToClient(string $a_file_name): void
447  {
448  $a_file_name = $this->prepareStorage($a_file_name);
449  switch ($this->format) {
450  case self::FORMAT_BIFF:
451  $a_mime_type = MimeType::APPLICATION__VND_MS_EXCEL;
452  break;
453 
454  case self::FORMAT_XML:
455  $a_mime_type = MimeType::APPLICATION__VND_OPENXMLFORMATS_OFFICEDOCUMENT_SPREADSHEETML_SHEET;
456  break;
457  default:
458  $a_mime_type = MimeType::APPLICATION__OCTET_STREAM;
459  break;
460  }
461  $tmp_name = ilFileUtils::ilTempnam();
462 
463  $writer = IOFactory::createWriter($this->workbook, $this->format);
464  $writer->save($tmp_name);
465 
466  ilFileDelivery::deliverFileAttached($tmp_name, $a_file_name, $a_mime_type, true);
467  }
468 
474  public function writeToFile(string $a_file): void
475  {
476  $a_file = $this->prepareStorage($a_file);
477 
478  $writer = IOFactory::createWriter($this->workbook, $this->format);
479  $writer->save($a_file);
480  }
481 
485  public function writeToTmpFile(): string
486  {
487  $writer = IOFactory::createWriter($this->workbook, $this->format);
489  $writer->save($filename);
490 
491  return $filename;
492  }
493 
497  public function setBold(string $a_coords): void
498  {
499  $this->workbook->getActiveSheet()->getStyle($a_coords)->getFont()->setBold(true);
500  }
501 
505  public function setColors(
506  string $a_coords,
507  string $a_background,
508  ?string $a_font = null
509  ): void {
510  $opts = [
511  'fill' => [
512  'fillType' => Fill::FILL_SOLID,
513  'color' => ['rgb' => $a_background]
514  ]
515  ];
516 
517  if ($a_font) {
518  $opts['font'] = [
519  'color' => ['rgb' => $a_font]
520  ];
521  }
522 
523  $this->workbook->getActiveSheet()->getStyle($a_coords)->applyFromArray($opts);
524  }
525 
529  public function setBorders(
530  string $a_coords,
531  bool $a_top,
532  bool $a_right = false,
533  bool $a_bottom = false,
534  bool $a_left = false
535  ): void {
536  $style = $this->workbook->getActiveSheet()->getStyle($a_coords);
537 
538  // :TODO: border styles?
539  if ($a_top) {
540  $style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
541  }
542  if ($a_right) {
543  $style->getBorders()->getRight()->setBorderStyle(Border::BORDER_THIN);
544  }
545  if ($a_bottom) {
546  $style->getBorders()->getBottom()->setBorderStyle(Border::BORDER_THIN);
547  }
548  if ($a_left) {
549  $style->getBorders()->getLeft()->setBorderStyle(Border::BORDER_THIN);
550  }
551  }
552 
556  public function getCoordByColumnAndRow(
557  int $pColumn = 1,
558  int $pRow = 1
559  ): string {
560  $col = $this->columnIndexAdjustment($pColumn);
561  $columnLetter = Coordinate::stringFromColumnIndex($col);
562 
563  return $columnLetter . $pRow;
564  }
565 
569  public function addLink(
570  int $a_row,
571  int $a_column,
572  string $a_path
573  ): void {
574  $coordinate = $this->getCoordByColumnAndRow($a_column, $a_row);
575  $this->workbook->getActiveSheet()->getCell($coordinate)->getHyperlink()->setUrl($a_path);
576  }
577 
582  public function columnIndexAdjustment(int $column): int
583  {
584  return ++$column;
585  }
586 
591  public function mergeCells(string $coordinatesRange): void
592  {
593  $this->workbook->getActiveSheet()->mergeCells($coordinatesRange);
594  }
595 
596  private function cleanupNonCharachters(string $string): string
597  {
598  return mb_ereg_replace('[' . implode('', $this->noncharacters) . ']', '', $string);
599  }
600 }
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)
cleanupNonCharachters(string $string)
setFormat(string $a_format)
Set file format.
setGlobalAutoSize()
Set all existing columns on all sheets to autosize.
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
prepareValue($value)
Prepare value for cell.
ilLanguage $lng
setColors(string $a_coords, string $a_background, ?string $a_font=null)
Set cell(s) colors.
setCell(int $a_row, int $col, $value, ?string $datatype=null, bool $disable_strip_tags_for_strings=false)
Set cell value.
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.
global $DIC
Definition: shib_login.php:26
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.
string $format
prepareString(string $value, bool $disable_strip_tags=false)
string $type
sendToClient(string $a_file_name)
Send workbook to client.