Skip to content

Commit

Permalink
[TASK] Refactor Integration tests : IndexerTest "Relation (MM) transl…
Browse files Browse the repository at this point in the history
…ation overlays"

Fixes: TYPO3-Solr#3092
Relates: TYPO3-Solr#2976, TYPO3-Solr#2977
  • Loading branch information
dkd-kaehm committed Dec 2, 2021
1 parent 1538c61 commit 82bfe55
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 44 deletions.
105 changes: 77 additions & 28 deletions Classes/ContentObject/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@

use ApacheSolrForTypo3\Solr\System\Language\FrontendOverlayService;
use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
use ApacheSolrForTypo3\Solr\Util;
use Doctrine\DBAL\Driver\Statement;
use ReflectionClass;
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

/**
* A content object (cObj) to resolve relations between database records
Expand All @@ -44,8 +46,8 @@
* foreignLabelField: Usually the label field to retrieve from the related records is determined automatically using TCA, using this option the desired field can be specified explicitly
* multiValue: whether to return related records suitable for a multi value field
* singleValueGlue: when not using multiValue, the related records need to be concatenated using a glue string, by default this is ", ". Using this option a custom glue can be specified. The custom value must be wrapped by pipe (|) characters.
* relationTableSortingField: field in an mm relation table to sort by, usually "sorting"
* enableRecursiveValueResolution: if the specified remote table's label field is a relation to another table, the value will be resolve by following the relation recursively.
* relationTableSortingField: field in a mm relation table to sort by, usually "sorting"
* enableRecursiveValueResolution: if the specified remote table's label field is a relation to another table, the value will be resolved by following the relation recursively.
* removeEmptyValues: Removes empty values when resolving relations, defaults to TRUE
* removeDuplicateValues: Removes duplicate values
*
Expand All @@ -60,17 +62,22 @@ class Relation extends AbstractContentObject
*
* @var array
*/
protected $configuration = [];
protected array $configuration = [];

/**
* @var TCAService
* @var TCAService|null
*/
protected $tcaService = null;
protected ?TCAService $tcaService = null;

/**
* @var FrontendOverlayService
* @var FrontendOverlayService|null
*/
protected $frontendOverlayService = null;
protected ?FrontendOverlayService $frontendOverlayService = null;

/**
* @var TypoScriptFrontendController|null
*/
protected ?TypoScriptFrontendController $typoScriptFrontendController = null;

/**
* Relation constructor.
Expand All @@ -79,21 +86,23 @@ class Relation extends AbstractContentObject
*/
public function __construct(ContentObjectRenderer $cObj, TCAService $tcaService = null, FrontendOverlayService $frontendOverlayService = null)
{
$this->cObj = $cObj;
parent::__construct($cObj);
$this->configuration['enableRecursiveValueResolution'] = 1;
$this->configuration['removeEmptyValues'] = 1;
$this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
$this->frontendOverlayService = $frontendOverlayService ?? GeneralUtility::makeInstance(FrontendOverlayService::class);
$this->typoScriptFrontendController = $this->getProtectedTsfeObjectFromContentObjectRenderer($cObj);
$this->frontendOverlayService = $frontendOverlayService ?? GeneralUtility::makeInstance(FrontendOverlayService::class, $this->tcaService, $this->typoScriptFrontendController);
}

