Skip to content

Commit

Permalink
Merge pull request #161 from moufmouf/filter_by_resultiterator
Browse files Browse the repository at this point in the history
Filtering by result iterator
  • Loading branch information
moufmouf committed Sep 2, 2019
2 parents 06fa71d + f7b5c72 commit 7f9e22f
Show file tree
Hide file tree
Showing 16 changed files with 379 additions and 112 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
],
"require" : {
"php" : ">=7.1",
"mouf/magic-query" : "^1.3.0",
"mouf/magic-query" : "^1.4.0",
"mouf/schema-analyzer": "^1.1.4",
"doctrine/dbal": "^2.9.2",
"psr/log": "~1.0",
Expand Down Expand Up @@ -70,7 +70,7 @@
}
},
"scripts": {
"phpstan": "php -d memory_limit=3G vendor/bin/phpstan analyse src -c phpstan.neon --level=7 --no-progress -vvv",
"phpstan": "php -d memory_limit=3G vendor/bin/phpstan analyse src -c phpstan.neon --no-progress -vvv",
"require-checker": "composer-require-checker check --config-file=composer-require-checker.json",
"test": "phpunit",
"csfix": "php-cs-fixer fix src/ && php-cs-fixer fix tests/",
Expand Down
44 changes: 44 additions & 0 deletions doc/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,50 @@ foreach ($userList as $user)
}
```

###Filtering by sub-query

`find` can also accept a result iterator (the result of a `find` method) as a filter.

```php
class CountryDao extends AbstractCountryDao {
/**
* Returns the list of countries whose country name starts by "$countryName"
*
* @param string $countryName
* @return Country[]
*/
public function findByCountryName($countryName) {
return $this->find("name LIKE :country", [ 'country' => $countryName.'%' ] );
}
}

