Skip to content

Commit

Permalink
Introduce the TabularData interface
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Jan 18, 2025
1 parent 6dbb173 commit e9ddd39
Show file tree
Hide file tree
Showing 11 changed files with 509 additions and 62 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ All Notable changes to `Csv` will be documented in this file
- `Writer::necessaryEnclosure`
- `TabularDataReader::selectAllExcept`
- `Statement::selectAllExcept`
- `ResultSet::createFromTabularData`
- `RdbmsResult`
- `TabularData`

### Deprecated

- `Writer::relaxEnclosure` use `Writer::necessaryEnclosure`
- `ResultSet::createFromTabularDataReader` use `ResultSet::createFromTabularData`

### Fixed

- `Comparison::CONTAINS` must check the value is a string before calling `str_compare` [#548](https://github.com/thephpleague/csv/pull/548) by [cage-is](https://github.com/cage-is)
- Fix testing to improve Debian integration [#549](https://github.com/thephpleague/csv/pull/549) by [David Prévot and tenzap](https://github.com/tenzap)
- `Bom::tryFromSequence` and `Bom::fromSequence` supports the `Reader` and `Writer` classes.
- `ResultSet::createFromRecords` now automatically set the header for any `TabularDataReader` or `PDOStatement` instance.

### Removed

Expand Down
18 changes: 11 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@
"require-dev": {
"ext-dom": "*",
"ext-xdebug": "*",
"friendsofphp/php-cs-fixer": "^3.64.0",
"friendsofphp/php-cs-fixer": "^3.68.1",
"phpbench/phpbench": "^1.3.1",
"phpstan/phpstan": "^1.12.11",
"phpstan/phpstan": "^1.12.15",
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"phpstan/phpstan-strict-rules": "^1.6.1",
"phpunit/phpunit": "^10.5.16 || ^11.4.3",
"symfony/var-dumper": "^6.4.8 || ^7.1.8"
"phpunit/phpunit": "^10.5.16 || ^11.5.3",
"symfony/var-dumper": "^6.4.8 || ^7.2.0"
},
"autoload": {
"psr-4": {
Expand Down Expand Up @@ -68,9 +68,13 @@
"test": "Runs full test suite"
},
"suggest": {
"ext-iconv" : "Needed to ease transcoding CSV using iconv stream filters",
"ext-dom" : "Required to use the XMLConverter and the HTMLConverter classes",
"ext-mbstring": "Needed to ease transcoding CSV using mb stream filters"
"ext-iconv" : "Needed to ease transcoding CSV using iconv stream filters",
"ext-mbstring": "Needed to ease transcoding CSV using mb stream filters",
"ext-pdo": "Required to use the package with the PDO extension",
"ext-sqlite3": "Required to use the package with the SQLite3 extension",
"ext-mysqli": "Requiered to use the package with the MySQLi extension",
"ext-pgsql": "Requiered to use the package with the PgSQL extension"
},
"extra": {
"branch-alias": {
Expand Down
1 change: 1 addition & 0 deletions docs/9.0/reader/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ the `League\Csv\TabularDataReader` interface.

<p class="message-notice">Starting with version <code>9.1.0</code>, <code>createFromPath</code> has its default <code>open_mode</code> parameter set to <code>r</code>.</p>
<p class="message-notice">Prior to <code>9.1.0</code>, the open mode was <code>r+</code> which looks for write permissions on the file and throws an <code>Exception</code> if the file cannot be opened with the permission set. For sake of clarity, it is strongly suggested to set <code>r</code> mode on the file to ensure it can be opened.</p>
<p class="message-info">Starting with version <code>9.22.0</code>, the class implements the <code>League\Csv\TabularData</code> interface.</p>

The `Reader` provides a convenient and straight forward API to access and handle CSV. While most
of its capabilities are explained in the [Tabular Data Reader documentation page](/9.0/reader/tabular-data-reader),
Expand Down
34 changes: 34 additions & 0 deletions docs/9.0/reader/resultset.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,40 @@ A `League\Csv\ResultSet` object represents the associated result set of processi
This object is returned from [Statement::process](/9.0/reader/statement/#apply-the-constraints-to-a-csv-document) execution.

<p class="message-info">Starting with version <code>9.6.0</code>, the class implements the <code>League\Csv\TabularDataReader</code> interface.</p>
<p class="message-info">Starting with version <code>9.22.0</code>, the class implements the <code>League\Csv\TabularData</code> interface.</p>

## Instantiation

<p class="message-notice">Starting with version <code>9.22.0</code></p>

The `ResultSet` object can be instantiated from other objects than `Statement`.

You can instantiate it directly any object that implements the `League\Csv\TabularData` like the `Reader` class:

```php
$resultSet = ResultSet::createFromTabularData(Reader::createFromPath('path/to/file.csv'));
```

But you can also instantiate it from RDBMS results using the `ResultSet::createFromRdbms` method:

```php
$db = new SQLite3( '/path/to/my/db.sqlite');
$stmt = $db->query("SELECT * FROM users");
$stmt instanceof SQLite3Result || throw new RuntimeException('SQLite3 results not available');

$user24 = ResultSet::createFromRdbms($stmt)->nth(23);
```

the `createFromRdbms` can be used with the following Database Extensions:

- SQLite3 (`SQLite3Result` object)
- MySQL Improved Extension (`mysqli_result` object)
- PostgreSQL (`PgSql\Result` object returned by the `pg_get_result`)
- PDO (`PDOStatement` object)

<p class="message-warning">Beware when using the <code>PDOStatement</code>, the class does not support rewinding the object.
As such using the instance on huge results will trigger high memory usage as all the data will be stored in a
<code>ArrayIterator</code> instance for cache to allow rewinding and inspecting the tabular data.</p>

## Selecting records

Expand Down
243 changes: 243 additions & 0 deletions src/RdbmsResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
<?php

/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace League\Csv;

use ArrayIterator;
use Iterator;
use mysqli_result;
use PDO;
use PDOStatement;
use PgSql\Result;
use RuntimeException;
use SQLite3Result;
use Throwable;
use ValueError;

use function array_column;
use function array_map;
use function pg_fetch_assoc;
use function pg_field_name;
use function pg_num_fields;
use function pg_result_seek;
use function range;

use const SQLITE3_ASSOC;

final class RdbmsResult implements TabularData
{
private function __construct(private readonly Iterator $records, private readonly array $headers)
{
}

public function getHeader(): array
{
return $this->headers;
}

public function getIterator(): Iterator
{
return $this->records;
}

public static function tryFrom(object $result): ?self
{
try {
return self::from($result);
} catch (Throwable) {
return null;
}
}

/**
* @throws RuntimeException If the DB result is unknown or unsupported
*/
public static function from(object $result): self
{
return new self(self::records($result), self::columnNames($result));
}

/**
* @throws RuntimeException If the DB result is unknown or unsupported or no column names information is found.
*
* @return array<string>
*/
public static function columnNames(object $result): array
{
return match (true) {
$result instanceof PDOStatement => array_map(
function (int $i) use ($result): string {
$metadata = $result->getColumnMeta($i);
false !== $metadata || throw new RuntimeException('Unable to get metadata for column '.$i);

return $metadata['name'];
},
range(0, $result->columnCount() - 1)
),
$result instanceof Result => array_map(fn (int $index) => pg_field_name($result, $index), range(0, pg_num_fields($result) - 1)),
$result instanceof mysqli_result => array_column($result->fetch_fields(), 'name'),
$result instanceof SQLite3Result => array_map($result->columnName(...), range(0, $result->numColumns() - 1)),
default => throw new ValueError('Unknown or unsupported RDBMS result object '.$result::class),
};
}

public static function records(object $result): Iterator
{
return match (true) {
$result instanceof SQLite3Result => new class ($result) implements Iterator {
private array|false $current;
private int $key = 0;

public function __construct(private SQLite3Result $result)
{
}

public function rewind(): void
{
$this->result->reset();
$this->current = $this->result->fetchArray(SQLITE3_ASSOC);
$this->key = 0;
}

public function current(): array|false
{
return $this->current;
}

public function key(): string|int|null
{
return $this->key;
}

public function next(): void
{
$this->current = $this->result->fetchArray(SQLITE3_ASSOC);
$this->key++;
}

public function valid(): bool
{
return false !== $this->current;
}
},
$result instanceof mysqli_result => new class ($result) implements Iterator {
private array|false|null $current;
private int $key = 0;

public function __construct(private mysqli_result $result)
{
}

public function rewind(): void
{
$this->result->data_seek(0);
$this->current = $this->result->fetch_assoc();
$this->key = 0;
}

public function current(): array|false|null
{
return $this->current;
}

public function key(): string|int|null
{
return $this->key;
}

public function next(): void
{
$this->current = $this->result->fetch_assoc();
$this->key++;
}

public function valid(): bool
{
return false !== $this->current
&& null !== $this->current;
}
},
$result instanceof Result => new class ($result) implements Iterator {
private array|false|null $current;
private int $key = 0;

public function __construct(private Result $result)
{
}

public function rewind(): void
{
pg_result_seek($this->result, 0);
$this->current = pg_fetch_assoc($this->result);
$this->key = 0;
}

public function current(): array|false|null
{
return $this->current;
}

public function key(): string|int|null
{
return $this->key;
}

public function next(): void
{
$this->current = pg_fetch_assoc($this->result);
$this->key++;
}

public function valid(): bool
{
return false !== $this->current
&& null !== $this->current;
}
},
$result instanceof PDOStatement => new class ($result) implements Iterator {
private ?ArrayIterator $cacheIterator;

public function __construct(private PDOStatement $result)
{
}

public function rewind(): void
{
$this->cacheIterator ??= new ArrayIterator($this->result->fetchAll(PDO::FETCH_ASSOC));
$this->cacheIterator->rewind();
}

public function current(): mixed
{
return $this->cacheIterator?->current() ?? false;
}

public function key(): string|int|null
{
return $this->cacheIterator?->key() ?? null;
}

public function next(): void
{
$this->cacheIterator?->next();
}

public function valid(): bool
{
return $this->cacheIterator?->valid() ?? false;
}
},
default => throw new ValueError('Unknown or unsupported RDBMS result object '.$result::class),
};
}
}
Loading

0 comments on commit e9ddd39

Please sign in to comment.