/**
* Executes the SOLR_RELATION content object.
*
* Resolves relations between records. Currently supported relations are
* Resolves relations between records. Currently, supported relations are
* TYPO3-style m:n relations.
* May resolve single value and multi value relations.
*
* @inheritDoc
* @throws AspectNotFoundException
*/
public function render($conf = [])
{
Expand Down Expand Up @@ -121,9 +130,12 @@ public function render($conf = [])
* Gets the related items of the current record's configured field.
*
* @param ContentObjectRenderer $parentContentObject parent content object
*
* @return array Array of related items, values already resolved from related records
*
* @throws AspectNotFoundException
*/
protected function getRelatedItems(ContentObjectRenderer $parentContentObject)
protected function getRelatedItems(ContentObjectRenderer $parentContentObject): array
{
list($table, $uid) = explode(':', $parentContentObject->currentRecord);
$uid = (int) $uid;
Expand All @@ -146,14 +158,15 @@ protected function getRelatedItems(ContentObjectRenderer $parentContentObject)
}

/**
* Gets the related items from a table using a n:m relation.
* Gets the related items from a table using the n:m relation.
*
* @param string $localTableName Local table name
* @param int $localRecordUid Local record uid
* @param array $localFieldTca The local table's TCA
* @return array Array of related items, values already resolved from related records
* @throws AspectNotFoundException
*/
protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid, array $localFieldTca)
protected function getRelatedItemsFromMMTable(string $localTableName, int $localRecordUid, array $localFieldTca): array
{
$relatedItems = [];
$foreignTableName = $localFieldTca['config']['foreign_table'];
Expand Down Expand Up @@ -193,7 +206,7 @@ protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid,

return $this->getRelatedItems($contentObject);
} else {
if (Util::getLanguageUid() > 0) {
if ($this->getLanguageUid() > 0) {
$record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
}
$relatedItems[] = $record[$foreignTableLabelField];
Expand All @@ -208,14 +221,15 @@ protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid,
* and TypoScript configuration
*
* @param array $foreignTableTca The foreign table's TCA
* @return string The field to use for the related item's label
*
* @return string|null The field to use for the related item's label
*/
protected function resolveForeignTableLabelField(array $foreignTableTca)
protected function resolveForeignTableLabelField(array $foreignTableTca): ?string
{
$foreignTableLabelField = $foreignTableTca['ctrl']['label'];
$foreignTableLabelField = $foreignTableTca['ctrl']['label'] ?? null;

// when foreignLabelField is not enabled we can return directly
if (empty($this->configuration['foreignLabelField'])) {
if (empty($this->configuration['foreignLabelField'] ?? null)) {
return $foreignTableLabelField;
}

Expand All @@ -236,13 +250,15 @@ protected function resolveForeignTableLabelField(array $foreignTableTca)
* @param array $localFieldTca The local table's TCA
* @param ContentObjectRenderer $parentContentObject parent content object
* @return array Array of related items, values already resolved from related records
* @throws AspectNotFoundException
*/
protected function getRelatedItemsFromForeignTable(
$localTableName,
$localRecordUid,
array $localFieldTca,
string $localTableName,
int $localRecordUid,
array $localFieldTca,
ContentObjectRenderer $parentContentObject
) {
): array
{
$relatedItems = [];
$foreignTableName = $localFieldTca['config']['foreign_table'];
$foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
Expand Down Expand Up @@ -289,15 +305,18 @@ protected function getRelatedItemsFromForeignTable(
* @param string $foreignTableName Related record table name
*
* @return string
*
* @throws AspectNotFoundException
*/
protected function resolveRelatedValue(
array $relatedRecord,
$foreignTableTca,
$foreignTableLabelField,
ContentObjectRenderer $parentContentObject,
$foreignTableName = ''
) {
if (Util::getLanguageUid() > 0 && !empty($foreignTableName)) {
): string
{
if ($this->getLanguageUid() > 0 && !empty($foreignTableName)) {
$relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
}

Expand Down Expand Up @@ -336,17 +355,17 @@ protected function resolveRelatedValue(
$parentContentObject->data = $backupRecord;
}

return $parentContentObject->stdWrap($value, $this->configuration);
return $parentContentObject->stdWrap($value, $this->configuration) ?? '';
}

/**
* Return records via relation.
*
* @param string $foreignTable The table to fetch records from.
* @param int[] ...$uids The uids to fetch from table.
* @param int ...$uids The uids to fetch from table.
* @return array
*/
protected function getRelatedRecords($foreignTable, int ...$uids): array
protected function getRelatedRecords(string $foreignTable, int ...$uids): array
{
/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
Expand Down Expand Up @@ -382,4 +401,34 @@ protected function sortByKeyInIN(Statement $statement, string $columnName, ...$a
ksort($records);
return $records;
}

/**
* Returns current language id fetched from object properties chain TSFE->context->language aspect->id.
*
* @return int
* @throws AspectNotFoundException
*/
protected function getLanguageUid(): int
{
return (int)$this->typoScriptFrontendController->getContext()->getPropertyFromAspect('language', 'id');
}

/**
* Returns inaccessible {@link ContentObjectRenderer::typoScriptFrontendController} via reflection.
*
* The TypoScriptFrontendController object must be delegated to the whole object aggregation on indexing stack,
* to be able to use Contexts properties and proceed indexing request.
*
* @Todo: Ask TYPO3 core to make the method {@link ContentObjectRenderer::getTypoScriptFrontendController()} public.
*
* @param ContentObjectRenderer $cObj
* @return TypoScriptFrontendController|null
*/
protected function getProtectedTsfeObjectFromContentObjectRenderer(ContentObjectRenderer $cObj): ?TypoScriptFrontendController
{
$reflection = new ReflectionClass($cObj);
$property = $reflection->getProperty('typoScriptFrontendController');
$property->setAccessible(true);
return $property->getValue($cObj);
}
}
2 changes: 2 additions & 0 deletions Classes/FrontendEnvironment/Tsfe.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ protected function initializeTsfe(int $pageId, int $language = 0, ?int $rootPage
* @throws SiteNotFoundException
* @throws DBALDriverException
* @throws Exception\Exception
*
* @todo : Call `$globalsTSFE->newCObj($serverRequest);` each time the TSFE requested. And then remove {@link getServerRequestForTsfeByPageIdAndLanguageId()} method.
*/
public function getTsfeByPageIdAndLanguageId(int $pageId, int $language = 0, ?int $rootPageId = null): ?TypoScriptFrontendController
{
Expand Down
35 changes: 20 additions & 15 deletions Classes/System/Language/FrontendOverlayService.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@

use ApacheSolrForTypo3\Solr\System\TCA\TCAService;
use ApacheSolrForTypo3\Solr\Util;
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

Expand All @@ -43,9 +45,9 @@ class FrontendOverlayService
protected $tcaService = null;

/**
* @var TypoScriptFrontendController
* @var TypoScriptFrontendController|null
*/
protected $tsfe = null;
protected ?TypoScriptFrontendController $tsfe = null;

/**
* Relation constructor.
Expand All @@ -55,7 +57,7 @@ class FrontendOverlayService
public function __construct(TCAService $tcaService = null, TypoScriptFrontendController $tsfe = null)
{
$this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
$this->tsfe = $tsfe ?? $GLOBALS['TSFE'];
$this->tsfe = $tsfe;
}

/**
Expand All @@ -64,16 +66,18 @@ public function __construct(TCAService $tcaService = null, TypoScriptFrontendCon
* @param string $tableName
* @param array $record
* @return array
* @throws AspectNotFoundException
*/
public function getOverlay($tableName, $record)
public function getOverlay(string $tableName, array $record): ?array
{
$currentLanguageUid = $this->tsfe->getContext()->getPropertyFromAspect('language', 'id');
if ($tableName === 'pages') {
// @extensionScannerIgnoreLine
return $this->tsfe->sys_page->getPageOverlay($record, Util::getLanguageUid());
return $this->tsfe->sys_page->getPageOverlay($record, $currentLanguageUid);
}

// @extensionScannerIgnoreLine
return $this->tsfe->sys_page->getRecordOverlay($tableName, $record, Util::getLanguageUid());
return $this->tsfe->sys_page->getRecordOverlay($tableName, $record, $currentLanguageUid);
}

/**
Expand All @@ -84,15 +88,17 @@ public function getOverlay($tableName, $record)
* @param string $field
* @param int $uid
* @return int
* @throws AspectNotFoundException
*/
public function getUidOfOverlay($table, $field, $uid)
public function getUidOfOverlay(string $table, string $field, int $uid): int
{
$contextsLanguageId = $this->tsfe->getContext()->getPropertyFromAspect('language', 'id');
// when no language is set at all we do not need to overlay
if (Util::getLanguageUid() === null) {
if ($contextsLanguageId === null) {
return $uid;
}
// when no language is set we can return the passed recordUid
if (!(Util::getLanguageUid() > 0)) {
if (!($contextsLanguageId > 0)) {
return $uid;
}

Expand All @@ -104,8 +110,7 @@ public function getUidOfOverlay($table, $field, $uid)
}

$overlayUid = $this->getLocalRecordUidFromOverlay($table, $record);
$uid = ($overlayUid !== 0) ? $overlayUid : $uid;
return $uid;
return ($overlayUid !== 0) ? $overlayUid : $uid;
}

/**
Expand All @@ -114,8 +119,9 @@ public function getUidOfOverlay($table, $field, $uid)
* @param string $localTableName
* @param array $originalRecord
* @return int
* @throws AspectNotFoundException
*/
protected function getLocalRecordUidFromOverlay($localTableName, $originalRecord)
protected function getLocalRecordUidFromOverlay(string $localTableName, array $originalRecord): int
{
$overlayRecord = $this->getOverlay($localTableName, $originalRecord);

Expand All @@ -136,10 +142,9 @@ protected function getLocalRecordUidFromOverlay($localTableName, $originalRecord
*/
protected function getRecord($localTableName, $localRecordUid)
{
/** @var QueryBuilder $queryBuilder */
/* @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($localTableName);

$record = $queryBuilder->select('*')->from($localTableName)->where($queryBuilder->expr()->eq('uid', $localRecordUid))->execute()->fetch();
return $record;
return $queryBuilder->select('*')->from($localTableName)->where($queryBuilder->expr()->eq('uid', $localRecordUid))->execute()->fetch();
}
}
3 changes: 3 additions & 0 deletions Classes/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

Expand Down Expand Up @@ -146,6 +147,8 @@ public static function containsOneOfTheStrings($haystack, array $needles)
/**
* Returns the current language ID from the active context.
* @return int
* @throws AspectNotFoundException
* @todo: Remove all usages of this method for all usages in isolated/capsuled TSFE approach.
*/
public static function getLanguageUid(): int
{
Expand Down
2 changes: 1 addition & 1 deletion Tests/Integration/IndexQueue/IndexerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ public function canIndexItemWithMMRelationAndAdditionalWhere()
}

/**
* This testcase should check if we can queue an custom record with MM relations and respect the additionalWhere clause.
* This testcase should check if we can queue a custom record with MM relations and respect the additionalWhere clause.
*
* @test
*/
Expand Down

0 comments on commit 82bfe55

Please sign in to comment.