‪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  foreach (‪GeneralUtility::trimExplode(',', $tablelist, true) as $tableName) {
265  // @todo: Loop could be restricted in MM local when MM_oppositeUsage is used.
266  $this->tableArray[$tableName] = [];
267  $deleteField = ‪$GLOBALS['TCA'][$tableName]['ctrl']['delete'] ?? false;
268  if ($this->checkIfDeleted && $deleteField) {
269  if (!isset($this->additionalWhere[$tableName])) {
270  $this->additionalWhere[$tableName] = '';
271  }
272  // @todo: Omit ' AND ' and QueryHelper::stripLogicalOperatorPrefix() in consumers
273  $this->additionalWhere[$tableName] .= ' AND ' . $tableName . '.' . $deleteField . '=0';
274  }
275  }
276  if (is_array($this->tableArray)) {
277  reset($this->tableArray);
278  } else {
279  // No tables
280  return;
281  }
282  // Set first and second tables:
283  // Is the first table
284  $this->firstTable = (string)key($this->tableArray);
285  next($this->tableArray);
286  // Now, populate the internal itemArray and tableArray arrays:
287  // If MM, then call this function to do that:
288  if ($MMtable) {
289  if ($MMuid) {
290  $this->‪readMM($MMtable, $MMuid, $mmOppositeTable);
291  $this->‪purgeItemArray();
292  } else {
293  // Revert to readList() for new records in order to load possible default values from $itemlist
294  $this->‪readList($itemlist, $conf);
295  $this->‪purgeItemArray();
296  }
297  } elseif ($MMuid && ($conf['foreign_field'] ?? false)) {
298  // If not MM but foreign_field, the read the records by the foreign_field
299  $this->‪readForeignField($MMuid, $conf);
300  } else {
301  // If not MM, then explode the itemlist by "," and traverse the list:
302  $this->‪readList($itemlist, $conf);
303  // Do automatic default_sortby, if any
304  if (isset($conf['foreign_default_sortby']) && $conf['foreign_default_sortby']) {
305  $this->‪sortList($conf['foreign_default_sortby']);
306  }
307  }
308  }
309 
313  public function ‪setUseLiveParentIds($useLiveParentIds)
314  {
315  $this->useLiveParentIds = (bool)$useLiveParentIds;
316  }
317 
321  public function ‪setUseLiveReferenceIds($useLiveReferenceIds)
322  {
323  $this->useLiveReferenceIds = (bool)$useLiveReferenceIds;
324  }
325 
332  protected function ‪readList($itemlist, array $configuration)
333  {
334  if (trim((string)$itemlist) !== '') {
335  // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
336  // if there were spaces in the list... I suppose this is better overall...
337  $tempItemArray = ‪GeneralUtility::trimExplode(',', $itemlist);
338  // If the second table is set and the ID number is less than zero (later)
339  // then the record is regarded to come from the second table...
340  $secondTable = (string)(key($this->tableArray) ?? '');
341  foreach ($tempItemArray as $key => $val) {
342  // Will be set to "true" if the entry was a real table/id
343  $isSet = false;
344  // Extract table name and id. This is in the formula [tablename]_[id]
345  // where table name MIGHT contain "_", hence the reversion of the string!
346  $val = strrev($val);
347  $parts = explode('_', $val, 2);
348  $theID = strrev($parts[0]);
349  // Check that the id IS an integer:
351  // Get the table name: If a part of the exploded string, use that.
352  // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
353  $theTable = trim($parts[1] ?? '')
354  ? strrev(trim($parts[1] ?? ''))
355  : ($secondTable && $theID < 0 ? $secondTable : $this->firstTable);
356  // If the ID is not blank and the table name is among the names in the inputted tableList
357  if ((string)$theID != '' && $theID && $theTable && isset($this->tableArray[$theTable])) {
358  // Get ID as the right value:
359  $theID = $secondTable ? abs((int)$theID) : (int)$theID;
360  // Register ID/table name in internal arrays:
361  $this->itemArray[$key]['id'] = $theID;
362  $this->itemArray[$key]['table'] = $theTable;
363  $this->tableArray[$theTable][] = $theID;
364  // Set update-flag
365  $isSet = true;
366  }
367  }
368  // If it turns out that the value from the list was NOT a valid reference to a table-record,
369  // then we might still set it as a NO_TABLE value:
370  if (!$isSet && $this->registerNonTableValues) {
371  $this->itemArray[$key]['id'] = $tempItemArray[$key];
372  $this->itemArray[$key]['table'] = '_NO_TABLE';
373  $this->nonTableArray[] = $tempItemArray[$key];
374  }
375  }
376 
377  // Skip if not dealing with IRRE in a CSV list on a workspace
378  if (!isset($configuration['type']) || ($configuration['type'] !== 'inline' && $configuration['type'] !== 'file')
379  || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
380  || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
381  || $this->getWorkspaceId() === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
382  ) {
383  return;
384  }
385 
386  // Fetch live record data
387  if ($this->useLiveReferenceIds) {
388  foreach ($this->itemArray as &$item) {
389  $item['id'] = $this->‪getLiveDefaultId($item['table'], $item['id']);
390  }
391  } else {
392  // Directly overlay workspace data
393  $this->itemArray = [];
394  $foreignTable = $configuration['foreign_table'];
395  $ids = $this->‪getResolver($foreignTable, $this->tableArray[$foreignTable])->‪get();
396  foreach ($ids as $id) {
397  $this->itemArray[] = [
398  'id' => $id,
399  'table' => $foreignTable,
400  ];
401  }
402  }
403  }
404  }
405 
413  protected function ‪sortList($sortby)
414  {
415  // Sort directly without fetching additional data
416  if ($sortby === 'uid') {
417  usort(
418  $this->itemArray,
419  static function ($a, $b) {
420  return $a['id'] < $b['id'] ? -1 : 1;
421  }
422  );
423  } elseif (count($this->tableArray) === 1) {
424  reset($this->tableArray);
425  $table = (string)key($this->tableArray);
426  $connection = $this->‪getConnectionForTableName($table);
427  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
428 
429  foreach (array_chunk(current($this->tableArray), $maxBindParameters - 10, true) as $chunk) {
430  if (empty($chunk)) {
431  continue;
432  }
433  $this->itemArray = [];
434  $this->tableArray = [];
435  $queryBuilder = $connection->createQueryBuilder();
436  $queryBuilder->getRestrictions()->removeAll();
437  $queryBuilder->select('uid')
438  ->from($table)
439  ->where(
440  $queryBuilder->expr()->in(
441  'uid',
442  $queryBuilder->createNamedParameter($chunk, ‪Connection::PARAM_INT_ARRAY)
443  )
444  );
445  foreach (‪QueryHelper::parseOrderBy((string)$sortby) as $orderPair) {
446  [$fieldName, $order] = $orderPair;
447  $queryBuilder->addOrderBy($fieldName, $order);
448  }
449  $statement = $queryBuilder->executeQuery();
450  while ($row = $statement->fetchAssociative()) {
451  $this->itemArray[] = ['id' => $row['uid'], 'table' => $table];
452  $this->tableArray[$table][] = $row['uid'];
453  }
454  }
455  }
456  }
457 
470  protected function ‪readMM($tableName, ‪$uid, $mmOppositeTable)
471  {
472  $theTable = null;
473  $queryBuilder = $this->‪getConnectionForTableName($tableName)->‪createQueryBuilder();
474  $queryBuilder->getRestrictions()->removeAll();
475  $queryBuilder->select('*')->from($tableName);
476  // Default
477  $uidLocal_field = 'uid_local';
478  $uidForeign_field = 'uid_foreign';
479  $sorting_field = 'sorting';
480  // In case of a reverse relation
481  if ($this->MM_is_foreign) {
482  $uidLocal_field = 'uid_foreign';
483  $uidForeign_field = 'uid_local';
484  $sorting_field = 'sorting_foreign';
485  if ($this->MM_isMultiTableRelationship) {
486  // Be backwards compatible! When allowing more than one table after
487  // having previously allowed only one table, this case applies.
488  if ($this->currentTable == $this->MM_isMultiTableRelationship) {
489  $expression = $queryBuilder->expr()->or(
490  $queryBuilder->expr()->eq(
491  'tablenames',
492  $queryBuilder->createNamedParameter($this->currentTable)
493  ),
494  $queryBuilder->expr()->eq(
495  'tablenames',
496  $queryBuilder->createNamedParameter('')
497  )
498  );
499  } else {
500  $expression = $queryBuilder->expr()->eq(
501  'tablenames',
502  $queryBuilder->createNamedParameter($this->currentTable)
503  );
504  }
505  $queryBuilder->andWhere($expression);
506  }
507  $theTable = $mmOppositeTable;
508  }
509  if ($this->MM_table_where) {
510  $queryBuilder->andWhere(
511  ‪QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)‪$uid, ‪QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), $this->MM_table_where)))
512  );
513  }
514  foreach ($this->MM_match_fields as $field => $value) {
515  $queryBuilder->andWhere(
516  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value))
517  );
518  }
519  $queryBuilder->andWhere(
520  $queryBuilder->expr()->eq(
521  $uidLocal_field,
522  $queryBuilder->createNamedParameter((int)‪$uid, ‪Connection::PARAM_INT)
523  )
524  );
525  $queryBuilder->orderBy($sorting_field);
526  $queryBuilder->addOrderBy($uidForeign_field);
527  $statement = $queryBuilder->executeQuery();
528  $itemArray = [];
529  while ($row = $statement->fetchAssociative()) {
530  // Default
531  if (!$this->MM_is_foreign) {
532  // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
533  $theTable = !empty($row['tablenames']) ? $row['tablenames'] : $this->firstTable;
534  }
535  if (($row[$uidForeign_field] || $theTable === 'pages') && $theTable && isset($this->tableArray[$theTable])) {
536  $item = [
537  'id' => $row[$uidForeign_field],
538  'table' => $theTable,
539  ];
540  $itemArray[] = $item;
541  $this->tableArray[$theTable][] = $row[$uidForeign_field];
542  }
543  }
544  $this->itemArray = $itemArray;
545  }
546 
554  public function ‪writeMM($MM_tableName, ‪$uid, $prependTableName = false)
555  {
556  $connection = $this->‪getConnectionForTableName($MM_tableName);
557  $expressionBuilder = $connection->createQueryBuilder()->expr();
558 
559  // In case of a reverse relation
560  if ($this->MM_is_foreign) {
561  $uidLocal_field = 'uid_foreign';
562  $uidForeign_field = 'uid_local';
563  $sorting_field = 'sorting_foreign';
564  } else {
565  // default
566  $uidLocal_field = 'uid_local';
567  $uidForeign_field = 'uid_foreign';
568  $sorting_field = 'sorting';
569  }
570  // If there are tables...
571  $tableC = count($this->tableArray);
572  if ($tableC) {
573  // Boolean: does the field "tablename" need to be filled?
574  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
575  $c = 0;
576  $additionalWhere_tablenames = '';
577  if ($this->MM_is_foreign && $prep) {
578  $additionalWhere_tablenames = $expressionBuilder->eq(
579  'tablenames',
580  $expressionBuilder->literal($this->currentTable)
581  );
582  }
583  $additionalWhere = $expressionBuilder->and();
584  // Add WHERE clause if configured
585  if ($this->MM_table_where) {
586  $additionalWhere = $additionalWhere->with(
588  str_replace('###THIS_UID###', (string)‪$uid, $this->MM_table_where)
589  )
590  );
591  }
592  // Select, update or delete only those relations that match the configured fields
593  foreach ($this->MM_match_fields as $field => $value) {
594  $additionalWhere = $additionalWhere->with($expressionBuilder->eq($field, $expressionBuilder->literal((string)$value)));
595  }
596 
597  $queryBuilder = $connection->createQueryBuilder();
598  $queryBuilder->getRestrictions()->removeAll();
599  $queryBuilder->select($uidForeign_field)
600  ->from($MM_tableName)
601  ->where($queryBuilder->expr()->eq(
602  $uidLocal_field,
603  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
604  ))
605  ->orderBy($sorting_field);
606 
607  if ($prep) {
608  $queryBuilder->addSelect('tablenames');
609  }
610  if ($this->multiple) {
611  $queryBuilder->addSelect('uid');
612  }
613  if ($additionalWhere_tablenames) {
614  $queryBuilder->andWhere($additionalWhere_tablenames);
615  }
616  if ($additionalWhere->count()) {
617  $queryBuilder->andWhere($additionalWhere);
618  }
619 
620  $result = $queryBuilder->executeQuery();
621  $oldMMs = [];
622  // This array is similar to $oldMMs but also holds the uid of the MM-records if 'multiple' is true.
623  // If the UID is present it will be used to update sorting and delete MM-records.
624  // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs.
625  $oldMMs_inclUid = [];
626  while ($row = $result->fetchAssociative()) {
627  if (!$this->MM_is_foreign && $prep) {
628  $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]];
629  } else {
630  $oldMMs[] = $row[$uidForeign_field];
631  }
632  $oldMMs_inclUid[] = (int)($row['uid'] ?? 0);
633  }
634  // For each item, insert it:
635  foreach ($this->itemArray as $val) {
636  $c++;
637  if ($prep || $val['table'] === '_NO_TABLE') {
638  // Insert current table if needed
639  if ($this->MM_is_foreign) {
640  $tablename = $this->currentTable;
641  } else {
642  $tablename = $val['table'];
643  }
644  } else {
645  $tablename = '';
646  }
647  if (!$this->MM_is_foreign && $prep) {
648  $item = [$val['table'], $val['id']];
649  } else {
650  $item = $val['id'];
651  }
652  if (in_array($item, $oldMMs)) {
653  $oldMMs_index = array_search($item, $oldMMs);
654  // In principle, selecting on the UID is all we need to do
655  // if a uid field is available since that is unique!
656  // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
657  $queryBuilder = $connection->createQueryBuilder();
658  $queryBuilder->update($MM_tableName)
659  ->set($sorting_field, $c)
660  ->where(
661  $expressionBuilder->eq(
662  $uidLocal_field,
663  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
664  ),
665  $expressionBuilder->eq(
666  $uidForeign_field,
667  $queryBuilder->createNamedParameter($val['id'], ‪Connection::PARAM_INT)
668  )
669  );
670 
671  if ($additionalWhere->count()) {
672  $queryBuilder->andWhere($additionalWhere);
673  }
674  if ($this->multiple) {
675  $queryBuilder->andWhere(
676  $expressionBuilder->eq(
677  'uid',
678  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMMs_index], ‪Connection::PARAM_INT)
679  )
680  );
681  }
682  if ($tablename) {
683  $queryBuilder->andWhere(
684  $expressionBuilder->eq(
685  'tablenames',
686  $queryBuilder->createNamedParameter($tablename)
687  )
688  );
689  }
690 
691  $queryBuilder->executeStatement();
692  // Remove the item from the $oldMMs array so after this
693  // foreach loop only the ones that need to be deleted are in there.
694  unset($oldMMs[$oldMMs_index]);
695  // Remove the item from the $oldMMs_inclUid array so after this
696  // foreach loop only the ones that need to be deleted are in there.
697  unset($oldMMs_inclUid[$oldMMs_index]);
698  } else {
699  $insertFields = $this->MM_match_fields;
700  $insertFields[$uidLocal_field] = ‪$uid;
701  $insertFields[$uidForeign_field] = $val['id'];
702  $insertFields[$sorting_field] = $c;
703  if ($tablename) {
704  $insertFields['tablenames'] = $tablename;
705  $insertFields = $this->‪completeOppositeUsageValues($tablename, $insertFields);
706  }
707  $connection->insert($MM_tableName, $insertFields);
708  if ($this->MM_is_foreign) {
709  $this->‪updateRefIndex($val['table'], $val['id']);
710  }
711  }
712  }
713  // Delete all not-used relations:
714  if (is_array($oldMMs) && !empty($oldMMs)) {
715  $queryBuilder = $connection->createQueryBuilder();
716  $removeClauses = $queryBuilder->expr()->or();
717  $updateRefIndex_records = [];
718  foreach ($oldMMs as $oldMM_key => $mmItem) {
719  // If UID field is present, of course we need only use that for deleting.
720  if ($this->multiple) {
721  $removeClauses = $removeClauses->with($queryBuilder->expr()->eq(
722  'uid',
723  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMM_key], ‪Connection::PARAM_INT)
724  ));
725  } else {
726  if (is_array($mmItem)) {
727  $removeClauses = $removeClauses->with(
728  $queryBuilder->expr()->and(
729  $queryBuilder->expr()->eq(
730  'tablenames',
731  $queryBuilder->createNamedParameter($mmItem[0])
732  ),
733  $queryBuilder->expr()->eq(
734  $uidForeign_field,
735  $queryBuilder->createNamedParameter($mmItem[1], ‪Connection::PARAM_INT)
736  )
737  )
738  );
739  } else {
740  $removeClauses = $removeClauses->with(
741  $queryBuilder->expr()->eq(
742  $uidForeign_field,
743  $queryBuilder->createNamedParameter($mmItem, ‪Connection::PARAM_INT)
744  )
745  );
746  }
747  }
748  if ($this->MM_is_foreign) {
749  if (is_array($mmItem)) {
750  $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]];
751  } else {
752  $updateRefIndex_records[] = [$this->firstTable, $mmItem];
753  }
754  }
755  }
756 
757  $queryBuilder->delete($MM_tableName)
758  ->where(
759  $queryBuilder->expr()->eq(
760  $uidLocal_field,
761  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
762  ),
763  $removeClauses
764  );
765 
766  if ($additionalWhere_tablenames) {
767  $queryBuilder->andWhere($additionalWhere_tablenames);
768  }
769  if ($additionalWhere->count()) {
770  $queryBuilder->andWhere($additionalWhere);
771  }
772 
773  $queryBuilder->executeStatement();
774 
775  // Update ref index:
776  foreach ($updateRefIndex_records as $pair) {
777  $this->‪updateRefIndex($pair[0], $pair[1]);
778  }
779  }
780  // Update ref index; In DataHandler it is not certain that this will happen because
781  // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
782  // This could also have been fixed in updateDB in DataHandler, however I decided to do it here ...
783  $this->‪updateRefIndex($this->currentTable, ‪$uid);
784  }
785  }
786 
794  protected function ‪readForeignField(‪$uid, $conf)
795  {
796  if ($this->useLiveParentIds) {
797  ‪$uid = $this->‪getLiveDefaultId($this->currentTable, ‪$uid);
798  }
799 
800  $key = 0;
801  ‪$uid = (int)‪$uid;
802  // skip further processing if $uid does not
803  // point to a valid parent record
804  if (‪$uid === 0) {
805  return;
806  }
807 
808  $foreign_table = $conf['foreign_table'];
809  $foreign_table_field = $conf['foreign_table_field'] ?? '';
810  $useDeleteClause = !$this->undeleteRecord;
811  $foreign_match_fields = is_array($conf['foreign_match_fields'] ?? false) ? $conf['foreign_match_fields'] : [];
812  $queryBuilder = $this->‪getConnectionForTableName($foreign_table)
814  $queryBuilder->getRestrictions()
815  ->removeAll();
816  // Use the deleteClause (e.g. "deleted=0") on this table
817  if ($useDeleteClause) {
818  $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
819  }
820 
821  $queryBuilder->select('uid')
822  ->from($foreign_table);
823 
824  // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
825  if (!empty($conf['symmetric_field'])) {
826  $queryBuilder->where(
827  $queryBuilder->expr()->or(
828  $queryBuilder->expr()->eq(
829  $conf['foreign_field'],
830  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
831  ),
832  $queryBuilder->expr()->eq(
833  $conf['symmetric_field'],
834  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
835  )
836  )
837  );
838  } else {
839  $queryBuilder->where($queryBuilder->expr()->eq(
840  $conf['foreign_field'],
841  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
842  ));
843  }
844  // If it's requested to look for the parent uid AND the parent table,
845  // add an additional SQL-WHERE clause
846  if ($foreign_table_field && $this->currentTable) {
847  $queryBuilder->andWhere(
848  $queryBuilder->expr()->eq(
849  $foreign_table_field,
850  $queryBuilder->createNamedParameter($this->currentTable)
851  )
852  );
853  }
854  // Add additional where clause if foreign_match_fields are defined
855  foreach ($foreign_match_fields as $field => $value) {
856  $queryBuilder->andWhere(
857  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value))
858  );
859  }
860  // Select children from the live(!) workspace only
861  if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
862  $queryBuilder->getRestrictions()->add(
863  GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getWorkspaceId())
864  );
865  }
866  // Get the correct sorting field
867  // Specific manual sortby for data handled by this field
868  $sortby = '';
869  if (!empty($conf['foreign_sortby'])) {
870  if (!empty($conf['symmetric_sortby']) && !empty($conf['symmetric_field'])) {
871  // Sorting depends on, from which side of the relation we're looking at it
872  // This requires bypassing automatic quoting and setting of the default sort direction
873  // @todo Doctrine - generalize to standard SQL to guarantee database independence
874  $queryBuilder->getConcreteQueryBuilder()->orderBy(
875  'CASE
876  WHEN ' . $queryBuilder->expr()->eq($conf['foreign_field'], ‪$uid) . '
877  THEN ' . $queryBuilder->quoteIdentifier($conf['foreign_sortby']) . '
878  ELSE ' . $queryBuilder->quoteIdentifier($conf['symmetric_sortby']) . '
879  END'
880  );
881  } else {
882  // Regular single-side behaviour
883  $sortby = $conf['foreign_sortby'];
884  }
885  } elseif (!empty($conf['foreign_default_sortby'])) {
886  // Specific default sortby for data handled by this field
887  $sortby = $conf['foreign_default_sortby'];
888  } elseif (!empty(‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'])) {
889  // Manual sortby for all table records
890  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
891  } elseif (!empty(‪$GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'])) {
892  // Default sortby for all table records
893  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
894  }
895 
896  if (!empty($sortby)) {
897  foreach (‪QueryHelper::parseOrderBy($sortby) as $orderPair) {
898  [$fieldName, $sorting] = $orderPair;
899  $queryBuilder->addOrderBy($fieldName, $sorting);
900  }
901  }
902 
903  // Get the rows from storage
904  $rows = [];
905  $result = $queryBuilder->executeQuery();
906  while ($row = $result->fetchAssociative()) {
907  $rows[(int)$row['uid']] = $row;
908  }
909  if (!empty($rows)) {
910  $sortby = $queryBuilder->getOrderBy();
911  $ids = $this->‪getResolver($foreign_table, array_keys($rows), $sortby)->‪get();
912  foreach ($ids as $id) {
913  $this->itemArray[$key]['id'] = $id;
914  $this->itemArray[$key]['table'] = $foreign_table;
915  $this->tableArray[$foreign_table][] = $id;
916  $key++;
917  }
918  }
919  }
920 
928  public function ‪writeForeignField($conf, $parentUid, $updateToUid = 0)
929  {
930  if ($this->useLiveParentIds) {
931  $parentUid = $this->‪getLiveDefaultId($this->currentTable, $parentUid);
932  if (!empty($updateToUid)) {
933  $updateToUid = $this->‪getLiveDefaultId($this->currentTable, $updateToUid);
934  }
935  }
936 
937  // Ensure all values are set.
938  $conf += [
939  'foreign_table' => '',
940  'foreign_field' => '',
941  'symmetric_field' => '',
942  'foreign_table_field' => '',
943  'foreign_match_fields' => [],
944  ];
945 
946  $c = 0;
947  $foreign_table = $conf['foreign_table'];
948  $foreign_field = $conf['foreign_field'];
949  $symmetric_field = $conf['symmetric_field'] ?? '';
950  $foreign_table_field = $conf['foreign_table_field'];
951  $foreign_match_fields = $conf['foreign_match_fields'];
952  // If there are table items and we have a proper $parentUid
953  if (‪MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
954  // If updateToUid is not a positive integer, set it to '0', so it will be ignored
955  if (!(‪MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
956  $updateToUid = 0;
957  }
958  $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($foreign_table);
959  ‪$fields = 'uid,pid,' . $foreign_field;
960  // Consider the symmetric field if defined:
961  if ($symmetric_field) {
962  ‪$fields .= ',' . $symmetric_field;
963  }
964  // Consider workspaces if defined and currently used:
965  if ($considerWorkspaces) {
966  ‪$fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
967  }
968  // Update all items
969  foreach ($this->itemArray as $val) {
970  ‪$uid = $val['id'];
971  $table = $val['table'];
972  $row = [];
973  // Fetch the current (not overwritten) relation record if we should handle symmetric relations
974  if ($symmetric_field || $considerWorkspaces) {
975  $row = BackendUtility::getRecord($table, ‪$uid, ‪$fields, '', true);
976  if (empty($row)) {
977  continue;
978  }
979  }
980  $isOnSymmetricSide = false;
981  if ($symmetric_field) {
982  $isOnSymmetricSide = ‪self::isOnSymmetricSide((string)$parentUid, $conf, $row);
983  }
984  $updateValues = $foreign_match_fields;
985  // No update to the uid is requested, so this is the normal behaviour
986  // just update the fields and care about sorting
987  if (!$updateToUid) {
988  // Always add the pointer to the parent uid
989  if ($isOnSymmetricSide) {
990  $updateValues[$symmetric_field] = $parentUid;
991  } else {
992  $updateValues[$foreign_field] = $parentUid;
993  }
994  // If it is configured in TCA also to store the parent table in the child record, just do it
995  if ($foreign_table_field && $this->currentTable) {
996  $updateValues[$foreign_table_field] = $this->currentTable;
997  }
998  // Get the correct sorting field
999  // Specific manual sortby for data handled by this field
1000  $sortby = '';
1001  if ($conf['foreign_sortby'] ?? false) {
1002  $sortby = $conf['foreign_sortby'];
1003  } elseif (‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'] ?? false) {
1004  // manual sortby for all table records
1005  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1006  }
1007  // Apply sorting on the symmetric side
1008  // (it depends on who created the relation, so what uid is in the symmetric_field):
1009  if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
1010  $sortby = $conf['symmetric_sortby'];
1011  } else {
1012  $tempSortBy = [];
1013  foreach (‪QueryHelper::parseOrderBy($sortby) as $orderPair) {
1014  [$fieldName, $order] = $orderPair;
1015  if ($order !== null) {
1016  $tempSortBy[] = implode(' ', $orderPair);
1017  } else {
1018  $tempSortBy[] = $fieldName;
1019  }
1020  }
1021  $sortby = implode(',', $tempSortBy);
1022  }
1023  if ($sortby) {
1024  $updateValues[$sortby] = ++$c;
1025  }
1026  } else {
1027  if ($isOnSymmetricSide) {
1028  $updateValues[$symmetric_field] = $updateToUid;
1029  } else {
1030  $updateValues[$foreign_field] = $updateToUid;
1031  }
1032  }
1033  // Update accordant fields in the database:
1034  if (!empty($updateValues)) {
1035  // Update tstamp if any foreign field value has changed
1036  if (!empty(‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'])) {
1037  $updateValues[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = ‪$GLOBALS['EXEC_TIME'];
1038  }
1039  $this->‪getConnectionForTableName($table)
1040  ->‪update(
1041  $table,
1042  $updateValues,
1043  ['uid' => (int)‪$uid]
1044  );
1045  $this->‪updateRefIndex($table, ‪$uid);
1046  }
1047  }
1048  }
1049  }
1050 
1057  public function ‪getValueArray($prependTableName = false)
1058  {
1059  // INIT:
1060  $valueArray = [];
1061  $tableC = count($this->tableArray);
1062  // If there are tables in the table array:
1063  if ($tableC) {
1064  // If there are more than ONE table in the table array, then always prepend table names:
1065  $prep = $tableC > 1 || $prependTableName;
1066  // Traverse the array of items:
1067  foreach ($this->itemArray as $val) {
1068  $valueArray[] = ($prep && $val['table'] !== '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
1069  }
1070  }
1071  // Return the array
1072  return $valueArray;
1073  }
1074 
1081  public function ‪getFromDB()
1082  {
1083  // Traverses the tables listed:
1084  foreach ($this->tableArray as $table => $ids) {
1085  if (is_array($ids) && !empty($ids)) {
1086  $connection = $this->‪getConnectionForTableName($table);
1087  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1088 
1089  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1090  $queryBuilder = $connection->createQueryBuilder();
1091  $queryBuilder->getRestrictions()->removeAll();
1092  $queryBuilder->select('*')
1093  ->from($table)
1094  ->where($queryBuilder->expr()->in(
1095  'uid',
1096  $queryBuilder->createNamedParameter($chunk, ‪Connection::PARAM_INT_ARRAY)
1097  ));
1098  if ($this->additionalWhere[$table] ?? false) {
1099  $queryBuilder->andWhere(
1100  ‪QueryHelper::stripLogicalOperatorPrefix($this->additionalWhere[$table])
1101  );
1102  }
1103  $statement = $queryBuilder->executeQuery();
1104  while ($row = $statement->fetchAssociative()) {
1105  $this->results[$table][$row['uid']] = $row;
1106  }
1107  }
1108  }
1109  }
1110  return $this->results;
1111  }
1112 
1127  public function ‪getResolvedItemArray(): array
1128  {
1129  $itemArray = [];
1130  foreach ($this->itemArray as $item) {
1131  if (isset($this->results[$item['table']][$item['id']])) {
1132  $itemArray[] = [
1133  'table' => $item['table'],
1134  'uid' => $item['id'],
1135  'record' => $this->results[$item['table']][$item['id']],
1136  ];
1137  }
1138  }
1139  return $itemArray;
1140  }
1141 
1148  public function ‪countItems($returnAsArray = true)
1149  {
1150  $count = count($this->itemArray);
1151  if ($returnAsArray) {
1152  $count = [$count];
1153  }
1154  return $count;
1155  }
1156 
1166  protected function ‪updateRefIndex($table, ‪$uid): array
1167  {
1168  if ($this->referenceIndexUpdater) {
1169  // Add to update registry if given
1170  $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)‪$uid, $this->getWorkspaceId());
1171  }
1172  return [];
1173  }
1174 
1184  public function ‪convertItemArray()
1185  {
1186  // conversion is only required in a workspace context
1187  // (the case that version ids are submitted in a live context are rare)
1188  if ($this->getWorkspaceId() === 0) {
1189  return false;
1190  }
1191 
1192  $hasBeenConverted = false;
1193  foreach ($this->tableArray as $tableName => $ids) {
1194  if (empty($ids) || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
1195  continue;
1196  }
1197 
1198  // convert live ids to version ids if available
1199  $convertedIds = $this->‪getResolver($tableName, $ids)
1201  ->‪setKeepMovePlaceholder(false)
1202  ->‪processVersionOverlays($ids);
1203  foreach ($this->itemArray as $index => $item) {
1204  if ($item['table'] !== $tableName) {
1205  continue;
1206  }
1207  $currentItemId = $item['id'];
1208  if (
1209  !isset($convertedIds[$currentItemId])
1210  || $currentItemId === $convertedIds[$currentItemId]
1211  ) {
1212  continue;
1213  }
1214  // adjust local item to use resolved version id
1215  $this->itemArray[$index]['id'] = $convertedIds[$currentItemId];
1216  $hasBeenConverted = true;
1217  }
1218  // update per-table reference for ids
1219  if ($hasBeenConverted) {
1220  $this->tableArray[$tableName] = array_values($convertedIds);
1221  }
1222  }
1223 
1224  return $hasBeenConverted;
1225  }
1226 
1238  public function ‪purgeItemArray($workspaceId = null)
1239  {
1240  if ($workspaceId === null) {
1241  $workspaceId = $this->getWorkspaceId();
1242  } else {
1243  $workspaceId = (int)$workspaceId;
1244  }
1246  // Ensure, only live relations are in the items Array
1247  if ($workspaceId === 0) {
1248  $purgeCallback = 'purgeVersionedIds';
1249  } else {
1250  // Otherwise, ensure that live relations are purged if version exists
1251  $purgeCallback = 'purgeLiveVersionedIds';
1252  }
1253 
1254  $itemArrayHasBeenPurged = $this->‪purgeItemArrayHandler($purgeCallback, $workspaceId);
1255  $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1256  return $itemArrayHasBeenPurged;
1257  }
1258 
1264  public function ‪processDeletePlaceholder()
1265  {
1266  if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1267  return false;
1268  }
1269 
1270  return $this->‪purgeItemArrayHandler('purgeDeletePlaceholder', $this->getWorkspaceId());
1271  }
1272 
1279  protected function ‪purgeItemArrayHandler($purgeCallback, int $workspaceId)
1280  {
1281  $itemArrayHasBeenPurged = false;
1282 
1283  foreach ($this->tableArray as $itemTableName => $itemIds) {
1284  if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1285  continue;
1286  }
1287 
1288  $purgedItemIds = [];
1289  $callable = [$this, $purgeCallback];
1290  if (is_callable($callable)) {
1291  $purgedItemIds = $callable($itemTableName, $itemIds, $workspaceId);
1292  }
1293 
1294  $removedItemIds = array_diff($itemIds, $purgedItemIds);
1295  foreach ($removedItemIds as $removedItemId) {
1296  $this->‪removeFromItemArray($itemTableName, $removedItemId);
1297  }
1298  $this->tableArray[$itemTableName] = $purgedItemIds;
1299  if (!empty($removedItemIds)) {
1300  $itemArrayHasBeenPurged = true;
1301  }
1302  }
1303 
1304  return $itemArrayHasBeenPurged;
1305  }
1306 
1313  protected function ‪purgeVersionedIds($tableName, array $ids)
1314  {
1315  $ids = $this->‪sanitizeIds($ids);
1316  $ids = (array)array_combine($ids, $ids);
1317  $connection = $this->‪getConnectionForTableName($tableName);
1318  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1319 
1320  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1321  $queryBuilder = $connection->createQueryBuilder();
1322  $queryBuilder->getRestrictions()->removeAll();
1323  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1324  ->from($tableName)
1325  ->where(
1326  $queryBuilder->expr()->in(
1327  'uid',
1328  $queryBuilder->createNamedParameter($chunk, ‪Connection::PARAM_INT_ARRAY)
1329  ),
1330  $queryBuilder->expr()->neq(
1331  't3ver_wsid',
1332  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
1333  )
1334  )
1335  ->orderBy('t3ver_state', 'DESC')
1336  ->executeQuery();
1337 
1338  while ($version = $result->fetchAssociative()) {
1339  $versionId = $version['uid'];
1340  if (isset($ids[$versionId])) {
1341  unset($ids[$versionId]);
1342  }
1343  }
1344  }
1345 
1346  return array_values($ids);
1347  }
1348 
1365  protected function ‪purgeLiveVersionedIds(string $tableName, array $candidateUidList, int $targetWorkspaceUid): array
1366  {
1367  $candidateUidList = $this->‪sanitizeIds($candidateUidList);
1368  $candidateUidList = array_combine($candidateUidList, $candidateUidList);
1369  $connection = $this->‪getConnectionForTableName($tableName);
1370  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1371 
1372  foreach (array_chunk($candidateUidList, $maxBindParameters - 10, true) as $chunk) {
1373  $queryBuilder = $connection->createQueryBuilder();
1374  $queryBuilder->getRestrictions()->removeAll();
1375  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state', 't3ver_wsid')
1376  ->from($tableName)
1377  ->where(
1378  $queryBuilder->expr()->in(
1379  'uid',
1380  $queryBuilder->createNamedParameter($chunk, ‪Connection::PARAM_INT_ARRAY)
1381  ),
1382  $queryBuilder->expr()->neq(
1383  't3ver_wsid',
1384  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
1385  )
1386  )
1387  ->orderBy('t3ver_state', 'DESC')
1388  ->executeQuery();
1389  while ($workspaceRow = $result->fetchAssociative()) {
1390  $rowVersionUid = (int)$workspaceRow['uid'];
1391  $rowLiveUid = (int)$workspaceRow['t3ver_oid'];
1392  $rowWorkspaceUid = (int)$workspaceRow['t3ver_wsid'];
1393  if ($rowWorkspaceUid !== $targetWorkspaceUid) {
1394  // If this row t3ver_wsid does not match requested workspace,
1395  // the row is a row of a different workspace and has to be
1396  // removed from result set.
1397  unset($candidateUidList[$rowVersionUid]);
1398  continue;
1399  }
1400  if (isset($candidateUidList[$rowLiveUid]) && isset($candidateUidList[$rowVersionUid])) {
1401  // This is a workspace row that overlays a live candidate,
1402  // so live needs to be removed from the candidate list.
1403  unset($candidateUidList[$rowLiveUid]);
1404  }
1405  }
1406  }
1407 
1408  return array_values($candidateUidList);
1409  }
1410 
1417  protected function ‪purgeDeletePlaceholder($tableName, array $ids)
1418  {
1419  $ids = $this->‪sanitizeIds($ids);
1420  $ids = array_combine($ids, $ids) ?: [];
1421  $connection = $this->‪getConnectionForTableName($tableName);
1422  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1423 
1424  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1425  $queryBuilder = $connection->createQueryBuilder();
1426  $queryBuilder->getRestrictions()->removeAll();
1427  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1428  ->from($tableName)
1429  ->where(
1430  $queryBuilder->expr()->in(
1431  't3ver_oid',
1432  $queryBuilder->createNamedParameter($chunk, ‪Connection::PARAM_INT_ARRAY)
1433  ),
1434  $queryBuilder->expr()->eq(
1435  't3ver_wsid',
1436  $queryBuilder->createNamedParameter(
1437  $this->getWorkspaceId(),
1439  )
1440  ),
1441  $queryBuilder->expr()->eq(
1442  't3ver_state',
1443  $queryBuilder->createNamedParameter(
1444  VersionState::DELETE_PLACEHOLDER->value,
1446  )
1447  )
1448  )
1449  ->executeQuery();
1450 
1451  while ($version = $result->fetchAssociative()) {
1452  $liveId = $version['t3ver_oid'];
1453  if (isset($ids[$liveId])) {
1454  unset($ids[$liveId]);
1455  }
1456  }
1457  }
1458 
1459  return array_values($ids);
1460  }
1461 
1462  protected function ‪removeFromItemArray($tableName, $id)
1463  {
1464  foreach ($this->itemArray as $index => $item) {
1465  if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) {
1466  unset($this->itemArray[$index]);
1467  return true;
1468  }
1469  }
1470  return false;
1471  }
1472 
1481  protected static function ‪isOnSymmetricSide($parentUid, $parentConf, $childRec)
1482  {
1483  return ‪MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1484  && $parentConf['symmetric_field']
1485  && $parentUid == $childRec[$parentConf['symmetric_field']];
1486  }
1487 
1496  protected function ‪completeOppositeUsageValues($tableName, array $referenceValues)
1497  {
1498  if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1499  // @todo: count($this->MM_oppositeUsage[$tableName]) > 1 is buggy.
1500  // Scenario: Suppose a foreign table has two (!) fields that link to a sys_category. Relations can
1501  // then be correctly set for both fields when editing the foreign records. But when editing a sys_category
1502  // record (local side) and adding a relation to a table that has two category relation fields, the 'fieldname'
1503  // entry in mm-table can not be decided and ends up empty. Neither of the foreign table fields then recognize
1504  // the relation as being set.
1505  // One simple solution is to either simply pick the *first* field, or set *both* relations, but this
1506  // is a) guesswork and b) it may be that in practice only *one* field is actually shown due to record
1507  // types "showitem".
1508  // Brain melt increases with tt_content field 'selected_category' in combination with
1509  // 'category_field' for record types 'menu_categorized_pages' and 'menu_categorized_content' next
1510  // to casual 'categories' field. However, 'selected_category' is a 'oneToMany' and not a 'manyToMany'.
1511  // Hard nut ...
1512  return $referenceValues;
1513  }
1514 
1515  $fieldName = $this->MM_oppositeUsage[$tableName][0];
1516  if (empty(‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1517  return $referenceValues;
1518  }
1519 
1520  $configuration = ‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1521  if (!empty($configuration['MM_match_fields'])) {
1522  // @todo: In the end, MM_match_fields does not make sense. The 'tablename' and 'fieldname' restriction
1523  // in addition to uid_local and uid_foreign used when multiple 'foreign' tables and/or multiple fields
1524  // of one table refer to a single 'local' table having an mm table with these four fields, is already
1525  // clear when looking at 'MM_oppositeUsage' of the local table. 'MM_match_fields' should thus probably
1526  // fall altogether. The only information carried here are the field names of 'tablename' and 'fieldname'
1527  // within the mm table itself, which we should hard code. This is partially assumed in DefaultTcaSchema
1528  // already.
1529  $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1530  }
1531 
1532  return $referenceValues;
1533  }
1534 
1543  protected function ‪getLiveDefaultId($tableName, $id)
1544  {
1545  $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1546  if ($liveDefaultId === null) {
1547  $liveDefaultId = $id;
1548  }
1549  return (int)$liveDefaultId;
1550  }
1551 
1557  protected function ‪sanitizeIds(array $ids): array
1558  {
1559  return array_filter($ids);
1560  }
1561 
1567  protected function ‪getResolver($tableName, array $ids, array $sortingStatement = null)
1568  {
1569  $resolver = GeneralUtility::makeInstance(
1570  PlainDataResolver::class,
1571  $tableName,
1572  $ids,
1573  $sortingStatement
1574  );
1575  $resolver->setWorkspaceId($this->getWorkspaceId());
1576  $resolver->setKeepDeletePlaceholder(true);
1577  $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1578  return $resolver;
1579  }
1580 
1584  protected function ‪getConnectionForTableName(string $tableName)
1585  {
1586  return GeneralUtility::makeInstance(ConnectionPool::class)
1587  ->getConnectionForTable($tableName);
1588  }
1589 }
‪TYPO3\CMS\Core\Database\Query\QueryHelper\parseOrderBy
‪static array array[] parseOrderBy(string $input)
Definition: QueryHelper.php:44
‪TYPO3\CMS\Core\Database\RelationHandler\purgeItemArray
‪bool purgeItemArray($workspaceId=null)
Definition: RelationHandler.php:1219
‪TYPO3\CMS\Core\DataHandling\PlainDataResolver\processVersionOverlays
‪int[] processVersionOverlays(array $ids)
Definition: PlainDataResolver.php:151
‪TYPO3\CMS\Core\DataHandling\PlainDataResolver
Definition: PlainDataResolver.php:34
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\Database\RelationHandler\readList
‪readList($itemlist, array $configuration)
Definition: RelationHandler.php:313
‪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:106
‪TYPO3\CMS\Core\Database\RelationHandler\sortList
‪sortList($sortby)
Definition: RelationHandler.php:394
‪TYPO3\CMS\Core\Database\RelationHandler\purgeDeletePlaceholder
‪array purgeDeletePlaceholder($tableName, array $ids)
Definition: RelationHandler.php:1398
‪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:909
‪TYPO3\CMS\Core\Database\RelationHandler\getConnectionForTableName
‪Connection getConnectionForTableName(string $tableName)
Definition: RelationHandler.php:1565
‪TYPO3\CMS\Core\Database\RelationHandler\$tableArray
‪array $tableArray
Definition: RelationHandler.php:48
‪TYPO3\CMS\Core\Database\RelationHandler\getFromDB
‪array getFromDB()
Definition: RelationHandler.php:1062
‪TYPO3\CMS\Core\Database\RelationHandler\setUseLiveReferenceIds
‪setUseLiveReferenceIds($useLiveReferenceIds)
Definition: RelationHandler.php:302
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Core\Database\RelationHandler\isPurged
‪bool isPurged()
Definition: RelationHandler.php:194
‪TYPO3\CMS\Core\Database\RelationHandler\purgeLiveVersionedIds
‪purgeLiveVersionedIds(string $tableName, array $candidateUidList, int $targetWorkspaceUid)
Definition: RelationHandler.php:1346
‪TYPO3\CMS\Core\Database\RelationHandler\writeMM
‪writeMM($MM_tableName, $uid, $prependTableName=false)
Definition: RelationHandler.php:535
‪TYPO3\CMS\Core\Database\RelationHandler\convertItemArray
‪bool convertItemArray()
Definition: RelationHandler.php:1165
‪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:1538
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Core\Database\RelationHandler\purgeVersionedIds
‪array purgeVersionedIds($tableName, array $ids)
Definition: RelationHandler.php:1294
‪TYPO3\CMS\Core\Database\RelationHandler\completeOppositeUsageValues
‪array completeOppositeUsageValues($tableName, array $referenceValues)
Definition: RelationHandler.php:1477
‪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:1038
‪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:1524
‪TYPO3\CMS\Core\Database\RelationHandler\updateRefIndex
‪array updateRefIndex($table, $uid)
Definition: RelationHandler.php:1147
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Database\RelationHandler\countItems
‪mixed countItems($returnAsArray=true)
Definition: RelationHandler.php:1129
‪TYPO3\CMS\Core\Database\RelationHandler\readForeignField
‪readForeignField($uid, $conf)
Definition: RelationHandler.php:775
‪TYPO3\CMS\Core\Database\RelationHandler\$registerNonTableValues
‪bool $registerNonTableValues
Definition: RelationHandler.php:41
‪TYPO3\CMS\Core\Database\RelationHandler\getResolvedItemArray
‪getResolvedItemArray()
Definition: RelationHandler.php:1108
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Core\Database\RelationHandler\readMM
‪readMM($tableName, $uid, $mmOppositeTable)
Definition: RelationHandler.php:451
‪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:294
‪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:1462
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Database\RelationHandler\purgeItemArrayHandler
‪bool purgeItemArrayHandler($purgeCallback, int $workspaceId)
Definition: RelationHandler.php:1260
‪TYPO3\CMS\Core\Database\RelationHandler\getResolver
‪PlainDataResolver getResolver($tableName, array $ids, array $sortingStatement=null)
Definition: RelationHandler.php:1548
‪TYPO3\CMS\Core\Database\RelationHandler\setWorkspaceId
‪setWorkspaceId($workspaceId)
Definition: RelationHandler.php:174
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\DataHandling\ReferenceIndexUpdater
Definition: ReferenceIndexUpdater.php:35
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT_ARRAY
‪const PARAM_INT_ARRAY
Definition: Connection.php:72
‪TYPO3\CMS\Core\Database\RelationHandler\removeFromItemArray
‪removeFromItemArray($tableName, $id)
Definition: RelationHandler.php:1443
‪TYPO3\CMS\Core\Database\RelationHandler\processDeletePlaceholder
‪bool processDeletePlaceholder()
Definition: RelationHandler.php:1245
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Core\Database\Connection\update
‪int update(string $tableName, array $data, array $identifier=[], array $types=[])
Definition: Connection.php:270
‪TYPO3\CMS\Core\Database
Definition: Connection.php:18
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39