|i', LF, $cells); } fclose($handle); } if ($maximumColumns > $maximumCellCount) { $maximumCellCount = $maximumColumns; } foreach ($multiArray as &$row) { for ($key = 0; $key < $maximumCellCount; $key++) { if ( $maximumColumns > 0 && $maximumColumns < $maximumCellCount && $key >= $maximumColumns ) { if (isset($row[$key])) { unset($row[$key]); } } elseif (!isset($row[$key])) { $row[$key] = ''; } } } return $multiArray; } /** * Takes a row and returns a CSV string of the values with $delim (default is ,) and $quote (default is ") as separator chars. * * @param string[] $row Input array of values * @param string $delim Delimited, default is comma * @param string $quote Quote-character to wrap around the values. * @param int $type Output behaviour concerning potentially harmful control literals * @return string A single line of CSV */ public static function csvValues(array $row, string $delim = ',', string $quote = '"', int $type = self::TYPE_REMOVE_CONTROLS): string { $resource = fopen('php://temp', 'w'); if (!is_resource($resource)) { throw new \RuntimeException('Cannot open temporary data stream for writing', 1625556521); } $modifier = CsvStreamFilter::applyStreamFilter($resource, false); array_map(self::assertCellValueType(...), $row); if ($type === self::TYPE_REMOVE_CONTROLS) { $row = array_map(self::removeControlLiterals(...), $row); } elseif ($type === self::TYPE_PREFIX_CONTROLS) { $row = array_map(self::prefixControlLiterals(...), $row); } fputcsv($resource, $modifier($row), $delim, $quote, '\\'); fseek($resource, 0); return stream_get_contents($resource); } /** * Prefixes control literals at the beginning of a cell value with a single quote * * (e.g. `=+value` --> `'=+value`) */ protected static function prefixControlLiterals(bool|int|float|string|null $cellValue): bool|int|float|string|null { if (!self::shallFilterValue($cellValue)) { return $cellValue; } $cellValue = (string)$cellValue; return preg_replace('#^([\t\v=+*%/@-])#', '\'${1}', $cellValue); } /** * Removes control literals from the beginning of a cell value * * (e.g. `=+value` --> `value`) */ protected static function removeControlLiterals(bool|int|float|string|null $cellValue): bool|int|float|string|null { if (!self::shallFilterValue($cellValue)) { return $cellValue; } $cellValue = (string)$cellValue; return preg_replace('#^([\t\v=+*%/@-]+)+#', '', $cellValue); } /** * Asserts scalar or null types for given cell value. */ protected static function assertCellValueType(mixed $cellValue): void { // int, float, string, bool, null if ($cellValue === null || is_scalar($cellValue)) { return; } throw new \RuntimeException( sprintf('Unexpected type %s for cell value', gettype($cellValue)), 1625562833 ); } /** * Whether cell value shall be filtered. * * This applies to everything that is not or cannot be represented * as boolean, integer or float. */ protected static function shallFilterValue(bool|int|float|string|null $cellValue): bool { return $cellValue !== null && !is_bool($cellValue) && !is_numeric($cellValue) && !MathUtility::canBeInterpretedAsInteger($cellValue) && !MathUtility::canBeInterpretedAsFloat($cellValue); } }