386 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			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;
 | 
						|
    }
 | 
						|
}
 |