‪TYPO3CMS  ‪main
RelationHandler.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
29 
36 {
42  public ‪$registerNonTableValues = false;
43 
50  public ‪$tableArray = [];
51 
58  public $itemArray = [];
59 
65  public $nonTableArray = [];
66 
70  public $additionalWhere = [];
71 
77  public $checkIfDeleted = true;
78 
84  protected $firstTable = '';
85 
92  protected $MM_is_foreign = false;
93 
99  protected $MM_isMultiTableRelationship = '';
100 
106  protected $currentTable;
107 
114  public $undeleteRecord;
115 
119  protected array $MM_match_fields = [];
120 
128  protected bool $multiple = false;
129 
135  protected $MM_table_where = '';
136 
142  protected $MM_oppositeUsage;
143 
147  protected $referenceIndexUpdater;
148 
152  protected $useLiveParentIds = true;
153 
157  protected $useLiveReferenceIds = true;
158 
162  protected $workspaceId;
163 
167  protected $purged = false;
168 
174  public $results = [];
175 
179  protected function getWorkspaceId(): int
180  {
181  $backendUser = ‪$GLOBALS['BE_USER'] ?? null;
182  if (!isset($this->‪workspaceId)) {
183  $this->‪workspaceId = $backendUser instanceof ‪BackendUserAuthentication ? (int)($backendUser->workspace) : 0;
184  }
185  return $this->workspaceId;
186  }
187 
193  public function ‪setWorkspaceId($workspaceId): void
194  {
195  $this->‪workspaceId = (int)$workspaceId;
196  }
197 
203  public function ‪setReferenceIndexUpdater(‪ReferenceIndexUpdater $updater): void
204  {
205  $this->referenceIndexUpdater = $updater;
206  }
207 
213  public function ‪isPurged()
214  {
215  return $this->purged;
216  }
217 
228  public function ‪start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = [])
229  {
230  $conf = (array)$conf;
231  // SECTION: MM reverse relations
232  $this->MM_is_foreign = (bool)($conf['MM_opposite_field'] ?? false);
233  $this->MM_table_where = $conf['MM_table_where'] ?? null;
234  $this->multiple = (bool)($conf['multiple'] ?? false);
235  $this->MM_match_fields = (isset($conf['MM_match_fields']) && is_array($conf['MM_match_fields'])) ? $conf['MM_match_fields'] : [];
236  $this->currentTable = $currentTable;
237  if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) {
238  $this->MM_oppositeUsage = $conf['MM_oppositeUsage'];
239  }
240  $mmOppositeTable = '';
241  if ($this->MM_is_foreign) {
242  $allowedTableList = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
243  // Normally, $conf['allowed'] can contain a list of tables,
244  // but as we are looking at an MM relation from the foreign side,
245  // it only makes sense to allow one table in $conf['allowed'].
246  [$mmOppositeTable] = GeneralUtility::trimExplode(',', $allowedTableList);
247  // Only add the current table name if there is more than one allowed
248  // field. We must be sure this has been done at least once before accessing
249  // the "columns" part of TCA for a table.
250  $mmOppositeAllowed = (string)(‪$GLOBALS['TCA'][$mmOppositeTable]['columns'][$conf['MM_opposite_field'] ?? '']['config']['allowed'] ?? '');
251  if ($mmOppositeAllowed !== '') {
252  $mmOppositeAllowedTables = explode(',', $mmOppositeAllowed);
253  if ($mmOppositeAllowed === '*' || count($mmOppositeAllowedTables) > 1) {
254  $this->MM_isMultiTableRelationship = $mmOppositeAllowedTables[0];
255  }
256  }
257  }
258  // SECTION: normal MM relations
259  // If the table list is "*" then all tables are used in the list:
260  if (trim($tablelist) === '*') {
261  $tablelist = implode(',', array_keys(‪$GLOBALS['TCA']));
262  }
263  // The tables are traversed and internal arrays are initialized:
264  $tempTableArray = GeneralUtility::trimExplode(',', $tablelist, true);
265  foreach ($tempTableArray as $val) {
266  $tName = trim($val);
267  $this->tableArray[$tName] = [];
268  $deleteField = ‪$GLOBALS['TCA'][$tName]['ctrl']['delete'] ?? false;
269  if ($this->checkIfDeleted && $deleteField) {
270  $fieldN = $tName . '.' . $deleteField;
271  if (!isset($this->additionalWhere[$tName])) {
272  $this->additionalWhere[$tName] = '';
273  }
274  $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
275  }
276  }
277  if (is_array($this->tableArray)) {
278  reset($this->tableArray);
279  } else {
280  // No tables
281  return;
282  }
283  // Set first and second tables:
284  // Is the first table
285  $this->firstTable = (string)key($this->tableArray);
286  next($this->tableArray);
287  // Now, populate the internal itemArray and tableArray arrays:
288  // If MM, then call this function to do that:
289  if ($MMtable) {
290  if ($MMuid) {
291  $this->‪readMM($MMtable, $MMuid, $mmOppositeTable);
292  $this->‪purgeItemArray();
293  } else {
294  // Revert to readList() for new records in order to load possible default values from $itemlist
295  $this->‪readList($itemlist, $conf);
296  $this->‪purgeItemArray();
297  }
298  } elseif ($MMuid && ($conf['foreign_field'] ?? false)) {
299  // If not MM but foreign_field, the read the records by the foreign_field
300  $this->‪readForeignField($MMuid, $conf);
301  } else {
302  // If not MM, then explode the itemlist by "," and traverse the list:
303  $this->‪readList($itemlist, $conf);
304  // Do automatic default_sortby, if any
305  if (isset($conf['foreign_default_sortby']) && $conf['foreign_default_sortby']) {
306  $this->‪sortList($conf['foreign_default_sortby']);
307  }
308  }
309  }
310 
314  public function ‪setUseLiveParentIds($useLiveParentIds)
315  {
316  $this->useLiveParentIds = (bool)$useLiveParentIds;
317  }
318 
322  public function ‪setUseLiveReferenceIds($useLiveReferenceIds)
323  {
324  $this->useLiveReferenceIds = (bool)$useLiveReferenceIds;
325  }
326 
333  protected function ‪readList($itemlist, array $configuration)
334  {
335  if (trim((string)$itemlist) !== '') {
336  // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
337  // if there were spaces in the list... I suppose this is better overall...
338  $tempItemArray = GeneralUtility::trimExplode(',', $itemlist);
339  // If the second table is set and the ID number is less than zero (later)
340  // then the record is regarded to come from the second table...
341  $secondTable = (string)(key($this->tableArray) ?? '');
342  foreach ($tempItemArray as $key => $val) {
343  // Will be set to "true" if the entry was a real table/id
344  $isSet = false;
345  // Extract table name and id. This is in the formula [tablename]_[id]
346  // where table name MIGHT contain "_", hence the reversion of the string!
347  $val = strrev($val);
348  $parts = explode('_', $val, 2);
349  $theID = strrev($parts[0]);
350  // Check that the id IS an integer:
352  // Get the table name: If a part of the exploded string, use that.
353  // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
354  $theTable = trim($parts[1] ?? '')
355  ? strrev(trim($parts[1] ?? ''))
356  : ($secondTable && $theID < 0 ? $secondTable : $this->firstTable);
357  // If the ID is not blank and the table name is among the names in the inputted tableList
358  if ((string)$theID != '' && $theID && $theTable && isset($this->tableArray[$theTable])) {
359  // Get ID as the right value:
360  $theID = $secondTable ? abs((int)$theID) : (int)$theID;
361  // Register ID/table name in internal arrays:
362  $this->itemArray[$key]['id'] = $theID;
363  $this->itemArray[$key]['table'] = $theTable;
364  $this->tableArray[$theTable][] = $theID;
365  // Set update-flag
366  $isSet = true;
367  }
368  }
369  // If it turns out that the value from the list was NOT a valid reference to a table-record,
370  // then we might still set it as a NO_TABLE value:
371  if (!$isSet && $this->registerNonTableValues) {
372  $this->itemArray[$key]['id'] = $tempItemArray[$key];
373  $this->itemArray[$key]['table'] = '_NO_TABLE';
374  $this->nonTableArray[] = $tempItemArray[$key];
375  }
376  }
377 
378  // Skip if not dealing with IRRE in a CSV list on a workspace
379  if (!isset($configuration['type']) || ($configuration['type'] !== 'inline' && $configuration['type'] !== 'file')
380  || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
381  || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
382  || $this->getWorkspaceId() === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
383  ) {
384  return;
385  }
386 
387  // Fetch live record data
388  if ($this->useLiveReferenceIds) {
389  foreach ($this->itemArray as &$item) {
390  $item['id'] = $this->‪getLiveDefaultId($item['table'], $item['id']);
391  }
392  } else {
393  // Directly overlay workspace data
394  $this->itemArray = [];
395  $foreignTable = $configuration['foreign_table'];
396  $ids = $this->‪getResolver($foreignTable, $this->tableArray[$foreignTable])->‪get();
397  foreach ($ids as $id) {
398  $this->itemArray[] = [
399  'id' => $id,
400  'table' => $foreignTable,
401  ];
402  }
403  }
404  }
405  }
406 
414  protected function ‪sortList($sortby)
415  {
416  // Sort directly without fetching additional data
417  if ($sortby === 'uid') {
418  usort(
419  $this->itemArray,
420  static function ($a, $b) {
421  return $a['id'] < $b['id'] ? -1 : 1;
422  }
423  );
424  } elseif (count($this->tableArray) === 1) {
425  reset($this->tableArray);
426  $table = (string)key($this->tableArray);
427  $connection = $this->‪getConnectionForTableName($table);
428  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
429 
430  foreach (array_chunk(current($this->tableArray), $maxBindParameters - 10, true) as $chunk) {
431  if (empty($chunk)) {
432  continue;
433  }
434  $this->itemArray = [];
435  $this->tableArray = [];
436  $queryBuilder = $connection->createQueryBuilder();
437  $queryBuilder->getRestrictions()->removeAll();
438  $queryBuilder->select('uid')
439  ->from($table)
440  ->where(
441  $queryBuilder->expr()->in(
442  'uid',
443  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
444  )
445  );
446  foreach (‪QueryHelper::parseOrderBy((string)$sortby) as $orderPair) {
447  [$fieldName, $order] = $orderPair;
448  $queryBuilder->addOrderBy($fieldName, $order);
449  }
450  $statement = $queryBuilder->executeQuery();
451  while ($row = $statement->fetchAssociative()) {
452  $this->itemArray[] = ['id' => $row['uid'], 'table' => $table];
453  $this->tableArray[$table][] = $row['uid'];
454  }
455  }
456  }
457  }
458 
471  protected function ‪readMM($tableName, ‪$uid, $mmOppositeTable)
472  {
473  $key = 0;
474  $theTable = null;
475  $queryBuilder = $this->‪getConnectionForTableName($tableName)
477  $queryBuilder->getRestrictions()->removeAll();
478  $queryBuilder->select('*')->from($tableName);
479  // In case of a reverse relation
480  if ($this->MM_is_foreign) {
481  $uidLocal_field = 'uid_foreign';
482  $uidForeign_field = 'uid_local';
483  $sorting_field = 'sorting_foreign';
484  if ($this->MM_isMultiTableRelationship) {
485  // Be backwards compatible! When allowing more than one table after
486  // having previously allowed only one table, this case applies.
487  if ($this->currentTable == $this->MM_isMultiTableRelationship) {
488  $expression = $queryBuilder->expr()->or(
489  $queryBuilder->expr()->eq(
490  'tablenames',
491  $queryBuilder->createNamedParameter($this->currentTable)
492  ),
493  $queryBuilder->expr()->eq(
494  'tablenames',
495  $queryBuilder->createNamedParameter('')
496  )
497  );
498  } else {
499  $expression = $queryBuilder->expr()->eq(
500  'tablenames',
501  $queryBuilder->createNamedParameter($this->currentTable)
502  );
503  }
504  $queryBuilder->andWhere($expression);
505  }
506  $theTable = $mmOppositeTable;
507  } else {
508  // Default
509  $uidLocal_field = 'uid_local';
510  $uidForeign_field = 'uid_foreign';
511  $sorting_field = 'sorting';
512  }
513  if ($this->MM_table_where) {
514  $queryBuilder->andWhere(
515  ‪QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)‪$uid, ‪QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), $this->MM_table_where)))
516  );
517  }
518  foreach ($this->MM_match_fields as $field => $value) {
519  $queryBuilder->andWhere(
520  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value))
521  );
522  }
523  $queryBuilder->andWhere(
524  $queryBuilder->expr()->eq(
525  $uidLocal_field,
526  $queryBuilder->createNamedParameter((int)‪$uid, ‪Connection::PARAM_INT)
527  )
528  );
529  $queryBuilder->orderBy($sorting_field);
530  $queryBuilder->addOrderBy($uidForeign_field);
531  $statement = $queryBuilder->executeQuery();
532  while ($row = $statement->fetchAssociative()) {
533  // Default
534  if (!$this->MM_is_foreign) {
535  // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
536  $theTable = !empty($row['tablenames']) ? $row['tablenames'] : $this->firstTable;
537  }
538  if (($row[$uidForeign_field] || $theTable === 'pages') && $theTable && isset($this->tableArray[$theTable])) {
539  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
540  $this->itemArray[$key]['table'] = $theTable;
541  $this->tableArray[$theTable][] = $row[$uidForeign_field];
542  } elseif ($this->registerNonTableValues) {
543  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
544  $this->itemArray[$key]['table'] = '_NO_TABLE';
545  $this->nonTableArray[] = $row[$uidForeign_field];
546  }
547  $key++;
548  }
549  }
550 
558  public function ‪writeMM($MM_tableName, ‪$uid, $prependTableName = false)
559  {
560  $connection = $this->‪getConnectionForTableName($MM_tableName);
561  $expressionBuilder = $connection->createQueryBuilder()->expr();
562 
563  // In case of a reverse relation
564  if ($this->MM_is_foreign) {
565  $uidLocal_field = 'uid_foreign';
566  $uidForeign_field = 'uid_local';
567  $sorting_field = 'sorting_foreign';
568  } else {
569  // default
570  $uidLocal_field = 'uid_local';
571  $uidForeign_field = 'uid_foreign';
572  $sorting_field = 'sorting';
573  }
574  // If there are tables...
575  $tableC = count($this->tableArray);
576  if ($tableC) {
577  // Boolean: does the field "tablename" need to be filled?
578  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
579  $c = 0;
580  $additionalWhere_tablenames = '';
581  if ($this->MM_is_foreign && $prep) {
582  $additionalWhere_tablenames = $expressionBuilder->eq(
583  'tablenames',
584  $expressionBuilder->literal($this->currentTable)
585  );
586  }
587  $additionalWhere = $expressionBuilder->and();
588  // Add WHERE clause if configured
589  if ($this->MM_table_where) {
590  $additionalWhere = $additionalWhere->with(
592  str_replace('###THIS_UID###', (string)‪$uid, $this->MM_table_where)
593  )
594  );
595  }
596  // Select, update or delete only those relations that match the configured fields
597  foreach ($this->MM_match_fields as $field => $value) {
598  $additionalWhere = $additionalWhere->with($expressionBuilder->eq($field, $expressionBuilder->literal($value)));
599  }
600 
601  $queryBuilder = $connection->createQueryBuilder();
602  $queryBuilder->getRestrictions()->removeAll();
603  $queryBuilder->select($uidForeign_field)
604  ->from($MM_tableName)
605  ->where($queryBuilder->expr()->eq(
606  $uidLocal_field,
607  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
608  ))
609  ->orderBy($sorting_field);
610 
611  if ($prep) {
612  $queryBuilder->addSelect('tablenames');
613  }
614  if ($this->multiple) {
615  $queryBuilder->addSelect('uid');
616  }
617  if ($additionalWhere_tablenames) {
618  $queryBuilder->andWhere($additionalWhere_tablenames);
619  }
620  if ($additionalWhere->count()) {
621  $queryBuilder->andWhere($additionalWhere);
622  }
623 
624  $result = $queryBuilder->executeQuery();
625  $oldMMs = [];
626  // This array is similar to $oldMMs but also holds the uid of the MM-records if 'multiple' is true.
627  // If the UID is present it will be used to update sorting and delete MM-records.
628  // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs.
629  $oldMMs_inclUid = [];
630  while ($row = $result->fetchAssociative()) {
631  if (!$this->MM_is_foreign && $prep) {
632  $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]];
633  } else {
634  $oldMMs[] = $row[$uidForeign_field];
635  }
636  $oldMMs_inclUid[] = (int)($row['uid'] ?? 0);
637  }
638  // For each item, insert it:
639  foreach ($this->itemArray as $val) {
640  $c++;
641  if ($prep || $val['table'] === '_NO_TABLE') {
642  // Insert current table if needed
643  if ($this->MM_is_foreign) {
644  $tablename = $this->currentTable;
645  } else {
646  $tablename = $val['table'];
647  }
648  } else {
649  $tablename = '';
650  }
651  if (!$this->MM_is_foreign && $prep) {
652  $item = [$val['table'], $val['id']];
653  } else {
654  $item = $val['id'];
655  }
656  if (in_array($item, $oldMMs)) {
657  $oldMMs_index = array_search($item, $oldMMs);
658  // In principle, selecting on the UID is all we need to do
659  // if a uid field is available since that is unique!
660  // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
661  $queryBuilder = $connection->createQueryBuilder();
662  $queryBuilder->update($MM_tableName)
663  ->set($sorting_field, $c)
664  ->where(
665  $expressionBuilder->eq(
666  $uidLocal_field,
667  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
668  ),
669  $expressionBuilder->eq(
670  $uidForeign_field,
671  $queryBuilder->createNamedParameter($val['id'], ‪Connection::PARAM_INT)
672  )
673  );
674 
675  if ($additionalWhere->count()) {
676  $queryBuilder->andWhere($additionalWhere);
677  }
678  if ($this->multiple) {
679  $queryBuilder->andWhere(
680  $expressionBuilder->eq(
681  'uid',
682  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMMs_index], ‪Connection::PARAM_INT)
683  )
684  );
685  }
686  if ($tablename) {
687  $queryBuilder->andWhere(
688  $expressionBuilder->eq(
689  'tablenames',
690  $queryBuilder->createNamedParameter($tablename)
691  )
692  );
693  }
694 
695  $queryBuilder->executeStatement();
696  // Remove the item from the $oldMMs array so after this
697  // foreach loop only the ones that need to be deleted are in there.
698  unset($oldMMs[$oldMMs_index]);
699  // Remove the item from the $oldMMs_inclUid array so after this
700  // foreach loop only the ones that need to be deleted are in there.
701  unset($oldMMs_inclUid[$oldMMs_index]);
702  } else {
703  $insertFields = $this->MM_match_fields;
704  $insertFields[$uidLocal_field] = ‪$uid;
705  $insertFields[$uidForeign_field] = $val['id'];
706  $insertFields[$sorting_field] = $c;
707  if ($tablename) {
708  $insertFields['tablenames'] = $tablename;
709  $insertFields = $this->‪completeOppositeUsageValues($tablename, $insertFields);
710  }
711  $connection->insert($MM_tableName, $insertFields);
712  if ($this->MM_is_foreign) {
713  $this->‪updateRefIndex($val['table'], $val['id']);
714  }
715  }
716  }
717  // Delete all not-used relations:
718  if (is_array($oldMMs) && !empty($oldMMs)) {
719  $queryBuilder = $connection->createQueryBuilder();
720  $removeClauses = $queryBuilder->expr()->or();
721  $updateRefIndex_records = [];
722  foreach ($oldMMs as $oldMM_key => $mmItem) {
723  // If UID field is present, of course we need only use that for deleting.
724  if ($this->multiple) {
725  $removeClauses = $removeClauses->with($queryBuilder->expr()->eq(
726  'uid',
727  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMM_key], ‪Connection::PARAM_INT)
728  ));
729  } else {
730  if (is_array($mmItem)) {
731  $removeClauses = $removeClauses->with(
732  $queryBuilder->expr()->and(
733  $queryBuilder->expr()->eq(
734  'tablenames',
735  $queryBuilder->createNamedParameter($mmItem[0])
736  ),
737  $queryBuilder->expr()->eq(
738  $uidForeign_field,
739  $queryBuilder->createNamedParameter($mmItem[1], ‪Connection::PARAM_INT)
740  )
741  )
742  );
743  } else {
744  $removeClauses = $removeClauses->with(
745  $queryBuilder->expr()->eq(
746  $uidForeign_field,
747  $queryBuilder->createNamedParameter($mmItem, ‪Connection::PARAM_INT)
748  )
749  );
750  }
751  }
752  if ($this->MM_is_foreign) {
753  if (is_array($mmItem)) {
754  $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]];
755  } else {
756  $updateRefIndex_records[] = [$this->firstTable, $mmItem];
757  }
758  }
759  }
760 
761  $queryBuilder->delete($MM_tableName)
762  ->where(
763  $queryBuilder->expr()->eq(
764  $uidLocal_field,
765  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
766  ),
767  $removeClauses
768  );
769 
770  if ($additionalWhere_tablenames) {
771  $queryBuilder->andWhere($additionalWhere_tablenames);
772  }
773  if ($additionalWhere->count()) {
774  $queryBuilder->andWhere($additionalWhere);
775  }
776 
777  $queryBuilder->executeStatement();
778 
779  // Update ref index:
780  foreach ($updateRefIndex_records as $pair) {
781  $this->‪updateRefIndex($pair[0], $pair[1]);
782  }
783  }
784  // Update ref index; In DataHandler it is not certain that this will happen because
785  // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
786  // This could also have been fixed in updateDB in DataHandler, however I decided to do it here ...
787  $this->‪updateRefIndex($this->currentTable, ‪$uid);
788  }
789  }
790 
798  protected function ‪readForeignField(‪$uid, $conf)
799  {
800  if ($this->useLiveParentIds) {
801  ‪$uid = $this->‪getLiveDefaultId($this->currentTable, ‪$uid);
802  }
803 
804  $key = 0;
805  ‪$uid = (int)‪$uid;
806  // skip further processing if $uid does not
807  // point to a valid parent record
808  if (‪$uid === 0) {
809  return;
810  }
811 
812  $foreign_table = $conf['foreign_table'];
813  $foreign_table_field = $conf['foreign_table_field'] ?? '';
814  $useDeleteClause = !$this->undeleteRecord;
815  $foreign_match_fields = is_array($conf['foreign_match_fields'] ?? false) ? $conf['foreign_match_fields'] : [];
816  $queryBuilder = $this->‪getConnectionForTableName($foreign_table)
818  $queryBuilder->getRestrictions()
819  ->removeAll();
820  // Use the deleteClause (e.g. "deleted=0") on this table
821  if ($useDeleteClause) {
822  $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
823  }
824 
825  $queryBuilder->select('uid')
826  ->from($foreign_table);
827 
828  // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
829  if (!empty($conf['symmetric_field'])) {
830  $queryBuilder->where(
831  $queryBuilder->expr()->or(
832  $queryBuilder->expr()->eq(
833  $conf['foreign_field'],
834  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
835  ),
836  $queryBuilder->expr()->eq(
837  $conf['symmetric_field'],
838  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
839  )
840  )
841  );
842  } else {
843  $queryBuilder->where($queryBuilder->expr()->eq(
844  $conf['foreign_field'],
845  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
846  ));
847  }
848  // If it's requested to look for the parent uid AND the parent table,
849  // add an additional SQL-WHERE clause
850  if ($foreign_table_field && $this->currentTable) {
851  $queryBuilder->andWhere(
852  $queryBuilder->expr()->eq(
853  $foreign_table_field,
854  $queryBuilder->createNamedParameter($this->currentTable)
855  )
856  );
857  }
858  // Add additional where clause if foreign_match_fields are defined
859  foreach ($foreign_match_fields as $field => $value) {
860  $queryBuilder->andWhere(
861  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value))
862  );
863  }
864  // Select children from the live(!) workspace only
865  if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
866  $queryBuilder->getRestrictions()->add(
867  GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getWorkspaceId())
868  );
869  }
870  // Get the correct sorting field
871  // Specific manual sortby for data handled by this field
872  $sortby = '';
873  if (!empty($conf['foreign_sortby'])) {
874  if (!empty($conf['symmetric_sortby']) && !empty($conf['symmetric_field'])) {
875  // Sorting depends on, from which side of the relation we're looking at it
876  // This requires bypassing automatic quoting and setting of the default sort direction
877  // @TODO: Doctrine: generalize to standard SQL to guarantee database independency
878  $queryBuilder->add(
879  'orderBy',
880  'CASE
881  WHEN ' . $queryBuilder->expr()->eq($conf['foreign_field'], ‪$uid) . '
882  THEN ' . $queryBuilder->quoteIdentifier($conf['foreign_sortby']) . '
883  ELSE ' . $queryBuilder->quoteIdentifier($conf['symmetric_sortby']) . '
884  END'
885  );
886  } else {
887  // Regular single-side behaviour
888  $sortby = $conf['foreign_sortby'];
889  }
890  } elseif (!empty($conf['foreign_default_sortby'])) {
891  // Specific default sortby for data handled by this field
892  $sortby = $conf['foreign_default_sortby'];
893  } elseif (!empty(‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'])) {
894  // Manual sortby for all table records
895  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
896  } elseif (!empty(‪$GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'])) {
897  // Default sortby for all table records
898  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
899  }
900 
901  if (!empty($sortby)) {
902  foreach (‪QueryHelper::parseOrderBy($sortby) as $orderPair) {
903  [$fieldName, $sorting] = $orderPair;
904  $queryBuilder->addOrderBy($fieldName, $sorting);
905  }
906  }
907 
908  // Get the rows from storage
909  $rows = [];
910  $result = $queryBuilder->executeQuery();
911  while ($row = $result->fetchAssociative()) {
912  $rows[(int)$row['uid']] = $row;
913  }
914  if (!empty($rows)) {
915  // Retrieve the parsed and prepared ORDER BY configuration for the resolver
916  $sortby = $queryBuilder->getQueryPart('orderBy');
917  $ids = $this->‪getResolver($foreign_table, array_keys($rows), $sortby)->‪get();
918  foreach ($ids as $id) {
919  $this->itemArray[$key]['id'] = $id;
920  $this->itemArray[$key]['table'] = $foreign_table;
921  $this->tableArray[$foreign_table][] = $id;
922  $key++;
923  }
924  }
925  }
926 
934  public function ‪writeForeignField($conf, $parentUid, $updateToUid = 0)
935  {
936  if ($this->useLiveParentIds) {
937  $parentUid = $this->‪getLiveDefaultId($this->currentTable, $parentUid);
938  if (!empty($updateToUid)) {
939  $updateToUid = $this->‪getLiveDefaultId($this->currentTable, $updateToUid);
940  }
941  }
942 
943  // Ensure all values are set.
944  $conf += [
945  'foreign_table' => '',
946  'foreign_field' => '',
947  'symmetric_field' => '',
948  'foreign_table_field' => '',
949  'foreign_match_fields' => [],
950  ];
951 
952  $c = 0;
953  $foreign_table = $conf['foreign_table'];
954  $foreign_field = $conf['foreign_field'];
955  $symmetric_field = $conf['symmetric_field'] ?? '';
956  $foreign_table_field = $conf['foreign_table_field'];
957  $foreign_match_fields = $conf['foreign_match_fields'];
958  // If there are table items and we have a proper $parentUid
959  if (‪MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
960  // If updateToUid is not a positive integer, set it to '0', so it will be ignored
961  if (!(‪MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
962  $updateToUid = 0;
963  }
964  $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($foreign_table);
965  ‪$fields = 'uid,pid,' . $foreign_field;
966  // Consider the symmetric field if defined:
967  if ($symmetric_field) {
968  ‪$fields .= ',' . $symmetric_field;
969  }
970  // Consider workspaces if defined and currently used:
971  if ($considerWorkspaces) {
972  ‪$fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
973  }
974  // Update all items
975  foreach ($this->itemArray as $val) {
976  ‪$uid = $val['id'];
977  $table = $val['table'];
978  $row = [];
979  // Fetch the current (not overwritten) relation record if we should handle symmetric relations
980  if ($symmetric_field || $considerWorkspaces) {
981  $row = BackendUtility::getRecord($table, ‪$uid, ‪$fields, '', true);
982  if (empty($row)) {
983  continue;
984  }
985  }
986  $isOnSymmetricSide = false;
987  if ($symmetric_field) {
988  $isOnSymmetricSide = ‪self::isOnSymmetricSide((string)$parentUid, $conf, $row);
989  }
990  $updateValues = $foreign_match_fields;
991  // No update to the uid is requested, so this is the normal behaviour
992  // just update the fields and care about sorting
993  if (!$updateToUid) {
994  // Always add the pointer to the parent uid
995  if ($isOnSymmetricSide) {
996  $updateValues[$symmetric_field] = $parentUid;
997  } else {
998  $updateValues[$foreign_field] = $parentUid;
999  }
1000  // If it is configured in TCA also to store the parent table in the child record, just do it
1001  if ($foreign_table_field && $this->currentTable) {
1002  $updateValues[$foreign_table_field] = $this->currentTable;
1003  }
1004  // Get the correct sorting field
1005  // Specific manual sortby for data handled by this field
1006  $sortby = '';
1007  if ($conf['foreign_sortby'] ?? false) {
1008  $sortby = $conf['foreign_sortby'];
1009  } elseif (‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'] ?? false) {
1010  // manual sortby for all table records
1011  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1012  }
1013  // Apply sorting on the symmetric side
1014  // (it depends on who created the relation, so what uid is in the symmetric_field):
1015  if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
1016  $sortby = $conf['symmetric_sortby'];
1017  } else {
1018  $tempSortBy = [];
1019  foreach (‪QueryHelper::parseOrderBy($sortby) as $orderPair) {
1020  [$fieldName, $order] = $orderPair;
1021  if ($order !== null) {
1022  $tempSortBy[] = implode(' ', $orderPair);
1023  } else {
1024  $tempSortBy[] = $fieldName;
1025  }
1026  }
1027  $sortby = implode(',', $tempSortBy);
1028  }
1029  if ($sortby) {
1030  $updateValues[$sortby] = ++$c;
1031  }
1032  } else {
1033  if ($isOnSymmetricSide) {
1034  $updateValues[$symmetric_field] = $updateToUid;
1035  } else {
1036  $updateValues[$foreign_field] = $updateToUid;
1037  }
1038  }
1039  // Update accordant fields in the database:
1040  if (!empty($updateValues)) {
1041  // Update tstamp if any foreign field value has changed
1042  if (!empty(‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'])) {
1043  $updateValues[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = ‪$GLOBALS['EXEC_TIME'];
1044  }
1045  $this->‪getConnectionForTableName($table)
1046  ->‪update(
1047  $table,
1048  $updateValues,
1049  ['uid' => (int)‪$uid]
1050  );
1051  $this->‪updateRefIndex($table, ‪$uid);
1052  }
1053  }
1054  }
1055  }
1056 
1063  public function ‪getValueArray($prependTableName = false)
1064  {
1065  // INIT:
1066  $valueArray = [];
1067  $tableC = count($this->tableArray);
1068  // If there are tables in the table array:
1069  if ($tableC) {
1070  // If there are more than ONE table in the table array, then always prepend table names:
1071  $prep = $tableC > 1 || $prependTableName;
1072  // Traverse the array of items:
1073  foreach ($this->itemArray as $val) {
1074  $valueArray[] = ($prep && $val['table'] !== '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
1075  }
1076  }
1077  // Return the array
1078  return $valueArray;
1079  }
1080 
1087  public function ‪getFromDB()
1088  {
1089  // Traverses the tables listed:
1090  foreach ($this->tableArray as $table => $ids) {
1091  if (is_array($ids) && !empty($ids)) {
1092  $connection = $this->‪getConnectionForTableName($table);
1093  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1094 
1095  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1096  $queryBuilder = $connection->createQueryBuilder();
1097  $queryBuilder->getRestrictions()->removeAll();
1098  $queryBuilder->select('*')
1099  ->from($table)
1100  ->where($queryBuilder->expr()->in(
1101  'uid',
1102  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1103  ));
1104  if ($this->additionalWhere[$table] ?? false) {
1105  $queryBuilder->andWhere(
1106  ‪QueryHelper::stripLogicalOperatorPrefix($this->additionalWhere[$table])
1107  );
1108  }
1109  $statement = $queryBuilder->executeQuery();
1110  while ($row = $statement->fetchAssociative()) {
1111  $this->results[$table][$row['uid']] = $row;
1112  }
1113  }
1114  }
1115  }
1116  return $this->results;
1117  }
1118 
1133  public function ‪getResolvedItemArray(): array
1134  {
1135  $itemArray = [];
1136  foreach ($this->itemArray as $item) {
1137  if (isset($this->results[$item['table']][$item['id']])) {
1138  $itemArray[] = [
1139  'table' => $item['table'],
1140  'uid' => $item['id'],
1141  'record' => $this->results[$item['table']][$item['id']],
1142  ];
1143  }
1144  }
1145  return $itemArray;
1146  }
1147 
1154  public function ‪countItems($returnAsArray = true)
1155  {
1156  $count = count($this->itemArray);
1157  if ($returnAsArray) {
1158  $count = [$count];
1159  }
1160  return $count;
1161  }
1162 
1172  protected function ‪updateRefIndex($table, ‪$uid): array
1173  {
1174  if ($this->referenceIndexUpdater) {
1175  // Add to update registry if given
1176  $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)‪$uid, $this->getWorkspaceId());
1177  }
1178  return [];
1179  }
1180 
1190  public function ‪convertItemArray()
1191  {
1192  // conversion is only required in a workspace context
1193  // (the case that version ids are submitted in a live context are rare)
1194  if ($this->getWorkspaceId() === 0) {
1195  return false;
1196  }
1197 
1198  $hasBeenConverted = false;
1199  foreach ($this->tableArray as $tableName => $ids) {
1200  if (empty($ids) || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
1201  continue;
1202  }
1203 
1204  // convert live ids to version ids if available
1205  $convertedIds = $this->‪getResolver($tableName, $ids)
1207  ->‪setKeepMovePlaceholder(false)
1208  ->‪processVersionOverlays($ids);
1209  foreach ($this->itemArray as $index => $item) {
1210  if ($item['table'] !== $tableName) {
1211  continue;
1212  }
1213  $currentItemId = $item['id'];
1214  if (
1215  !isset($convertedIds[$currentItemId])
1216  || $currentItemId === $convertedIds[$currentItemId]
1217  ) {
1218  continue;
1219  }
1220  // adjust local item to use resolved version id
1221  $this->itemArray[$index]['id'] = $convertedIds[$currentItemId];
1222  $hasBeenConverted = true;
1223  }
1224  // update per-table reference for ids
1225  if ($hasBeenConverted) {
1226  $this->tableArray[$tableName] = array_values($convertedIds);
1227  }
1228  }
1229 
1230  return $hasBeenConverted;
1231  }
1232 
1244  public function ‪purgeItemArray($workspaceId = null)
1245  {
1246  if ($workspaceId === null) {
1247  $workspaceId = $this->getWorkspaceId();
1248  } else {
1249  $workspaceId = (int)$workspaceId;
1250  }
1252  // Ensure, only live relations are in the items Array
1253  if ($workspaceId === 0) {
1254  $purgeCallback = 'purgeVersionedIds';
1255  } else {
1256  // Otherwise, ensure that live relations are purged if version exists
1257  $purgeCallback = 'purgeLiveVersionedIds';
1258  }
1259 
1260  $itemArrayHasBeenPurged = $this->‪purgeItemArrayHandler($purgeCallback);
1261  $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1262  return $itemArrayHasBeenPurged;
1263  }
1264 
1270  public function ‪processDeletePlaceholder()
1271  {
1272  if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1273  return false;
1274  }
1275 
1276  return $this->‪purgeItemArrayHandler('purgeDeletePlaceholder');
1277  }
1278 
1285  protected function ‪purgeItemArrayHandler($purgeCallback)
1286  {
1287  $itemArrayHasBeenPurged = false;
1288 
1289  foreach ($this->tableArray as $itemTableName => $itemIds) {
1290  if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1291  continue;
1292  }
1293 
1294  $purgedItemIds = [];
1295  $callable = [$this, $purgeCallback];
1296  if (is_callable($callable)) {
1297  $purgedItemIds = $callable($itemTableName, $itemIds);
1298  }
1299 
1300  $removedItemIds = array_diff($itemIds, $purgedItemIds);
1301  foreach ($removedItemIds as $removedItemId) {
1302  $this->‪removeFromItemArray($itemTableName, $removedItemId);
1303  }
1304  $this->tableArray[$itemTableName] = $purgedItemIds;
1305  if (!empty($removedItemIds)) {
1306  $itemArrayHasBeenPurged = true;
1307  }
1308  }
1309 
1310  return $itemArrayHasBeenPurged;
1311  }
1312 
1319  protected function ‪purgeVersionedIds($tableName, array $ids)
1320  {
1321  $ids = $this->‪sanitizeIds($ids);
1322  $ids = (array)array_combine($ids, $ids);
1323  $connection = $this->‪getConnectionForTableName($tableName);
1324  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1325 
1326  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1327  $queryBuilder = $connection->createQueryBuilder();
1328  $queryBuilder->getRestrictions()->removeAll();
1329  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1330  ->from($tableName)
1331  ->where(
1332  $queryBuilder->expr()->in(
1333  'uid',
1334  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1335  ),
1336  $queryBuilder->expr()->neq(
1337  't3ver_wsid',
1338  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
1339  )
1340  )
1341  ->orderBy('t3ver_state', 'DESC')
1342  ->executeQuery();
1343 
1344  while ($version = $result->fetchAssociative()) {
1345  $versionId = $version['uid'];
1346  if (isset($ids[$versionId])) {
1347  unset($ids[$versionId]);
1348  }
1349  }
1350  }
1351 
1352  return array_values($ids);
1353  }
1354 
1361  protected function ‪purgeLiveVersionedIds($tableName, array $ids)
1362  {
1363  $ids = $this->‪sanitizeIds($ids);
1364  $ids = (array)array_combine($ids, $ids);
1365  $connection = $this->‪getConnectionForTableName($tableName);
1366  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1367 
1368  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1369  $queryBuilder = $connection->createQueryBuilder();
1370  $queryBuilder->getRestrictions()->removeAll();
1371  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1372  ->from($tableName)
1373  ->where(
1374  $queryBuilder->expr()->in(
1375  't3ver_oid',
1376  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1377  ),
1378  $queryBuilder->expr()->neq(
1379  't3ver_wsid',
1380  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
1381  )
1382  )
1383  ->orderBy('t3ver_state', 'DESC')
1384  ->executeQuery();
1386  while ($version = $result->fetchAssociative()) {
1387  $versionId = $version['uid'];
1388  $liveId = $version['t3ver_oid'];
1389  if (isset($ids[$liveId]) && isset($ids[$versionId])) {
1390  unset($ids[$liveId]);
1391  }
1392  }
1393  }
1394 
1395  return array_values($ids);
1396  }
1397 
1404  protected function ‪purgeDeletePlaceholder($tableName, array $ids)
1405  {
1406  $ids = $this->‪sanitizeIds($ids);
1407  $ids = array_combine($ids, $ids) ?: [];
1408  $connection = $this->‪getConnectionForTableName($tableName);
1409  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1410 
1411  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1412  $queryBuilder = $connection->createQueryBuilder();
1413  $queryBuilder->getRestrictions()->removeAll();
1414  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1415  ->from($tableName)
1416  ->where(
1417  $queryBuilder->expr()->in(
1418  't3ver_oid',
1419  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1420  ),
1421  $queryBuilder->expr()->eq(
1422  't3ver_wsid',
1423  $queryBuilder->createNamedParameter(
1424  $this->getWorkspaceId(),
1426  )
1427  ),
1428  $queryBuilder->expr()->eq(
1429  't3ver_state',
1430  $queryBuilder->createNamedParameter(
1431  VersionState::DELETE_PLACEHOLDER->value,
1433  )
1434  )
1435  )
1436  ->executeQuery();
1437 
1438  while ($version = $result->fetchAssociative()) {
1439  $liveId = $version['t3ver_oid'];
1440  if (isset($ids[$liveId])) {
1441  unset($ids[$liveId]);
1442  }
1443  }
1444  }
1445 
1446  return array_values($ids);
1447  }
1448 
1449  protected function ‪removeFromItemArray($tableName, $id)
1450  {
1451  foreach ($this->itemArray as $index => $item) {
1452  if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) {
1453  unset($this->itemArray[$index]);
1454  return true;
1455  }
1456  }
1457  return false;
1458  }
1459 
1468  protected static function ‪isOnSymmetricSide($parentUid, $parentConf, $childRec)
1469  {
1470  return ‪MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1471  && $parentConf['symmetric_field']
1472  && $parentUid == $childRec[$parentConf['symmetric_field']];
1473  }
1474 
1483  protected function ‪completeOppositeUsageValues($tableName, array $referenceValues)
1484  {
1485  if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1486  // @todo: count($this->MM_oppositeUsage[$tableName]) > 1 is buggy.
1487  // Scenario: Suppose a foreign table has two (!) fields that link to a sys_category. Relations can
1488  // then be correctly set for both fields when editing the foreign records. But when editing a sys_category
1489  // record (local side) and adding a relation to a table that has two category relation fields, the 'fieldname'
1490  // entry in mm-table can not be decided and ends up empty. Neither of the foreign table fields then recognize
1491  // the relation as being set.
1492  // One simple solution is to either simply pick the *first* field, or set *both* relations, but this
1493  // is a) guesswork and b) it may be that in practice only *one* field is actually shown due to record
1494  // types "showitem".
1495  // Brain melt increases with tt_content field 'selected_category' in combination with
1496  // 'category_field' for record types 'menu_categorized_pages' and 'menu_categorized_content' next
1497  // to casual 'categories' field. However, 'selected_category' is a 'oneToMany' and not a 'manyToMany'.
1498  // Hard nut ...
1499  return $referenceValues;
1500  }
1501 
1502  $fieldName = $this->MM_oppositeUsage[$tableName][0];
1503  if (empty(‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1504  return $referenceValues;
1505  }
1506 
1507  $configuration = ‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1508  if (!empty($configuration['MM_match_fields'])) {
1509  // @todo: In the end, MM_match_fields does not make sense. The 'tablename' and 'fieldname' restriction
1510  // in addition to uid_local and uid_foreign used when multiple 'foreign' tables and/or multiple fields
1511  // of one table refer to a single 'local' table having an mm table with these four fields, is already
1512  // clear when looking at 'MM_oppositeUsage' of the local table. 'MM_match_fields' should thus probably
1513  // fall altogether. The only information carried here are the field names of 'tablename' and 'fieldname'
1514  // within the mm table itself, which we should hard code. This is partially assumed in DefaultTcaSchema
1515  // already.
1516  $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1517  }
1518 
1519  return $referenceValues;
1520  }
1521 
1530  protected function ‪getLiveDefaultId($tableName, $id)
1531  {
1532  $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1533  if ($liveDefaultId === null) {
1534  $liveDefaultId = $id;
1535  }
1536  return (int)$liveDefaultId;
1537  }
1538 
1544  protected function ‪sanitizeIds(array $ids): array
1545  {
1546  return array_filter($ids);
1547  }
1548 
1554  protected function ‪getResolver($tableName, array $ids, array $sortingStatement = null)
1555  {
1556  $resolver = GeneralUtility::makeInstance(
1557  PlainDataResolver::class,
1558  $tableName,
1559  $ids,
1560  $sortingStatement
1561  );
1562  $resolver->setWorkspaceId($this->getWorkspaceId());
1563  $resolver->setKeepDeletePlaceholder(true);
1564  $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1565  return $resolver;
1566  }
1567 
1571  protected function ‪getConnectionForTableName(string $tableName)
1572  {
1573  return GeneralUtility::makeInstance(ConnectionPool::class)
1574  ->getConnectionForTable($tableName);
1575  }
1576 }
‪TYPO3\CMS\Core\Database\Query\QueryHelper\parseOrderBy
‪static array array[] parseOrderBy(string $input)
Definition: QueryHelper.php:44
‪TYPO3\CMS\Core\Database\RelationHandler\purgeItemArrayHandler
‪bool purgeItemArrayHandler($purgeCallback)
Definition: RelationHandler.php:1266
‪TYPO3\CMS\Core\Database\RelationHandler\purgeItemArray
‪bool purgeItemArray($workspaceId=null)
Definition: RelationHandler.php:1225
‪TYPO3\CMS\Core\DataHandling\PlainDataResolver\processVersionOverlays
‪int[] processVersionOverlays(array $ids)
Definition: PlainDataResolver.php:151
‪TYPO3\CMS\Core\Database\RelationHandler\purgeLiveVersionedIds
‪array purgeLiveVersionedIds($tableName, array $ids)
Definition: RelationHandler.php:1342
‪TYPO3\CMS\Core\DataHandling\PlainDataResolver
Definition: PlainDataResolver.php:34
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:50
‪TYPO3\CMS\Core\Database\RelationHandler\readList
‪readList($itemlist, array $configuration)
Definition: RelationHandler.php:314
‪TYPO3\CMS\Core\Database\Query\QueryHelper\quoteDatabaseIdentifiers
‪static quoteDatabaseIdentifiers(Connection $connection, string $sql)
Definition: QueryHelper.php:224
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation\getMaxBindParameters
‪static getMaxBindParameters(DoctrineAbstractPlatform $platform)
Definition: PlatformInformation.php:110
‪TYPO3\CMS\Core\Database\RelationHandler\sortList
‪sortList($sortby)
Definition: RelationHandler.php:395
‪TYPO3\CMS\Core\Database\RelationHandler\purgeDeletePlaceholder
‪array purgeDeletePlaceholder($tableName, array $ids)
Definition: RelationHandler.php:1385
‪TYPO3\CMS\Core\Database\RelationHandler
Definition: RelationHandler.php:36
‪TYPO3\CMS\Core\Versioning\VersionState
‪VersionState
Definition: VersionState.php:22
‪TYPO3\CMS\Core\DataHandling\PlainDataResolver\setKeepMovePlaceholder
‪PlainDataResolver setKeepMovePlaceholder($keepMovePlaceholder)
Definition: PlainDataResolver.php:115
‪TYPO3\CMS\Core\DataHandling\PlainDataResolver\get
‪int[] get()
Definition: PlainDataResolver.php:124
‪TYPO3\CMS\Core\Database\RelationHandler\setReferenceIndexUpdater
‪setReferenceIndexUpdater(ReferenceIndexUpdater $updater)
Definition: RelationHandler.php:184
‪TYPO3\CMS\Core\Database\RelationHandler\writeForeignField
‪writeForeignField($conf, $parentUid, $updateToUid=0)
Definition: RelationHandler.php:915
‪TYPO3\CMS\Core\Database\RelationHandler\getConnectionForTableName
‪Connection getConnectionForTableName(string $tableName)
Definition: RelationHandler.php:1552
‪TYPO3\CMS\Core\Database\RelationHandler\$tableArray
‪array $tableArray
Definition: RelationHandler.php:48
‪TYPO3\CMS\Core\Database\RelationHandler\getFromDB
‪array getFromDB()
Definition: RelationHandler.php:1068
‪TYPO3\CMS\Core\Database\RelationHandler\setUseLiveReferenceIds
‪setUseLiveReferenceIds($useLiveReferenceIds)
Definition: RelationHandler.php:303
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Core\Database\RelationHandler\isPurged
‪bool isPurged()
Definition: RelationHandler.php:194
‪TYPO3\CMS\Core\Database\Connection\update
‪int update($tableName, array $data, array $identifier, array $types=[])
Definition: Connection.php:294
‪TYPO3\CMS\Core\Database\RelationHandler\writeMM
‪writeMM($MM_tableName, $uid, $prependTableName=false)
Definition: RelationHandler.php:539
‪TYPO3\CMS\Core\Database\RelationHandler\convertItemArray
‪bool convertItemArray()
Definition: RelationHandler.php:1171
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\Database\RelationHandler\sanitizeIds
‪sanitizeIds(array $ids)
Definition: RelationHandler.php:1525
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Core\Database\RelationHandler\purgeVersionedIds
‪array purgeVersionedIds($tableName, array $ids)
Definition: RelationHandler.php:1300
‪TYPO3\CMS\Core\Database\RelationHandler\completeOppositeUsageValues
‪array completeOppositeUsageValues($tableName, array $referenceValues)
Definition: RelationHandler.php:1464
‪TYPO3\CMS\Core\DataHandling\PlainDataResolver\setKeepDeletePlaceholder
‪PlainDataResolver setKeepDeletePlaceholder($keepDeletePlaceholder)
Definition: PlainDataResolver.php:103
‪TYPO3\CMS\Core\Database\RelationHandler\getValueArray
‪array getValueArray($prependTableName=false)
Definition: RelationHandler.php:1044
‪TYPO3\CMS\Core\Database\RelationHandler\workspaceId
‪array< int, $itemArray=array();public array $nonTableArray=array();public array $additionalWhere=array();public bool $checkIfDeleted=true;protected string $firstTable='';protected bool $MM_is_foreign=false;protected string $MM_isMultiTableRelationship='';protected string $currentTable;public bool $undeleteRecord;protected array $MM_match_fields=[];protected bool $multiple=false;protected string $MM_table_where='';protected array $MM_oppositeUsage;protected ReferenceIndexUpdater|null $referenceIndexUpdater;protected bool $useLiveParentIds=true;protected bool $useLiveReferenceIds=true;protected int|null $workspaceId;protected bool $purged=false;public array $results=array();protected function getWorkspaceId():int { $backendUser=$GLOBALS[ 'BE_USER'] ?? null;if(!isset( $this->workspaceId)) { $this-> workspaceId
Definition: RelationHandler.php:164
‪TYPO3\CMS\Core\Database\Connection\createQueryBuilder
‪createQueryBuilder()
Definition: Connection.php:114
‪TYPO3\CMS\Core\Database\RelationHandler\start
‪start($itemlist, $tablelist, $MMtable='', $MMuid=0, $currentTable='', $conf=[])
Definition: RelationHandler.php:209
‪TYPO3\CMS\Core\Database\RelationHandler\getLiveDefaultId
‪int getLiveDefaultId($tableName, $id)
Definition: RelationHandler.php:1511
‪TYPO3\CMS\Core\Database\RelationHandler\updateRefIndex
‪array updateRefIndex($table, $uid)
Definition: RelationHandler.php:1153
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:60
‪TYPO3\CMS\Core\Database\RelationHandler\countItems
‪mixed countItems($returnAsArray=true)
Definition: RelationHandler.php:1135
‪TYPO3\CMS\Core\Database\RelationHandler\readForeignField
‪readForeignField($uid, $conf)
Definition: RelationHandler.php:779
‪TYPO3\CMS\Core\Database\RelationHandler\$registerNonTableValues
‪bool $registerNonTableValues
Definition: RelationHandler.php:41
‪TYPO3\CMS\Core\Database\RelationHandler\getResolvedItemArray
‪getResolvedItemArray()
Definition: RelationHandler.php:1114
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:39
‪TYPO3\CMS\Core\Database\RelationHandler\readMM
‪readMM($tableName, $uid, $mmOppositeTable)
Definition: RelationHandler.php:452
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:171
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\Database\RelationHandler\setUseLiveParentIds
‪setUseLiveParentIds($useLiveParentIds)
Definition: RelationHandler.php:295
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation
Definition: PlatformInformation.php:33
‪TYPO3\CMS\Core\Database\RelationHandler\isOnSymmetricSide
‪static bool isOnSymmetricSide($parentUid, $parentConf, $childRec)
Definition: RelationHandler.php:1449
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Database\RelationHandler\getResolver
‪PlainDataResolver getResolver($tableName, array $ids, array $sortingStatement=null)
Definition: RelationHandler.php:1535
‪TYPO3\CMS\Core\Database\RelationHandler\setWorkspaceId
‪setWorkspaceId($workspaceId)
Definition: RelationHandler.php:174
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:51
‪TYPO3\CMS\Core\DataHandling\ReferenceIndexUpdater
Definition: ReferenceIndexUpdater.php:35
‪TYPO3\CMS\Core\Database\RelationHandler\removeFromItemArray
‪removeFromItemArray($tableName, $id)
Definition: RelationHandler.php:1430
‪TYPO3\CMS\Core\Database\RelationHandler\processDeletePlaceholder
‪bool processDeletePlaceholder()
Definition: RelationHandler.php:1251
‪TYPO3\CMS\Core\Database
Definition: Connection.php:18
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39