[BUGFIX] Statement::rowCount not reliable for SELECT queries
[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 $queryBuilderProphecy->count('uid')->shouldBeCalled()->willReturn($queryBuilderProphecy);
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 $queryBuilderProphecy->count('uid')->shouldBeCalled()->willReturn($queryBuilderProphecy);
632
633 // First db call returns $secondRow, second returns $thirdRow, which points back to $initialRow -> exception
634 $statementProphecy->fetchColumn(0)->willReturn(1);
635 $statementProphecy->fetch()->willReturn($secondRow, $thirdRow);
636
637 $this->expectException(InvalidParentRowLoopException::class);
638 $this->expectExceptionCode(1464110956);
639 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow);
640 }
641
642 /**
643 * @test
644 */
645 public function getDataStructureIdentifierThrowsExceptionIfNoValidPointerFoundUntilRoot()
646 {
647 $fieldTca = [
648 'config' => [
649 'ds_pointerField' => 'tx_templavoila_ds',
650 'ds_pointerField_searchParent' => 'pid',
651 ]
652 ];
653 $initialRow = [
654 'uid' => 3,
655 'pid' => 2,
656 'tx_templavoila_ds' => null,
657 ];
658 $secondRow = [
659 'uid' => 2,
660 'pid' => 1,
661 'tx_templavoila_ds' => null,
662 ];
663 $thirdRow = [
664 'uid' => 1,
665 'pid' => 0,
666 'tx_templavoila_ds' => null,
667 ];
668
669 // Prophecies and revelations for a lot of the database stack classes
670 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
671 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
672 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
673 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
674 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
675 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
676 $statementProphecy = $this->prophesize(Statement::class);
677
678 // Register connection pool revelation in framework, this is the entry point used by system under test
679 // Two queries are done, so we need two instances
680 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
681 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
682
683 // Simulate method call flow on database objects and verify correct query is built
684 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
685 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
686 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
687 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
688 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
689 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
690 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
691 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
692 $queryBuilderProphecy->createNamedParameter(1, 1)->willReturn(1);
693 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
694 $expressionBuilderProphecy->eq('uid', 1)->shouldBeCalled()->willReturn('uid = 1');
695 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
696 $queryBuilderProphecy->where('uid = 1')->shouldBeCalled()->willReturn($queryBuilderRevelation);
697 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
698 $queryBuilderProphecy->count('uid')->shouldBeCalled()->willReturn($queryBuilderRevelation);
699 $statementProphecy->fetchColumn(0)->shouldBeCalled()->willReturn(1);
700
701 // First db call returns $secondRow, second returns $thirdRow. $thirdRow has pid 0 and still no ds -> exception
702 $statementProphecy->fetch()->willReturn($secondRow, $thirdRow);
703
704 $this->expectException(InvalidParentRowRootException::class);
705 $this->expectExceptionCode(1464112555);
706 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow);
707 }
708
709 /**
710 * @test
711 */
712 public function getDataStructureIdentifierThrowsExceptionIfNoValidPointerValueFound()
713 {
714 $fieldTca = [
715 'config' => [
716 'ds_pointerField' => 'aPointerField',
717 ]
718 ];
719 $row = [
720 'aPointerField' => null,
721 ];
722 $this->expectException(InvalidPointerFieldValueException::class);
723 $this->expectExceptionCode(1464114011);
724 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
725 }
726
727 /**
728 * @test
729 */
730 public function getDataStructureIdentifierThrowsExceptionIfResorvedPointerValueIsIntegerButDsFieldNameIsNotConfigured()
731 {
732 $fieldTca = [
733 'config' => [
734 'ds_pointerField' => 'aPointerField',
735 ]
736 ];
737 $row = [
738 'aPointerField' => 3,
739 ];
740 $this->expectException(InvalidTcaException::class);
741 $this->expectExceptionCode(1464115639);
742 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
743 }
744
745 /**
746 * @test
747 */
748 public function getDataStructureIdentifierThrowsExceptionIfDsTableFieldIsMisconfigured()
749 {
750 $fieldTca = [
751 'config' => [
752 'ds_pointerField' => 'aPointerField',
753 'ds_tableField' => 'misconfigured',
754 ]
755 ];
756 $row = [
757 'aPointerField' => 3,
758 ];
759 $this->expectException(InvalidTcaException::class);
760 $this->expectExceptionCode(1464116002);
761 (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row);
762 }
763
764 /**
765 * @test
766 */
767 public function getDataStructureIdentifierReturnsValidIdentifierForPointerField()
768 {
769 $fieldTca = [
770 'config' => [
771 'ds_pointerField' => 'aPointerField',
772 ]
773 ];
774 $row = [
775 'uid' => 42,
776 'aPointerField' => '<T3DataStructure>...',
777 ];
778 $expected = '{"type":"record","tableName":"aTableName","uid":42,"fieldName":"aPointerField"}';
779 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row));
780 }
781
782 /**
783 * @test
784 */
785 public function getDataStructureIdentifierReturnsValidIdentifierForParentLookup()
786 {
787 $fieldTca = [
788 'config' => [
789 'ds_pointerField' => 'tx_templavoila_ds',
790 'ds_pointerField_searchParent' => 'pid',
791 ]
792 ];
793 $initialRow = [
794 'uid' => 3,
795 'pid' => 2,
796 'tx_templavoila_ds' => null,
797 ];
798 $secondRow = [
799 'uid' => 2,
800 'pid' => 1,
801 'tx_templavoila_ds' => 0,
802 ];
803 $thirdRow = [
804 'uid' => 1,
805 'pid' => 0,
806 'tx_templavoila_ds' => '<T3DataStructure>...',
807 ];
808
809 // Prophecies and revelations for a lot of the database stack classes
810 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
811 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
812 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
813 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
814 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
815 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
816 $statementProphecy = $this->prophesize(Statement::class);
817
818 // Register connection pool revelation in framework, this is the entry point used by system under test
819 // Two queries are done, so we need two instances
820 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
821 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
822
823 // Simulate method call flow on database objects and verify correct query is built
824 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
825 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
826 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
827 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
828 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
829 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
830 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
831 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
832 $queryBuilderProphecy->createNamedParameter(1, 1)->willReturn(1);
833 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
834 $expressionBuilderProphecy->eq('uid', 1)->shouldBeCalled()->willReturn('uid = 1');
835 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
836 $queryBuilderProphecy->where('uid = 1')->shouldBeCalled()->willReturn($queryBuilderRevelation);
837 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
838 $queryBuilderProphecy->count('uid')->shouldBeCalled()->willReturn($queryBuilderRevelation);
839 $statementProphecy->fetchColumn(0)->shouldBeCalled()->willReturn(1);
840
841 // First db call returns $secondRow, second returns $thirdRow. $thirdRow resolves ds
842 $statementProphecy->fetch()->willReturn($secondRow, $thirdRow);
843
844 $expected = '{"type":"record","tableName":"aTableName","uid":1,"fieldName":"tx_templavoila_ds"}';
845 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow));
846 }
847
848 /**
849 * @test
850 */
851 public function getDataStructureIdentifierReturnsValidIdentifierForParentLookupAndBreaksLoop()
852 {
853 $fieldTca = [
854 'config' => [
855 'ds_pointerField' => 'tx_templavoila_ds',
856 'ds_pointerField_searchParent' => 'pid',
857 ]
858 ];
859 $initialRow = [
860 'uid' => 3,
861 'pid' => 2,
862 'tx_templavoila_ds' => null,
863 ];
864 $secondRow = [
865 'uid' => 2,
866 'pid' => 1,
867 'tx_templavoila_ds' => '<T3DataStructure>...',
868 ];
869
870 // Prophecies and revelations for a lot of the database stack classes
871 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
872 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
873 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
874 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
875 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
876 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
877 $statementProphecy = $this->prophesize(Statement::class);
878
879 // Register connection pool revelation in framework, this is the entry point used by system under test
880 // Two queries are done, so we need two instances
881 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
882
883 // Simulate method call flow on database objects and verify correct query is built
884 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
885 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
886 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
887 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
888 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
889 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
890 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
891 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
892 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
893 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
894 $queryBuilderProphecy->count('uid')->shouldBeCalled()->willReturn($queryBuilderRevelation);
895 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
896 $statementProphecy->fetchColumn(0)->shouldBeCalled()->willReturn(1);
897
898 // First db call returns $secondRow. $secendRow resolves DS and does not look further up
899 $statementProphecy->fetch()->willReturn($secondRow);
900
901 $expected = '{"type":"record","tableName":"aTableName","uid":2,"fieldName":"tx_templavoila_ds"}';
902 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow));
903 }
904
905 /**
906 * @test
907 */
908 public function getDataStructureIdentifierReturnsValidIdentifierForParentLookupAndPrefersSubField()
909 {
910 $fieldTca = [
911 'config' => [
912 'ds_pointerField' => 'tx_templavoila_ds',
913 'ds_pointerField_searchParent' => 'pid',
914 'ds_pointerField_searchParent_subField' => 'tx_templavoila_next_ds',
915 ]
916 ];
917 $initialRow = [
918 'uid' => 3,
919 'pid' => 2,
920 'tx_templavoila_ds' => null,
921 'tx_templavoila_next_ds' => null,
922 ];
923 $secondRow = [
924 'uid' => 2,
925 'pid' => 1,
926 'tx_templavoila_ds' => '<T3DataStructure>...',
927 'tx_templavoila_next_ds' => 'anotherDataStructure',
928 ];
929
930 // Prophecies and revelations for a lot of the database stack classes
931 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
932 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
933 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
934 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
935 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
936 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
937 $statementProphecy = $this->prophesize(Statement::class);
938
939 // Register connection pool revelation in framework, this is the entry point used by system under test
940 // Two queries are done, so we need two instances
941 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
942
943 // Simulate method call flow on database objects and verify correct query is built
944 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
945 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
946 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
947 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
948 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
949 $queryBuilderProphecy->addSelect('tx_templavoila_next_ds')->shouldBeCalled();
950 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
951 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
952 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
953 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
954 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
955 $queryBuilderProphecy->count('uid')->shouldBeCalled()->willReturn($queryBuilderRevelation);
956 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
957 $statementProphecy->fetchColumn(0)->shouldBeCalled()->willReturn(1);
958
959 // First db call returns $secondRow. $secendRow resolves DS and does not look further up
960 $statementProphecy->fetch()->willReturn($secondRow);
961
962 $expected = '{"type":"record","tableName":"aTableName","uid":2,"fieldName":"tx_templavoila_next_ds"}';
963 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow));
964 }
965
966 /**
967 * @test
968 */
969 public function getDataStructureIdentifierReturnsValidIdentifierForTableAndFieldPointer()
970 {
971 $fieldTca = [
972 'config' => [
973 'ds_pointerField' => 'aPointerField',
974 'ds_tableField' => 'foreignTableName:foreignTableField',
975 ]
976 ];
977 $row = [
978 'uid' => 3,
979 'pid' => 2,
980 'aPointerField' => 42,
981 ];
982 $expected = '{"type":"record","tableName":"foreignTableName","uid":42,"fieldName":"foreignTableField"}';
983 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $row));
984 }
985
986 /**
987 * @test
988 */
989 public function getDataStructureIdentifierReturnsValidIdentifierForTableAndFieldPointerWithParentLookup()
990 {
991 $fieldTca = [
992 'config' => [
993 'ds_pointerField' => 'tx_templavoila_ds',
994 'ds_pointerField_searchParent' => 'pid',
995 'ds_pointerField_searchParent_subField' => 'tx_templavoila_next_ds',
996 'ds_tableField' => 'foreignTableName:foreignTableField',
997 ]
998 ];
999 $initialRow = [
1000 'uid' => 3,
1001 'pid' => 2,
1002 'tx_templavoila_ds' => null,
1003 'tx_templavoila_next_ds' => null,
1004 ];
1005 $secondRow = [
1006 'uid' => 2,
1007 'pid' => 1,
1008 'tx_templavoila_ds' => '<T3DataStructure>...',
1009 'tx_templavoila_next_ds' => '42',
1010 ];
1011
1012 // Prophecies and revelations for a lot of the database stack classes
1013 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
1014 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
1015 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
1016 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
1017 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
1018 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
1019 $statementProphecy = $this->prophesize(Statement::class);
1020
1021 // Register connection pool revelation in framework, this is the entry point used by system under test
1022 // Two queries are done, so we need two instances
1023 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
1024
1025 // Simulate method call flow on database objects and verify correct query is built
1026 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1027 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
1028 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
1029 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
1030 $queryBuilderProphecy->select('uid', 'pid', 'tx_templavoila_ds')->shouldBeCalled();
1031 $queryBuilderProphecy->addSelect('tx_templavoila_next_ds')->shouldBeCalled();
1032 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1033 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
1034 $queryBuilderProphecy->createNamedParameter(2, 1)->willReturn(2);
1035 $expressionBuilderProphecy->eq('uid', 2)->shouldBeCalled()->willReturn('uid = 2');
1036 $queryBuilderProphecy->where('uid = 2')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1037 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
1038 $queryBuilderProphecy->count('uid')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1039 $statementProphecy->fetchColumn(0)->shouldBeCalled()->willReturn(1);
1040
1041 // First db call returns $secondRow. $secendRow resolves DS and does not look further up
1042 $statementProphecy->fetch()->willReturn($secondRow);
1043
1044 $expected = '{"type":"record","tableName":"foreignTableName","uid":42,"fieldName":"foreignTableField"}';
1045 $this->assertSame($expected, (new FlexFormTools())->getDataStructureIdentifier($fieldTca, 'aTableName', 'aFieldName', $initialRow));
1046 }
1047
1048 /**
1049 * @test
1050 */
1051 public function parseDataStructureByIdentifierThrowsExceptionWithEmptyString()
1052 {
1053 $this->expectException(InvalidIdentifierException::class);
1054 $this->expectExceptionCode(1478100828);
1055 (new FlexFormTools())->parseDataStructureByIdentifier('');
1056 }
1057
1058 /**
1059 * @test
1060 */
1061 public function parseDataStructureByIdentifierIfIdentifierDoesNotResolveToArray()
1062 {
1063 $this->expectException(\RuntimeException::class);
1064 $this->expectExceptionCode(1478345642);
1065 (new FlexFormTools())->parseDataStructureByIdentifier('egon');
1066 }
1067
1068 /**
1069 * @test
1070 */
1071 public function parseDataStructureByIdentifierCallsRegisteredHook()
1072 {
1073 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1074 DataStructureParsePreProcessHookThrowException::class,
1075 ];
1076 $this->expectException(\RuntimeException::class);
1077 $this->expectExceptionCode(1478112411);
1078 (new FlexFormTools())->parseDataStructureByIdentifier('{"some":"input"}');
1079 }
1080
1081 /**
1082 * @test
1083 */
1084 public function parseDataStructureByIdentifierThrowsExceptionIfHookReturnsNoString()
1085 {
1086 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1087 DataStructureParsePreProcessHookReturnObject::class
1088 ];
1089 $this->expectException(\RuntimeException::class);
1090 $this->expectExceptionCode(1478168512);
1091 (new FlexFormTools())->parseDataStructureByIdentifier('{"some":"input"}');
1092 }
1093
1094 /**
1095 * @test
1096 */
1097 public function parseDataStructureByIdentifierUsesCasualLogicIfHookReturnsNoIdentifier()
1098 {
1099 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1100 DataStructureParsePreProcessHookReturnEmptyString::class
1101 ];
1102 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1103 <T3DataStructure>
1104 <sheets></sheets>
1105 </T3DataStructure>
1106 ';
1107 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1108 $expected = [
1109 'sheets' => '',
1110 ];
1111 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1112 }
1113
1114 /**
1115 * @test
1116 */
1117 public function parseDataStructureByIdentifierParsesDataStructureReturnedByHook()
1118 {
1119 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1120 DataStructureParsePreProcessHookReturnString::class
1121 ];
1122 $identifier = '{"type":"myExtension"}';
1123 $expected = [
1124 'sheets' => '',
1125 ];
1126 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1127 }
1128
1129 /**
1130 * @test
1131 */
1132 public function parseDataStructureByIdentifierParsesDataStructureFromFirstMatchingHook()
1133 {
1134 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1135 DataStructureParsePreProcessHookReturnEmptyString::class,
1136 DataStructureParsePreProcessHookReturnString::class,
1137 DataStructureParsePreProcessHookThrowException::class
1138 ];
1139 $identifier = '{"type":"myExtension"}';
1140 $expected = [
1141 'sheets' => '',
1142 ];
1143 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1144 }
1145
1146 /**
1147 * @test
1148 */
1149 public function parseDataStructureByIdentifierThrowsExceptionForInvalidSyntax()
1150 {
1151 $this->expectException(InvalidIdentifierException::class);
1152 $this->expectExceptionCode(1478104554);
1153 (new FlexFormTools())->parseDataStructureByIdentifier('{"type":"bernd"}');
1154 }
1155
1156 /**
1157 * @test
1158 */
1159 public function parseDataStructureByIdentifierThrowsExceptionForIncompleteTcaSyntax()
1160 {
1161 $this->expectException(\RuntimeException::class);
1162 $this->expectExceptionCode(1478113471);
1163 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName"}';
1164 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1165 }
1166
1167 /**
1168 * @test
1169 */
1170 public function parseDataStructureByIdentifierThrowsExceptionForInvalidTcaSyntaxPointer()
1171 {
1172 $this->expectException(InvalidIdentifierException::class);
1173 $this->expectExceptionCode(1478105491);
1174 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1175 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1176 }
1177
1178 /**
1179 * @test
1180 */
1181 public function parseDataStructureByIdentifierResolvesTcaSyntaxPointer()
1182 {
1183 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1184 <T3DataStructure>
1185 <sheets></sheets>
1186 </T3DataStructure>
1187 ';
1188 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1189 $expected = [
1190 'sheets' => '',
1191 ];
1192 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1193 }
1194
1195 /**
1196 * @test
1197 */
1198 public function parseDataStructureByIdentifierThrowsExceptionForIncompleteRecordSyntax()
1199 {
1200 $this->expectException(\RuntimeException::class);
1201 $this->expectExceptionCode(1478113873);
1202 $identifier = '{"type":"record","tableName":"foreignTableName","uid":42}';
1203 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1204 }
1205
1206 /**
1207 * @test
1208 */
1209 public function parseDataStructureByIdentifierResolvesRecordSyntaxPointer()
1210 {
1211 // Prophecies and revelations for a lot of the database stack classes
1212 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
1213 $queryBuilderRevelation = $queryBuilderProphecy->reveal();
1214 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
1215 $queryRestrictionContainerProphecy = $this->prophesize(QueryRestrictionContainerInterface::class);
1216 $queryRestrictionContainerRevelation = $queryRestrictionContainerProphecy->reveal();
1217 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
1218 $statementProphecy = $this->prophesize(Statement::class);
1219
1220 // Register connection pool revelation in framework, this is the entry point used by system under test
1221 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
1222
1223 // Simulate method call flow on database objects and verify correct query is built
1224 $connectionPoolProphecy->getQueryBuilderForTable('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1225 $queryRestrictionContainerProphecy->removeAll()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
1226 $queryRestrictionContainerProphecy->add(Argument::cetera())->shouldBeCalled();
1227 $queryBuilderProphecy->getRestrictions()->shouldBeCalled()->willReturn($queryRestrictionContainerRevelation);
1228 $queryBuilderProphecy->select('dataprot')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1229 $queryBuilderProphecy->from('aTableName')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1230 $queryBuilderProphecy->expr()->shouldBeCalled()->willReturn($expressionBuilderProphecy->reveal());
1231 $queryBuilderProphecy->createNamedParameter(42, 1)->willReturn(42);
1232 $expressionBuilderProphecy->eq('uid', 42)->shouldBeCalled()->willReturn('uid = 42');
1233 $queryBuilderProphecy->where('uid = 42')->shouldBeCalled()->willReturn($queryBuilderRevelation);
1234 $queryBuilderProphecy->execute()->shouldBeCalled()->willReturn($statementProphecy->reveal());
1235 $statementProphecy->fetchColumn(0)->willReturn('
1236 <T3DataStructure>
1237 <sheets></sheets>
1238 </T3DataStructure>
1239 ');
1240 $identifier = '{"type":"record","tableName":"aTableName","uid":42,"fieldName":"dataprot"}';
1241 $expected = [
1242 'sheets' => '',
1243 ];
1244 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1245 }
1246
1247 /**
1248 * @test
1249 */
1250 public function parseDataStructureByIdentifierThrowsExceptionIfDataStructureFileDoesNotExist()
1251 {
1252 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default']
1253 = 'FILE:EXT:core/Does/Not/Exist.xml';
1254 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1255 $this->expectException(\RuntimeException::class);
1256 $this->expectExceptionCode(1478105826);
1257 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1258 }
1259
1260 /**
1261 * @test
1262 */
1263 public function parseDataStructureByIdentifierFetchesFromFile()
1264 {
1265 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default']
1266 = ' FILE:EXT:core/Tests/Unit/Configuration/FlexForm/Fixtures/DataStructureWithSheet.xml ';
1267 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1268 $expected = [
1269 'sheets' => [
1270 'sDEF' => [
1271 'ROOT' => [
1272 'type' => 'array',
1273 'el' => [
1274 'aFlexField' => [
1275 'TCEforms' => [
1276 'label' => 'aFlexFieldLabel',
1277 'config' => [
1278 'type' => 'input',
1279 ],
1280 ],
1281 ],
1282 ],
1283 'TCEforms' => [
1284 'sheetTitle' => 'aTitle',
1285 ],
1286 ],
1287 ],
1288 ]
1289 ];
1290 $this->assertEquals($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1291 }
1292
1293 /**
1294 * @test
1295 */
1296 public function parseDataStructureByIdentifierThrowsExceptionForInvalidXmlStructure()
1297 {
1298 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1299 <T3DataStructure>
1300 <sheets>
1301 <bar>
1302 </sheets>
1303 </T3DataStructure>
1304 ';
1305 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1306 $this->expectException(InvalidIdentifierException::class);
1307 $this->expectExceptionCode(1478106090);
1308 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1309 }
1310
1311 /**
1312 * @test
1313 */
1314 public function parseDataStructureByIdentifierThrowsExceptionIfStructureHasBothSheetAndRoot()
1315 {
1316 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1317 <T3DataStructure>
1318 <ROOT></ROOT>
1319 <sheets></sheets>
1320 </T3DataStructure>
1321 ';
1322 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1323 $this->expectException(\RuntimeException::class);
1324 $this->expectExceptionCode(1440676540);
1325 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1326 }
1327
1328 /**
1329 * @test
1330 */
1331 public function parseDataStructureByIdentifierCreatesDefaultSheet()
1332 {
1333 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1334 <T3DataStructure>
1335 <ROOT>
1336 <TCEforms>
1337 <sheetTitle>aTitle</sheetTitle>
1338 </TCEforms>
1339 <type>array</type>
1340 <el>
1341 <aFlexField>
1342 <TCEforms>
1343 <label>aFlexFieldLabel</label>
1344 <config>
1345 <type>input</type>
1346 </config>
1347 </TCEforms>
1348 </aFlexField>
1349 </el>
1350 </ROOT>
1351 </T3DataStructure>
1352 ';
1353 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1354 $expected = [
1355 'sheets' => [
1356 'sDEF' => [
1357 'ROOT' => [
1358 'type' => 'array',
1359 'el' => [
1360 'aFlexField' => [
1361 'TCEforms' => [
1362 'label' => 'aFlexFieldLabel',
1363 'config' => [
1364 'type' => 'input',
1365 ],
1366 ],
1367 ],
1368 ],
1369 'TCEforms' => [
1370 'sheetTitle' => 'aTitle',
1371 ],
1372 ],
1373 ],
1374 ]
1375 ];
1376 $this->assertEquals($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1377 }
1378
1379 /**
1380 * @test
1381 */
1382 public function parseDataStructureByIdentifierResolvesExtReferenceForSingleSheets()
1383 {
1384 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1385 <T3DataStructure>
1386 <sheets>
1387 <aSheet>
1388 EXT:core/Tests/Unit/Configuration/FlexForm/Fixtures/DataStructureOfSingleSheet.xml
1389 </aSheet>
1390 </sheets>
1391 </T3DataStructure>
1392 ';
1393 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1394 $expected = [
1395 'sheets' => [
1396 'aSheet' => [
1397 'ROOT' => [
1398 'type' => 'array',
1399 'el' => [
1400 'aFlexField' => [
1401 'TCEforms' => [
1402 'label' => 'aFlexFieldLabel',
1403 'config' => [
1404 'type' => 'input',
1405 ],
1406 ],
1407 ],
1408 ],
1409 'TCEforms' => [
1410 'sheetTitle' => 'aTitle',
1411 ],
1412 ],
1413 ],
1414 ]
1415 ];
1416 $this->assertEquals($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1417 }
1418
1419 /**
1420 * @test
1421 */
1422 public function parseDataStructureByIdentifierResolvesExtReferenceForSingleSheetsWithFilePrefix()
1423 {
1424 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1425 <T3DataStructure>
1426 <sheets>
1427 <aSheet>
1428 FILE:EXT:core/Tests/Unit/Configuration/FlexForm/Fixtures/DataStructureOfSingleSheet.xml
1429 </aSheet>
1430 </sheets>
1431 </T3DataStructure>
1432 ';
1433 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1434 $expected = [
1435 'sheets' => [
1436 'aSheet' => [
1437 'ROOT' => [
1438 'type' => 'array',
1439 'el' => [
1440 'aFlexField' => [
1441 'TCEforms' => [
1442 'label' => 'aFlexFieldLabel',
1443 'config' => [
1444 'type' => 'input',
1445 ],
1446 ],
1447 ],
1448 ],
1449 'TCEforms' => [
1450 'sheetTitle' => 'aTitle',
1451 ],
1452 ],
1453 ],
1454 ]
1455 ];
1456 $this->assertEquals($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1457 }
1458
1459 /**
1460 * @test
1461 */
1462 public function parseDataStructureByIdentifierCallsPostProcessHook()
1463 {
1464 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1465 <T3DataStructure>
1466 <sheets></sheets>
1467 </T3DataStructure>
1468 ';
1469 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1470 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1471 DataStructureParsePostProcessHookThrowException::class,
1472 ];
1473 $this->expectException(\RuntimeException::class);
1474 $this->expectExceptionCode(1478351691);
1475 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1476 }
1477
1478 /**
1479 * @test
1480 */
1481 public function parseDataStructureByIdentifierThrowsExceptionIfPostProcessHookReturnsNoArray()
1482 {
1483 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1484 <T3DataStructure>
1485 <sheets></sheets>
1486 </T3DataStructure>
1487 ';
1488 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1489 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1490 DataStructureParsePostProcessHookReturnString::class,
1491 ];
1492 $this->expectException(\RuntimeException::class);
1493 $this->expectExceptionCode(1478350806);
1494 (new FlexFormTools())->parseDataStructureByIdentifier($identifier);
1495 }
1496
1497 /**
1498 * @test
1499 */
1500 public function parseDataStructureByIdentifierPostProcessHookManipulatesDataStructure()
1501 {
1502 $GLOBALS['TCA']['aTableName']['columns']['aFieldName']['config']['ds']['default'] = '
1503 <T3DataStructure>
1504 <sheets></sheets>
1505 </T3DataStructure>
1506 ';
1507 $identifier = '{"type":"tca","tableName":"aTableName","fieldName":"aFieldName","dataStructureKey":"default"}';
1508 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][FlexFormTools::class]['flexParsing'] = [
1509 DataStructureParsePostProcessHookReturnArray::class,
1510 ];
1511 $expected = [
1512 'sheets' => [
1513 'foo' => 'bar'
1514 ]
1515 ];
1516 $this->assertSame($expected, (new FlexFormTools())->parseDataStructureByIdentifier($identifier));
1517 }
1518
1519 /**
1520 * @test
1521 */
1522 public function traverseFlexFormXmlDataRecurseDoesNotFailOnNotExistingField()
1523 {
1524 $dataStruct = [
1525 'dummy_field' => [
1526 'TCEforms' => [
1527 'config' => [],
1528 ],
1529 ],
1530 ];
1531 $pA = [
1532 'vKeys' => ['ES'],
1533 'callBackMethod_value' => 'dummy',
1534 ];
1535 $editData = '';
1536 /** @var \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools|\PHPUnit_Framework_MockObject_MockObject $subject */
1537 $subject = $this->getMockBuilder(FlexFormTools::class)
1538 ->setMethods(['executeCallBackMethod'])
1539 ->getMock();
1540 $subject->expects($this->never())->method('executeCallBackMethod');
1541 $subject->traverseFlexFormXMLData_recurse($dataStruct, $editData, $pA);
1542 }
1543
1544 /**
1545 * @test
1546 */
1547 public function traverseFlexFormXmlDataRecurseDoesNotFailOnNotExistingArrayField()
1548 {
1549 $dataStruct = [
1550 'dummy_field' => [
1551 'type' => 'array',
1552 'el' => 'field_not_in_data',
1553 ],
1554 ];
1555 $pA = [
1556 'vKeys' => ['ES'],
1557 'callBackMethod_value' => 'dummy',
1558 ];
1559 $editData = [
1560 'field' => [
1561 'el' => 'dummy',
1562 ],
1563 ];
1564 $editData2 = '';
1565 /** @var \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools|\PHPUnit_Framework_MockObject_MockObject $subject */
1566 $subject = $this->createMock(FlexFormTools::class);
1567 $this->assertEquals(
1568 $subject->traverseFlexFormXMLData_recurse($dataStruct, $editData, $pA),
1569 $subject->traverseFlexFormXMLData_recurse($dataStruct, $editData2, $pA)
1570 );
1571 }
1572 }