Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Serializer\Encoder; Chris@0: Chris@0: use Symfony\Component\Serializer\Exception\InvalidArgumentException; Chris@0: Chris@0: /** Chris@0: * Encodes CSV data. Chris@0: * Chris@0: * @author Kévin Dunglas Chris@14: * @author Oliver Hoff Chris@0: */ Chris@0: class CsvEncoder implements EncoderInterface, DecoderInterface Chris@0: { Chris@0: const FORMAT = 'csv'; Chris@14: const DELIMITER_KEY = 'csv_delimiter'; Chris@14: const ENCLOSURE_KEY = 'csv_enclosure'; Chris@14: const ESCAPE_CHAR_KEY = 'csv_escape_char'; Chris@14: const KEY_SEPARATOR_KEY = 'csv_key_separator'; Chris@14: const HEADERS_KEY = 'csv_headers'; Chris@0: Chris@0: private $delimiter; Chris@0: private $enclosure; Chris@0: private $escapeChar; Chris@0: private $keySeparator; Chris@0: Chris@0: /** Chris@0: * @param string $delimiter Chris@0: * @param string $enclosure Chris@0: * @param string $escapeChar Chris@0: * @param string $keySeparator Chris@0: */ Chris@0: public function __construct($delimiter = ',', $enclosure = '"', $escapeChar = '\\', $keySeparator = '.') Chris@0: { Chris@0: $this->delimiter = $delimiter; Chris@0: $this->enclosure = $enclosure; Chris@0: $this->escapeChar = $escapeChar; Chris@0: $this->keySeparator = $keySeparator; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@17: public function encode($data, $format, array $context = []) Chris@0: { Chris@0: $handle = fopen('php://temp,', 'w+'); Chris@0: Chris@17: if (!\is_array($data)) { Chris@17: $data = [[$data]]; Chris@0: } elseif (empty($data)) { Chris@17: $data = [[]]; Chris@0: } else { Chris@0: // Sequential arrays of arrays are considered as collections Chris@0: $i = 0; Chris@0: foreach ($data as $key => $value) { Chris@17: if ($i !== $key || !\is_array($value)) { Chris@17: $data = [$data]; Chris@0: break; Chris@0: } Chris@0: Chris@0: ++$i; Chris@0: } Chris@0: } Chris@0: Chris@14: list($delimiter, $enclosure, $escapeChar, $keySeparator, $headers) = $this->getCsvOptions($context); Chris@0: Chris@14: foreach ($data as &$value) { Chris@17: $flattened = []; Chris@14: $this->flatten($value, $flattened, $keySeparator); Chris@14: $value = $flattened; Chris@14: } Chris@14: unset($value); Chris@0: Chris@14: $headers = array_merge(array_values($headers), array_diff($this->extractHeaders($data), $headers)); Chris@14: Chris@14: fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar); Chris@14: Chris@14: $headers = array_fill_keys($headers, ''); Chris@14: foreach ($data as $row) { Chris@14: fputcsv($handle, array_replace($headers, $row), $delimiter, $enclosure, $escapeChar); Chris@0: } Chris@0: Chris@0: rewind($handle); Chris@0: $value = stream_get_contents($handle); Chris@0: fclose($handle); Chris@0: Chris@0: return $value; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function supportsEncoding($format) Chris@0: { Chris@0: return self::FORMAT === $format; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@17: public function decode($data, $format, array $context = []) Chris@0: { Chris@0: $handle = fopen('php://temp', 'r+'); Chris@0: fwrite($handle, $data); Chris@0: rewind($handle); Chris@0: Chris@0: $headers = null; Chris@0: $nbHeaders = 0; Chris@17: $headerCount = []; Chris@17: $result = []; Chris@0: Chris@14: list($delimiter, $enclosure, $escapeChar, $keySeparator) = $this->getCsvOptions($context); Chris@14: Chris@14: while (false !== ($cols = fgetcsv($handle, 0, $delimiter, $enclosure, $escapeChar))) { Chris@17: $nbCols = \count($cols); Chris@0: Chris@0: if (null === $headers) { Chris@0: $nbHeaders = $nbCols; Chris@0: Chris@0: foreach ($cols as $col) { Chris@14: $header = explode($keySeparator, $col); Chris@14: $headers[] = $header; Chris@17: $headerCount[] = \count($header); Chris@0: } Chris@0: Chris@0: continue; Chris@0: } Chris@0: Chris@17: $item = []; Chris@0: for ($i = 0; ($i < $nbCols) && ($i < $nbHeaders); ++$i) { Chris@14: $depth = $headerCount[$i]; Chris@0: $arr = &$item; Chris@0: for ($j = 0; $j < $depth; ++$j) { Chris@0: // Handle nested arrays Chris@0: if ($j === ($depth - 1)) { Chris@0: $arr[$headers[$i][$j]] = $cols[$i]; Chris@0: Chris@0: continue; Chris@0: } Chris@0: Chris@0: if (!isset($arr[$headers[$i][$j]])) { Chris@17: $arr[$headers[$i][$j]] = []; Chris@0: } Chris@0: Chris@0: $arr = &$arr[$headers[$i][$j]]; Chris@0: } Chris@0: } Chris@0: Chris@0: $result[] = $item; Chris@0: } Chris@0: fclose($handle); Chris@0: Chris@0: if (empty($result) || isset($result[1])) { Chris@0: return $result; Chris@0: } Chris@0: Chris@0: // If there is only one data line in the document, return it (the line), the result is not considered as a collection Chris@0: return $result[0]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function supportsDecoding($format) Chris@0: { Chris@0: return self::FORMAT === $format; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Flattens an array and generates keys including the path. Chris@0: * Chris@0: * @param array $array Chris@0: * @param array $result Chris@14: * @param string $keySeparator Chris@0: * @param string $parentKey Chris@0: */ Chris@14: private function flatten(array $array, array &$result, $keySeparator, $parentKey = '') Chris@0: { Chris@0: foreach ($array as $key => $value) { Chris@17: if (\is_array($value)) { Chris@14: $this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator); Chris@0: } else { Chris@0: $result[$parentKey.$key] = $value; Chris@0: } Chris@0: } Chris@0: } Chris@14: Chris@14: private function getCsvOptions(array $context) Chris@14: { Chris@14: $delimiter = isset($context[self::DELIMITER_KEY]) ? $context[self::DELIMITER_KEY] : $this->delimiter; Chris@14: $enclosure = isset($context[self::ENCLOSURE_KEY]) ? $context[self::ENCLOSURE_KEY] : $this->enclosure; Chris@14: $escapeChar = isset($context[self::ESCAPE_CHAR_KEY]) ? $context[self::ESCAPE_CHAR_KEY] : $this->escapeChar; Chris@14: $keySeparator = isset($context[self::KEY_SEPARATOR_KEY]) ? $context[self::KEY_SEPARATOR_KEY] : $this->keySeparator; Chris@17: $headers = isset($context[self::HEADERS_KEY]) ? $context[self::HEADERS_KEY] : []; Chris@14: Chris@17: if (!\is_array($headers)) { Chris@17: throw new InvalidArgumentException(sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, \gettype($headers))); Chris@14: } Chris@14: Chris@17: return [$delimiter, $enclosure, $escapeChar, $keySeparator, $headers]; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @return string[] Chris@14: */ Chris@14: private function extractHeaders(array $data) Chris@14: { Chris@17: $headers = []; Chris@17: $flippedHeaders = []; Chris@14: Chris@14: foreach ($data as $row) { Chris@14: $previousHeader = null; Chris@14: Chris@14: foreach ($row as $header => $_) { Chris@14: if (isset($flippedHeaders[$header])) { Chris@14: $previousHeader = $header; Chris@14: continue; Chris@14: } Chris@14: Chris@14: if (null === $previousHeader) { Chris@17: $n = \count($headers); Chris@14: } else { Chris@14: $n = $flippedHeaders[$previousHeader] + 1; Chris@14: Chris@17: for ($j = \count($headers); $j > $n; --$j) { Chris@14: ++$flippedHeaders[$headers[$j] = $headers[$j - 1]]; Chris@14: } Chris@14: } Chris@14: Chris@14: $headers[$n] = $header; Chris@14: $flippedHeaders[$header] = $n; Chris@14: $previousHeader = $header; Chris@14: } Chris@14: } Chris@14: Chris@14: return $headers; Chris@14: } Chris@0: }