Restriction builder
Table of Contents
Database tables in TYPO3 that can be managed in the backend have TCA definitions that specify how single fields and rows of the table should be handled and displayed by the framework.
The ctrl
section of a table's TCA array specifies optional framework-internal
handling of soft deletes and language overlays: For instance, when a row is
deleted in the backend using the page or list module, many tables are configured
to not drop that row entirely from the table, but to set a field (often
deleted
) for that row from 0
to 1
. Similar mechanisms apply for start and
end times, and to language and workspace overlays as well. See the
Table properties (ctrl) chapter in the TCA reference for details on this topic.
However, these mechanisms come at a price: developers of extensions dealing with low-level queries must take care that overlaid or deleted rows are not included in the result set of a simple query.
This is where this "automatic restriction" enters the picture: The construct is
created on top of native Doctrine DBAL as a
TYPO3-specific extension. It automatically adds WHERE
expressions that
suppress rows which are marked as deleted or have exceeded their "active"
lifecycle. All this is based on the TCA configuration of the affected table.
Rationale
A developer might ask why they need to do all this to themselves, and why this extra material is added on top of a low-level query layer when "just a simple query" should be fired. The construct implements some important design goals:
- Simple: Query creation should be easy to handle without a developer having to deal too much with the tedious TCA details..
- Cope with developer laziness: If the framework would force a developer to always add casual restrictions for every single query, this is easy to forget. We are all lazy, aren't we?
- Security: When in doubt, it is better to show a little too little than too much. It is much better to deal with a customer complaining that some records are not displayed than to show too many records. The former is "just a bug", while the latter can easily lead to a serious privilege escalation security issue.
- Automatic query upgrades:
If a table was originally designed without a soft delete and a delete flag
is later added and registered in TCA, queries executed on that table will
automatically upgrade and the according
deleted = 0
restriction will be added. - Handing over restriction details to the framework: Having the restriction expressions handled by the framework gives it the ability to change details without breaking the extension code. This may well happen in the future, and a happy little upgrade path for such cases may prove very useful later.
- Flexibility: The class construct is designed in a way so that developers can extend or or substitute it with their own restrictions if that makes sense for modeling the domain in question.
Main construct
The restriction builder is called whenever a SELECT
or COUNT
query is executed using either \TYPO3\
or \TYPO3\
. The QueryBuilder allows manipulation of those restrictions, while the
simplified Connection class does not. When a query
deals with multiple tables in a join, restrictions are added for all affected
tables.
Each single restriction such as a
\TYPO3\
or a
\TYPO3\
is modeled
as a single class that implements the
\TYPO3\
. Each
restriction looks up in TCA whether it should be applied. If so, the
according expressions are added to the WHERE
clause when compiling the
final statement.
Multiple restrictions can be grouped into containers which implement the
\TYPO3\
.
The \TYPO3\
is always added by default: It adds the
\TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Deleted Restriction \TYPO3\
,CMS\ Core\ Database\ Query\ Restriction\ Hidden Restriction \TYPO3\
and theCMS\ Core\ Database\ Query\ Restriction\ Start Time Restriction \TYPO3\
.CMS\ Core\ Database\ Query\ Restriction\ End Time Restriction
Note that this applies to all contexts in which a query is executed: It does not
matter whether a query is created from a frontend, a backend, or a
CLI call, they all add the
Default
unless explicitly stated otherwise by an
extension developer.
Note
Having this Default
used everywhere is the second
iteration of this code construct:
The first variant automatically added contextual restrictions. For instance, a query triggered by a call in the backend did not add the hidden flag, while a query triggered in the frontend did. We quickly figured out that this leads to a huge mess: The distinction between frontend, backend and CLI is not that sharp in TYPO3, so for example the frontend behaves much more like a backend call when using the admin panel.
The currently active variant is much easier: It always adds sane defaults everywhere, a developer only has to deal with details if they do not fit. The Core Team hopes that this approach is a good balance between hidden magic, security, transparency, and convenience.
Restrictions
\TYPO3\
(default)CMS\ Core\ Database\ Query\ Restriction\ Deleted Restriction - Evaluates
['ctrl']
, adds for instance['delete'] AND deleted = 0
ifTCA
is specified.['a Table'] ['ctrl'] ['delete'] = 'deleted' \TYPO3\
(default)CMS\ Core\ Database\ Query\ Restriction\ Hidden Restriction - Evaluates
['ctrl']
, adds['enablecolumns'] ['disabled'] AND hidden = 0
ifhidden
is specified as field name. \TYPO3\
(default)CMS\ Core\ Database\ Query\ Restriction\ Start Time Restriction - Evaluates
['ctrl']
, typically adds something like['enablecolumns'] ['starttime'] AND (`tt_content`.`starttime` <= 1475580240)
. \TYPO3\
(default)CMS\ Core\ Database\ Query\ Restriction\ End Time Restriction - Evaluates
['ctrl']
.['enablecolumns'] ['endtime'] \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Frontend Group Restriction - Evaluates
['enablecolumns']
.['fe_ group'] \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Rootlevel Restriction - Match records on root level, adds
AND (`pid` = 0)
\TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Workspace Restriction - The workspace restriction limits an SQL query to only select records which are "online" and in live or current workspace.
Note
As an important note and limitation of any workspace-related restrictions,
fetching the exact records need to be handled after the SQL results are
fetched, by overlaying the records with
\TYPO3\
,
\TYPO3\
or
\TYPO3\
.
When a restriction needs to be enforced, a restriction could implement the
interface \TYPO3\
.
If a restriction implements Enforceable
, the
following applies:
->remove
will remove all restrictions except the ones that implement the interfaceAll () Enforceable
.Query Restriction Interface ->remove
will remove a restriction completely, also restrictions that implement the interfaceBy Type () Enforceable
.Query Restriction Interface
QueryRestrictionContainer
\TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Default Restriction Container -
Adds
\TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Deleted Restriction \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Hidden Restriction \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Start Time Restriction \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ End Time Restriction
This container is always added if not told otherwise.
\TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Frontend Restriction Container -
Adds
\TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Deleted Restriction \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Hidden Restriction \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Start Time Restriction \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ End Time Restriction \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Workspace Restriction \TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Frontend Group Restriction
This container should be added by a developer to a query when creating query statements in frontend context or when handling frontend stuff from within CLI calls.
\TYPO3\
CMS\ Core\ Database\ Query\ Restriction\ Limit To Tables Restriction Container - This restriction container applies added restrictions only to the given table aliases. See Limit restrictions to tables for more information. Enforced restrictions are treated equally to all other restrictions.
Limit restrictions to tables
With \TYPO3\
it is possible to apply restrictions to a query only for a given set of tables,
or - to be precise - table aliases. Since it is a restriction container, it can
be added to the restrictions of the query builder and can hold restrictions
itself.
Examples
If you want to apply one or more restrictions to only one table, that is
possible as follows. Let us say you have content in the tt_
table
with a relation to categories. Now you want to get all records with their
categories except those that are hidden. In this case, the hidden restriction
should apply only to the tt_
table, not to the sys_
or sys_
table.
// use TYPO3\CMS\Core\Database\Connection;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()
->removeByType(HiddenRestriction::class)
->add(
GeneralUtility::makeInstance(LimitToTablesRestrictionContainer::class)
->addForTables(GeneralUtility::makeInstance(HiddenRestriction::class), ['tt'])
);
$queryBuilder->select('tt.uid', 'tt.header', 'sc.title')
->from('tt_content', 'tt')
->from('sys_category', 'sc')
->from('sys_category_record_mm', 'scmm')
->where(
$queryBuilder->expr()->eq(
'scmm.uid_foreign',
$queryBuilder->quoteIdentifier('tt.uid')
),
$queryBuilder->expr()->eq(
'scmm.uid_local',
$queryBuilder->quoteIdentifier('sc.uid')
),
$queryBuilder->expr()->eq(
'tt.uid',
$queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
)
);
Read how to correctly instantiate a query builder with the connection pool.
In addition, it is possible to restrict the complete set of restrictions of a query builder to a given set of table aliases:
// use TYPO3\CMS\Core\Database\Connection;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(HiddenRestriction::class));
$queryBuilder->getRestrictions()->limitRestrictionsToTables(['c2']);
$queryBuilder
->select('c1.*')
->from('tt_content', 'c1')
->leftJoin('c1', 'tt_content', 'c2', 'c1.parent_field = c2.uid')
->orWhere(
$queryBuilder->expr()->isNull('c2.uid'),
$queryBuilder->expr()->eq(
'c2.pid',
$queryBuilder->createNamedParameter(1, Connection::PARAM_INT)
)
);
Read how to correctly instantiate a query builder with the connection pool.
Which results in:
SELECT "c1".*
FROM "tt_content" "c1"
LEFT JOIN "tt_content" "c2" ON c1.parent_field = c2.uid
WHERE (("c2"."uid" IS NULL) OR ("c2"."pid" = 1))
AND ("c2"."hidden" = 0))
Custom restrictions
It is possible to add additional query restrictions by adding class names as key
to $GLOBALS
.
These restriction objects will be added to any select query executed using the
QueryBuilder.
If these added restriction objects additionally implement
\TYPO3\
and return true
in the to be implemented method is
,
calling $query
such restrictions
will still be applied to the query.
If an enforced restriction must be removed, it can still be removed with
$query
.
Implementers of custom restrictions can therefore have their restrictions always enforced, or even not applied at all, by returning an empty expression in certain cases.
To add a custom restriction class, use the following snippet:
<?php
declare(strict_types=1);
use MyVendor\MyExtension\Database\Query\Restriction\CustomRestriction;
defined('TYPO3') or die();
$GLOBALS['TYPO3_CONF_VARS']['DB']['additionalQueryRestrictions'][CustomRestriction::class] ??= [];
Note
The class name must be the array key and the value must always be an array, which is reserved for options given to the restriction objects.
Attention
Restrictions added by third-party extensions will impact the whole system. Therefore this API does not allow removing restrictions added by the system and adding restrictions should be handled with care.
Removing third party restrictions is possible, by setting the option
disabled
for a restriction to true
in global TYPO3 configuration
or ext_
of an extension:
Examples
Often the default restrictions are sufficient. Nothing needs to be done in those cases.
However, many backend modules still want to show disabled records and remove the start time and end time restrictions to allow administration of those records for an editor. A typical setup from within a backend module:
// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\Connection;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
// SELECT `uid`, `bodytext` FROM `tt_content` WHERE (`pid` = 42) AND (`tt_content`.`deleted` = 0)
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
// Remove all restrictions but add DeletedRestriction again
$queryBuilder
->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$result = $queryBuilder
->select('uid', 'bodytext')
->from('tt_content')
->where($queryBuilder->expr()->eq(
'pid',
$queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)
))
->executeQuery()
->fetchAllAssociative(();
Read how to correctly instantiate a query builder with the connection pool.
The Deleted
should be kept in almost all cases. Usually, the
only extension that dismisses that flag is the recycler module to list and
resurrect deleted records. Any object implementing the
Query
can be given to ->add
. This allows
extensions to deliver own restrictions.
An alternative to the recommended way of first removing all restrictions and
then adding needed ones again (using ->remove
, then ->add
)
is to kick specific restrictions with a call to ->remove
:
// use TYPO3\CMS\Core\Utility\GeneralUtility;
// use TYPO3\CMS\Core\Database\ConnectionPool;
// use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
// use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
// Remove starttime and endtime, but keep hidden and deleted
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder
->getRestrictions()
->removeByType(StartTimeRestriction::class)
->removeByType(EndTimeRestriction::class);
Read how to correctly instantiate a query builder with the connection pool.
In the frontend it is often needed to swap the Default
with the Frontend
:
// use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer
// Remove default restrictions and add list of default frontend restrictions
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
Note that ->set
resets any previously specified
restrictions. Any class instance implementing
Query
can be given to
->set
. This allows extensions to deliver and use an custom
set of restrictions for own query statements if needed.
Tip
It can be very helpful to debug the final statements created by the
Restriction
using debug
right
before the final call to $query
. Just take
care these calls do not
end up in production code.