Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/bc.yml_ → .github/workflows/bc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ on:

name: backwards compatibility

permissions:
contents: read

jobs:
roave_bc_check:
uses: yiisoft/actions/.github/workflows/bc.yml@master
Expand Down
159 changes: 60 additions & 99 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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)
Expand Down
18 changes: 7 additions & 11 deletions composer.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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": {
Expand All @@ -63,11 +63,6 @@
},
"files": ["tests/bootstrap.php"]
},
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
Expand All @@ -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"
}
}
25 changes: 10 additions & 15 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]);
};
4 changes: 3 additions & 1 deletion src/FieldMapper/ArrayFieldMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading