meilisearch/Classes/ContentObject/Relation.php
2021-04-17 00:26:33 +02:00

386 lines
16 KiB
PHP

<?php
namespace WapplerSystems\Meilisearch\ContentObject;
/***************************************************************
* Copyright notice
*
* (c) 2011-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Language\FrontendOverlayService;
use WapplerSystems\Meilisearch\System\TCA\TCAService;
use WapplerSystems\Meilisearch\Util;
use Doctrine\DBAL\Driver\Statement;
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;
/**
* A content object (cObj) to resolve relations between database records
*
* Configuration options:
*
* localField: the record's field to use to resolve relations
* 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.
* removeEmptyValues: Removes empty values when resolving relations, defaults to TRUE
* removeDuplicateValues: Removes duplicate values
*
* @author Ingo Renner <ingo@typo3.org>
*/
class Relation extends AbstractContentObject
{
const CONTENT_OBJECT_NAME = 'SOLR_RELATION';
/**
* Content object configuration
*
* @var array
*/
protected $configuration = [];
/**
* @var TCAService
*/
protected $tcaService = null;
/**
* @var FrontendOverlayService
*/
protected $frontendOverlayService = null;
/**
* Relation constructor.
* @param TCAService|null $tcaService
* @param FrontendOverlayService|null $frontendOverlayService
*/
public function __construct(ContentObjectRenderer $cObj, TCAService $tcaService = null, FrontendOverlayService $frontendOverlayService = null)
{
$this->cObj = $cObj;
$this->configuration['enableRecursiveValueResolution'] = 1;
$this->configuration['removeEmptyValues'] = 1;
$this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
$this->frontendOverlayService = $frontendOverlayService ?? GeneralUtility::makeInstance(FrontendOverlayService::class);
}
/**
* Executes the SOLR_RELATION content object.
*
* Resolves relations between records. Currently supported relations are
* TYPO3-style m:n relations.
* May resolve single value and multi value relations.
*
* @inheritDoc
*/
public function render($conf = [])
{
$this->configuration = array_merge($this->configuration, $conf);
$relatedItems = $this->getRelatedItems($this->cObj);
if (!empty($this->configuration['removeDuplicateValues'])) {
$relatedItems = array_unique($relatedItems);
}
if (empty($conf['multiValue'])) {
// single value, need to concatenate related items
$singleValueGlue = !empty($conf['singleValueGlue']) ? trim($conf['singleValueGlue'], '|') : ', ';
$result = implode($singleValueGlue, $relatedItems);
} else {
// multi value, need to serialize as content objects must return strings
$result = serialize($relatedItems);
}
return $result;
}
/**
* 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
*/
protected function getRelatedItems(ContentObjectRenderer $parentContentObject)
{
list($table, $uid) = explode(':', $parentContentObject->currentRecord);
$uid = (int) $uid;
$field = $this->configuration['localField'];
if (!$this->tcaService->getHasConfigurationForField($table, $field)) {
return [];
}
$overlayUid = $this->frontendOverlayService->getUidOfOverlay($table, $field, $uid);
$fieldTCA = $this->tcaService->getConfigurationForField($table, $field);
if (isset($fieldTCA['config']['MM']) && trim($fieldTCA['config']['MM']) !== '') {
$relatedItems = $this->getRelatedItemsFromMMTable($table, $overlayUid, $fieldTCA);
} else {
$relatedItems = $this->getRelatedItemsFromForeignTable($table, $overlayUid, $fieldTCA, $parentContentObject);
}
return $relatedItems;
}
/**
* Gets the related items from a table using a 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
*/
protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid, array $localFieldTca)
{
$relatedItems = [];
$foreignTableName = $localFieldTca['config']['foreign_table'];
$foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
$foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
$mmTableName = $localFieldTca['config']['MM'];
// Remove the first option of foreignLabelField for recursion
if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
$foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
unset($foreignTableLabelFieldArr[0]);
$this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
}
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
$selectUids = $relationHandler->tableArray[$foreignTableName];
if (!is_array($selectUids) || count($selectUids) <= 0) {
return $relatedItems;
}
$relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
foreach ($relatedRecords as $record) {
if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
&& $this->configuration['enableRecursiveValueResolution']
) {
if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
$foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
unset($foreignTableLabelFieldArr[0]);
$this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
}
$this->configuration['localField'] = $foreignTableLabelField;
$contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObject->start($record, $foreignTableName);
return $this->getRelatedItems($contentObject);
} else {
if (Util::getLanguageUid() > 0) {
$record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
}
$relatedItems[] = $record[$foreignTableLabelField];
}
}
return $relatedItems;
}
/**
* Resolves the field to use as the related item's label depending on TCA
* and TypoScript configuration
*
* @param array $foreignTableTca The foreign table's TCA
* @return string The field to use for the related item's label
*/
protected function resolveForeignTableLabelField(array $foreignTableTca)
{
$foreignTableLabelField = $foreignTableTca['ctrl']['label'];
// when foreignLabelField is not enabled we can return directly
if (empty($this->configuration['foreignLabelField'])) {
return $foreignTableLabelField;
}
if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
} else {
$foreignTableLabelField = $this->configuration['foreignLabelField'];
}
return $foreignTableLabelField;
}
/**
* Gets the related items from a table using a 1:n relation.
*
* @param string $localTableName Local table name
* @param int $localRecordUid Local record uid
* @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
*/
protected function getRelatedItemsFromForeignTable(
$localTableName,
$localRecordUid,
array $localFieldTca,
ContentObjectRenderer $parentContentObject
) {
$relatedItems = [];
$foreignTableName = $localFieldTca['config']['foreign_table'];
$foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
$foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
/** @var $relationHandler RelationHandler */
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$itemList = $parentContentObject->data[$this->configuration['localField']] ?? '';
$relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
$selectUids = $relationHandler->tableArray[$foreignTableName];
if (!is_array($selectUids) || count($selectUids) <= 0) {
return $relatedItems;
}
$relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
foreach ($relatedRecords as $relatedRecord) {
$resolveRelatedValue = $this->resolveRelatedValue(
$relatedRecord,
$foreignTableTca,
$foreignTableLabelField,
$parentContentObject,
$foreignTableName
);
if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
$relatedItems[] = $resolveRelatedValue;
}
}
return $relatedItems;
}
/**
* Resolves the value of the related field. If the related field's value is
* a relation itself, this method takes care of resolving it recursively.
*
* @param array $relatedRecord Related record as array
* @param array $foreignTableTca TCA of the related table
* @param string $foreignTableLabelField Field name of the foreign label field
* @param ContentObjectRenderer $parentContentObject cObject
* @param string $foreignTableName Related record table name
*
* @return string
*/
protected function resolveRelatedValue(
array $relatedRecord,
$foreignTableTca,
$foreignTableLabelField,
ContentObjectRenderer $parentContentObject,
$foreignTableName = ''
) {
if (Util::getLanguageUid() > 0 && !empty($foreignTableName)) {
$relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
}
$value = $relatedRecord[$foreignTableLabelField];
if (
!empty($foreignTableName)
&& isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
&& $this->configuration['enableRecursiveValueResolution']
) {
// backup
$backupRecord = $parentContentObject->data;
$backupConfiguration = $this->configuration;
// adjust configuration for next level
$this->configuration['localField'] = $foreignTableLabelField;
$parentContentObject->data = $relatedRecord;
if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
list(, $this->configuration['foreignLabelField']) = explode('.',
$this->configuration['foreignLabelField'], 2);
} else {
$this->configuration['foreignLabelField'] = '';
}
// recursion
$relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
$foreignTableName,
$relatedRecord['uid'],
$foreignTableTca['columns'][$foreignTableLabelField],
$parentContentObject
);
$value = array_pop($relatedItemsFromForeignTable);
// restore
$this->configuration = $backupConfiguration;
$parentContentObject->data = $backupRecord;
}
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.
* @return array
*/
protected function getRelatedRecords($foreignTable, int ...$uids): array
{
/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
$queryBuilder->select('*')
->from($foreignTable)
->where($queryBuilder->expr()->in('uid', $uids));
if (isset($this->configuration['additionalWhereClause'])) {
$queryBuilder->andWhere($this->configuration['additionalWhereClause']);
}
$statement = $queryBuilder->execute();
return $this->sortByKeyInIN($statement, 'uid', ...$uids);
}
/**
* Sorts the result set by key in array for IN values.
* Simulates MySqls ORDER BY FIELD(fieldname, COPY_OF_IN_FOR_WHERE)
* Example: SELECT * FROM a_table WHERE field_name IN (2, 3, 4) SORT BY FIELD(field_name, 2, 3, 4)
*
*
* @param Statement $statement
* @param string $columnName
* @param array $arrayWithValuesForIN
* @return array
*/
protected function sortByKeyInIN(Statement $statement, string $columnName, ...$arrayWithValuesForIN) : array
{
$records = [];
while ($record = $statement->fetch()) {
$indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
$records[$indexNumber] = $record;
}
ksort($records);
return $records;
}
}