如何在 TYPO3 中向 Doctrine DBAL 编写复杂查询?

Howto rewrite complex query to Doctrine DBAL in TYPO3?

我正在努力将复杂的 SQL 查询迁移到 TYPO3 中的 Doctrine DBAL。我的旧存储库查询如下所示:

$enableFields = $GLOBALS['TSFE']->sys_page->enableFields('tx_test');

// calculate distance between geo coordinates
$distance = '';
if ($geoData) {
    $distance = ', 3956 * 2 * ASIN( SQRT (POWER ( SIN((' . $geoData['latitude'] . ' - abs(tx_test.latitude)) * pi() / 180 / 2), 2) + COS(' . $geoData['latitude'] . ' * pi() / 180) * ' . 'COS(abs(tx_test.latitude)' .
        ' * pi() / 180) * POWER (SIN((' . $geoData['longitude'] .
        ' - tx_test.longitude)' .
        ' * pi() / 180 / 2), 2))) AS distance';
}

// General query
$sql = 'SELECT * ' . $distance .
       ' FROM tx_test ' .
       ' WHERE DATE_FORMAT(FROM_UNIXTIME(start), "%Y") = '.(int)$settings['flexformYear'].' AND published = 1 ' . $enableFields;

if($headline) {
    $sql .= ' AND headline like '.$GLOBALS['TYPO3_DB']->quoteStr($headline).'%';
}

// ... and some more ...

$query = $this->createQuery();
$results = $query->statement($sql)->execute();

迁移到 Doctrine

现在我可以轻松删除 $GLOBALS['TYPO3_DB']->quoteStr() 并将上面代码的最后两行替换为:

$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_test');
$results = $connection->executeQuery($sql)->fetchAll();

但是这个 returns 结果数组而不是带有附加子对象的对象,例如。 FileReference 对象或其他附加模型。

还有其他方法可以达到预期的效果吗?如果不是,我如何 secure/sanitize 用户输入 $headline?我需要自己为联接表编写 SQL 吗?

这是一些距离计算的一部分,应该适合您关于使用 Doctrine 和引用的用例。

使用 Doctrine 你也会得到数组形式的记录。如果您需要 Extbase 对象,请参阅下文。

获取 Doctrine DBAL QueryBuilder 的 TYPO3 风格:

$q = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable($table = 'tx_....');
$q->select(
  ...
  'a.lat',
  'a.lng'
)
  ->from($table /*, 'alias if you want' */);

我在进行这样的计算时并没有经常使用流畅的界面(尽管当然可以通过使用带有 $q->selectLiteral() 的任何 MySQL 函数来做到这一点)。

参数

为了防止 SQL 注入,您应该使用 $q->quote() / $q->quoteIdentifier() 引用所有可能的用户输入或使用参数 $q->createNamedParameter().

约束示例

这只是一部分限制条件。它包含一个示例,说明如何根据搜索功能中常见的条件组合它们。

if ((float)$searchObject->radiusKm > .5) {
    $_radiusOrs = [
        'IF (
            ' . $q->quoteIdentifier('lat') . ' = 0,
            100000,
            12742 * ASIN(
                SQRT(
                    POWER(
                        SIN(
                            ( ' . $q->quote((float)$searchObject->lat) . ' 
                                - ABS( ' . $q->quoteIdentifier('lat') . ' ) 
                            ) * 0.0087266
                        ), 
                        2
                    )
                    +
                    COS( ' . $q->quote((float)$searchObject->lng) . ' * 0.01745329 ) * COS(
                        ABS( ' . $q->quoteIdentifier('lat') . ' ) * 0.01745329
                    ) * POWER(
                        SIN( 
                            ( ' . $q->quote((float)$searchObject->lng) . ' 
                                - ' . $q->quoteIdentifier('lng') . ' 
                            ) * 0.0087266 
                        ), 
                        2
                    )
                 )
            )
        ) < ' . $q->quote((float)$searchObject->radiusKm),
    ];

    $q->andWhere(
        $q->expr()->orX(...$_radiusOrs)
    );
}
...
$aRes = $q->execute()->fetchAll();

(如果你想调试:你得到 SQL with $q->getSQL(), $q->getParameters()

映射到 Extbase 对象

$dataMapper = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::class);
$objects = $dataMapper->map(YourExtbaseModel::class, $aRes);            

我建议:仅当对象很少或内存足够且不关心性能时才使用 Extbase 对象。大多数情况下,您应该尽量避免使用普通数组。

Extbase 查询构建器允许在输出到 Fluid 模板时进行一些优化:传递 \TYPO3\CMS\Extbase\Persistence\Generic\QueryResult 允许在其上使用 f:paginate ViewHelper,它不必实例化所有对象,但仅显示在当前页面上的那些。当使用 Doctrine QueryBuilder 时,这种可能性还不存在(现在?)。因此,现在使用 Extbase 模型应该是最后的手段,而不是默认的。它似乎是 "best practice" - 恕我直言,这不再是真的。