Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
CSV
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
3 / 3
10
100.00% covered (success)
100.00%
1 / 1
 build
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
 stringifyRow
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 collectColumnNamesFromHeterogeneousObjects
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/** @noinspection PhpUnhandledExceptionInspection */
3declare(strict_types=1);
4
5// TODO unit Test
6
7class CSV {
8  const allowedDelimiters = [',', ';', '|', '\t', '\s'];
9  const allowedEnclosures = ['"', "'"];
10  const allowedLineEndings = ["\r\n", "\r", "\n"]; // Win / Mac / Unix
11
12  /**
13   * builds string containing data CSV from a collections of heterogeneous arrays
14   *
15   * example:
16   * $data = [
17   *  [
18   *      "color" => "green",
19   *      "form" => "circle"
20   *  ],
21   *  [
22   *      "color" => "blue",
23   *      "pattern" => "dotted"
24   *  ]
25   * ]
26   * echo CSV::build($data);
27   * color,form,pattern
28   * green,circle,
29   * blue,,dotted
30   *
31   *
32   * @param array $data - array of (assoc) arrays. keys are columns names, values are cell values
33   * @param array $columnNames - names of columns to be written in csv. if empty, it will take all keys from the
34   *      provided arrays as columns
35   * @param string $delimiter - delimiter. falls back to default if not allowed
36   * @param string $enclosure - enclosure. falls back to default if not allowed
37   * @param string $lineDelimiter - line-delimiter. falls back to default if not allowed
38   * @return string
39   */
40  static function build(array  $data, array $columnNames = [],
41                        string $delimiter = ',', string $enclosure = '"', string $lineDelimiter = '\n'): string {
42    $columns = (is_array($columnNames) and count($columnNames))
43      ? $columnNames
44      : CSV::collectColumnNamesFromHeterogeneousObjects($data);
45    $enclosure = in_array($enclosure, CSV::allowedEnclosures) ? $enclosure : '"';
46    $delimiter = in_array($delimiter, CSV::allowedDelimiters) ? $delimiter : ',';
47    $lineDelimiter = in_array($lineDelimiter, CSV::allowedLineEndings) ? $lineDelimiter : "\n";
48
49    $csvRows = [];
50
51    $csvRows[] = CSV::stringifyRow($columns, $delimiter, $enclosure);
52
53    foreach ($data as $set) {
54      $row = [];
55
56      foreach ($columns as $column) {
57        $row[] = $set[$column] ?? '';
58      }
59
60      $csvRows[] = CSV::stringifyRow($row, $delimiter, $enclosure);
61    }
62
63    return implode($lineDelimiter, $csvRows);
64  }
65
66  private static function stringifyRow($row, $delimiter, $enclosure): string {
67    return implode(
68      $delimiter,
69      array_map(
70        function($cell) use ($enclosure, $delimiter) {
71          return $enclosure . preg_replace('#(\\' . $enclosure . ')#', '`', (string) $cell) . $enclosure;
72        },
73        $row
74      )
75    );
76  }
77
78  /**
79   * @param array $data - an array ofd assoc arrays
80   * @return array - all used keys once
81   */
82  static function collectColumnNamesFromHeterogeneousObjects(array $data): array {
83    return array_values(array_unique(array_reduce($data, function($agg, $array) {
84      return array_merge($agg, array_keys($array));
85    }, [])));
86  }
87
88}