class UserDao extends AbstractUserDao {
/**
* @var TestCountryDao
*/
private $countryDao;

public function __construct(TDBMService $tdbmService, TestCountryDao $countryDao)
{
parent::__construct($tdbmService);
$this->countryDao = $countryDao;
}

/**
* Returns the list of users whose country name starts by "$countryName"
*
* @param string $countryName
* @return User[]
*/
public function getUsersByCountryName($countryName) {
return $this->find($this->countryDao->findByCountryName($countryName));
}
}
```

See? The `UserDao::getUsersByCountryName` method is making use of the `CountryDao::findByCountryName` method.
It essentially says: "find all the users related to the result iterator of the countries starting with 'XXX'".

###Complex joins

![Users, roles and rights](images/user_role_right.png)
Expand Down
6 changes: 4 additions & 2 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
parameters:
level: 7
inferPrivatePropertyTypeFromConstructor: true
ignoreErrors:
- "#Method JsonSerializable::jsonSerialize\\(\\) invoked with 1 parameter, 0 required.#"
- "#Method .*::.* should return .* but returns .*TheCodingMachine\\\\TDBM\\\\AbstractTDBMObject#"
Expand All @@ -8,13 +10,13 @@ parameters:
- "#Call to an undefined method object::#"
- "#expects TheCodingMachine\\\\TDBM\\\\AbstractTDBMObject, object given.#"
- "#should return array<TheCodingMachine\\\\TDBM\\\\AbstractTDBMObject> but returns array<int, object>#"
- "#expects array<string>, array<int, int|string> given.#"
#- "#expects array<string>, array<int, int|string> given.#"
- "/Parameter #. \\$types of method Doctrine\\\\DBAL\\\\Connection::.*() expects array<int|string>, array<int, Doctrine\\\\DBAL\\\\Types\\\\Type> given./"
- "#Method TheCodingMachine\\\\TDBM\\\\Schema\\\\ForeignKey::.*() should return .* but returns array<string>|string.#"
-
message: '#Result of && is always false.#'
path: src/Test/Dao/Bean/Generated/ArticleBaseBean.php
reportUnmatchedIgnoredErrors: false
#reportUnmatchedIgnoredErrors: false
includes:
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon

48 changes: 47 additions & 1 deletion src/QueryFactory/AbstractQueryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,38 @@ abstract class AbstractQueryFactory implements QueryFactory
*/
protected $orderBy;

/**
* @var string|null
*/
protected $magicSql;
/**
* @var string|null
*/
protected $magicSqlCount;
/**
* @var string|null
*/
protected $magicSqlSubQuery;
protected $columnDescList;
protected $subQueryColumnDescList;
/**
* @var string
*/
protected $mainTable;

/**
* @param TDBMService $tdbmService
* @param Schema $schema
* @param OrderByAnalyzer $orderByAnalyzer
* @param string|UncheckedOrderBy|null $orderBy
*/
public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, $orderBy)
public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, string $mainTable, $orderBy)
{
$this->tdbmService = $tdbmService;
$this->schema = $schema;
$this->orderByAnalyzer = $orderByAnalyzer;
$this->orderBy = $orderBy;
$this->mainTable = $mainTable;
}

/**
Expand Down Expand Up @@ -214,6 +230,15 @@ public function getMagicSqlCount() : string
return $this->magicSqlCount;
}

public function getMagicSqlSubQuery() : string
{
if ($this->magicSqlSubQuery === null) {
$this->compute();
}

return $this->magicSqlSubQuery;
}

public function getColumnDescriptors() : array
{
if ($this->columnDescList === null) {
Expand All @@ -223,6 +248,27 @@ public function getColumnDescriptors() : array
return $this->columnDescList;
}

/**
* @return string[][] An array of column descriptors. Value is an array with those keys: table, column
*/
public function getSubQueryColumnDescriptors() : array
{
if ($this->subQueryColumnDescList === null) {
$columns = $this->tdbmService->getPrimaryKeyColumns($this->mainTable);
$descriptors = [];
foreach ($columns as $column) {
$descriptors[] = [
'table' => $this->mainTable,
'column' => $column
];
}
$this->subQueryColumnDescList = $descriptors;
}

return $this->subQueryColumnDescList;
}


/**
* Sets the ORDER BY directive executed in SQL.
*
Expand Down
19 changes: 19 additions & 0 deletions src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,4 +395,23 @@ protected function getTableGroupName(array $relatedTables): string
sort($relatedTables);
return implode('_``_', $relatedTables);
}

/**
* Returns a sub-query to be used in another query.
* A sub-query is similar to a query except it returns only the primary keys of the table (to be used as filters)
*
* @return string
*/
public function getMagicSqlSubQuery(): string
{
throw new TDBMException('Using resultset generated from findFromRawSql as subqueries is unsupported for now.');
}

/**
* @return string[][] An array of column descriptors. Value is an array with those keys: table, column
*/
public function getSubQueryColumnDescriptors(): array
{
throw new TDBMException('Using resultset generated from findFromRawSql as subqueries is unsupported for now.');
}
}
8 changes: 5 additions & 3 deletions src/QueryFactory/FindObjectsFromSqlQueryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
use TheCodingMachine\TDBM\OrderByAnalyzer;
use TheCodingMachine\TDBM\TDBMException;
use TheCodingMachine\TDBM\TDBMService;
use function implode;

/**
* This class is in charge of creating the MagicQuery SQL based on parameters passed to findObjectsFromSql method.
*/
class FindObjectsFromSqlQueryFactory extends AbstractQueryFactory
{
private $mainTable;
private $from;
private $filterString;
private $cache;
Expand All @@ -26,8 +26,7 @@ class FindObjectsFromSqlQueryFactory extends AbstractQueryFactory

public function __construct(string $mainTable, string $from, $filterString, $orderBy, TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, SchemaAnalyzer $schemaAnalyzer, Cache $cache, string $cachePrefix)
{
parent::__construct($tdbmService, $schema, $orderByAnalyzer, $orderBy);
$this->mainTable = $mainTable;
parent::__construct($tdbmService, $schema, $orderByAnalyzer, $mainTable, $orderBy);
$this->from = $from;
$this->filterString = $filterString;
$this->schemaAnalyzer = $schemaAnalyzer;
Expand Down Expand Up @@ -55,6 +54,7 @@ protected function compute(): void
}, $pkColumnNames);

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

// Add joins on inherited tables if necessary
if (count($allFetchedTables) > 1) {
Expand Down Expand Up @@ -89,6 +89,7 @@ protected function compute(): void
if (!empty($this->filterString)) {
$sql .= ' WHERE '.$this->filterString;
$countSql .= ' WHERE '.$this->filterString;
$subQuery .= ' WHERE '.$this->filterString;
}

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

$this->magicSql = $sql;
$this->magicSqlCount = $countSql;
$this->magicSqlSubQuery = $subQuery;
$this->columnDescList = $columnDescList;
}

Expand Down
16 changes: 10 additions & 6 deletions src/QueryFactory/FindObjectsQueryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
use Doctrine\DBAL\Schema\Schema;
use TheCodingMachine\TDBM\OrderByAnalyzer;
use TheCodingMachine\TDBM\TDBMService;
use function implode;

/**
* This class is in charge of creating the MagicQuery SQL based on parameters passed to findObjects method.
*/
class FindObjectsQueryFactory extends AbstractQueryFactory
{
private $mainTable;
private $additionalTablesFetch;
private $filterString;
/**
Expand All @@ -24,8 +24,7 @@ class FindObjectsQueryFactory extends AbstractQueryFactory

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

$pkColumnNames = $this->tdbmService->getPrimaryKeyColumns($this->mainTable);
$pkColumnNames = array_map(function ($pkColumn) {
return $this->tdbmService->getConnection()->quoteIdentifier($this->mainTable).'.'.$this->tdbmService->getConnection()->quoteIdentifier($pkColumn);
$mysqlPlatform = new MySqlPlatform();
$pkColumnNames = array_map(function ($pkColumn) use ($mysqlPlatform) {
return $mysqlPlatform->quoteIdentifier($this->mainTable).'.'.$mysqlPlatform->quoteIdentifier($pkColumn);
}, $pkColumnNames);

$subQuery = 'SELECT DISTINCT '.implode(', ', $pkColumnNames).' FROM MAGICJOIN('.$this->mainTable.')';

if (count($pkColumnNames) === 1 || $this->tdbmService->getConnection()->getDatabasePlatform() instanceof MySqlPlatform) {
$countSql = 'SELECT COUNT(DISTINCT '.implode(', ', $pkColumnNames).') FROM MAGICJOIN('.$this->mainTable.')';
} else {
$countSql = 'SELECT COUNT(*) FROM (SELECT DISTINCT '.implode(', ', $pkColumnNames).' FROM MAGICJOIN('.$this->mainTable.')) tmp';
$countSql = 'SELECT COUNT(*) FROM ('.$subQuery.') tmp';
}

if (!empty($this->filterString)) {
$sql .= ' WHERE '.$this->filterString;
$countSql .= ' WHERE '.$this->filterString;
$subQuery .= ' WHERE '.$this->filterString;
}

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

$this->magicSql = $sql;
$this->magicSqlCount = $countSql;
$this->magicSqlSubQuery = $subQuery;
$this->columnDescList = $columnDescList;

$this->cache->save($key, [
Expand Down
15 changes: 14 additions & 1 deletion src/QueryFactory/QueryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,20 @@ public function getMagicSql() : string;
public function getMagicSqlCount() : string;

/**
* @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
* Returns a sub-query to be used in another query.
* A sub-query is similar to a query except it returns only the primary keys of the table (to be used as filters)
*
* @return string
*/
public function getMagicSqlSubQuery() : string;

/**
* @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
*/
public function getColumnDescriptors() : array;

/**
* @return string[][] An array of column descriptors. Value is an array with those keys: table, column
*/
public function getSubQueryColumnDescriptors() : array;
}
34 changes: 34 additions & 0 deletions src/ResultIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

namespace TheCodingMachine\TDBM;

use Doctrine\DBAL\Platforms\MySqlPlatform;
use Psr\Log\NullLogger;
use function array_map;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Statement;
use function array_pop;
use function is_array;
use function is_int;
use Mouf\Database\MagicQuery;
Expand Down Expand Up @@ -47,8 +49,14 @@ class ResultIterator implements Result, \ArrayAccess, \JsonSerializable
private $objectStorage;
private $className;

/**
* @var TDBMService
*/
private $tdbmService;
private $parameters;
/**
* @var MagicQuery
*/
private $magicQuery;

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

return $clone;
}

/**
* @internal
* @return string
*/
public function _getSubQuery(): string
{
$this->magicQuery->setOutputDialect(new MySqlPlatform());
try {
$sql = $this->magicQuery->build($this->queryFactory->getMagicSqlSubQuery(), $this->parameters);
} finally {
$this->magicQuery->setOutputDialect($this->tdbmService->getConnection()->getDatabasePlatform());
}
$primaryKeyColumnDescs = $this->queryFactory->getSubQueryColumnDescriptors();

if (count($primaryKeyColumnDescs) > 1) {
throw new TDBMException('You cannot use in a sub-query a table that has a primary key on more that 1 column.');
}

$pkDesc = array_pop($primaryKeyColumnDescs);

$mysqlPlatform = new MySqlPlatform();
$sql = $mysqlPlatform->quoteIdentifier($pkDesc['table']).'.'.$mysqlPlatform->quoteIdentifier($pkDesc['column']).' IN ('.$sql.')';

return $sql;
}
}
Loading

0 comments on commit 7f9e22f

Please sign in to comment.