Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Straighten context aware query builder #2

Open
wants to merge 2 commits into
base: feature/context-query
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@

use TYPO3\CMS\Core\Utility\GeneralUtility;

class SelectIdentifier
class ColumnIdentifier
{
/**
* @var string
*/
private $fieldName;
private $columnName;

/**
* @var null|string
Expand All @@ -39,7 +39,7 @@ class SelectIdentifier
*/
private $database;

public static function fromExpression(string $expression): self
public static function fromSelectExpression(string $expression): self
{
$expression = str_replace(["\t", "\n", ' '], ' ', $expression);
$expression = str_ireplace(' as ', ' AS ', $expression);
Expand All @@ -58,33 +58,33 @@ public static function fromExpression(string $expression): self
}

if (strpos($identifier, '.') === false) {
$fieldName = $identifier;
$columnName = $identifier;
} else {
list($prefix, $database, $tableName, $fieldName) = array_pad(
list($prefix, $database, $tableName, $columnName) = array_pad(
explode('.', $identifier, 4),
-4,
null
);
if (!empty($suffix)) {
throw new \InvalidArgumentException(
'SelectIdentifier::fromExpression() could not parse the expression ' . $expression . '.',
'ColumnIdentifier::fromSelectExpression() could not parse the expression ' . $expression . '.',
1567606568
);
}
}

return GeneralUtility::makeInstance(
static::class,
$fieldName,
$columnName,
$alias ?? null,
$tableName ?? null,
$database ?? null
);
}

public function __construct(string $fieldName, string $alias = null, string $tableName = null, string $database = null)
public function __construct(string $columnName, string $alias = null, string $tableName = null, string $database = null)
{
$this->fieldName = $fieldName;
$this->columnName = $columnName;
$this->alias = $alias;
$this->tableName = $tableName;
$this->database = $database;
Expand All @@ -103,10 +103,10 @@ public function quoteForSelect(\Closure $handler): string
if ($this->tableName !== null) {
$values[] = $handler($this->tableName);
}
if ($this->fieldName !== '*') {
$values[] = $handler($this->fieldName);
if ($this->columnName !== '*') {
$values[] = $handler($this->columnName);
} else {
$values[] = $this->fieldName;
$values[] = $this->columnName;
}
$identifier = implode('.', $values);
// Quote the alias for the current fieldName, if given
Expand All @@ -119,7 +119,7 @@ public function quoteForSelect(\Closure $handler): string
/**
* @return string
*/
public function getFieldName(): string
public function getColumnName(): string
{
return $this->fieldName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,23 @@

use TYPO3\CMS\Core\Utility\GeneralUtility;

class SelectIdentifierCollection implements \IteratorAggregate
class ColumnIdentifierCollection implements \IteratorAggregate
{
/**
* @var SelectIdentifier[]
* @var ColumnIdentifier[]
*/
private $identifiers;

public static function fromExpressions(string ...$expressions): self
{
$identifiers = array_map(
function (string $expression) {
return SelectIdentifier::fromExpression($expression);
},
$expressions
);
return GeneralUtility::makeInstance(static::class, ...$identifiers);
}

public function __construct(SelectIdentifier ...$identifiers)
public function __construct(ColumnIdentifier ...$identifiers)
{
$this->identifiers = $identifiers;
}

public function quoteForSelect(\Closure $handler): array
{
return array_map(
function (SelectIdentifier $identifier) use ($handler) {
return $identifier->quoteForSelect($handler);
},
$this->identifiers
);
}

public function hasFieldName(TableIdentifier $tableIdentifier, string $fieldName): bool
public function hasColumnName(TableIdentifier $tableIdentifier, string $columnName): bool
{
$tableName = $tableIdentifier->getAlias() ?? $tableIdentifier->getTableName();
foreach ($this->identifiers as $identifier) {
if ($identifier->getFieldName() !== $fieldName) {
if ($identifier->getFieldName() !== $columnName) {
continue;
}
// either `tableAlias.field` or just `field`
Expand Down
126 changes: 73 additions & 53 deletions typo3/sysext/core/Classes/Database/Query/ContextAwareQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,48 +18,37 @@
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Context\LanguageAspectView;
use TYPO3\CMS\Core\Database\Query\Context\WorkspaceAspectView;
use TYPO3\CMS\Core\Database\Query\View\LanguageAspectView;
use TYPO3\CMS\Core\Database\Query\View\WorkspaceAspectView;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface;
use TYPO3\CMS\Core\Database\Query\Restriction\RecordRestrictionInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
* Object oriented approach to building SQL queries.
* Object oriented approach to building context aware SQL queries.
*
* This is an advanced query over the simple Doctrine DBAL / TYPO3 QueryBuilder by taking into account the
* context - that is next to enableFields the proper fetching of resolved langauge and workspace records, if given.
*
*
* For this to work, the concept of map/reduce is applied. Example:
*
* - Fetch all records that match live and workspace ID
* - Fetch all records that match the target language + the fallback records
*
* Then filter out the criteria that match based on enableFields + "overlays".
* It's an advanced query builder by taking into account the context - that is
* the proper fetching of resolved language and workspace records, if given.
*
* @api
*/
class ContextAwareQueryBuilder extends QueryBuilder
final class ContextAwareQueryBuilder extends QueryBuilder
{
/**
* @var Context
*/
protected $context;

/**
* @var SelectIdentifierCollection
*/
private $selectIdentifierCollection;

/**
* @var TableIdentifier
*/
private $tableIdentifier;

/**
* Initializes a new QueryBuilder.
* Initializes a new context aware query builder.
*
* @param Connection $connection The DBAL Connection.
* @param Connection $connection
* @param Context $context
* @param QueryRestrictionContainerInterface $restrictionContainer
* @param \Doctrine\DBAL\Query\QueryBuilder $concreteQueryBuilder
Expand All @@ -76,83 +65,114 @@ public function __construct(
$this->context = $context;
}

public function select(string ...$selects): QueryBuilder
{
$this->selectIdentifierCollection = SelectIdentifierCollection::fromExpressions(...$selects);
return parent::select(...$selects);
}

/**
* @inheritdoc
*/
public function from(string $from, string $alias = null): QueryBuilder
{
$this->tableIdentifier = TableIdentifier::create($from, $alias);

return parent::from($from, $alias);
}

/**
* Executes this query using the bound parameters and their types.
*
* @return \Doctrine\DBAL\Driver\Statement|ContextAwareStatement|int
* @inheritdoc
*/
public function execute()
{
if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
return parent::execute();
}

if (!$this->context->hasAspect('workspace') && !$this->context->hasAspect('language')) {
return parent::execute();
}

$aspectViewQueries = [];
$inlineViewQueries = $this->getInlineViewQueries();
$innerQuery = end($inlineViewQueries);

if ($this->context->hasAspect('workspace')) {
$workspaceAspectView = new WorkspaceAspectView($this->connection, $this->context->getAspect('workspace'));
$aspectViewQueries[] = $workspaceAspectView->buildQuery($this->tableIdentifier, $this->selectIdentifierCollection);
while ($outerQuery = prev($inlineViewQueries)) {
$innerQuery = $this->mergeInlineView($outerQuery, $innerQuery);
}

if ($this->context->hasAspect('language')) {
$languageAspectView = new LanguageAspectView($this->connection, $this->context->getAspect('language'));
$aspectViewQueries[] = $languageAspectView->buildQuery($this->tableIdentifier, $this->selectIdentifierCollection);
if ($innerQuery !== false) {
$this->mergeInlineView($this, $innerQuery);
}

$innerQuery = end($aspectViewQueries);
$originalWhereConditions = $this->addAdditionalWhereConditions();

while ($outerQuery = prev($aspectViewQueries)) {
$innerQuery = $this->mergeInlineView($outerQuery, $innerQuery);
}
$result = $this->concreteQueryBuilder->execute();

$this->mergeInlineView($this, $innerQuery);
$this->concreteQueryBuilder->add('where', $originalWhereConditions, false);

return $this->concreteQueryBuilder->execute();
return $result;
}

/**
* Deep clone of the QueryBuilder
* @see \Doctrine\DBAL\Query\QueryBuilder::__clone()
* @inheritdoc
*/
protected function getQueriedTables(): array
{
$queriedTables = parent::getQueriedTables();
// Fakes the table name of the from clause otherwise the query restrictions won't work.
$tableAlias = $this->tableIdentifier->getAlias() ?? $this->tableIdentifier->getTableName();
$queriedTables[$tableAlias] = $this->tableIdentifier->getTableName();

return $queriedTables;
}

/**
* @inheritdoc
*/
public function __clone()
{
parent::__clone();
$this->context = clone $this->context;
}

/**
* @todo Make views plugable like restrictions.
* @todo Collect all column identifier used by the projection and selection.
*/
private function getInlineViewQueries(): array
{
$inlineViewQueries = [];

if ($this->context->hasAspect('workspace')) {
$workspaceAspectView = new WorkspaceAspectView($this->connection, $this->context->getAspect('workspace'));
$inlineViewQueries[] = $workspaceAspectView->buildQuery($this->tableIdentifier, null);
}

if ($this->context->hasAspect('language')) {
$languageAspectView = new LanguageAspectView($this->connection, $this->context->getAspect('language'));
$inlineViewQueries[] = $languageAspectView->buildQuery($this->tableIdentifier, null);
}

return array_filter($inlineViewQueries);
}

/**
* @todo Throw exception when an inner parameter is already set
*/
private function mergeInlineView(QueryBuilder $outerQueryBuilder, QueryBuilder $innerQueryBuilder): QueryBuilder
{
$outerFromPart = $outerQueryBuilder->getQueryPart('from');

if (
!isset($outerFromPart[0]['table'])
|| $outerFromPart[0]['table'] !== $this->quoteIdentifier($this->tableIdentifier->getTableName())
) {
throw new \Exception('Inline view is not compatible with outer query.', 1574599278);
}

$outerQueryBuilder->add(
'from',
[
[
'table' => sprintf('(%s)', $innerQueryBuilder->getSQL()),
'alias' => $this->quoteIdentifier(
$this->tableIdentifier->getAlias() ?? $this->tableIdentifier->getTableName()
)
'alias' => $outerFromPart[0]['alias'] ?? $outerFromPart[0]['table']
]
],
false
);

foreach ($innerQueryBuilder->getParameters() as $key => $value) {
// @todo Throw exception if parameter is already set
$outerQueryBuilder->setParameter(
$key,
$value,
Expand Down
Loading