Skip to content

Commit 7f9e22f

Browse files
authored
Merge pull request #161 from moufmouf/filter_by_resultiterator
Filtering by result iterator
2 parents 06fa71d + f7b5c72 commit 7f9e22f

16 files changed

+379
-112
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
],
2020
"require" : {
2121
"php" : ">=7.1",
22-
"mouf/magic-query" : "^1.3.0",
22+
"mouf/magic-query" : "^1.4.0",
2323
"mouf/schema-analyzer": "^1.1.4",
2424
"doctrine/dbal": "^2.9.2",
2525
"psr/log": "~1.0",
@@ -70,7 +70,7 @@
7070
}
7171
},
7272
"scripts": {
73-
"phpstan": "php -d memory_limit=3G vendor/bin/phpstan analyse src -c phpstan.neon --level=7 --no-progress -vvv",
73+
"phpstan": "php -d memory_limit=3G vendor/bin/phpstan analyse src -c phpstan.neon --no-progress -vvv",
7474
"require-checker": "composer-require-checker check --config-file=composer-require-checker.json",
7575
"test": "phpunit",
7676
"csfix": "php-cs-fixer fix src/ && php-cs-fixer fix tests/",

doc/quickstart.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,50 @@ foreach ($userList as $user)
459459
}
460460
```
461461

462+
###Filtering by sub-query
463+
464+
`find` can also accept a result iterator (the result of a `find` method) as a filter.
465+
466+
```php
467+
class CountryDao extends AbstractCountryDao {
468+
/**
469+
* Returns the list of countries whose country name starts by "$countryName"
470+
*
471+
* @param string $countryName
472+
* @return Country[]
473+
*/
474+
public function findByCountryName($countryName) {
475+
return $this->find("name LIKE :country", [ 'country' => $countryName.'%' ] );
476+
}
477+
}
478+
479+
class UserDao extends AbstractUserDao {
480+
/**
481+
* @var TestCountryDao
482+
*/
483+
private $countryDao;
484+
485+
public function __construct(TDBMService $tdbmService, TestCountryDao $countryDao)
486+
{
487+
parent::__construct($tdbmService);
488+
$this->countryDao = $countryDao;
489+
}
490+
491+
/**
492+
* Returns the list of users whose country name starts by "$countryName"
493+
*
494+
* @param string $countryName
495+
* @return User[]
496+
*/
497+
public function getUsersByCountryName($countryName) {
498+
return $this->find($this->countryDao->findByCountryName($countryName));
499+
}
500+
}
501+
```
502+
503+
See? The `UserDao::getUsersByCountryName` method is making use of the `CountryDao::findByCountryName` method.
504+
It essentially says: "find all the users related to the result iterator of the countries starting with 'XXX'".
505+
462506
###Complex joins
463507

464508
![Users, roles and rights](images/user_role_right.png)

phpstan.neon

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
parameters:
2+
level: 7
3+
inferPrivatePropertyTypeFromConstructor: true
24
ignoreErrors:
35
- "#Method JsonSerializable::jsonSerialize\\(\\) invoked with 1 parameter, 0 required.#"
46
- "#Method .*::.* should return .* but returns .*TheCodingMachine\\\\TDBM\\\\AbstractTDBMObject#"
@@ -8,13 +10,13 @@ parameters:
810
- "#Call to an undefined method object::#"
911
- "#expects TheCodingMachine\\\\TDBM\\\\AbstractTDBMObject, object given.#"
1012
- "#should return array<TheCodingMachine\\\\TDBM\\\\AbstractTDBMObject> but returns array<int, object>#"
11-
- "#expects array<string>, array<int, int|string> given.#"
13+
#- "#expects array<string>, array<int, int|string> given.#"
1214
- "/Parameter #. \\$types of method Doctrine\\\\DBAL\\\\Connection::.*() expects array<int|string>, array<int, Doctrine\\\\DBAL\\\\Types\\\\Type> given./"
1315
- "#Method TheCodingMachine\\\\TDBM\\\\Schema\\\\ForeignKey::.*() should return .* but returns array<string>|string.#"
1416
-
1517
message: '#Result of && is always false.#'
1618
path: src/Test/Dao/Bean/Generated/ArticleBaseBean.php
17-
reportUnmatchedIgnoredErrors: false
19+
#reportUnmatchedIgnoredErrors: false
1820
includes:
1921
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
2022

src/QueryFactory/AbstractQueryFactory.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,38 @@ abstract class AbstractQueryFactory implements QueryFactory
3434
*/
3535
protected $orderBy;
3636

37+
/**
38+
* @var string|null
39+
*/
3740
protected $magicSql;
41+
/**
42+
* @var string|null
43+
*/
3844
protected $magicSqlCount;
45+
/**
46+
* @var string|null
47+
*/
48+
protected $magicSqlSubQuery;
3949
protected $columnDescList;
50+
protected $subQueryColumnDescList;
51+
/**
52+
* @var string
53+
*/
54+
protected $mainTable;
4055

4156
/**
4257
* @param TDBMService $tdbmService
4358
* @param Schema $schema
4459
* @param OrderByAnalyzer $orderByAnalyzer
4560
* @param string|UncheckedOrderBy|null $orderBy
4661
*/
47-
public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, $orderBy)
62+
public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, string $mainTable, $orderBy)
4863
{
4964
$this->tdbmService = $tdbmService;
5065
$this->schema = $schema;
5166
$this->orderByAnalyzer = $orderByAnalyzer;
5267
$this->orderBy = $orderBy;
68+
$this->mainTable = $mainTable;
5369
}
5470

5571
/**
@@ -214,6 +230,15 @@ public function getMagicSqlCount() : string
214230
return $this->magicSqlCount;
215231
}
216232

233+
public function getMagicSqlSubQuery() : string
234+
{
235+
if ($this->magicSqlSubQuery === null) {
236+
$this->compute();
237+
}
238+
239+
return $this->magicSqlSubQuery;
240+
}
241+
217242
public function getColumnDescriptors() : array
218243
{
219244
if ($this->columnDescList === null) {
@@ -223,6 +248,27 @@ public function getColumnDescriptors() : array
223248
return $this->columnDescList;
224249
}
225250

251+
/**
252+
* @return string[][] An array of column descriptors. Value is an array with those keys: table, column
253+
*/
254+
public function getSubQueryColumnDescriptors() : array
255+
{
256+
if ($this->subQueryColumnDescList === null) {
257+
$columns = $this->tdbmService->getPrimaryKeyColumns($this->mainTable);
258+
$descriptors = [];
259+
foreach ($columns as $column) {
260+
$descriptors[] = [
261+
'table' => $this->mainTable,
262+
'column' => $column
263+
];
264+
}
265+
$this->subQueryColumnDescList = $descriptors;
266+
}
267+
268+
return $this->subQueryColumnDescList;
269+
}
270+
271+
226272
/**
227273
* Sets the ORDER BY directive executed in SQL.
228274
*

src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,4 +395,23 @@ protected function getTableGroupName(array $relatedTables): string
395395
sort($relatedTables);
396396
return implode('_``_', $relatedTables);
397397
}
398+
399+
/**
400+
* Returns a sub-query to be used in another query.
401+
* A sub-query is similar to a query except it returns only the primary keys of the table (to be used as filters)
402+
*
403+
* @return string
404+
*/
405+
public function getMagicSqlSubQuery(): string
406+
{
407+
throw new TDBMException('Using resultset generated from findFromRawSql as subqueries is unsupported for now.');
408+
}
409+
410+
/**
411+
* @return string[][] An array of column descriptors. Value is an array with those keys: table, column
412+
*/
413+
public function getSubQueryColumnDescriptors(): array
414+
{
415+
throw new TDBMException('Using resultset generated from findFromRawSql as subqueries is unsupported for now.');
416+
}
398417
}

src/QueryFactory/FindObjectsFromSqlQueryFactory.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
use TheCodingMachine\TDBM\OrderByAnalyzer;
1212
use TheCodingMachine\TDBM\TDBMException;
1313
use TheCodingMachine\TDBM\TDBMService;
14+
use function implode;
1415

1516
/**
1617
* This class is in charge of creating the MagicQuery SQL based on parameters passed to findObjectsFromSql method.
1718
*/
1819
class FindObjectsFromSqlQueryFactory extends AbstractQueryFactory
1920
{
20-
private $mainTable;
2121
private $from;
2222
private $filterString;
2323
private $cache;
@@ -26,8 +26,7 @@ class FindObjectsFromSqlQueryFactory extends AbstractQueryFactory
2626

2727
public function __construct(string $mainTable, string $from, $filterString, $orderBy, TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, SchemaAnalyzer $schemaAnalyzer, Cache $cache, string $cachePrefix)
2828
{
29-
parent::__construct($tdbmService, $schema, $orderByAnalyzer, $orderBy);
30-
$this->mainTable = $mainTable;
29+
parent::__construct($tdbmService, $schema, $orderByAnalyzer, $mainTable, $orderBy);
3130
$this->from = $from;
3231
$this->filterString = $filterString;
3332
$this->schemaAnalyzer = $schemaAnalyzer;
@@ -55,6 +54,7 @@ protected function compute(): void
5554
}, $pkColumnNames);
5655

5756
$countSql = 'SELECT COUNT(DISTINCT '.implode(', ', $pkColumnNames).') FROM '.$this->from;
57+
$subQuery = 'SELECT DISTINCT '.implode(', ', $pkColumnNames).' FROM '.$this->from;
5858

5959
// Add joins on inherited tables if necessary
6060
if (count($allFetchedTables) > 1) {
@@ -89,6 +89,7 @@ protected function compute(): void
8989
if (!empty($this->filterString)) {
9090
$sql .= ' WHERE '.$this->filterString;
9191
$countSql .= ' WHERE '.$this->filterString;
92+
$subQuery .= ' WHERE '.$this->filterString;
9293
}
9394

9495
if (!empty($orderString)) {
@@ -101,6 +102,7 @@ protected function compute(): void
101102

102103
$this->magicSql = $sql;
103104
$this->magicSqlCount = $countSql;
105+
$this->magicSqlSubQuery = $subQuery;
104106
$this->columnDescList = $columnDescList;
105107
}
106108

src/QueryFactory/FindObjectsQueryFactory.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
use Doctrine\DBAL\Schema\Schema;
99
use TheCodingMachine\TDBM\OrderByAnalyzer;
1010
use TheCodingMachine\TDBM\TDBMService;
11+
use function implode;
1112

1213
/**
1314
* This class is in charge of creating the MagicQuery SQL based on parameters passed to findObjects method.
1415
*/
1516
class FindObjectsQueryFactory extends AbstractQueryFactory
1617
{
17-
private $mainTable;
1818
private $additionalTablesFetch;
1919
private $filterString;
2020
/**
@@ -24,8 +24,7 @@ class FindObjectsQueryFactory extends AbstractQueryFactory
2424

2525
public function __construct(string $mainTable, array $additionalTablesFetch, $filterString, $orderBy, TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, Cache $cache)
2626
{
27-
parent::__construct($tdbmService, $schema, $orderByAnalyzer, $orderBy);
28-
$this->mainTable = $mainTable;
27+
parent::__construct($tdbmService, $schema, $orderByAnalyzer, $mainTable, $orderBy);
2928
$this->additionalTablesFetch = $additionalTablesFetch;
3029
$this->filterString = $filterString;
3130
$this->cache = $cache;
@@ -48,19 +47,23 @@ protected function compute(): void
4847
$sql = 'SELECT DISTINCT '.implode(', ', $columnsList).' FROM MAGICJOIN('.$this->mainTable.')';
4948

5049
$pkColumnNames = $this->tdbmService->getPrimaryKeyColumns($this->mainTable);
51-
$pkColumnNames = array_map(function ($pkColumn) {
52-
return $this->tdbmService->getConnection()->quoteIdentifier($this->mainTable).'.'.$this->tdbmService->getConnection()->quoteIdentifier($pkColumn);
50+
$mysqlPlatform = new MySqlPlatform();
51+
$pkColumnNames = array_map(function ($pkColumn) use ($mysqlPlatform) {
52+
return $mysqlPlatform->quoteIdentifier($this->mainTable).'.'.$mysqlPlatform->quoteIdentifier($pkColumn);
5353
}, $pkColumnNames);
5454

55+
$subQuery = 'SELECT DISTINCT '.implode(', ', $pkColumnNames).' FROM MAGICJOIN('.$this->mainTable.')';
56+
5557
if (count($pkColumnNames) === 1 || $this->tdbmService->getConnection()->getDatabasePlatform() instanceof MySqlPlatform) {
5658
$countSql = 'SELECT COUNT(DISTINCT '.implode(', ', $pkColumnNames).') FROM MAGICJOIN('.$this->mainTable.')';
5759
} else {
58-
$countSql = 'SELECT COUNT(*) FROM (SELECT DISTINCT '.implode(', ', $pkColumnNames).' FROM MAGICJOIN('.$this->mainTable.')) tmp';
60+
$countSql = 'SELECT COUNT(*) FROM ('.$subQuery.') tmp';
5961
}
6062

6163
if (!empty($this->filterString)) {
6264
$sql .= ' WHERE '.$this->filterString;
6365
$countSql .= ' WHERE '.$this->filterString;
66+
$subQuery .= ' WHERE '.$this->filterString;
6467
}
6568

6669
if (!empty($orderString)) {
@@ -69,6 +72,7 @@ protected function compute(): void
6972

7073
$this->magicSql = $sql;
7174
$this->magicSqlCount = $countSql;
75+
$this->magicSqlSubQuery = $subQuery;
7276
$this->columnDescList = $columnDescList;
7377

7478
$this->cache->save($key, [

src/QueryFactory/QueryFactory.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,20 @@ public function getMagicSql() : string;
3131
public function getMagicSqlCount() : string;
3232

3333
/**
34-
* @return mixed[][] An array of column descriptors. The key is in the form "$tableName____$columnName". Value is an array with those keys: as, table, colum, type, tableGroup
34+
* Returns a sub-query to be used in another query.
35+
* A sub-query is similar to a query except it returns only the primary keys of the table (to be used as filters)
36+
*
37+
* @return string
38+
*/
39+
public function getMagicSqlSubQuery() : string;
40+
41+
/**
42+
* @return mixed[][] An array of column descriptors. The key is in the form "$tableName____$columnName". Value is an array with those keys: as, table, column, type, tableGroup
3543
*/
3644
public function getColumnDescriptors() : array;
45+
46+
/**
47+
* @return string[][] An array of column descriptors. Value is an array with those keys: table, column
48+
*/
49+
public function getSubQueryColumnDescriptors() : array;
3750
}

src/ResultIterator.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
namespace TheCodingMachine\TDBM;
55

6+
use Doctrine\DBAL\Platforms\MySqlPlatform;
67
use Psr\Log\NullLogger;
78
use function array_map;
89
use Doctrine\DBAL\Connection;
910
use Doctrine\DBAL\Statement;
11+
use function array_pop;
1012
use function is_array;
1113
use function is_int;
1214
use Mouf\Database\MagicQuery;
@@ -47,8 +49,14 @@ class ResultIterator implements Result, \ArrayAccess, \JsonSerializable
4749
private $objectStorage;
4850
private $className;
4951

52+
/**
53+
* @var TDBMService
54+
*/
5055
private $tdbmService;
5156
private $parameters;
57+
/**
58+
* @var MagicQuery
59+
*/
5260
private $magicQuery;
5361

5462
/**
@@ -354,4 +362,30 @@ public function withParameters(array $parameters) : ResultIterator
354362

355363
return $clone;
356364
}
365+
366+
/**
367+
* @internal
368+
* @return string
369+
*/
370+
public function _getSubQuery(): string
371+
{
372+
$this->magicQuery->setOutputDialect(new MySqlPlatform());
373+
try {
374+
$sql = $this->magicQuery->build($this->queryFactory->getMagicSqlSubQuery(), $this->parameters);
375+
} finally {
376+
$this->magicQuery->setOutputDialect($this->tdbmService->getConnection()->getDatabasePlatform());
377+
}
378+
$primaryKeyColumnDescs = $this->queryFactory->getSubQueryColumnDescriptors();
379+
380+
if (count($primaryKeyColumnDescs) > 1) {
381+
throw new TDBMException('You cannot use in a sub-query a table that has a primary key on more that 1 column.');
382+
}
383+
384+
$pkDesc = array_pop($primaryKeyColumnDescs);
385+
386+
$mysqlPlatform = new MySqlPlatform();
387+
$sql = $mysqlPlatform->quoteIdentifier($pkDesc['table']).'.'.$mysqlPlatform->quoteIdentifier($pkDesc['column']).' IN ('.$sql.')';
388+
389+
return $sql;
390+
}
357391
}

0 commit comments

Comments
 (0)