Skip to content

Commit

Permalink
adding Reader::supportsHeaderAsRecordKeys method
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Apr 28, 2017
1 parent 775d39f commit 3ffbf6a
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 63 deletions.
2 changes: 1 addition & 1 deletion docs/9.0/index.md
Expand Up @@ -29,7 +29,7 @@ $csv = Reader::createFromPath('/path/to/your/csv/file.csv')
->setHeaderOffset(0) //set the CSV header offset
;

//get 25 rows starting from the 11th row
//get 25 records starting from the 11th row
$stmt = (new Statement())
->offset(10)
->limit(25)
Expand Down
44 changes: 41 additions & 3 deletions docs/9.0/reader/index.md
Expand Up @@ -10,9 +10,10 @@ title: CSV document Reader connection

class Reader extends AbstractCsv implements IteratorAggregate
{
public function fetchDelimitersOccurrence(array $delimiters, int $nbRows = 1): array
public function fetchDelimitersOccurrence(array $delimiters, int $nb_records = 1): array
public function getHeader(): array
public function getHeaderOffset(): int|null
public function supportsHeaderAsRecordKeys(): bool
public function getIterator(): Iterator
public function getRecords(): Iterator
public function setHeaderOffset(?int $offset): self
Expand All @@ -29,6 +30,7 @@ Many examples in this reference require an CSV file. We will use the following f
"First Name","Last Name",E-mail
john,doe,john.doe@example.com
jane,doe,jane.doe@example.com
john,john,john.john@example.com

## Detecting the delimiter character

Expand Down Expand Up @@ -125,7 +127,9 @@ $header = $csv->getHeader(); //triggers a Exception
~~~php
<?php

public Reader::getIterator(void): Iterator
public Reader::getRecords(void): Iterator
public Reader::supportsHeaderAsRecordKeys(): bool
~~~

The `Reader` class let's you access all its records using the `Reader::getRecords` method. This method which accepts no argument and returns an `Iterator` containing all CSV document records will:
Expand Down Expand Up @@ -159,7 +163,7 @@ foreach ($records as $offset => $record) {

### Usage with a specified header

If a header offset is set, the found header record will be combine to the CSV records to return associated arrays whose indexes are composed of the header values.
If a header offset is set, the found header record will be combine to the CSV records to return associated arrays whose keys are composed of the header values.

~~~php
<?php
Expand All @@ -183,7 +187,41 @@ foreach ($records as $offset => $record) {

<p class="message-notice">If a record header is used, it will be skipped from the iteration.</p>

<p class="message-warning">If the record header contains non unique values, a <code>RuntimeException</code> exception will be triggered.</p>
### Exception

A `RuntimeException` exception will be triggered if the record used as a header contains non unique values. To determine the state of the selected record you can used the`Reader::supportsHeaderAsRecordKeys` method which returns `true` if `Reader::getHeader` returns

- an empty array;
- or, an array containing unique string values;

~~~php
<?php

use League\Csv\Reader;

$reader = Reader::createFromPath('/path/to/my/file.csv')
->setHeaderOffset(3)
;

//var_export($reader->getHeader()) returns something like
// array(
// 'john',
// 'john',
// 'john.john@example.com'
// );
//
$reader->supportsHeaderAsRecordKeys(); //return false;
$reader->setHeaderOffset(0);
//var_export($reader->getHeader()) returns something like
// array(
// 'First Name',
// 'Last Name',
// 'E-mail'
// );
//
$reader->supportsHeaderAsRecordKeys(); //return true;
~~~


### Easing iteration

Expand Down
6 changes: 3 additions & 3 deletions docs/9.0/writer/index.md
Expand Up @@ -44,16 +44,16 @@ public Writer::insertAll(iterable $records): int

use League\Csv\Writer;

$rows = [
$records = [
[1, 2, 3],
['foo', 'bar', 'baz'],
['john', 'doe', 'john.doe@example.com'],
];

$writer = Writer::createFromPath('/path/to/saved/file.csv', 'w+');
$writer->insertOne(['john', 'doe', 'john.doe@example.com']);
$writer->insertAll($rows); //using an array
$writer->insertAll(new ArrayIterator($rows)); //using a Traversable object
$writer->insertAll($records); //using an array
$writer->insertAll(new ArrayIterator($records)); //using a Traversable object
~~~

In the above example, all CSV records are saved to `/path/to/saved/file.csv`
Expand Down
120 changes: 64 additions & 56 deletions src/Reader.php
Expand Up @@ -126,31 +126,13 @@ protected function getCellCount(string $delimiter, int $nb_records): int
return is_array($record) && count($record) > 1;
};

$this->document->setFlags(SplFileObject::READ_CSV);
$this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
$this->document->setCsvControl($delimiter, $this->enclosure, $this->escape);
$iterator = new CallbackFilterIterator(new LimitIterator($this->document, 0, $nb_records), $filter);

return count(iterator_to_array($iterator, false), COUNT_RECURSIVE);
}

/**
* @inheritdoc
*/
public function getIterator(): Iterator
{
$bom = $this->getInputBOM();
$header = $this->getHeader();
$this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
$this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$normalized = function ($record): bool {
return is_array($record) && $record != [null];
};

$iterator = $this->combineHeader(new CallbackFilterIterator($this->document, $normalized), $header);

return $this->stripBOM($iterator, $bom);
}

/**
* Returns the CSV records in an iterator object.
*
Expand All @@ -174,15 +156,45 @@ public function getRecords(): Iterator
return $this->getIterator();
}

/**
* @inheritdoc
*/
public function getIterator(): Iterator
{
$bom = $this->getInputBOM();
if (!$this->supportsHeaderAsRecordKeys()) {
throw new RuntimeException('The header record must be empty or a flat array with unique string values');
}
$this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
$this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$normalized = function ($record): bool {
return is_array($record) && $record != [null];
};

$iterator = $this->combineHeader(new CallbackFilterIterator($this->document, $normalized));

return $this->stripBOM($iterator, $bom);
}

/**
* Returns wether the selected header can be combine to each record
*
* A valid header must be empty or contains unique string field names
*
* @return bool
*/
public function supportsHeaderAsRecordKeys(): bool
{
$header = $this->getHeader();

return empty($header) || $header === array_unique(array_filter($header, 'is_string'));
}

/**
* Returns the CSV record header
*
* The returned header is represented as an array of string values
*
* @throws RuntimeException If the header offset is an integer
* and the corresponding record is missing
* or is an empty array
*
* @return string[]
*/
public function getHeader(): array
Expand All @@ -192,47 +204,61 @@ public function getHeader(): array
}

$this->is_header_loaded = true;
if (null === $this->header_offset) {
$this->header = [];

return $this->header;
$this->header = [];
if (null !== $this->header_offset) {
$this->header = $this->setHeader($this->header_offset);
}

return $this->header;
}

/**
* Determine the CSV record header
*
* @param int $offset
*
* @throws RuntimeException If the header offset is an integer
* and the corresponding record is missing
* or is an empty array
*
* @return string[]
*/
protected function setHeader(int $offset): array
{
$this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
$this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
$this->document->seek($this->header_offset);
$this->header = $this->document->current();
if (empty($this->header)) {
throw new RuntimeException(sprintf('The header record does not exist or is empty at offset: `%s`', $this->header_offset));
$this->document->seek($offset);
$header = $this->document->current();
if (empty($header)) {
throw new RuntimeException(sprintf('The header record does not exist or is empty at offset: `%s`', $offset));
}

if (0 === $this->header_offset) {
$this->header = $this->removeBOM($this->header, mb_strlen($this->getInputBOM()), $this->enclosure);
if (0 === $offset) {
$header = $this->removeBOM($header, mb_strlen($this->getInputBOM()), $this->enclosure);
}

return $this->header;
return $header;
}

/**
* Add the CSV header if present and valid
*
* @param Iterator $iterator
* @param string[] $header
*
* @return Iterator
*/
protected function combineHeader(Iterator $iterator, array $header): Iterator
protected function combineHeader(Iterator $iterator): Iterator
{
if (null === $this->header_offset) {
return $iterator;
}

$header = $this->filterColumnNames($header);
$header_count = count($header);
$iterator = new CallbackFilterIterator($iterator, function (array $record, int $offset): bool {
return $offset != $this->header_offset;
});

$header = $this->getHeader();
$header_count = count($header);
$mapper = function (array $record) use ($header_count, $header): array {
if ($header_count != count($record)) {
$record = array_slice(array_pad($record, $header_count, null), 0, $header_count);
Expand All @@ -244,24 +270,6 @@ protected function combineHeader(Iterator $iterator, array $header): Iterator
return new MapIterator($iterator, $mapper);
}

/**
* Validates the array to be used by the fetchAssoc method
*
* @param array $keys
*
* @throws RuntimeException If the submitted array fails the assertion
*
* @return array
*/
protected function filterColumnNames(array $keys): array
{
if (empty($keys) || $keys === array_unique(array_filter($keys, 'is_string'))) {
return $keys;
}

throw new RuntimeException('Use a flat array with unique string values');
}

/**
* Strip the BOM sequence if present
*
Expand Down

0 comments on commit 3ffbf6a

Please sign in to comment.