Skip to content

Commit 988d25a

Browse files
authored
Merge pull request hoogi91#48 from hoogi91/feature/select-spreadsheet-on-save
Add data handler hook to pre-select uploaded spreadsheet
2 parents df30e84 + 4844fec commit 988d25a

3 files changed

Lines changed: 268 additions & 0 deletions

File tree

Classes/Hooks/DataHandlerHook.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace Hoogi91\Spreadsheets\Hooks;
4+
5+
use TYPO3\CMS\Backend\Utility\BackendUtility;
6+
use TYPO3\CMS\Core\DataHandling\DataHandler;
7+
use TYPO3\CMS\Core\Resource\FileReference;
8+
use TYPO3\CMS\Core\Resource\FileRepository;
9+
use TYPO3\CMS\Core\Utility\GeneralUtility;
10+
11+
/**
12+
* Class DataHandlerHook
13+
* @package Hoogi91\Spreadsheets\Hooks
14+
*/
15+
class DataHandlerHook
16+
{
17+
private static $records = [];
18+
19+
/**
20+
* Post hook to set default spreadsheet selection for newly created items
21+
*
22+
* @param string|mixed $status Status which should be "new" to activate this hook
23+
* @param string|mixed $table Table which should be "tt_content" to activate this hook
24+
* @param int|string|mixed $id Temporary ID used to search for real new uid
25+
* @param array $fieldArray Field array that has been saved to database
26+
* @param DataHandler $dataHandler Data handler instance
27+
*
28+
* @return void
29+
*/
30+
public function processDatamap_afterDatabaseOperations( // @codingStandardsIgnoreLine
31+
$status,
32+
$table,
33+
$id,
34+
array $fieldArray,
35+
DataHandler $dataHandler
36+
): void {
37+
// skip processing for unknown uid, wrong table, status or not updated assets
38+
$uid = $dataHandler->substNEWwithIDs[$id] ?? (is_int($id) ? $id : null);
39+
if ($uid === null
40+
|| $table !== 'tt_content'
41+
|| !array_key_exists('tx_spreadsheets_assets', $fieldArray)
42+
|| !in_array($status, ['new', 'update'], true)) {
43+
return;
44+
}
45+
46+
// skip if not spreadsheet table or bodytext is already filled
47+
$CType = $fieldArray['CType'] ?? $this->getBackendRecordField($uid, 'CType');
48+
if ($CType !== 'spreadsheets_table') {
49+
return;
50+
}
51+
52+
// truncate bodytext after update if assets have been removed
53+
if ($fieldArray['tx_spreadsheets_assets'] === 0) {
54+
if ($status === 'update') {
55+
$dataHandler->updateDB('tt_content', $uid, ['bodytext' => '']);
56+
}
57+
return;
58+
}
59+
60+
/** @var FileRepository $fileRepository */
61+
$fileRepository = GeneralUtility::makeInstance(FileRepository::class);
62+
/** @var FileReference[] $relations */
63+
$relations = $fileRepository->findByRelation('tt_content', 'tx_spreadsheets_assets', $uid);
64+
if (empty($relations)) {
65+
return;
66+
}
67+
68+
// update bodytext to default file selection
69+
if (empty($this->getBackendRecordField($uid, 'bodytext')) === true) {
70+
$dataHandler->updateDB('tt_content', $uid, ['bodytext' => 'spreadsheet://' . $relations[0]->getUid()]);
71+
}
72+
}
73+
74+
/**
75+
* Get backend record field but load entry once
76+
*
77+
* @param int $uid UID of tt_content record
78+
* @param string $field Field to extract
79+
*
80+
* @return mixed|null
81+
*/
82+
private function getBackendRecordField(int $uid, string $field)
83+
{
84+
if (!isset(self::$records[$uid])) {
85+
self::$records[$uid] = BackendUtility::getRecord('tt_content', $uid); // @codeCoverageIgnore
86+
}
87+
return self::$records[$uid][$field] ?? null;
88+
}
89+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
namespace Hoogi91\Spreadsheets\Tests\Unit\Hooks;
4+
5+
use Hoogi91\Spreadsheets\Hooks\DataHandlerHook;
6+
use Hoogi91\Spreadsheets\Tests\Unit\FileRepositoryMockTrait;
7+
use Nimut\TestingFramework\TestCase\UnitTestCase;
8+
use PHPUnit\Framework\MockObject\MockObject;
9+
use Psr\Container\ContainerInterface;
10+
use TYPO3\CMS\Core\DataHandling\DataHandler;
11+
use TYPO3\CMS\Core\Resource\FileReference;
12+
use TYPO3\CMS\Core\Utility\GeneralUtility;
13+
14+
/**
15+
* Class DataHandlerHookTest
16+
* @package Hoogi91\Spreadsheets\Tests\Unit\Hooks
17+
*/
18+
class DataHandlerHookTest extends UnitTestCase
19+
{
20+
use FileRepositoryMockTrait;
21+
22+
private const DATA_HANDLER_NEW_IDS = [
23+
'NEW123456' => 123456,
24+
];
25+
26+
public function setUp()
27+
{
28+
parent::setUp();
29+
// default record has no bodytext
30+
\Closure::bind(function () {
31+
self::$records[123456] = ['bodytext' => ''];
32+
}, null, DataHandlerHook::class)->call(new DataHandlerHook());
33+
}
34+
35+
public function tearDown()
36+
{
37+
parent::tearDown();
38+
// reset data handler static property bindings
39+
\Closure::bind(function () {
40+
self::$records = [];
41+
}, null, DataHandlerHook::class)->call(new DataHandlerHook());
42+
gc_collect_cycles();
43+
}
44+
45+
/**
46+
* @dataProvider datamapProvider
47+
*/
48+
public function testProcessDatamapHook(
49+
array $hookParams,
50+
bool $updateTriggered = false,
51+
array $updateParams = []
52+
): void {
53+
$dataHandlerMock = $this->createMock(DataHandler::class);
54+
$dataHandlerMock->substNEWwithIDs = self::DATA_HANDLER_NEW_IDS;
55+
if ($updateTriggered === true) {
56+
$dataHandlerMock->expects(self::once())->method('updateDB')->with(...array_values($updateParams));
57+
} else {
58+
$dataHandlerMock->expects(self::never())->method('updateDB');
59+
}
60+
61+
// append data handler to hook params
62+
$hookParams[] = $dataHandlerMock;
63+
(new DataHandlerHook())->processDatamap_afterDatabaseOperations(...array_values($hookParams));
64+
}
65+
66+
/**
67+
* @dataProvider datamapWithFileReferenceProvider
68+
*/
69+
public function testProcessDatamapHookWithFileRelations(
70+
array $hookParams,
71+
array $references,
72+
bool $updateTriggered = false,
73+
array $updateParams = [],
74+
callable $closure = null
75+
): void {
76+
// update file repository mock with given reference uid's
77+
$references = array_map(function ($reference) {
78+
$mock = $this->getMockBuilder(FileReference::class)->disableOriginalConstructor()->getMock();
79+
$mock->method('getUid')->willReturn($reference);
80+
return $mock;
81+
}, $references);
82+
$fileRepositoryMock = $this->getFileRepositoryMock();
83+
$fileRepositoryMock->method('findByRelation')->willReturn($references);
84+
85+
/** @var ContainerInterface|MockObject $container */
86+
$container = $this->getContainerMock();
87+
$container->method('get')->willReturn($fileRepositoryMock);
88+
GeneralUtility::setContainer($container);
89+
90+
// update statically saved entries got with backend utility
91+
if ($closure !== null) {
92+
\Closure::bind($closure, null, DataHandlerHook::class)->call(new DataHandlerHook());
93+
}
94+
95+
// now start process datamap hook test
96+
$this->testProcessDatamapHook($hookParams, $updateTriggered, $updateParams);
97+
}
98+
99+
public function datamapProvider(): array
100+
{
101+
return [
102+
'[NEW/UPDATED] ID is not mapped or integer' => [
103+
'hookParams' => self::hookParams(['id' => 'NEW123abc']),
104+
],
105+
'[NEW/UPDATED] is not in tt_content' => [
106+
'hookParams' => self::hookParams(['table' => 'pages']),
107+
],
108+
'[NEW/UPDATED] is not in valid status' => [
109+
'hookParams' => self::hookParams(['status' => 'unknown']),
110+
],
111+
'[NEW] is not a spreadsheet table' => [
112+
'hookParams' => self::hookParams(['fields' => ['CType' => 'textpic']]),
113+
],
114+
'[UPDATED] is not a spreadsheet table' => [
115+
// force call backend utility which will return null on default
116+
'hookParams' => self::hookParams(['status' => 'update', 'fields' => ['CType' => null]]),
117+
],
118+
'[NEW] has clean assets field' => [
119+
'hookParams' => self::hookParams(['fields' => ['tx_spreadsheets_assets' => 0]]),
120+
],
121+
'[UPDATED] has clean assets field' => [
122+
'hookParams' => self::hookParams(['status' => 'update', 'fields' => ['tx_spreadsheets_assets' => 0]]),
123+
'updateTriggered' => true,
124+
'updateParams' => ['tt_content', 123456, ['bodytext' => '']],
125+
],
126+
];
127+
}
128+
129+
public function datamapWithFileReferenceProvider(): array
130+
{
131+
return [
132+
'[NEW/UPDATED] file reference is not found' => [
133+
'hookParams' => self::hookParams(),
134+
'references' => [],
135+
'updateTriggered' => false,
136+
'updateParams' => [],
137+
],
138+
'[NEW/UPDATED] bodytext is not empty' => [
139+
'hookParams' => self::hookParams(),
140+
'references' => [123],
141+
'updateTriggered' => false,
142+
'updateParams' => [],
143+
'closure' => function () {
144+
self::$records[123456] = [
145+
'CType' => 'spreadsheets_table',
146+
'tx_spreadsheets_assets' => 1,
147+
'bodytext' => 'spreadsheet://456',
148+
];
149+
},
150+
],
151+
'[NEW] saved and bodytext gets updated' => [
152+
// uses file repo mock reference ID
153+
'hookParams' => self::hookParams(),
154+
'references' => [456],
155+
'updateTriggered' => true,
156+
'updateParams' => ['tt_content', 123456, ['bodytext' => 'spreadsheet://456']],
157+
],
158+
];
159+
}
160+
161+
private static function hookParams(array $data = []): array
162+
{
163+
return [
164+
'status' => $data['status'] ?? 'new',
165+
'table' => $data['table'] ?? 'tt_content',
166+
'id' => $data['id'] ?? array_keys(self::DATA_HANDLER_NEW_IDS)[0],
167+
'fields' => array_replace_recursive(
168+
[
169+
'CType' => 'spreadsheets_table',
170+
'tx_spreadsheets_assets' => 1, // on default every request has one asset
171+
],
172+
$data['fields'] ?? []
173+
),
174+
];
175+
}
176+
}

ext_localconf.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,8 @@
2828
'priority' => 30,
2929
'class' => \Hoogi91\Spreadsheets\Form\Element\DataInputElement::class,
3030
];
31+
32+
// register data handler hook
33+
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \Hoogi91\Spreadsheets\Hooks\DataHandlerHook::class;
3134
}
3235
})();

0 commit comments

Comments
 (0)