Skip to content

Commit 8b13132

Browse files
committed
Simplify ResultSet instantiation
1 parent 45c4618 commit 8b13132

File tree

8 files changed

+252
-61
lines changed

8 files changed

+252
-61
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ All Notable changes to `Csv` will be documented in this file
1010
- `TabularDataReader::selectAllExcept`
1111
- `Statement::selectAllExcept`
1212
- `ResultSet::createFromTabularData`
13-
- `RdbmsResult`
14-
- `TabularData`
13+
- `ResultSet::createFromRdbms`
14+
- `RdbmsResult` class to allow converting RDBMS result into `ResultSet`
15+
- `TabularData` interface
1516

1617
### Deprecated
1718

1819
- `Writer::relaxEnclosure` use `Writer::necessaryEnclosure`
1920
- `ResultSet::createFromTabularDataReader` use `ResultSet::createFromTabularData`
21+
- `ResultSet::createFromRecords` use `ResultSet::createFromTabularData`
22+
- `ResultSet::__construct` use `ResultSet::createFromTabularData`
2023

2124
### Fixed
2225

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
layout: default
3+
title: Tabular Data Importer
4+
---
5+
6+
# Tabular Data
7+
8+
<p class="message-notice">Starting with version <code>9.22.0</code></p>
9+
10+
Since version `9.6` the package provides a common API to works with tabular data like structure. A tabular data
11+
is data organized in rows and columns. The fact that the package aim at interacting mainly with CSV does not
12+
restrict its usage to CSV document only, In fact if you can provide a tabular data structure to the package
13+
it should be able to manipulate such data with ease. Hence, the introduction of the `TabularData` interface.to allow
14+
interoperates with any tabular structure.
15+
16+
As seen by the package a tabular data is:
17+
18+
- a collection of similar records (preferably consistent in their size);
19+
- an optional header with unique values;
20+
21+
This `TabularData` interface such contract by extending PHP's `IteratorAggregate` interface and by providing the
22+
`getHeader` method which returns a list of unique string (which can be empty if no header is provided).
23+
24+
```php
25+
interface TabularData extends IteratorAggregate
26+
{
27+
/** @return list<string> */
28+
public function getHeader(): array;
29+
}
30+
```
31+
32+
## Basic Usage
33+
34+
Once a `TabularData` implementing object is given to the `ResultSet` class it can be manipulated and inspected as if
35+
it was a CSV document. It will effectively access the full reading API provided by the package.
36+
37+
For instance the `Reader` class implements the `TabularData` interface as such you can instantiate directly
38+
a `ResultSet` instance using the following code:
39+
40+
```php
41+
$resultSet = ResultSet::createFromTabularData(
42+
Reader::createFromPath('path/to/file.csv')
43+
);
44+
```
45+
46+
## Database Importer usage
47+
48+
A common source of tabular data are RDBMS result. From listing the content of a table to returning the result of
49+
a complex query on multiple tables with joins, RDBMS result are always express as tabular data. As such it is possible
50+
to convert them and manipulate via the package. To ease such manipulation the `ResultSet` class exposes the
51+
`ResultSet::createFromRdbms` method:
52+
53+
```php
54+
$connection = new SQLite3( '/path/to/my/db.sqlite');
55+
$stmt = $connection->query("SELECT * FROM users");
56+
$stmt instanceof SQLite3Result || throw new RuntimeException('SQLite3 results not available');
57+
58+
$user24 = ResultSet::createFromRdbms($stmt)->nth(23);
59+
```
60+
61+
the `createFromRdbms` can be used with the following Database Extensions:
62+
63+
- SQLite3 (`SQLite3Result` object)
64+
- MySQL Improved Extension (`mysqli_result` object)
65+
- PostgreSQL (`PgSql\Result` object returned by the `pg_get_result`)
66+
- PDO (`PDOStatement` object)
67+
68+
Behind the scene the named constructor leverages the `League\Csv\RdbmsResult` class which implements the `TabularData` interface.
69+
This class is responsible from converting RDBMS results into `TabularData` instances. But you can also use the class
70+
as a standalone feature to quickly
71+
72+
- retrieve column names from the listed Database extensions as follows:
73+
74+
```php
75+
$connection = pg_connect("dbname=publisher");
76+
$result = pg_query($connection, "SELECT * FROM authors");
77+
$result !== false || throw new RuntimeException('PostgreSQL results not available');
78+
79+
$names = RdbmsResult::columnNames($result);
80+
//will return ['firstname', 'lastname', ...]
81+
```
82+
83+
- convert the result into an `Iterator` using the `records` public static method.
84+
85+
```php
86+
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
87+
$connection = new mysqli("localhost", "my_user", "my_password", "world");
88+
$result = $connection->query("SELECT * FROM authors");
89+
$result instanceOf mysqli_result || throw new RuntimeException('MySQL results not available');
90+
foreach (RdbmsResult::records($stmt) as $record) {
91+
// returns each found record which match the processed query.
92+
}
93+
```
94+
95+
<p class="message-warning">The <code>PDOStatement</code> class does not support rewinding the object.
96+
To work around this limitation, the <code>RdbmsResult</code> stores the results in a
97+
<code>ArrayIterator</code> instance for cache which can lead to huge memory usage if the
98+
returned <code>PDOStatement</code> result is huge.</p>
99+
100+
## Generic Importer Logic
101+
102+
Implementing the `TabularData` should be straightforward, you can easily convert any structure into a `TabularData` instance
103+
using the following logic. Keep in mind that the codebase to generate an instance may vary depending on the source and the
104+
size of your data but the logic should stay the same.
105+
106+
```php
107+
use League\Csv\ResultSet;
108+
use League\Csv\TabularData;
109+
110+
$payload = <<<JSON
111+
[
112+
{"id": 1, "firstname": "Jonn", "lastname": "doe", "email": "john@example.com"},
113+
{"id": 2, "firstname": "Jane", "lastname": "doe", "email": "jane@example.com"},
114+
]
115+
JSON;
116+
117+
$tabularData = new class ($payload) implements TabularData {
118+
private readonly array $header;
119+
private readonly ArrayIterator $records;
120+
public function __construct(string $payload)
121+
{
122+
try {
123+
$data = json_decode($payload, true);
124+
$this->header = array_keys($data[0] ?? []);
125+
$this->records = new ArrayIterator($data);
126+
} catch (Throwable $exception) {
127+
throw new ValueError('The provided JSON payload could not be converted into a Tabular Data instance.', previous: $exception);
128+
}
129+
}
130+
131+
public function getHeader() : array
132+
{
133+
return $this->header;
134+
}
135+
136+
public function getIterator() : Iterator
137+
{
138+
return $this->records;
139+
}
140+
};
141+
142+
$resultSet = ResultSet::createFromTabularData($tabularData);
143+
```

