Passed
Push — master ( 4dc916...6fdd76 )
by Thomas
02:09
created

CSV::getHeaders()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 5
nop 3
dl 0
loc 14
ccs 10
cts 10
cp 1
crap 5
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * Load a CSV file
4
 *
5
 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
6
 * @copyright Copyright (c) 2009-2017 FluentDOM Contributors
7
 */
8
9
namespace FluentDOM\Loader\Text {
10
11
  use FluentDOM\DOM\Document;
12
  use FluentDOM\DOM\DocumentFragment;
13
  use FluentDOM\DOM\Element;
14
  use FluentDOM\Loadable;
15
  use FluentDOM\Loader\Options;
16
  use FluentDOM\Loader\Result;
17
  use FluentDOM\Loader\Supports;
18
  use FluentDOM\Utility\QualifiedName;
19
20
  /**
21
   * Load a CSV file
22
   */
23
  class CSV implements Loadable {
24
25
    use Supports;
26
27
    const XMLNS = 'urn:carica-json-dom.2013';
28
    const DEFAULT_QNAME = '_';
29
30
    private $_delimiter = ',';
31
    private $_enclosure = '"';
32
    private $_escape = '\\';
33
34
    /**
35
     * @return string[]
36
     */
37 13
    public function getSupported(): array {
38 13
      return ['text/csv'];
39
    }
40
41
    /**
42
     * @see Loadable::load
43
     * @param mixed $source
44
     * @param string $contentType
45
     * @param array|\Traversable|Options $options
46
     * @return Document|Result|NULL
47
     * @throws \InvalidArgumentException
48
     * @throws \FluentDOM\Exceptions\InvalidSource
49
     */
50 8
    public function load($source, string $contentType, $options = []) {
51 8
      $options = $this->getOptions($options);
52 8
      $hasHeaderLine = isset($options['HEADER']) ? (bool)$options['HEADER'] : !isset($options['FIELDS']);
53 8
      $this->configure($options);
54 8
      if ($this->supports($contentType) && ($lines = $this->getLines($source, $options))) {
55 7
        $document = new Document('1.0', 'UTF-8');
56 7
        $document->appendChild($list = $document->createElementNS(self::XMLNS, 'json:json'));
57 7
        $list->setAttributeNS(self::XMLNS, 'json:type', 'array');
58 7
        $this->appendLines($list, $lines, $hasHeaderLine, $options['FIELDS'] ?? NULL);
59 7
        return $document;
60
      }
61 1
      return NULL;
62
    }
63
64
    /**
65
     * @see Loadable::loadFragment
66
     *
67
     * @param string $source
68
     * @param string $contentType
69
     * @param array|\Traversable|Options $options
70
     * @return DocumentFragment|NULL
71
     * @throws \InvalidArgumentException
72
     * @throws \FluentDOM\Exceptions\InvalidSource
73
     */
74 3
    public function loadFragment($source, string $contentType, $options = []) {
75 3
      $options = $this->getOptions($options);
76 3
      $options[Options::ALLOW_FILE] = FALSE;
77 3
      $hasHeaderLine = isset($options['FIELDS']) ? FALSE : (isset($options['HEADER']) && $options['HEADER']);
78 3
      $this->configure($options);
79 3
      if ($this->supports($contentType) && ($lines = $this->getLines($source, $options))) {
80 2
        $document = new Document('1.0', 'UTF-8');
81 2
        $fragment = $document->createDocumentFragment();
82 2
        $this->appendLines($fragment, $lines, $hasHeaderLine, $options['FIELDS'] ?? NULL);
83 2
        return $fragment;
84
      }
85 1
      return NULL;
86
    }
87
88
    /**
89
     * Append the provided lines to the parent.
90
     *
91
     * @param \DOMNode $parent
92
     * @param array|\Traversable $lines
93
     * @param $hasHeaderLine
94
     * @param array $columns
95
     */
96 9
    private function appendLines(\DOMNode $parent, $lines, $hasHeaderLine, array $columns = NULL) {
97 9
      $document = $parent instanceof \DOMDocument ? $parent : $parent->ownerDocument;
98 9
      $headers = NULL;
99
      /** @var array $record */
100 9
      foreach ($lines as $record) {
101 9
        if ($headers === NULL) {
102 9
          $headers = $this->getHeaders(
103 9
            $record, $hasHeaderLine, $columns
104
          );
105 9
          if ($hasHeaderLine) {
106 5
            continue;
107
          }
108
        }
109
        /** @var Element $node */
110 9
        $node = $parent->appendChild($document->createElement(self::DEFAULT_QNAME));
111 9
        foreach ($record as $index => $field) {
112 9
          if (isset($headers[$index])) {
113 9
            $this->appendField($node, $headers[$index], $field);
114
          }
115
        }
116
      }
117 9
    }
118
119
    /**
120
     * @param Element $parent
121
     * @param string $name
122
     * @param string $value
123
     */
124 9
    private function appendField(Element $parent, $name, $value) {
125 9
      $qname = QualifiedName::normalizeString($name, self::DEFAULT_QNAME);
126 9
      $child = $parent->appendElement($qname, $value);
127 9
      if ($qname !== $name) {
128 3
        $child->setAttributeNS(self::XMLNS, 'json:name', $name);
129
      }
130 9
    }
131
132
    /**
133
     * @param array $record
134
     * @param bool $hasHeaderLine
135
     * @param array|NULL $columns
136
     * @return array
137
     */
138 9
    private function getHeaders(array $record, $hasHeaderLine, $columns = NULL) {
139 9
      if (is_array($columns)) {
140 2
        $headers = [];
141 2
        foreach ($record as $index => $field) {
142 2
          $key = $hasHeaderLine ? $field : $index;
143 2
          $headers[$index] = $columns[$key] ?? FALSE;
144
        }
145 2
        return $headers;
146 7
      } elseif ($hasHeaderLine) {
147 5
        return $record;
148
      } else {
149 2
        return array_keys($record);
150
      }
151
    }
152
153
    /**
154
     * @param mixed $source
155
     * @param Options $options
156
     * @return NULL|\Traversable
157
     * @throws \FluentDOM\Exceptions\InvalidSource
158
     */
159 10
    private function getLines($source, Options $options) {
160 10
      $result = NULL;
161 10
      if (is_string($source)) {
162 7
        $options->isAllowed($sourceType = $options->getSourceType($source));
163 7
        if ($sourceType === Options::IS_FILE) {
164 1
          $result = new \SplFileObject($source);
165
        } else {
166 6
          $result = new \SplFileObject('data://text/csv;base64,'.base64_encode($source));
167
        }
168 7
        $result->setFlags(\SplFileObject::READ_CSV);
169 7
        $result->setCsvControl(
170 7
          $this->_delimiter,
171 7
          $this->_enclosure,
172 7
          $this->_escape
173
        );
174 3
      } elseif (is_array($source)) {
175 1
        $result = new \ArrayIterator($source);
176 2
      } elseif ($source instanceof \Traversable) {
177 1
        $result = $source;
178
      }
179 10
      return empty($result) ? NULL : $result;
180
    }
181
182
    /**
183
     * @param array|\Traversable|Options $options
184
     */
185 11
    private function configure($options) {
186 11
      $this->_delimiter = $options['DELIMITER'] ?? $this->_delimiter;
187 11
      $this->_enclosure = $options['ENCLOSURE'] ?? $this->_enclosure;
188 11
      $this->_escape = $options['ESCAPE'] ?? $this->_escape;
189 11
    }
190
191
    /**
192
     * @param array|\Traversable|Options $options
193
     * @return Options
194
     * @throws \InvalidArgumentException
195
     */
196 11
    public function getOptions($options) {
197 11
      $result = new Options(
198 11
        $options,
199
        [
200 11
          Options::CB_IDENTIFY_STRING_SOURCE => function($source) {
201 6
            return (is_string($source) && (FALSE !== strpos($source, "\n")));
202 11
          }
203
        ]
204
      );
205 11
      return $result;
206
    }
207
  }
208
209
}