yiisoft/data
yiisoft/data provides data providers, pagination, sorting, and filtering for Yii applications. A consistent API lets you read, paginate, sort, and filter data from arrays, databases, or custom sources, simplifying list and API development.
yiisoft/data is a contract package, not a framework. It ships three tiny interfaces plus a couple of reference implementations. You will spend most of your time implementing or composing these, not calling library methods.
composer require yiisoft/data
Look at these first, in this order:
Yiisoft\Data\Reader\DataReaderInterface — read(): iterableYiisoft\Data\Writer\DataWriterInterface — write(iterable $data): voidYiisoft\Data\Processor\DataProcessorInterface — process(iterable $data): iterableYiisoft\Data\ArrayData — the in-memory reader/writer used for tests and glueFirst real use case: give your domain code a source-agnostic way to stream rows. Instead of a controller calling $pdo->query(...) and a service expecting an array, you return a DataReaderInterface and let the consumer decide whether to iterate, paginate, or export.
use Yiisoft\Data\ArrayData;
$reader = new ArrayData([
['id' => 1, 'name' => 'Ada'],
['id' => 2, 'name' => 'Linus'],
]);
foreach ($reader->read() as $row) {
// ...
}
That is the whole starter loop. The value appears once you swap ArrayData for a DB- or API-backed reader without touching callers.
Reader as the universal source. Every data origin — SQL, Elasticsearch, CSV file, remote HTTP — becomes one class implementing read(). Return a generator for large sets, an array for small ones. Callers only know iterable.
final class PdoReader implements DataReaderInterface
{
public function __construct(private \PDO $pdo, private string $sql) {}
public function read(): iterable
{
$stmt = $this->pdo->query($this->sql);
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
yield $row;
}
}
}
Decoration over modification. Add filtering, sorting, limiting, or mapping as wrappers that take a reader and return a reader. This mirrors PSR-7 middleware and keeps each concern testable.
final class LimitingReader implements DataReaderInterface
{
public function __construct(
private DataReaderInterface $inner,
private int $offset,
private int $limit,
) {}
public function read(): iterable
{
$skipped = 0;
$emitted = 0;
foreach ($this->inner->read() as $row) {
if ($skipped++ < $this->offset) continue;
if ($emitted++ >= $this->limit) return;
yield $row;
}
}
}
Writer symmetry. Exporters, queue pushers, and log sinks implement write(). They should consume the iterable lazily and never call iterator_to_array() on it.
final class CsvWriter implements DataWriterInterface
{
public function __construct(private string $path) {}
public function write(iterable $data): void
{
$fh = fopen($this->path, 'wb');
try {
foreach ($data as $row) {
fputcsv($fh, $row);
}
} finally {
fclose($fh);
}
}
}
Processors for in-flight transforms. A processor takes an iterable and yields another. Use it for normalization, deduplication, batching, or augmenting rows before they reach a writer.
final class MappingProcessor implements DataProcessorInterface
{
public function __construct(private callable $fn) {}
public function process(iterable $data): iterable
{
foreach ($data as $item) {
yield ($this->fn)($item);
}
}
}
Yii3 DI wiring. Bind the interfaces to concrete defaults in your container config so services can type-hint DataReaderInterface without knowing the source. Override per-context with named autowiring references or factory closures.
Pipeline composition. The natural pattern is reader → processor(s) → writer. Keep each stage a small class so the pipeline reads like a dataflow diagram and each stage can be unit-tested with ArrayData.
read() may return a generator — treat it as single-pass. No count(), no rewinding, no indexing. If you need multiple passes, materialize into an array explicitly at the call site, and document that choice.
Never assume read() is cheap. The interface gives no hint about side effects. Calling it twice may issue two queries or two HTTP requests. Cache the result if you need it more than once.
Filtering and sorting semantics are not part of the contract. This package deliberately gives you the plumbing, not a query language. If you need cross-source filtering, define your own FilterInterface in your app layer and let each reader interpret it — don't try to bend the base interface.
Partial writes on exception. A write() that throws halfway leaves a truncated file or half-inserted batch. Wrap DB writes in a transaction and file writes in a temp-file-plus-rename pattern; the package will not do this for you.
Generators hide errors until iteration. Exceptions from a DB cursor or HTTP stream surface at foreach, not at construction. Structure your error handling around the iteration point, and log before re-throwing if the consumer swallows exceptions.
Debugging: convert in tests, stream in production. Use iterator_to_array($reader->read()) for assertions and var_dump sanity checks, but never ship that. If you need to inspect a generator mid-stream, wrap it with a logging processor rather than breaking laziness.
Version churn. At 46 stars and pre-1.0 maturity, namespace and interface signatures can shift between minors. Pin the version in composer.json, read the changelog on upgrade, and keep your implementations thin so a signature change is a one-line fix.
Extension points are your own interfaces. The package's three interfaces are the floor, not the ceiling. Add CountableReaderInterface, FilterableReaderInterface, or TransactionalWriterInterface in your app when you need guarantees; implement them alongside the base interfaces so generic consumers still work.
Config quirks are minimal — that's the point. There is no service provider, no facade, no global config. Any "configuration" you need lives in your DI definitions and constructor arguments. Resist the urge to add a config array; constructor injection is idiomatic here.
How can I help you explore Laravel packages today?