docs/9.0/reader/resultset.md

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -44,36 +44,7 @@ the `createFromRdbms` can be used with the following Database Extensions:
4444
As such using the instance on huge results will trigger high memory usage as all the data will be stored in a
4545
<code>ArrayIterator</code> instance for cache to allow rewinding and inspecting the tabular data.</p>
4646

47-
Behind the scene the named constructor leverages the `RdbmsResult` class which implements the `TabularData` interface.
48-
This class is responsible from converting RDBMS results into TabularData` instances. But you can also use the class
49-
to retrieve column names from the listed Database extensions as follow:
50-
51-
```php
52-
$db = new SQLite3( '/path/to/my/db.sqlite');
53-
$stmt = $db->query("SELECT * FROM users");
54-
$stmt instanceof SQLite3Result || throw new RuntimeException('SQLite3 results not available');
55-
56-
$names = RdbmsResult::columnNames($stmt);
57-
//will return ['firstname', 'lastname', ...]
58-
```
59-
60-
The same class can also convert the Database result into an `Iterator` using the `records` public static method.
61-
62-
```php
63-
$db = new SQLite3( '/path/to/my/db.sqlite');
64-
$stmt = $db->query(
65-
"SELECT *
66-
FROM users
67-
INNER JOIN permissions
68-
ON users.id = permissions.user_id
69-
WHERE users.is_active = 't'
70-
AND permissions.is_active = 't'"
71-
);
72-
$stmt instanceof SQLite3Result || throw new RuntimeException('SQLite3 results not available');
73-
foreach (RdbmsResult::records($stmt) as $record) {
74-
// returns each found record which match the processed query.
75-
}
76-
```
47+
Please refer to the [TabularData Importer](/9.0/interoperability/tabular-data-importer) for more information.
7748

7849
## Selecting records
7950

docs/_data/menu.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ version:
2727
Force Enclosure : '/9.0/interoperability/enclose-field/'
2828
Handling Delimiter : '/9.0/interoperability/swap-delimiter/'
2929
Formula Injection : '/9.0/interoperability/escape-formula-injection/'
30+
Tabular Data: '/9.0/interoperability/tabular-data-importer/'
3031
Converting Records:
3132
Overview: '/9.0/converter/'
3233
Charset Converter: '/9.0/converter/charset/'

src/FragmentFinder.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace League\Csv;
1515

16+
use Iterator;
17+
1618
use function array_filter;
1719
use function array_map;
1820
use function array_reduce;
@@ -117,7 +119,17 @@ private function find(array $parsedExpression, TabularDataReader $tabularDataRea
117119

118120
$selections = array_filter($selections, fn (array $selection) => -1 !== $selection['start']);
119121
if ([] === $selections) {
120-
return [ResultSet::createFromRecords()];
122+
return [ResultSet::createFromTabularData(new class () implements TabularData {
123+
public function getHeader(): array
124+
{
125+
return [];
126+
}
127+
128+
public function getIterator(): Iterator
129+
{
130+
return MapIterator::toIterator([]);
131+
}
132+
})];
121133
}
122134

123135
if (self::TYPE_ROW === $type) {
@@ -143,7 +155,17 @@ private function find(array $parsedExpression, TabularDataReader $tabularDataRea
143155
);
144156

145157
return [match ([]) {
146-
$columns => ResultSet::createFromRecords(),
158+
$columns => ResultSet::createFromTabularData(new class () implements TabularData {
159+
public function getHeader(): array
160+
{
161+
return [];
162+
}
163+
164+
public function getIterator(): Iterator
165+
{
166+
return MapIterator::toIterator([]);
167+
}
168+
}),
147169
default => Statement::create()->select(...$columns)->process($tabularDataReader),
148170
}];
149171
}

src/ResultSet.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,6 @@ public static function createFromTabularData(TabularData $records): self
9898
return new self($records->getIterator(), $records->getHeader());
9999
}
100100

101-
/**
102-
* Returns a new instance from a collection without header.
103-
*/
104-
public static function createFromRecords(iterable $records = []): self
105-
{
106-
return new self(MapIterator::toIterator($records));
107-
}
108-
109101
public function __destruct()
110102
{
111103
unset($this->records);
@@ -675,4 +667,13 @@ public static function createFromTabularDataReader(TabularDataReader $reader): s
675667
{
676668
return self::createFromTabularData($reader);
677669
}
670+
671+
/**
672+
* Returns a new instance from a collection without header.
673+
*/
674+
#[Deprecated(message:'use League\Csv\ResultSet::createFromTabularData() instead', since:'league/csv:9.22.0')]
675+
public static function createFromRecords(iterable $records = []): self
676+
{
677+
return new self(MapIterator::toIterator($records));
678+
}
678679
}

src/ResultSetTest.php

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace League\Csv;
1515

16+
use ArrayIterator;
17+
use Iterator;
1618
use PHPUnit\Framework\Attributes\DataProvider;
1719
use PHPUnit\Framework\Attributes\Group;
1820
use SplTempFileObject;
@@ -51,27 +53,47 @@ protected function tearDown(): void
5153

5254
protected function tabularData(): TabularDataReader
5355
{
54-
return new ResultSet([
55-
['date', 'temperature', 'place'],
56-
['2011-01-01', '1', 'Galway'],
57-
['2011-01-02', '-1', 'Galway'],
58-
['2011-01-03', '0', 'Galway'],
59-
['2011-01-01', '6', 'Berkeley'],
60-
['2011-01-02', '8', 'Berkeley'],
61-
['2011-01-03', '5', 'Berkeley'],
62-
]);
56+
return ResultSet::createFromTabularData(new class () implements TabularData {
57+
public function getHeader(): array
58+
{
59+
return [];
60+
}
61+
62+
public function getIterator(): Iterator
63+
{
64+
return new ArrayIterator([
65+
['date', 'temperature', 'place'],
66+
['2011-01-01', '1', 'Galway'],
67+
['2011-01-02', '-1', 'Galway'],
68+
['2011-01-03', '0', 'Galway'],
69+
['2011-01-01', '6', 'Berkeley'],
70+
['2011-01-02', '8', 'Berkeley'],
71+
['2011-01-03', '5', 'Berkeley'],
72+
]);
73+
}
74+
});
6375
}
6476

6577
protected function tabularDataWithHeader(): TabularDataReader
6678
{
67-
return new ResultSet([
68-
['2011-01-01', '1', 'Galway'],
69-
['2011-01-02', '-1', 'Galway'],
70-
['2011-01-03', '0', 'Galway'],
71-
['2011-01-01', '6', 'Berkeley'],
72-
['2011-01-02', '8', 'Berkeley'],
73-
['2011-01-03', '5', 'Berkeley'],
74-
], ['date', 'temperature', 'place']);
79+
return ResultSet::createFromTabularData(new class () implements TabularData {
80+
public function getHeader(): array
81+
{
82+
return ['date', 'temperature', 'place'];
83+
}
84+
85+
public function getIterator(): Iterator
86+
{
87+
return new ArrayIterator([
88+
['2011-01-01', '1', 'Galway'],
89+
['2011-01-02', '-1', 'Galway'],
90+
['2011-01-03', '0', 'Galway'],
91+
['2011-01-01', '6', 'Berkeley'],
92+
['2011-01-02', '8', 'Berkeley'],
93+
['2011-01-03', '5', 'Berkeley'],
94+
]);
95+
}
96+
});
7597
}
7698

7799
public function testFilter(): void

0 commit comments

Comments
 (0)