520c714daf7f5563b86afb87f3310b0b3c6895ec
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Unit / Configuration / FlexForm / FlexFormToolsTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Doctrine\DBAL\Driver\Statement;
19 use Prophecy\Argument;
20 use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidCombinedPointerFieldException;
21 use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException;
22 use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowException;
23 use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowLoopException;
24 use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidParentRowRootException;
25 use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidPointerFieldValueException;
26 use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidSinglePointerFieldException;
27 use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaException;
28 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
29 use TYPO3\CMS\Core\Database\ConnectionPool;
30 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
31 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
32 use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface;
33 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureIdentifierPostProcessHookReturnArray;
34 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureIdentifierPostProcessHookReturnEmptyArray;
35 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureIdentifierPostProcessHookReturnString;
36 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureIdentifierPostProcessHookThrowException;
37 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureIdentifierPreProcessHookReturnArray;
38 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureIdentifierPreProcessHookReturnEmptyArray;
39 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureIdentifierPreProcessHookReturnString;
40 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureIdentifierPreProcessHookThrowException;
41 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureParsePostProcessHookReturnArray;
42 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureParsePostProcessHookReturnString;
43 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureParsePostProcessHookThrowException;
44 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureParsePreProcessHookReturnEmptyString;
45 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureParsePreProcessHookReturnObject;
46 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureParsePreProcessHookReturnString;
47 use TYPO3\CMS\Core\Tests\Unit\Configuration\FlexForm\Fixtures\DataStructureParsePreProcessHookThrowException;
48 use TYPO3\CMS\Core\Utility\GeneralUtility;
49
50 /**
51 * Test case
52 */
53 class FlexFormToolsTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
54 {
55 /**
56 * Subject is not notice free, disable E_NOTICES
57 */
58 protected static $suppressNotices = true;
59
60 /**
61 * @test
62 */
63 public function getDataStructureIdentifierCallsRegisteredPreProcessHook()
64 {
65 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
66 DataStructureIdentifierPreProcessHookThrowException::class,
67 ];
68 $this->expectException(\RuntimeException::class);
69 $this->expectExceptionCode(1478098527);
70 (new FlexFormTools())->getDataStructureIdentifier([], 'aTableName', 'aFieldName', []);
71 }
72
73 /**
74 * @test
75 */
76 public function getDataStructureIdentifierThrowsExceptionIfPreProcessHookReturnsNoArray()
77 {
78 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
79 DataStructureIdentifierPreProcessHookReturnString::class
80 ];
81 $this->expectException(\RuntimeException::class);
82 $this->expectExceptionCode(1478096535);
83 (new FlexFormTools())->getDataStructureIdentifier([], 'aTableName', 'aFieldName', []);
84 }
85
86 /**
87 * @test
88 */
89 public function getDataStructureIdentifierUsesCasualLogicIfPreProcessHookReturnsNoIdentifier()
90 {
91 $fieldTca = [
92 'config' => [
93 'ds' => [
94 'default' => '<T3DataStructure>...'
95 ],
96 ],
97 ];
98 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
99 DataStructureIdentifierPreProcessHookReturnEmptyArray::class
100 ];
101 $expected = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
102 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', []));
103 }
104
105 /**
106 * @test
107 */
108 public function getDataStructureIdentifierReturnsStringFromPreProcessHook()
109 {
110 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
111 DataStructureIdentifierPreProcessHookReturnArray::class
112 ];
113 $expected = '{"type":"myExtension","further":"data"}';
114 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier([], 'aTableName', 'aFieldName', []));
115 }
116
117 /**
118 * @test
119 */
120 public function getDataStructureIdentifierReturnsStringFromFirstMatchingPreProcessHook()
121 {
122 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
123 DataStructureIdentifierPreProcessHookReturnEmptyArray::class,
124 DataStructureIdentifierPreProcessHookReturnArray::class,
125 DataStructureIdentifierPreProcessHookThrowException::class
126 ];
127 $expected = '{"type":"myExtension","further":"data"}';
128 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier([], 'aTableName', 'aFieldName', []));
129 }
130
131 /**
132 * @test
133 */
134 public function getDataStructureIdentifierCallsRegisteredPostProcessHook()
135 {
136 $fieldTca = [
137 'config' => [
138 'ds' => [
139 'default' => '<T3DataStructure>...'
140 ],
141 ],
142 ];
143 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
144 DataStructureIdentifierPostProcessHookThrowException::class,
145 ];
146 $this->expectException(\RuntimeException::class);
147 $this->expectExceptionCode(1478342067);
148 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', []);
149 }
150
151 /**
152 * @test
153 */
154 public function getDataStructureIdentifierThrowsExceptionIfPostProcessHookReturnsNoArray()
155 {
156 $fieldTca = [
157 'config' => [
158 'ds' => [
159 'default' => '<T3DataStructure>...'
160 ],
161 ],
162 ];
163 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
164 DataStructureIdentifierPostProcessHookReturnString::class
165 ];
166 $this->expectException(\RuntimeException::class);
167 $this->expectExceptionCode(1478350835);
168 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', []);
169 }
170
171 /**
172 * @test
173 */
174 public function getDataStructureIdentifierThrowsExceptionIfPostProcessHookReturnsEmptyArray()
175 {
176 $fieldTca = [
177 'config' => [
178 'ds' => [
179 'default' => '<T3DataStructure>...'
180 ],
181 ],
182 ];
183 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
184 DataStructureIdentifierPostProcessHookReturnEmptyArray::class
185 ];
186 $this->expectException(\RuntimeException::class);
187 $this->expectExceptionCode(1478350835);
188 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', []);
189 }
190
191 /**
192 * @test
193 */
194 public function getDataStructureIdentifierPostProcessHookCanEnrichIdentifier()
195 {
196 $fieldTca = [
197 'config' => [
198 'ds' => [
199 'default' => '<T3DataStructure>...'
200 ],
201 ],
202 ];
203 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
204 DataStructureIdentifierPostProcessHookReturnArray::class
205 ];
206 $expected = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default","myExtensionData":"foo"}';
207 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', []));
208 }
209
210 /**
211 * @test
212 */
213 public function getDataStructureIdentifierThrowsExceptionIfDsIsNotAnArrayAndNoDsPointerField()
214 {
215 $fieldTca = [
216 'config' => [
217 'ds' => 'someStringOnly',
218 // no ds_pointerField,
219 ],
220 ];
221 $this->expectException(\RuntimeException::class);
222 $this->expectExceptionCode(1463826960);
223 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', []);
224 }
225
226 /**
227 * @test
228 */
229 public function getDataStructureIdentifierReturnsDefaultIfDsIsSetButNoDsPointerField()
230 {
231 $fieldTca = [
232 'config' => [
233 'ds' => [
234 'default' => '<T3DataStructure>...'
235 ],
236 ],
237 ];
238 $expected = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
239 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', []));
240 }
241
242 /**
243 * @test
244 */
245 public function getDataStructureIdentifierThrowsExceptionsIfNoDsPointerFieldIsSetAndDefaultDoesNotExist()
246 {
247 $fieldTca = [
248 'config' => [
249 'ds' => [],
250 ],
251 ];
252 $this->expectException(InvalidTcaException::class);
253 $this->expectExceptionCode(1463652560);
254 $this->assertSame('default', (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', []));
255 }
256
257 /**
258 * @test
259 */
260 public function getDataStructureIdentifierThrowsExceptionIfPointerFieldStringHasMoreThanTwoFields()
261 {
262 $fieldTca = [
263 'config' => [
264 'ds' => [],
265 'ds_pointerField' => 'first,second,third',
266 ],
267 ];
268 $this->expectException(\RuntimeException::class);
269 $this->expectExceptionCode(1463577497);
270 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', []);
271 }
272
273 /**
274 * @test
275 */
276 public function getDataStructureIdentifierThrowsExceptionIfPointerFieldWithStringSingleFieldDoesNotExist()
277 {
278 $fieldTca = [
279 'config' => [
280 'ds' => [],
281 'ds_pointerField' => 'notExist',
282 ],
283 ];
284 $row = [
285 'foo' => '',
286 ];
287 $this->expectException(\RuntimeException::class);
288 $this->expectExceptionCode(1463578899);
289 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
290 }
291
292 /**
293 * @test
294 */
295 public function getDataStructureIdentifierThrowsExceptionIfPointerFieldSWithTwoFieldsFirstDoesNotExist()
296 {
297 $fieldTca = [
298 'config' => [
299 'ds' => [],
300 'ds_pointerField' => 'notExist,second',
301 ],
302 ];
303 $row = [
304 'second' => '',
305 ];
306 $this->expectException(\RuntimeException::class);
307 $this->expectExceptionCode(1463578899);
308 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
309 }
310
311 /**
312 * @test
313 */
314 public function getDataStructureIdentifierThrowsExceptionIfPointerFieldSWithTwoFieldsSecondDoesNotExist()
315 {
316 $fieldTca = [
317 'config' => [
318 'ds' => [],
319 'ds_pointerField' => 'first,notExist',
320 ],
321 ];
322 $row = [
323 'first' => '',
324 ];
325 $this->expectException(\RuntimeException::class);
326 $this->expectExceptionCode(1463578900);
327 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
328 }
329
330 /**
331 * @test
332 */
333 public function getDataStructureIdentifierReturnsPointerFieldValueIfDataStructureExists()
334 {
335 $fieldTca = [
336 'config' => [
337 'ds' => [
338 'thePointerValue' => 'FILE:...'
339 ],
340 'ds_pointerField' => 'aField'
341 ],
342 ];
343 $row = [
344 'aField' => 'thePointerValue',
345 ];
346 $expected = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"thePointerValue"}';
347 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row));
348 }
349
350 /**
351 * @test
352 */
353 public function getDataStructureIdentifierReturnsDefaultIfPointerFieldValueDoesNotExist()
354 {
355 $fieldTca = [
356 'config' => [
357 'ds' => [
358 'default' => 'theDataStructure'
359 ],
360 'ds_pointerField' => 'aField'
361 ],
362 ];
363 $row = [
364 'aField' => 'thePointerValue',
365 ];
366 $expected = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
367 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row));
368 }
369
370 /**
371 * @test
372 */
373 public function getDataStructureIdentifierThrowsExceptionIfPointerFieldValueDoesNotExistAndDefaultToo()
374 {
375 $fieldTca = [
376 'config' => [
377 'ds' => [
378 'aDifferentDataStructure' => 'aDataStructure'
379 ],
380 'ds_pointerField' => 'aField'
381 ],
382 ];
383 $row = [
384 'aField' => 'aNotDefinedDataStructure',
385 ];
386 $this->expectException(InvalidSinglePointerFieldException::class);
387 $this->expectExceptionCode(1463653197);
388 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
389 }
390
391 /**
392 * Data provider for getDataStructureIdentifierReturnsValidNameForTwoFieldCombinations
393 */
394 public function getDataStructureIdentifierReturnsValidNameForTwoFieldCombinationsDataProvider()
395 {
396 return [
397 'direct match of two fields' => [
398 [
399 // $row
400 'firstField' => 'firstValue',
401 'secondField' => 'secondValue',
402 ],
403 [
404 // registered data structure names
405 'firstValue,secondValue' => '',
406 ],
407 // expected name
408 '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"firstValue,secondValue"}'
409 ],
410 'match on first field, * for second' => [
411 [
412 'firstField' => 'firstValue',
413 'secondField' => 'secondValue',
414 ],
415 [
416 'firstValue,*' => '',
417 ],
418 '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"firstValue,*"}'
419 ],
420 'match on second field, * for first' => [
421 [
422 'firstField' => 'firstValue',
423 'secondField' => 'secondValue',
424 ],
425 [
426 '*,secondValue' => '',
427 ],
428 '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"*,secondValue"}'
429 ],
430 'match on first field only' => [
431 [
432 'firstField' => 'firstValue',
433 'secondField' => 'secondValue',
434 ],
435 [
436 'firstValue' => '',
437 ],
438 '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"firstValue"}'
439 ],
440 'fallback to default' => [
441 [
442 'firstField' => 'firstValue',
443 'secondField' => 'secondValue',
444 ],
445 [
446 'default' => '',
447 ],
448 '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}'
449 ],
450 'chain falls through with no match on second value to *' => [
451 [
452 'firstField' => 'firstValue',
453 'secondField' => 'noMatch',
454 ],
455 [
456 'firstValue,secondValue' => '',
457 'firstValue,*' => '',
458 ],
459 '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"firstValue,*"}'
460 ],
461 'chain falls through with no match on first value to *' => [
462 [
463 'firstField' => 'noMatch',
464 'secondField' => 'secondValue',
465 ],
466 [
467 'firstValue,secondValue' => '',
468 '*,secondValue' => '',
469 ],
470 '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"*,secondValue"}'
471 ],
472 'chain falls through with no match on any field to default' => [
473 [
474 'firstField' => 'noMatch',
475 'secondField' => 'noMatchToo',
476 ],
477 [
478 'firstValue,secondValue' => '',
479 'secondValue,*' => '',
480 'default' => '',
481 ],
482 '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}'
483 ],
484 ];
485 }
486
487 /**
488 * @test
489 * @dataProvider getDataStructureIdentifierReturnsValidNameForTwoFieldCombinationsDataProvider
490 * @param array $row
491 * @param array $ds
492 * @param $expected
493 */
494 public function getDataStructureIdentifierReturnsValidNameForTwoFieldCombinations(array $row, array $ds, string $expected)
495 {
496 $fieldTca = [
497 'config' => [
498 'ds' => $ds,
499 'ds_pointerField' => 'firstField,secondField'
500 ],
501 ];
502 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row));
503 }
504
505 /**
506 * @test
507 */
508 public function getDataStructureIdentifierThrowsExceptionForTwoFieldsWithNoMatchAndNoDefault()
509 {
510 $fieldTca = [
511 'config' => [
512 'ds' => [
513 'firstValue,secondValue' => '',
514 ],
515 'ds_pointerField' => 'firstField,secondField'
516 ],
517 ];
518 $row = [
519 'firstField' => 'noMatch',
520 'secondField' => 'noMatchToo',
521 ];
522 $this->expectException(InvalidCombinedPointerFieldException::class);
523 $this->expectExceptionCode(1463678524);
524 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
525 }
526
527 /**
528 * @test
529 */
530 public function getDataStructureIdentifierThrowsExceptionIfParentRowLookupFails()
531 {
532 $fieldTca = [
533 'config' => [
534 'ds_pointerField' => 'tx_templavoila_ds',
535 'ds_pointerField_searchParent' => 'pid',
536 ]
537 ];
538 $row = [
539 'pid' => 42,
540 'tx_templavoila_ds' => null,
541 ];
542
543 // Prophecies and revelations for a lot of the database stack classes
544 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
545 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
546 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
547 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
548 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
549 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
550 $statementProphecy = $this->prophesize(Statement::class);
551
552 // Register connection pool revelation in framework, this is the entry point used by system under test
553 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
554
555 // Simulate method call flow on database objects and verify correct query is built
556 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
557 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
558 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
559 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
560 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
561 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
562 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
563 $queryBuilderProphecy->createNamedParameter(42, 1)->willReturn(42);
564 $expressionBuilderProphecy->eq('uid', 42)->shouldBeCalled()->willReturn('uid = 42');
565 $queryBuilderProphecy->where('uid = 42')->shouldBeCalled()->willReturn($queryBuilderRevelation);
566 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
567
568 // Error case that is tested here: Do not return a valid parent row from db -> exception should be thrown
569 $statementProphecy->rowCount()->shouldBeCalled()->willReturn(0);
570 $this->expectException(InvalidParentRowException::class);
571 $this->expectExceptionCode(1463833794);
572 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
573 }
574
575 /**
576 * @test
577 */
578 public function getDataStructureIdentifierThrowsExceptionIfParentRowsFormALoop()
579 {
580 $fieldTca = [
581 'config' => [
582 'ds_pointerField' => 'tx_templavoila_ds',
583 'ds_pointerField_searchParent' => 'pid',
584 ]
585 ];
586 $initialRow = [
587 'uid' => 3,
588 'pid' => 2,
589 'tx_templavoila_ds' => null,
590 ];
591 $secondRow = [
592 'uid' => 2,
593 'pid' => 1,
594 'tx_templavoila_ds' => null,
595 ];
596 $thirdRow = [
597 'uid' => 1,
598 'pid' => 3,
599 'tx_templavoila_ds' => null,
600 ];
601
602 // Prophecies and revelations for a lot of the database stack classes
603 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
604 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
605 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
606 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
607 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
608 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
609 $statementProphecy = $this->prophesize(Statement::class);
610
611 // Register connection pool revelation in framework, this is the entry point used by system under test
612 // Two queries are done, so we need two instances
613 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
614 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
615
616 // Simulate method call flow on database objects and verify correct query is built
617 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
618 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
619 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
620 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
621 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
622 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
623 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
624 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
625 $queryBuilderProphecy->createNamedParameter(1, 1)->willReturn(1);
626 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
627 $expressionBuilderProphecy->eq('uid', 1)->shouldBeCalled()->willReturn('uid = 1');
628 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
629 $queryBuilderProphecy->where('uid = 1')->shouldBeCalled()->willReturn($queryBuilderRevelation);
630 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
631 $statementProphecy->rowCount()->shouldBeCalled()->willReturn(1);
632
633 // First db call returns $secondRow, second returns $thirdRow, which points back to $initialRow -> exception
634 $statementProphecy->fetch()->willReturn($secondRow, $thirdRow);
635
636 $this->expectException(InvalidParentRowLoopException::class);
637 $this->expectExceptionCode(1464110956);
638 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow);
639 }
640
641 /**
642 * @test
643 */
644 public function getDataStructureIdentifierThrowsExceptionIfNoValidPointerFoundUntilRoot()
645 {
646 $fieldTca = [
647 'config' => [
648 'ds_pointerField' => 'tx_templavoila_ds',
649 'ds_pointerField_searchParent' => 'pid',
650 ]
651 ];
652 $initialRow = [
653 'uid' => 3,
654 'pid' => 2,
655 'tx_templavoila_ds' => null,
656 ];
657 $secondRow = [
658 'uid' => 2,
659 'pid' => 1,
660 'tx_templavoila_ds' => null,
661 ];
662 $thirdRow = [
663 'uid' => 1,
664 'pid' => 0,
665 'tx_templavoila_ds' => null,
666 ];
667
668 // Prophecies and revelations for a lot of the database stack classes
669 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
670 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
671 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
672 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
673 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
674 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
675 $statementProphecy = $this->prophesize(Statement::class);
676
677 // Register connection pool revelation in framework, this is the entry point used by system under test
678 // Two queries are done, so we need two instances
679 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
680 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
681
682 // Simulate method call flow on database objects and verify correct query is built
683 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
684 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
685 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
686 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
687 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
688 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
689 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
690 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
691 $queryBuilderProphecy->createNamedParameter(1, 1)->willReturn(1);
692 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
693 $expressionBuilderProphecy->eq('uid', 1)->shouldBeCalled()->willReturn('uid = 1');
694 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
695 $queryBuilderProphecy->where('uid = 1')->shouldBeCalled()->willReturn($queryBuilderRevelation);
696 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
697 $statementProphecy->rowCount()->shouldBeCalled()->willReturn(1);
698
699 // First db call returns $secondRow, second returns $thirdRow. $thirdRow has pid 0 and still no ds -> exception
700 $statementProphecy->fetch()->willReturn($secondRow, $thirdRow);
701
702 $this->expectException(InvalidParentRowRootException::class);
703 $this->expectExceptionCode(1464112555);
704 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow);
705 }
706
707 /**
708 * @test
709 */
710 public function getDataStructureIdentifierThrowsExceptionIfNoValidPointerValueFound()
711 {
712 $fieldTca = [
713 'config' => [
714 'ds_pointerField' => 'aPointerField',
715 ]
716 ];
717 $row = [
718 'aPointerField' => null,
719 ];
720 $this->expectException(InvalidPointerFieldValueException::class);
721 $this->expectExceptionCode(1464114011);
722 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
723 }
724
725 /**
726 * @test
727 */
728 public function getDataStructureIdentifierThrowsExceptionIfResorvedPointerValueIsIntegerButDsFieldNameIsNotConfigured()
729 {
730 $fieldTca = [
731 'config' => [
732 'ds_pointerField' => 'aPointerField',
733 ]
734 ];
735 $row = [
736 'aPointerField' => 3,
737 ];
738 $this->expectException(InvalidTcaException::class);
739 $this->expectExceptionCode(1464115639);
740 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
741 }
742
743 /**
744 * @test
745 */
746 public function getDataStructureIdentifierThrowsExceptionIfDsTableFieldIsMisconfigured()
747 {
748 $fieldTca = [
749 'config' => [
750 'ds_pointerField' => 'aPointerField',
751 'ds_tableField' => 'misconfigured',
752 ]
753 ];
754 $row = [
755 'aPointerField' => 3,
756 ];
757 $this->expectException(InvalidTcaException::class);
758 $this->expectExceptionCode(1464116002);
759 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
760 }
761
762 /**
763 * @test
764 */
765 public function getDataStructureIdentifierReturnsValidIdentifierForPointerField()
766 {
767 $fieldTca = [
768 'config' => [
769 'ds_pointerField' => 'aPointerField',
770 ]
771 ];
772 $row = [
773 'uid' => 42,
774 'aPointerField' => '<T3DataStructure>...',
775 ];
776 $expected = '{"type":"record","tableName":"aTableName","uid":42,"fieldName":"aPointerField"}';
777 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row));
778 }
779
780 /**
781 * @test
782 */
783 public function getDataStructureIdentifierReturnsValidIdentifierForParentLookup()
784 {
785 $fieldTca = [
786 'config' => [
787 'ds_pointerField' => 'tx_templavoila_ds',
788 'ds_pointerField_searchParent' => 'pid',
789 ]
790 ];
791 $initialRow = [
792 'uid' => 3,
793 'pid' => 2,
794 'tx_templavoila_ds' => null,
795 ];
796 $secondRow = [
797 'uid' => 2,
798 'pid' => 1,
799 'tx_templavoila_ds' => 0,
800 ];
801 $thirdRow = [
802 'uid' => 1,
803 'pid' => 0,
804 'tx_templavoila_ds' => '<T3DataStructure>...',
805 ];
806
807 // Prophecies and revelations for a lot of the database stack classes
808 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
809 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
810 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
811 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
812 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
813 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
814 $statementProphecy = $this->prophesize(Statement::class);
815
816 // Register connection pool revelation in framework, this is the entry point used by system under test
817 // Two queries are done, so we need two instances
818 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
819 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
820
821 // Simulate method call flow on database objects and verify correct query is built
822 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
823 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
824 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
825 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
826 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
827 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
828 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
829 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
830 $queryBuilderProphecy->createNamedParameter(1, 1)->willReturn(1);
831 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
832 $expressionBuilderProphecy->eq('uid', 1)->shouldBeCalled()->willReturn('uid = 1');
833 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
834 $queryBuilderProphecy->where('uid = 1')->shouldBeCalled()->willReturn($queryBuilderRevelation);
835 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
836 $statementProphecy->rowCount()->shouldBeCalled()->willReturn(1);
837
838 // First db call returns $secondRow, second returns $thirdRow. $thirdRow resolves ds
839 $statementProphecy->fetch()->willReturn($secondRow, $thirdRow);
840
841 $expected = '{"type":"record","tableName":"aTableName","uid":1,"fieldName":"tx_templavoila_ds"}';
842 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow));
843 }
844
845 /**
846 * @test
847 */
848 public function getDataStructureIdentifierReturnsValidIdentifierForParentLookupAndBreaksLoop()
849 {
850 $fieldTca = [
851 'config' => [
852 'ds_pointerField' => 'tx_templavoila_ds',
853 'ds_pointerField_searchParent' => 'pid',
854 ]
855 ];
856 $initialRow = [
857 'uid' => 3,
858 'pid' => 2,
859 'tx_templavoila_ds' => null,
860 ];
861 $secondRow = [
862 'uid' => 2,
863 'pid' => 1,
864 'tx_templavoila_ds' => '<T3DataStructure>...',
865 ];
866
867 // Prophecies and revelations for a lot of the database stack classes
868 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
869 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
870 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
871 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
872 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
873 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
874 $statementProphecy = $this->prophesize(Statement::class);
875
876 // Register connection pool revelation in framework, this is the entry point used by system under test
877 // Two queries are done, so we need two instances
878 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
879
880 // Simulate method call flow on database objects and verify correct query is built
881 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
882 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
883 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
884 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
885 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
886 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
887 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
888 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
889 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
890 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
891 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
892 $statementProphecy->rowCount()->shouldBeCalled()->willReturn(1);
893
894 // First db call returns $secondRow. $secendRow resolves DS and does not look further up
895 $statementProphecy->fetch()->willReturn($secondRow);
896
897 $expected = '{"type":"record","tableName":"aTableName","uid":2,"fieldName":"tx_templavoila_ds"}';
898 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow));
899 }
900
901 /**
902 * @test
903 */
904 public function getDataStructureIdentifierReturnsValidIdentifierForParentLookupAndPrefersSubField()
905 {
906 $fieldTca = [
907 'config' => [
908 'ds_pointerField' => 'tx_templavoila_ds',
909 'ds_pointerField_searchParent' => 'pid',
910 'ds_pointerField_searchParent_subField' => 'tx_templavoila_next_ds',
911 ]
912 ];
913 $initialRow = [
914 'uid' => 3,
915 'pid' => 2,
916 'tx_templavoila_ds' => null,
917 'tx_templavoila_next_ds' => null,
918 ];
919 $secondRow = [
920 'uid' => 2,
921 'pid' => 1,
922 'tx_templavoila_ds' => '<T3DataStructure>...',
923 'tx_templavoila_next_ds' => 'anotherDataStructure',
924 ];
925
926 // Prophecies and revelations for a lot of the database stack classes
927 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
928 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
929 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
930 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
931 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
932 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
933 $statementProphecy = $this->prophesize(Statement::class);
934
935 // Register connection pool revelation in framework, this is the entry point used by system under test
936 // Two queries are done, so we need two instances
937 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
938
939 // Simulate method call flow on database objects and verify correct query is built
940 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
941 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
942 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
943 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
944 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
945 $queryBuilderProphecy->addSelect('tx_templavoila_next_ds')->shouldBeCalled();
946 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
947 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
948 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
949 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
950 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
951 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
952 $statementProphecy->rowCount()->shouldBeCalled()->willReturn(1);
953
954 // First db call returns $secondRow. $secendRow resolves DS and does not look further up
955 $statementProphecy->fetch()->willReturn($secondRow);
956
957 $expected = '{"type":"record","tableName":"aTableName","uid":2,"fieldName":"tx_templavoila_next_ds"}';
958 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow));
959 }
960
961 /**
962 * @test
963 */
964 public function getDataStructureIdentifierReturnsValidIdentifierForTableAndFieldPointer()
965 {
966 $fieldTca = [
967 'config' => [
968 'ds_pointerField' => 'aPointerField',
969 'ds_tableField' => 'foreignTableName:foreignTableField',
970 ]
971 ];
972 $row = [
973 'uid' => 3,
974 'pid' => 2,
975 'aPointerField' => 42,
976 ];
977 $expected = '{"type":"record","tableName":"foreignTableName","uid":42,"fieldName":"foreignTableField"}';
978 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row));
979 }
980
981 /**
982 * @test
983 */
984 public function getDataStructureIdentifierReturnsValidIdentifierForTableAndFieldPointerWithParentLookup()
985 {
986 $fieldTca = [
987 'config' => [
988 'ds_pointerField' => 'tx_templavoila_ds',
989 'ds_pointerField_searchParent' => 'pid',
990 'ds_pointerField_searchParent_subField' => 'tx_templavoila_next_ds',
991 'ds_tableField' => 'foreignTableName:foreignTableField',
992 ]
993 ];
994 $initialRow = [
995 'uid' => 3,
996 'pid' => 2,
997 'tx_templavoila_ds' => null,
998 'tx_templavoila_next_ds' => null,
999 ];
1000 $secondRow = [
1001 'uid' => 2,
1002 'pid' => 1,
1003 'tx_templavoila_ds' => '<T3DataStructure>...',
1004 'tx_templavoila_next_ds' => '42',
1005 ];
1006
1007 // Prophecies and revelations for a lot of the database stack classes
1008 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
1009 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
1010 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
1011 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
1012 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
1013 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
1014 $statementProphecy = $this->prophesize(Statement::class);
1015
1016 // Register connection pool revelation in framework, this is the entry point used by system under test
1017 // Two queries are done, so we need two instances
1018 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
1019
1020 // Simulate method call flow on database objects and verify correct query is built
1021 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1022 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
1023 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
1024 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
1025 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
1026 $queryBuilderProphecy->addSelect('tx_templavoila_next_ds')->shouldBeCalled();
1027 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1028 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
1029 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
1030 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
1031 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1032 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
1033 $statementProphecy->rowCount()->shouldBeCalled()->willReturn(1);
1034
1035 // First db call returns $secondRow. $secendRow resolves DS and does not look further up
1036 $statementProphecy->fetch()->willReturn($secondRow);
1037
1038 $expected = '{"type":"record","tableName":"foreignTableName","uid":42,"fieldName":"foreignTableField"}';
1039 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow));
1040 }
1041
1042 /**
1043 * @test
1044 */
1045 public function parseDataStructureByIdentifierThrowsExceptionWithEmptyString()
1046 {
1047 $this->expectException(InvalidIdentifierException::class);
1048 $this->expectExceptionCode(1478100828);
1049 (new FlexFormTools())->parseDataStructureByIdentifier('');
1050 }
1051
1052 /**
1053 * @test
1054 */
1055 public function parseDataStructureByIdentifierIfIdentifierDoesNotResolveToArray()
1056 {
1057 $this->expectException(\RuntimeException::class);
1058 $this->expectExceptionCode(1478345642);
1059 (new FlexFormTools())->parseDataStructureByIdentifier('egon');
1060 }
1061
1062 /**
1063 * @test
1064 */
1065 public function parseDataStructureByIdentifierCallsRegisteredHook()
1066 {
1067 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1068 DataStructureParsePreProcessHookThrowException::class,
1069 ];
1070 $this->expectException(\RuntimeException::class);
1071 $this->expectExceptionCode(1478112411);
1072 (new FlexFormTools())->parseDataStructureByIdentifier('{"some":"input"}');
1073 }
1074
1075 /**
1076 * @test
1077 */
1078 public function parseDataStructureByIdentifierThrowsExceptionIfHookReturnsNoString()
1079 {
1080 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1081 DataStructureParsePreProcessHookReturnObject::class
1082 ];
1083 $this->expectException(\RuntimeException::class);
1084 $this->expectExceptionCode(1478168512);
1085 (new FlexFormTools())->parseDataStructureByIdentifier('{"some":"input"}');
1086 }
1087
1088 /**
1089 * @test
1090 */
1091 public function parseDataStructureByIdentifierUsesCasualLogicIfHookReturnsNoIdentifier()
1092 {
1093 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1094 DataStructureParsePreProcessHookReturnEmptyString::class
1095 ];
1096 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1097 <T3DataStructure>
1098 <sheets></sheets>
1099 </T3DataStructure>
1100 ';
1101 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1102 $expected = [
1103 'sheets' => '',
1104 ];
1105 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1106 }
1107
1108 /**
1109 * @test
1110 */
1111 public function parseDataStructureByIdentifierParsesDataStructureReturnedByHook()
1112 {
1113 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1114 DataStructureParsePreProcessHookReturnString::class
1115 ];
1116 $identifier = '{"type":"myExtension"}';
1117 $expected = [
1118 'sheets' => '',
1119 ];
1120 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1121 }
1122
1123 /**
1124 * @test
1125 */
1126 public function parseDataStructureByIdentifierParsesDataStructureFromFirstMatchingHook()
1127 {
1128 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1129 DataStructureParsePreProcessHookReturnEmptyString::class,
1130 DataStructureParsePreProcessHookReturnString::class,
1131 DataStructureParsePreProcessHookThrowException::class
1132 ];
1133 $identifier = '{"type":"myExtension"}';
1134 $expected = [
1135 'sheets' => '',
1136 ];
1137 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1138 }
1139
1140 /**
1141 * @test
1142 */
1143 public function parseDataStructureByIdentifierThrowsExceptionForInvalidSyntax()
1144 {
1145 $this->expectException(InvalidIdentifierException::class);
1146 $this->expectExceptionCode(1478104554);
1147 (new FlexFormTools())->parseDataStructureByIdentifier('{"type":"bernd"}');
1148 }
1149
1150 /**
1151 * @test
1152 */
1153 public function parseDataStructureByIdentifierThrowsExceptionForIncompleteTcaSyntax()
1154 {
1155 $this->expectException(\RuntimeException::class);
1156 $this->expectExceptionCode(1478113471);
1157 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName"}';
1158 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1159 }
1160
1161 /**
1162 * @test
1163 */
1164 public function parseDataStructureByIdentifierThrowsExceptionForInvalidTcaSyntaxPointer()
1165 {
1166 $this->expectException(InvalidIdentifierException::class);
1167 $this->expectExceptionCode(1478105491);
1168 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1169 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1170 }
1171
1172 /**
1173 * @test
1174 */
1175 public function parseDataStructureByIdentifierResolvesTcaSyntaxPointer()
1176 {
1177 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1178 <T3DataStructure>
1179 <sheets></sheets>
1180 </T3DataStructure>
1181 ';
1182 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1183 $expected = [
1184 'sheets' => '',
1185 ];
1186 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1187 }
1188
1189 /**
1190 * @test
1191 */
1192 public function parseDataStructureByIdentifierThrowsExceptionForIncompleteRecordSyntax()
1193 {
1194 $this->expectException(\RuntimeException::class);
1195 $this->expectExceptionCode(1478113873);
1196 $identifier = '{"type":"record","tableName":"foreignTableName","uid":42}';
1197 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1198 }
1199
1200 /**
1201 * @test
1202 */
1203 public function parseDataStructureByIdentifierResolvesRecordSyntaxPointer()
1204 {
1205 // Prophecies and revelations for a lot of the database stack classes
1206 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
1207 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
1208 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
1209 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
1210 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
1211 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
1212 $statementProphecy = $this->prophesize(Statement::class);
1213
1214 // Register connection pool revelation in framework, this is the entry point used by system under test
1215 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
1216
1217 // Simulate method call flow on database objects and verify correct query is built
1218 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1219 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
1220 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
1221 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
1222 $queryBuilderProphecy->select('dataprot')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1223 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1224 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
1225 $queryBuilderProphecy->createNamedParameter(42, 1)->willReturn(42);
1226 $expressionBuilderProphecy->eq('uid', 42)->shouldBeCalled()->willReturn('uid = 42');
1227 $queryBuilderProphecy->where('uid = 42')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1228 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
1229 $statementProphecy->fetchColumn(0)->willReturn('
1230 <T3DataStructure>
1231 <sheets></sheets>
1232 </T3DataStructure>
1233 ');
1234 $identifier = '{"type":"record","tableName":"aTableName","uid":42,"fieldName":"dataprot"}';
1235 $expected = [
1236 'sheets' => '',
1237 ];
1238 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1239 }
1240
1241 /**
1242 * @test
1243 */
1244 public function parseDataStructureByIdentifierThrowsExceptionIfDataStructureFileDoesNotExist()
1245 {
1246 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default']
1247 = 'FILE:EXT:core/Does/Not/Exist.xml';
1248 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1249 $this->expectException(\RuntimeException::class);
1250 $this->expectExceptionCode(1478105826);
1251 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1252 }
1253
1254 /**
1255 * @test
1256 */
1257 public function parseDataStructureByIdentifierFetchesFromFile()
1258 {
1259 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default']
1260 = ' FILE:EXT:core/Tests/Unit/Configuration/FlexForm/Fixtures/DataStructureWithSheet.xml ';
1261 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1262 $expected = [
1263 'sheets' => [
1264 'sDEF' => [
1265 'ROOT' => [
1266 'type' => 'array',
1267 'el' => [
1268 'aFlexField' => [
1269 'TCEforms' => [
1270 'label' => 'aFlexFieldLabel',
1271 'config' => [
1272 'type' => 'input',
1273 ],
1274 ],
1275 ],
1276 ],
1277 'TCEforms' => [
1278 'sheetTitle' => 'aTitle',
1279 ],
1280 ],
1281 ],
1282 ]
1283 ];
1284 $this->assertEquals($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1285 }
1286
1287 /**
1288 * @test
1289 */
1290 public function parseDataStructureByIdentifierThrowsExceptionForInvalidXmlStructure()
1291 {
1292 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1293 <T3DataStructure>
1294 <sheets>
1295 <bar>
1296 </sheets>
1297 </T3DataStructure>
1298 ';
1299 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1300 $this->expectException(InvalidIdentifierException::class);
1301 $this->expectExceptionCode(1478106090);
1302 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1303 }
1304
1305 /**
1306 * @test
1307 */
1308 public function parseDataStructureByIdentifierThrowsExceptionIfStructureHasBothSheetAndRoot()
1309 {
1310 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1311 <T3DataStructure>
1312 <ROOT></ROOT>
1313 <sheets></sheets>
1314 </T3DataStructure>
1315 ';
1316 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1317 $this->expectException(\RuntimeException::class);
1318 $this->expectExceptionCode(1440676540);
1319 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1320 }
1321
1322 /**
1323 * @test
1324 */
1325 public function parseDataStructureByIdentifierCreatesDefaultSheet()
1326 {
1327 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1328 <T3DataStructure>
1329 <ROOT>
1330 <TCEforms>
1331 <sheetTitle>aTitle</sheetTitle>
1332 </TCEforms>
1333 <type>array</type>
1334 <el>
1335 <aFlexField>
1336 <TCEforms>
1337 <label>aFlexFieldLabel</label>
1338 <config>
1339 <type>input</type>
1340 </config>
1341 </TCEforms>
1342 </aFlexField>
1343 </el>
1344 </ROOT>
1345 </T3DataStructure>
1346 ';
1347 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1348 $expected = [
1349 'sheets' => [
1350 'sDEF' => [
1351 'ROOT' => [
1352 'type' => 'array',
1353 'el' => [
1354 'aFlexField' => [
1355 'TCEforms' => [
1356 'label' => 'aFlexFieldLabel',
1357 'config' => [
1358 'type' => 'input',
1359 ],
1360 ],
1361 ],
1362 ],
1363 'TCEforms' => [
1364 'sheetTitle' => 'aTitle',
1365 ],
1366 ],
1367 ],
1368 ]
1369 ];
1370 $this->assertEquals($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1371 }
1372
1373 /**
1374 * @test
1375 */
1376 public function parseDataStructureByIdentifierResolvesExtReferenceForSingleSheets()
1377 {
1378 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1379 <T3DataStructure>
1380 <sheets>
1381 <aSheet>
1382 EXT:core/Tests/Unit/Configuration/FlexForm/Fixtures/DataStructureOfSingleSheet.xml
1383 </aSheet>
1384 </sheets>
1385 </T3DataStructure>
1386 ';
1387 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1388 $expected = [
1389 'sheets' => [
1390 'aSheet' => [
1391 'ROOT' => [
1392 'type' => 'array',
1393 'el' => [
1394 'aFlexField' => [
1395 'TCEforms' => [
1396 'label' => 'aFlexFieldLabel',
1397 'config' => [
1398 'type' => 'input',
1399 ],
1400 ],
1401 ],
1402 ],
1403 'TCEforms' => [
1404 'sheetTitle' => 'aTitle',
1405 ],
1406 ],
1407 ],
1408 ]
1409 ];
1410 $this->assertEquals($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1411 }
1412
1413 /**
1414 * @test
1415 */
1416 public function parseDataStructureByIdentifierResolvesExtReferenceForSingleSheetsWithFilePrefix()
1417 {
1418 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1419 <T3DataStructure>
1420 <sheets>
1421 <aSheet>
1422 FILE:EXT:core/Tests/Unit/Configuration/FlexForm/Fixtures/DataStructureOfSingleSheet.xml
1423 </aSheet>
1424 </sheets>
1425 </T3DataStructure>
1426 ';
1427 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1428 $expected = [
1429 'sheets' => [
1430 'aSheet' => [
1431 'ROOT' => [
1432 'type' => 'array',
1433 'el' => [
1434 'aFlexField' => [
1435 'TCEforms' => [
1436 'label' => 'aFlexFieldLabel',
1437 'config' => [
1438 'type' => 'input',
1439 ],
1440 ],
1441 ],
1442 ],
1443 'TCEforms' => [
1444 'sheetTitle' => 'aTitle',
1445 ],
1446 ],
1447 ],
1448 ]
1449 ];
1450 $this->assertEquals($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1451 }
1452
1453 /**
1454 * @test
1455 */
1456 public function parseDataStructureByIdentifierCallsPostProcessHook()
1457 {
1458 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1459 <T3DataStructure>
1460 <sheets></sheets>
1461 </T3DataStructure>
1462 ';
1463 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1464 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1465 DataStructureParsePostProcessHookThrowException::class,
1466 ];
1467 $this->expectException(\RuntimeException::class);
1468 $this->expectExceptionCode(1478351691);
1469 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1470 }
1471
1472 /**
1473 * @test
1474 */
1475 public function parseDataStructureByIdentifierThrowsExceptionIfPostProcessHookReturnsNoArray()
1476 {
1477 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1478 <T3DataStructure>
1479 <sheets></sheets>
1480 </T3DataStructure>
1481 ';
1482 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1483 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1484 DataStructureParsePostProcessHookReturnString::class,
1485 ];
1486 $this->expectException(\RuntimeException::class);
1487 $this->expectExceptionCode(1478350806);
1488 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1489 }
1490
1491 /**
1492 * @test
1493 */
1494 public function parseDataStructureByIdentifierPostProcessHookManipulatesDataStructure()
1495 {
1496 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1497 <T3DataStructure>
1498 <sheets></sheets>
1499 </T3DataStructure>
1500 ';
1501 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1502 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1503 DataStructureParsePostProcessHookReturnArray::class,
1504 ];
1505 $expected = [
1506 'sheets' => [
1507 'foo' => 'bar'
1508 ]
1509 ];
1510 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1511 }
1512
1513 /**
1514 * @test
1515 */
1516 public function traverseFlexFormXmlDataRecurseDoesNotFailOnNotExistingField()
1517 {
1518 $dataStruct = [
1519 'dummy_field' => [
1520 'TCEforms' => [
1521 'config' => [],
1522 ],
1523 ],
1524 ];
1525 $pA = [
1526 'vKeys' => ['ES'],
1527 'callBackMethod_value' => 'dummy',
1528 ];
1529 $editData = '';
1530 /** @var \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools|\PHPUnit_Framework_MockObject_MockObject $subject */
1531 $subject = $this->getMockBuilder(FlexFormTools::class)
1532 ->setMethods(['executeCallBackMethod'])
1533 ->getMock();
1534 $subject->expects($this->never())->method('executeCallBackMethod');
1535 $subject->traverseFlexFormXMLData_recurse($dataStruct, $editData, $pA);
1536 }
1537
1538 /**
1539 * @test
1540 */
1541 public function traverseFlexFormXmlDataRecurseDoesNotFailOnNotExistingArrayField()
1542 {
1543 $dataStruct = [
1544 'dummy_field' => [
1545 'type' => 'array',
1546 'el' => 'field_not_in_data',
1547 ],
1548 ];
1549 $pA = [
1550 'vKeys' => ['ES'],
1551 'callBackMethod_value' => 'dummy',
1552 ];
1553 $editData = [
1554 'field' => [
1555 'el' => 'dummy',
1556 ],
1557 ];
1558 $editData2 = '';
1559 /** @var \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools|\PHPUnit_Framework_MockObject_MockObject $subject */
1560 $subject = $this->createMock(FlexFormTools::class);
1561 $this->assertEquals(
1562 $subject->traverseFlexFormXMLData_recurse($dataStruct, $editData, $pA),
1563 $subject->traverseFlexFormXMLData_recurse($dataStruct, $editData2, $pA)
1564 );
1565 }
1566 }