FEATURE: Index resolved relations

* TCAIndexer is now able to resolve relations of any kind by using TYPO3
  Core API.
* Indexed will be a single string or an array, depending of how many
  relations were resolved.
* The same value will be indexed as shown by TCA in backend while
  editing or displaying.
This commit is contained in:
Daniel Siepmann 2016-12-13 16:55:57 +01:00
parent 2b4a3a5bd6
commit 0953f4bb1f
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
7 changed files with 347 additions and 18 deletions

View file

@ -20,9 +20,6 @@ namespace Leonmrni\SearchCore\Configuration;
* 02110-1301, USA. * 02110-1301, USA.
*/ */
/**
*
*/
class InvalidArgumentException extends \InvalidArgumentException class InvalidArgumentException extends \InvalidArgumentException
{ {
const OPTION_DOES_NOT_EXIST = 1481623127; const OPTION_DOES_NOT_EXIST = 1481623127;

View file

@ -0,0 +1,27 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index\TcaIndexer;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
class InvalidArgumentException extends \InvalidArgumentException
{
const COLUMN_DOES_NOT_EXIST = 1481632388;
const RECORD_NOT_FOUND = 1481643208;
}

View file

@ -0,0 +1,119 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index\TcaIndexer;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\SingletonInterface as Singleton;
/**
* Resolves relations from TCA using TCA.
*
* E.g. resolves mm relations, items for selects, group db, etc.
* Will replace the column with an array of resolved labels.
*/
class RelationResolver implements Singleton
{
/**
* @var \TYPO3\CMS\Backend\Form\DataPreprocessor
* @inject
*/
protected $formEngine;
/**
* Resolve relations for the given record.
*
* @param TcaTableService $service
* @param array $record
*/
public function resolveRelationsForRecord(TcaTableService $service, array &$record)
{
$preprocessedData = $this->formEngine->renderRecordRaw(
$service->getTableName(),
$record['uid'],
$record['pid'],
$record
);
foreach (array_keys($record) as $column) {
try {
$config = $service->getColumnConfig($column);
} catch (InvalidArgumentException $e) {
// Column is not configured.
continue;
}
if (! $this->isRelation($config)) {
continue;
}
$record[$column] = $this->resolveValue(
$preprocessedData[$column],
$config,
$column
);
}
}
/**
* Resolve the given value from TYPO3 API response.
*
* As FormEngine uses an internal format, we resolve it to a usable format
* for indexing.
*
* TODO: Unittest to break as soon as TYPO3 api has changed, so we know
* exactly that we need to tackle this place.
*
* @param string $value The value from FormEngine to resolve.
*
* @return array<String>|string
*/
protected function resolveValue($value)
{
$newValue = [];
if ($value === '' || $value === '0') {
return '';
}
if (strpos($value, '|') === false) {
return $value;
}
foreach (\TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $value) as $value) {
$value = substr($value, strpos($value, '|') + 1);
$value = rawurldecode($value);
$newValue[] = $value;
}
return $newValue;
}
/**
* @param array Column config.
* @return bool
*/
protected function isRelation(array &$config)
{
return isset($config['foreign_table'])
|| (isset($config['items']) && is_array($config['items']))
|| (isset($config['internal_type']) && strtolower($config['internal_type']) === 'db')
;
}
}

View file

@ -25,7 +25,7 @@ use Leonmrni\SearchCore\Domain\Index\IndexingException;
use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Backend\Utility\BackendUtility;
/** /**
* Encapsulate logik related to tca configuration. * Encapsulate logik related to TCA configuration.
*/ */
class TcaTableService class TcaTableService
{ {
@ -51,6 +51,11 @@ class TcaTableService
*/ */
protected $logger; protected $logger;
/**
* @var RelationResolver
*/
protected $relationResolver;
/** /**
* Inject log manager to get concrete logger from it. * Inject log manager to get concrete logger from it.
* *
@ -65,8 +70,11 @@ class TcaTableService
* @param string $tableName * @param string $tableName
* @param ConfigurationContainer $configuration * @param ConfigurationContainer $configuration
*/ */
public function __construct($tableName, ConfigurationContainerInterface $configuration) public function __construct(
{ $tableName,
RelationResolver $relationResolver,
ConfigurationContainerInterface $configuration
) {
if (!isset($GLOBALS['TCA'][$tableName])) { if (!isset($GLOBALS['TCA'][$tableName])) {
throw new IndexingException( throw new IndexingException(
'Table "' . $tableName . '" is not configured in TCA.', 'Table "' . $tableName . '" is not configured in TCA.',
@ -77,6 +85,7 @@ class TcaTableService
$this->tableName = $tableName; $this->tableName = $tableName;
$this->tca = &$GLOBALS['TCA'][$this->tableName]; $this->tca = &$GLOBALS['TCA'][$this->tableName];
$this->configuration = $configuration; $this->configuration = $configuration;
$this->relationResolver = $relationResolver;
} }
/** /**
@ -101,7 +110,7 @@ class TcaTableService
*/ */
public function prepareRecord(array &$record) public function prepareRecord(array &$record)
{ {
// TODO: Resolve values from 'items' like static select, radio or checkbox. $this->relationResolver->resolveRelationsForRecord($this, $record);
if (isset($record['uid']) && !isset($record['search_identifier'])) { if (isset($record['uid']) && !isset($record['search_identifier'])) {
$record['search_identifier'] = $record['uid']; $record['search_identifier'] = $record['uid'];
@ -144,8 +153,7 @@ class TcaTableService
array_filter( array_filter(
array_keys($this->tca['columns']), array_keys($this->tca['columns']),
function ($columnName) { function ($columnName) {
$columnConfig = $this->tca['columns'][$columnName]['config']; return !$this->isSystemField($columnName);
return !$this->isRelation($columnConfig) && !$this->isSystemField($columnName);
} }
) )
); );
@ -158,15 +166,6 @@ class TcaTableService
return implode(',', $fields); return implode(',', $fields);
} }
/**
* @param array
* @return bool
*/
protected function isRelation(array &$columnConfig)
{
return isset($columnConfig['foreign_table']);
}
/** /**
* @param string * @param string
* @return bool * @return bool
@ -189,4 +188,19 @@ class TcaTableService
return in_array($columnName, $systemFields); return in_array($columnName, $systemFields);
} }
/**
* @param string $columnName
* @return array
* @throws InvalidArgumentException
*/
public function getColumnConfig($columnName)
{
if (!isset($this->tca['columns'][$columnName])) {
throw new InvalidArgumentException('Column does not exist.', InvalidArgumentException::COLUMN_DOES_NOT_EXIST);
}
return $this->tca['columns'][$columnName]['config'];
}
} }

View file

@ -40,6 +40,8 @@ abstract class AbstractFunctionalTestCase extends CoreTestCase
{ {
parent::setUp(); parent::setUp();
\TYPO3\CMS\Core\Core\Bootstrap::getInstance()->initializeLanguageObject();
// Provide necessary configuration for extension // Provide necessary configuration for extension
$this->importDataSet('Tests/Functional/Fixtures/BasicSetup.xml'); $this->importDataSet('Tests/Functional/Fixtures/BasicSetup.xml');
$this->setUpFrontendRootPage(1, ['EXT:search_core/Tests/Functional/Fixtures/BasicSetup.ts']); $this->setUpFrontendRootPage(1, ['EXT:search_core/Tests/Functional/Fixtures/BasicSetup.ts']);

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<dataset>
<tt_content>
<uid>9</uid>
<pid>1</pid>
<tstamp>1480686370</tstamp>
<crdate>1480686370</crdate>
<hidden>0</hidden>
<sorting>72</sorting>
<CType>textmedia</CType>
<header>Record with relation to multiple sys category record</header>
<bodytext>some content</bodytext>
<media>0</media>
<layout>0</layout>
<deleted>0</deleted>
<cols>0</cols>
<starttime>0</starttime>
<endtime>0</endtime>
<colPos>0</colPos>
<categories>3</categories>
<filelink_sorting>0</filelink_sorting>
</tt_content>
<tt_content>
<uid>10</uid>
<pid>1</pid>
<tstamp>1480686370</tstamp>
<crdate>1480686370</crdate>
<hidden>0</hidden>
<sorting>72</sorting>
<CType>textmedia</CType>
<header>Record with relation to a single sys category record</header>
<bodytext>some content</bodytext>
<media>0</media>
<layout>0</layout>
<deleted>0</deleted>
<cols>0</cols>
<starttime>0</starttime>
<endtime>0</endtime>
<colPos>0</colPos>
<categories>1</categories>
<filelink_sorting>0</filelink_sorting>
</tt_content>
<sys_category>
<uid>1</uid>
<pid>1</pid>
<tstamp>1480686370</tstamp>
<crdate>1480686370</crdate>
<deleted>0</deleted>
<starttime>0</starttime>
<endtime>0</endtime>
<sorting>1</sorting>
<title>Category 1</title>
<description>Category for testing</description>
<parent>0</parent>
<items>1</items>
</sys_category>
<sys_category>
<uid>2</uid>
<pid>1</pid>
<tstamp>1480686370</tstamp>
<crdate>1480686370</crdate>
<deleted>0</deleted>
<starttime>0</starttime>
<endtime>0</endtime>
<sorting>1</sorting>
<title>Category 2</title>
<description>Another category for testing</description>
<parent>1</parent>
<items>1</items>
</sys_category>
<sys_category>
<uid>3</uid>
<pid>1</pid>
<tstamp>1480686370</tstamp>
<crdate>1480686370</crdate>
<deleted>1</deleted>
<starttime>0</starttime>
<endtime>0</endtime>
<sorting>1</sorting>
<title>Deleted category</title>
<description></description>
<parent>0</parent>
<items>1</items>
</sys_category>
<sys_category_record_mm>
<uid_local>1</uid_local>
<uid_foreign>9</uid_foreign>
<tablenames>tt_content</tablenames>
<fieldname>categories</fieldname>
<sorting>2</sorting>
<sorting_foreign>2</sorting_foreign>
</sys_category_record_mm>
<sys_category_record_mm>
<uid_local>2</uid_local>
<uid_foreign>9</uid_foreign>
<tablenames>tt_content</tablenames>
<fieldname>categories</fieldname>
<sorting>1</sorting>
<sorting_foreign>1</sorting_foreign>
</sys_category_record_mm>
<sys_category_record_mm>
<uid_local>3</uid_local>
<uid_foreign>9</uid_foreign>
<tablenames>tt_content</tablenames>
<fieldname>categories</fieldname>
<sorting>3</sorting>
<sorting_foreign>3</sorting_foreign>
</sys_category_record_mm>
<sys_category_record_mm>
<uid_local>2</uid_local>
<uid_foreign>10</uid_foreign>
<tablenames>tt_content</tablenames>
<fieldname>categories</fieldname>
<sorting>1</sorting>
<sorting_foreign>1</sorting_foreign>
</sys_category_record_mm>
</dataset>

View file

@ -123,4 +123,51 @@ class IndexTcaTableTest extends AbstractFunctionalTestCase
'Record was not indexed.' 'Record was not indexed.'
); );
} }
/**
* @test
*/
public function resolvesRelations()
{
$this->setUpBackendUserFromFixture(1);
$this->importDataSet('Tests/Functional/Fixtures/Indexing/ResolveRelations.xml');
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(IndexerFactory::class)
->getIndexer('tt_content')
->index()
;
$response = $this->client->request('typo3content/_search?q=*:*');
$this->assertArraySubset(
['_source' => [
'uid' => '9',
'CType' => 'textmedia', // Testing items
'categories' => ['Category 2', 'Category 1'], // Testing mm (with sorting)
]],
$response->getData()['hits']['hits'][0],
false,
'Record was not indexed with resolved category relation to a single value.'
);
$this->assertArraySubset(
['_source' => [
'uid' => '10',
'CType' => 'textmedia',
'categories' => ['Category 2'],
]],
$response->getData()['hits']['hits'][1],
false,
'Record was not indexed with resolved category relation to multiple values.'
);
$this->assertArraySubset(
['_source' => [
'uid' => '6',
'categories' => null,
]],
$response->getData()['hits']['hits'][2],
false,
'Record was indexed with resolved category relation, but should not have any.'
);
}
} }