diff --git a/Documentation/ApiOverview/Database/ExpressionBuilder/Index.rst b/Documentation/ApiOverview/Database/ExpressionBuilder/Index.rst index 6df1ba1732..1f3b107978 100644 --- a/Documentation/ApiOverview/Database/ExpressionBuilder/Index.rst +++ b/Documentation/ApiOverview/Database/ExpressionBuilder/Index.rst @@ -1,95 +1,104 @@ -.. include:: /Includes.rst.txt +.. include:: /Includes.rst.txt -.. _database-expression-builder: +.. _database-expression-builder: -================= -ExpressionBuilder -================= +================== +Expression builder +================== -The `ExpressionBuilder` class is responsible to dynamically create SQL query parts -for `WHERE` and `JOIN ON` conditions, functions like :php:`->min()` may also be used in -`SELECT` parts. +.. contents:: **Table of Contents** + :local: -It takes care of building query conditions while ensuring table and column names -are quoted within the created expressions / SQL fragments. It is a facade to -the actual Doctrine DBAL `ExpressionBuilder`. -The `ExpressionBuilder` is used within the context of the :ref:`QueryBuilder ` -to ensure queries are being build based on the requirements of the database platform in use. +Introduction +============ -An instance of the `ExpressionBuilder` is retrieved from the `QueryBuilder` object: +The :php:`\TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder` class is +responsible for dynamically creating SQL query parts for :sql:`WHERE` and +:sql:`JOIN ON` conditions. Functions like :php:`->min()` may also be used in +:sql:`SELECT` parts. -.. code-block:: php - :caption: EXT:some_extension/Classes/SomeClass.php +It takes care of building query conditions and ensures that table and column +names are quoted within the created expressions / SQL fragments. It is a facade +to the actual Doctrine DBAL :php:`ExpressionBuilder`. - $expressionBuilder = $queryBuilder->expr(); +The expression builder is used in the context of the :ref:`query builder +` to ensure that queries are built based on the +requirements of the database platform being used. -It is good practice to not assign an instance of the `ExpressionBuilder` to a variable but -to use it within the code flow of the `QueryBuilder` context directly: - -.. code-block:: php - :caption: EXT:some_extension/Classes/SomeClass.php - - // use TYPO3\CMS\Core\Utility\GeneralUtility; - // use TYPO3\CMS\Core\Database\ConnectionPool; - $rows = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content') - ->select('uid', 'header', 'bodytext') - ->from('tt_content') - ->where( - // `bodytext` = 'klaus' AND `header` = 'peter' - $queryBuilder->expr()->eq('bodytext', $queryBuilder->createNamedParameter('klaus')), - $queryBuilder->expr()->eq('header', $queryBuilder->createNamedParameter('peter')) - ) - ->execute() - ->fetchAllAssociative(); - -.. warning:: - - It is crucially important to quote values correctly to not introduce SQL injection attack - vectors to your application. See the - :ref:`section of the QueryBuilder ` for details. +Basic usage +=========== -Junctions -========= +An instance of the :php:`ExpressionBuilder` is retrieved from the +:php:`QueryBuilder` object: -.. versionchanged:: 11.5.10 - The methods :php:`andX()` and :php:`orX()` are deprecated and replaced - by :php:`and()` and :php:`or()` to match with `doctrine/dbal`, which - `deprecated `_ - these methods. +.. code-block:: php + :caption: EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php -* :php:`->and()` conjunction + $expressionBuilder = $queryBuilder->expr(); -* :php:`->or()` disjunction +It is good practice not to assign an instance of the :php:`ExpressionBuilder` to +a variable, but to use it directly within the code flow of the query builder +context: +.. literalinclude:: _MyTableRepository.php + :caption: EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php -Combine multiple single expressions with `AND` or `OR`. Nesting is possible, both methods are variadic and -take any number of argument which are all combined. It usually doesn't make much sense to hand over -zero or only one argument, though. +.. warning:: + It is of crucial importance to quote values correctly to not introduce SQL + injection attack vectors into your application. See the :ref:`according + section of the query builder ` + for details. -Example to find tt_content records: -.. code-block:: php - :caption: EXT:some_extension/Classes/SomeClass.php +Junctions +========= - // use TYPO3\CMS\Core\Utility\GeneralUtility; - // use TYPO3\CMS\Core\Database\ConnectionPool; - // WHERE - // (`tt_content`.`CType` = 'list') - // AND ( - // (`tt_content`.`list_type` = 'example_pi1') - // OR - // (`tt_content`.`list_type` = 'example_pi2') - // ) - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); - $queryBuilder->where( - $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('list')), - $queryBuilder->expr()->or( - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('example_pi1')), - $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('example_pi2')) - ) - ) +.. versionchanged:: 11.5.10 + The :php:`andX()` and :php:`orX()` methods are deprecated and replaced by + :php:`and()` and :php:`or()` to match with Doctrine DBAL, which `deprecated + `_ + these methods. + +* :php:`->and()` conjunction + +* :php:`->or()` disjunction + +Combine multiple single expressions with :sql:`AND` or :sql:`OR`. Nesting is +possible, both methods are variadic and accept any number of arguments, which +are all combined. However, it usually makes little sense to pass zero or only +one argument. + +Example to find :sql:`tt_content` records: + +.. code-block:: php + :caption: EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php + + // WHERE + // (`tt_content`.`CType` = 'list') + // AND ( + // (`tt_content`.`list_type` = 'example_pi1') + // OR + // (`tt_content`.`list_type` = 'example_pi2') + // ) + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); + $queryBuilder->where( + $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('list')), + $queryBuilder->expr()->or( + $queryBuilder->expr()->eq( + 'list_type', + $queryBuilder->createNamedParameter('example_pi1') + ), + $queryBuilder->expr()->eq( + 'list_type', + $queryBuilder->createNamedParameter('example_pi2') + ) + ) + ) + +Read :ref:`how to correctly instantiate ` +a query builder with the connection pool. Comparisons @@ -97,202 +106,219 @@ Comparisons A set of methods to create various comparison expressions or SQL functions: -* :php:`->eq($fieldName, $value)` "equal" comparison `=` +* :php:`->eq($fieldName, $value)` "equal" comparison `=` -* :php:`->neq($fieldName, $value)` "not equal" comparison `!=` +* :php:`->neq($fieldName, $value)` "not equal" comparison `!=` -* :php:`->lt($fieldName, $value)` "less than" comparison `<` +* :php:`->lt($fieldName, $value)` "less than" comparison `<` -* :php:`->lte($fieldName, $value)` "less than or equal" comparison `<=` +* :php:`->lte($fieldName, $value)` "less than or equal" comparison `<=` -* :php:`->gt($fieldName, $value)` "greater than" comparison `>` +* :php:`->gt($fieldName, $value)` "greater than" comparison `>` -* :php:`->gte($fieldName, $value)` "greater than or equal" comparison `>=` +* :php:`->gte($fieldName, $value)` "greater than or equal" comparison `>=` -* :php:`->isNull($fieldName)` "IS NULL" comparison +* :php:`->isNull($fieldName)` "IS NULL" comparison -* :php:`->isNotNull($fieldName)` "IS NOT NULL" comparison +* :php:`->isNotNull($fieldName)` "IS NOT NULL" comparison -* :php:`->like($fieldName, $value)` "LIKE" comparison +* :php:`->like($fieldName, $value)` "LIKE" comparison -* :php:`->notLike($fieldName, $value)` "NOT LIKE" comparison +* :php:`->notLike($fieldName, $value)` "NOT LIKE" comparison -* :php:`->in($fieldName, $valueArray)` "IN ()" comparison +* :php:`->in($fieldName, $valueArray)` "IN ()" comparison -* :php:`->notIn($fieldName, $valueArray)` "NOT IN ()" comparison +* :php:`->notIn($fieldName, $valueArray)` "NOT IN ()" comparison -* :php:`->inSet($fieldName, $value)` "FIND_IN_SET('42', `aField`)" Find a value in a comma separated list of values +* :php:`->inSet($fieldName, $value)` "FIND_IN_SET('42', `aField`)" + Find a value in a comma separated list of values -* :php:`->notInSet($fieldName, $value)` "NOT FIND_IN_SET('42', `aField`)" Find a value not in a comma separated list of values +* :php:`->notInSet($fieldName, $value)` "NOT FIND_IN_SET('42', `aField`)" + Find a value not in a comma separated list of values -* :php:`->bitAnd($fieldName, $value)` A bitwise AND operation `&` +* :php:`->bitAnd($fieldName, $value)` A bitwise AND operation `&` Remarks and warnings: -* The first argument :php:`$fieldName` is always quoted automatically. - -* All methods that have a :php:`$value` or :php:`$valueList` as second argument **must** be quoted, usually by calling - :ref:`$queryBuilder->createNamedParameter() ` or - :ref:`$queryBuilder->quoteIdentifier() `. **Failing to do so will end - up in SQL injections!** - -* :php:`->like()` and :php:`->notLike()` values **must** be **additionally** quoted with a call to - :ref:`$queryBuilder->escapeLikeWildcards($value) ` to - suppress the special meaning of `%` characters from `$value`. - - -Examples: - -.. code-block:: php - :caption: EXT:some_extension/Classes/SomeClass.php - - // `bodytext` = 'foo' - string comparison - ->eq('bodytext', $queryBuilder->createNamedParameter('foo')) - - // `tt_content`.`bodytext` = 'foo' - ->eq('tt_content.bodytext', $queryBuilder->createNamedParameter('foo')) +* The first argument :php:`$fieldName` is always quoted automatically. - // `aTableAlias`.`bodytext` = 'foo' - ->eq('aTableAlias.bodytext', $queryBuilder->createNamedParameter('foo')) +* All methods that have a :php:`$value` or :php:`$valueList` as second + argument **must** be quoted, usually by calling + :ref:`$queryBuilder->createNamedParameter() ` + or :ref:`$queryBuilder->quoteIdentifier() `. - // `uid` = 42 - integer comparison - ->eq('uid', $queryBuilder->createNamedParameter(42, \PDO::PARAM_INT)) + .. warning:: + Failing to quote will end up in :ref:`SQL injections `! - // `uid` >= 42 - ->gte('uid', $queryBuilder->createNamedParameter(42, \PDO::PARAM_INT)) +* :php:`->like()` and :php:`->notLike()` values **must** be **additionally** + quoted with a call to :ref:`$queryBuilder->escapeLikeWildcards($value) + ` to suppress the special + meaning of `%` characters from `$value`. - // `bodytext` LIKE 'klaus' - ->like( - 'bodytext', - $queryBuilder->createNamedParameter($queryBuilder->escapeLikeWildcards('klaus')) - ) - // `bodytext` LIKE '%klaus%' - ->like( - 'bodytext', - $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards('klaus') . '%') - ) - - // usergroup does not contain 42 - ->notInSet('usergroup', $queryBuilder->createNamedParameter('42')) - - // use TYPO3\CMS\Core\Database\Connection; - // `uid` IN (42, 0, 44) - properly sanitized, mind the intExplode and PARAM_INT_ARRAY - ->in( - 'uid', - $queryBuilder->createNamedParameter( - GeneralUtility::intExplode(',', '42, karl, 44', true), - Connection::PARAM_INT_ARRAY - ) - ) +Examples: - // use TYPO3\CMS\Core\Database\Connection; - // `CType` IN ('media', 'multimedia') - properly sanitized, mind the PARAM_STR_ARRAY - ->in( - 'CType', - $queryBuilder->createNamedParameter( - ['media', 'multimedia'], - Connection::PARAM_STR_ARRAY - ) - ) +.. code-block:: php + :caption: EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php + + // `bodytext` = 'foo' - string comparison + ->eq('bodytext', $queryBuilder->createNamedParameter('foo')) + + // `tt_content`.`bodytext` = 'foo' + ->eq('tt_content.bodytext', $queryBuilder->createNamedParameter('foo')) + + // `aTableAlias`.`bodytext` = 'foo' + ->eq('aTableAlias.bodytext', $queryBuilder->createNamedParameter('foo')) + + // `uid` = 42 - integer comparison + ->eq('uid', $queryBuilder->createNamedParameter(42, \PDO::PARAM_INT)) + + // `uid` >= 42 + ->gte('uid', $queryBuilder->createNamedParameter(42, \PDO::PARAM_INT)) + + // `bodytext` LIKE 'klaus' + ->like( + 'bodytext', + $queryBuilder->createNamedParameter( + $queryBuilder->escapeLikeWildcards('klaus') + ) + ) + + // `bodytext` LIKE '%klaus%' + ->like( + 'bodytext', + $queryBuilder->createNamedParameter( + '%' . $queryBuilder->escapeLikeWildcards('klaus') . '%' + ) + ) + + // usergroup does not contain 42 + ->notInSet('usergroup', $queryBuilder->createNamedParameter('42')) + + // use TYPO3\CMS\Core\Database\Connection; + // `uid` IN (42, 0, 44) - properly sanitized, mind the intExplode and PARAM_INT_ARRAY + ->in( + 'uid', + $queryBuilder->createNamedParameter( + GeneralUtility::intExplode(',', '42, karl, 44', true), + Connection::PARAM_INT_ARRAY + ) + ) + + // use TYPO3\CMS\Core\Database\Connection; + // `CType` IN ('media', 'multimedia') - properly sanitized, mind the PARAM_STR_ARRAY + ->in( + 'CType', + $queryBuilder->createNamedParameter( + ['media', 'multimedia'], + Connection::PARAM_STR_ARRAY + ) + ) Aggregate Functions =================== -Aggregate functions used in `SELECT` parts, often combined with `GROUP BY`. First argument is -the field name (or table name / alias with field name), second argument an optional alias. +Aggregate functions used in :sql:`SELECT` parts, often combined with +:sql:`GROUP BY`. The first argument is the field name (or table name / alias +with field name), the second argument is an optional alias. -* :php:`->min($fieldName, $alias = NULL)` "MIN()" calculation +* :php:`->min($fieldName, $alias = NULL)` "MIN()" calculation -* :php:`->max($fieldName, $alias = NULL)` "MAX()" calculation +* :php:`->max($fieldName, $alias = NULL)` "MAX()" calculation -* :php:`->avg($fieldName, $alias = NULL)` "AVG()" calculation +* :php:`->avg($fieldName, $alias = NULL)` "AVG()" calculation -* :php:`->sum($fieldName, $alias = NULL)` "SUM()" calculation - -* :php:`->count($fieldName, $alias = NULL)` "COUNT()" calculation +* :php:`->sum($fieldName, $alias = NULL)` "SUM()" calculation +* :php:`->count($fieldName, $alias = NULL)` "COUNT()" calculation Examples: -.. code-block:: php - :caption: EXT:some_extension/Classes/SomeClass.php - - // use TYPO3\CMS\Core\Utility\GeneralUtility; - // use TYPO3\CMS\Core\Database\ConnectionPool; - // Calculate the average creation timestamp of all rows from tt_content - // SELECT AVG(`crdate`) AS `averagecreation` FROM `tt_content` - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); - $result = $queryBuilder - ->addSelectLiteral( - $queryBuilder->expr()->avg('crdate', 'averagecreation') - ) - ->from('tt_content') - ->execute() - ->fetchAssociative(); - - // Distinct list of all existing endtime values from tt_content - // SELECT `uid`, MAX(`endtime`) AS `maxendtime` FROM `tt_content` GROUP BY `endtime` - $statement = $queryBuilder - ->select('uid') - ->addSelectLiteral( - $queryBuilder->expr()->max('endtime', 'maxendtime') - ) - ->from('tt_content') - ->groupBy('endtime') - ->execute(); +.. code-block:: php + :caption: EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php + + // Calculate the average creation timestamp of all rows from tt_content + // SELECT AVG(`crdate`) AS `averagecreation` FROM `tt_content` + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); + $result = $queryBuilder + ->addSelectLiteral( + $queryBuilder->expr()->avg('crdate', 'averagecreation') + ) + ->from('tt_content') + ->executeQuery() + ->fetchAssociative(); + + // Distinct list of all existing endtime values from tt_content + // SELECT `uid`, MAX(`endtime`) AS `maxendtime` FROM `tt_content` GROUP BY `endtime` + $statement = $queryBuilder + ->select('uid') + ->addSelectLiteral( + $queryBuilder->expr()->max('endtime', 'maxendtime') + ) + ->from('tt_content') + ->groupBy('endtime') + ->executeQuery(); + +Read :ref:`how to correctly instantiate ` +a query builder with the connection pool. Various Expressions =================== -TRIM ----- - -Using the TRIM expression makes sure fields get trimmed on database level. -See the examples below to get a better idea of what can be done: +trim() +------ -.. code-block:: php - :caption: EXT:some_extension/Classes/SomeClass.php +Using the :php:`->trim()` expression ensures that the fields are trimmed at the +database level. The following examples give a better idea of what is possible: - // use TYPO3\CMS\Core\Utility\GeneralUtility; - // use TYPO3\CMS\Core\Database\ConnectionPool; - // use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); - $queryBuilder->expr()->comparison( - $queryBuilder->expr()->trim($fieldName), - ExpressionBuilder::EQ, - $queryBuilder->createNamedParameter('', \PDO::PARAM_STR) - ); +.. code-block:: php + :caption: EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php -The call to :php:`$queryBuilder->expr()-trim()` can be one of the following: + // use TYPO3\CMS\Core\Database\Connection + // use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); + $queryBuilder->expr()->comparison( + $queryBuilder->expr()->trim($fieldName), + ExpressionBuilder::EQ, + $queryBuilder->createNamedParameter('', Connection::PARAM_STR) + ); -* :php:`trim('fieldName')` - results in :code:`TRIM("tableName"."fieldName")` -* :php:`trim('fieldName', AbstractPlatform::TRIM_LEADING, 'x')` - results in :code:`TRIM(LEADING "x" FROM "tableName"."fieldName")` -* :php:`trim('fieldName', AbstractPlatform::TRIM_TRAILING, 'x')` - results in :code:`TRIM(TRAILING "x" FROM "tableName"."fieldName")` -* :php:`trim('fieldName', AbstractPlatform::TRIM_BOTH, 'x')` - results in :code:`TRIM(BOTH "x" FROM "tableName"."fieldName")` +Read :ref:`how to correctly instantiate ` +a query builder with the connection pool. -LENGTH ------- +The call to :php:`$queryBuilder->expr()-trim()` can be one of the following: -The LENGTH string function can be used to return the length of a string in bytes, method -signature is fieldName with optional alias :php:`->length(string $fieldName, string $alias = null)`: - -.. code-block:: php - :caption: EXT:some_extension/Classes/SomeClass.php - - // use TYPO3\CMS\Core\Utility\GeneralUtility; - // use TYPO3\CMS\Core\Database\ConnectionPool; - // use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); - $queryBuilder->expr()->comparison( - $queryBuilder->expr()->length($fieldName), - ExpressionBuilder::GT, - $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) - ); +* :php:`trim('fieldName')` + results in :code:`TRIM("tableName"."fieldName")` +* :php:`trim('fieldName', AbstractPlatform::TRIM_LEADING, 'x')` + results in :code:`TRIM(LEADING "x" FROM "tableName"."fieldName")` +* :php:`trim('fieldName', AbstractPlatform::TRIM_TRAILING, 'x')` + results in :code:`TRIM(TRAILING "x" FROM "tableName"."fieldName")` +* :php:`trim('fieldName', AbstractPlatform::TRIM_BOTH, 'x')` + results in :code:`TRIM(BOTH "x" FROM "tableName"."fieldName")` + + +length() +-------- + +The :php:`->length()` string function can be used to return the length of a +string in bytes. The signature of the method signature is :php:`$fieldName` +with an optional alias :php:`->length(string $fieldName, string $alias = null)`: + +.. code-block:: php + :caption: EXT:my_extension/Classes/Domain/Repository/MyTableRepository.php + + // use TYPO3\CMS\Core\Database\Connection; + // use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); + $queryBuilder->expr()->comparison( + $queryBuilder->expr()->length($fieldName), + ExpressionBuilder::GT, + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ); + +Read :ref:`how to correctly instantiate ` +a query builder with the connection pool. diff --git a/Documentation/ApiOverview/Database/ExpressionBuilder/_MyTableRepository.php b/Documentation/ApiOverview/Database/ExpressionBuilder/_MyTableRepository.php new file mode 100644 index 0000000000..bda5f4a7fb --- /dev/null +++ b/Documentation/ApiOverview/Database/ExpressionBuilder/_MyTableRepository.php @@ -0,0 +1,43 @@ +connectionPool = $connectionPool; + } + + public function findSomething() + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME); + + $rows = $queryBuilder + ->select('uid', 'header', 'bodytext') + ->from(self::TABLE_NAME) + ->where( + // `bodytext` = 'klaus' AND `header` = 'peter' + $queryBuilder->expr()->eq( + 'bodytext', + $queryBuilder->createNamedParameter('klaus') + ), + $queryBuilder->expr()->eq( + 'header', + $queryBuilder->createNamedParameter('peter') + ) + ) + ->executeQuery() + ->fetchAllAssociative(); + + // ... + } +}