From 59079055c765e55db04b0b2349303e29668a5034 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 12 Dec 2025 17:47:35 +0300 Subject: [PATCH 1/5] composer.json --- composer.json | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 44026cb..6a2331d 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "yiisoft/data-db", "type": "library", - "description": "Database query adapter for yiisoft/data data providers", + "description": "Database query adapter for Yii Data", "keywords": [ "db", "data provider", @@ -35,10 +35,10 @@ "yiisoft/db": "^2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.89.2", + "friendsofphp/php-cs-fixer": "^3.92.0", "maglnet/composer-require-checker": "^4.7.1", - "phpunit/phpunit": "^10.5.52", - "rector/rector": "^2.2.8", + "phpunit/phpunit": "^10.5.60", + "rector/rector": "^2.2.14", "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", "vimeo/psalm": "^5.26.1 || ^6.9.1", @@ -49,7 +49,7 @@ "yiisoft/db-pgsql": "^2.0", "yiisoft/db-sqlite": "^2.0", "yiisoft/psr-dummy-provider": "^1.0.2", - "yiisoft/test-support": "^3.0" + "yiisoft/test-support": "^3.1" }, "autoload": { "psr-4": { @@ -63,11 +63,6 @@ }, "files": ["tests/bootstrap.php"] }, - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, "config": { "sort-packages": true, "allow-plugins": { @@ -78,6 +73,7 @@ "scripts": { "test": "phpunit --testdox --no-interaction", "test-watch": "phpunit-watcher watch", - "cs-fix": "php-cs-fixer fix" + "cs-fix": "php-cs-fixer fix", + "mutation": "roave-infection-static-analysis-plugin --threads=max --min-covered-msi=100" } } From a953860eb23450f587d1530261f2511b891dc762 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 12 Dec 2025 17:49:16 +0300 Subject: [PATCH 2/5] root --- .gitattributes | 21 +++++++++------------ rector.php | 25 ++++++++++--------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/.gitattributes b/.gitattributes index d2354a6..a36ea63 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,23 +16,20 @@ *.yml text # Ensure those won't be messed up with +*.phar binary *.png binary *.jpg binary *.gif binary *.ttf binary -# Ignore some meta files when creating an archive of this repository -/.github export-ignore -/.editorconfig export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.phpunit-watcher.yml export-ignore -/.styleci.yml export-ignore -/infection.json.dist export-ignore -/phpunit.xml.dist export-ignore -/psalm.xml export-ignore -/tests export-ignore -/docs export-ignore +# Exclude development and metadata files from distribution archive +* export-ignore +/src/ -export-ignore +/src/** -export-ignore +/composer.json -export-ignore +/README.md -export-ignore +/CHANGELOG.md -export-ignore +/LICENSE.md -export-ignore # Avoid merge conflicts in CHANGELOG # https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ diff --git a/rector.php b/rector.php index f1ee33d..54537c2 100644 --- a/rector.php +++ b/rector.php @@ -5,25 +5,20 @@ use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector; use Rector\Config\RectorConfig; use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; +use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; -use Rector\Set\ValueObject\LevelSetList; -return static function (RectorConfig $rectorConfig): void { - $rectorConfig->paths([ +return RectorConfig::configure() + ->withPaths([ __DIR__ . '/src', __DIR__ . '/tests', - ]); - - // register a single rule - $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); - - // define sets of rules - $rectorConfig->sets([ - LevelSetList::UP_TO_PHP_81, - ]); - - $rectorConfig->skip([ + ]) + ->withPhpSets(php81: true) + ->withRules([ + InlineConstructorDefaultToPropertyRector::class, + ]) + ->withSkip([ ClosureToArrowFunctionRector::class, + ReadOnlyPropertyRector::class, NullToStrictStringFuncCallArgRector::class, ]); -}; From 71e65444e560ae8f8bc02ab67f154145ea87432d Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 12 Dec 2025 17:50:04 +0300 Subject: [PATCH 3/5] bc.yml --- .github/workflows/{bc.yml_ => bc.yml} | 3 +++ 1 file changed, 3 insertions(+) rename .github/workflows/{bc.yml_ => bc.yml} (95%) diff --git a/.github/workflows/bc.yml_ b/.github/workflows/bc.yml similarity index 95% rename from .github/workflows/bc.yml_ rename to .github/workflows/bc.yml index 00041a9..b269391 100644 --- a/.github/workflows/bc.yml_ +++ b/.github/workflows/bc.yml @@ -23,6 +23,9 @@ on: name: backwards compatibility +permissions: + contents: read + jobs: roave_bc_check: uses: yiisoft/actions/.github/workflows/bc.yml@master From 5727501c8dc6e72607d4853c2cbce3669ecd3b6e Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 12 Dec 2025 17:51:35 +0300 Subject: [PATCH 4/5] phpdoc --- src/FieldMapper/ArrayFieldMapper.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/FieldMapper/ArrayFieldMapper.php b/src/FieldMapper/ArrayFieldMapper.php index 6763a50..20ac74c 100644 --- a/src/FieldMapper/ArrayFieldMapper.php +++ b/src/FieldMapper/ArrayFieldMapper.php @@ -16,7 +16,9 @@ final class ArrayFieldMapper implements FieldMapperInterface { /** - * @param (ExpressionInterface|string)[] $map The field mapping array where keys are field names and values are column names or expressions. + * @param (ExpressionInterface|string)[] $map The field mapping array where keys are field names and values are + * column names or expressions. + * * For example: * * ```php From d27b24675eb41b818e12e90973edd9f7d8c9c731 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 12 Dec 2025 21:57:46 +0300 Subject: [PATCH 5/5] readme --- README.md | 159 +++++++++++++++++++++--------------------------------- 1 file changed, 60 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index ad15a5e..69e7488 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ [![type-coverage](https://shepherd.dev/github/yiisoft/data-db/coverage.svg)](https://shepherd.dev/github/yiisoft/data-db) [![psalm-level](https://shepherd.dev/github/yiisoft/data-db/level.svg)](https://shepherd.dev/github/yiisoft/data-db) -The package provides `Yiisoft\Db\Query\Query` bindings for generic data abstractions. +The package provides [data reader](https://github.com/yiisoft/data?tab=readme-ov-file#reading-data) implementation based +on [Yii DB](https://github.com/yiisoft/db) query and a set of DB-specific filters. Detailed build statuses: @@ -40,124 +41,84 @@ composer require yiisoft/data-db ## General usage +The `QueryDataReader` wraps a database query to provide a flexible data reading interface: + ```php -use Yiisoft\Data\Db\Filter\All; -use Yiisoft\Data\Db\Filter\Equals; use Yiisoft\Data\Db\QueryDataReader; - -$typeId = filter_input(INPUT_GET, 'type_id', FILTER_VALIDATE_INT); -$countryId = filter_input(INPUT_GET, 'country_id', FILTER_VALIDATE_INT); -$parentId = filter_input(INPUT_GET, 'parent_id', FILTER_VALIDATE_INT); - -// OR -// $typeId = $_GET['type_id'] ?? null; -// $countryId = $_GET['country_id'] ?? null; -// $parentId = $_GET['parent_id'] ?? null; - -// OR -// $params = $request->getQueryParams(); -// $typeId = $params['type_id'] ?? null; -// $countryId = $params['country_id'] ?? null; -// $parentId = $params['parent_id'] ?? null; - -// OR same with ArrayHelper::getValue(); - - -$query = $arFactory->createQueryTo(AR::class); - -$filter = new All( - (new Equals('type_id', $typeId)), - (new Equals('country_id', $countryId)), - (new Equals('parent_id', $parentId)) +use Yiisoft\Data\Reader\Filter\Equals; +use Yiisoft\Data\Reader\Filter\GreaterThan; +use Yiisoft\Data\Reader\Filter\Like; +use Yiisoft\Data\Reader\Filter\AndX; +use Yiisoft\Data\Reader\Sort; +use Yiisoft\Db\Query\Query; + +$query = (new Query($db))->from('customer'); +$dataReader = new QueryDataReader($query); + +// Iterate through results +foreach ($dataReader->read() as $customer) { + // ... process each customer ... +} + +// Read a single record +$customer = $dataReader->readOne(); + +// Get total count +$total = $dataReader->count(); + +// Sorting +$sort = Sort::any(['name', 'email'])->withOrderString('-name,email'); +$dataReader = $dataReader->withSort($sort); + +// Filtering +$filter = new AndX( + new Equals('status', 'active'), + new GreaterThan('age', 18), + new Like('name', 'John') ); +$dataReader = $dataReader->withFilter($filter); -$dataReader = (new QueryDataReader($query)) - ->withFilter($filter); +// Pagination +$dataReader = $dataReader + ->withOffset(20) + ->withLimit(10); ``` -If $typeId, $countryId and $parentId equals NULL that generate SQL like: - -```sql -SELECT AR::tableName().* FROM AR::tableName() WHERE type_id IS NULL AND country_id IS NULL AND parent_id IS NULL -``` +### Field mapping -If we want ignore not existing arguments (i.e. not set in $_GET/queryParams), we can use withIgnoreNull(true) method: +Map data reader field names to database columns: ```php -$typeId = 1; -$countryId = null; -$parentId = null; - -$filter = new All( - (new Equals('type_id', $typeId))->withIgnoreNull(true), - (new Equals('country_id', $countryId))->withIgnoreNull(true), - (new Equals('parent_id', $parentId))->withIgnoreNull(true) +use Yiisoft\Data\Db\QueryDataReader; +use Yiisoft\Data\Reader\Filter\Equals; + +$dataReader = new QueryDataReader( + query: $query, + fieldMapper: [ + 'userName' => 'user_name', + 'createdAt' => 'created_at', + ] ); -$dataReader = (new QueryDataReader($query)) - ->withFilter($filter); -``` - -That generate SQL like: - -```sql -SELECT AR::tableName().* FROM AR::tableName() WHERE type_id = 1 +// Now you can filter and sort by 'userName' and it will use 'user_name' column +$filter = new Equals('userName', 'admin'); ``` -If query joins several tables with same column name, pass table name as 3-th filter arguments +### Batch processing -```php -$equalsTableOne = (new Equals('id', 1, 'table_one'))->withIgnoreNull(true); -$equalsTableTwo = (new Equals('id', 100, 'table_two'))->withIgnoreNull(true); -``` - -## Current filters/processors - -### Compare - -- Equals - = -- NotEquals - != -- GreaterThan - > -- GreaterThanOrEqual - >= -- In -- LessThan - < -- LessThanOrEqual - <= -- Not -- Like\ILIke -- Exists -- Between - -#### Filter "Like" or "ILike" - -This filters has methods `withBoth`, `withoutBoth`, `withStart`, `withoutStart`, `withEnd`, `withoutEnd` +Process large datasets in batches to reduce memory usage: ```php -$filter = new Like('column', 'value'); -$dataReader = (new QueryDataReader($query))->withFilter($filter); -//column LIKE '%value%' +use Yiisoft\Data\Db\QueryDataReader; -$filter = (new Like('column', 'value'))->withoutStart(); -$dataReader = (new QueryDataReader($query))->withFilter($filter); -//column LIKE 'value%' +$dataReader = new QueryDataReader($query); +$dataReader = $dataReader->withBatchSize(100); -$filter = (new Like('column', 'value'))->withoutEnd(); -$dataReader = (new QueryDataReader($query))->withFilter($filter); -//column LIKE '%value' +foreach ($dataReader->read() as $item) { + // Items are fetched in batches of 100 +} ``` -#### Filter "Exists" - -Takes only one argument with type of`Yiisoft\Db\Query\Query` - -#### Filter "Not" - -Takes only one argument with type of`Yiisoft\Data\Reader\Filter\FilterInterface` - -### Group - -- All - and -- Any - or - ## Documentation - [Internals](docs/internals.md)