‪TYPO3CMS  11.5
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;
30 
37 {
43  protected ‪$fetchAllFields = true;
44 
50  public ‪$registerNonTableValues = false;
51 
58  public ‪$tableArray = [];
59 
66  public $itemArray = [];
67 
73  public $nonTableArray = [];
74 
78  public $additionalWhere = [];
79 
85  public $checkIfDeleted = true;
86 
92  protected $firstTable = '';
93 
100  protected $MM_is_foreign = false;
101 
107  protected $MM_isMultiTableRelationship = '';
108 
114  protected $currentTable;
115 
122  public $undeleteRecord;
123 
130  protected $MM_match_fields = [];
131 
137  protected $MM_hasUidField;
138 
144  protected $MM_insert_fields = [];
145 
151  protected $MM_table_where = '';
152 
158  protected $MM_oppositeUsage;
159 
166  protected $updateReferenceIndex = true;
167 
171  protected $referenceIndexUpdater;
172 
176  protected $useLiveParentIds = true;
177 
181  protected $useLiveReferenceIds = true;
182 
186  protected $workspaceId;
187 
191  protected $purged = false;
192 
198  public $results = [];
199 
205  protected function getWorkspaceId(): int
206  {
207  $backendUser = ‪$GLOBALS['BE_USER'] ?? null;
208  if (!isset($this->‪workspaceId)) {
209  $this->‪workspaceId = $backendUser instanceof ‪BackendUserAuthentication ? (int)($backendUser->workspace) : 0;
210  }
211  return $this->workspaceId;
212  }
213 
219  public function ‪setWorkspaceId($workspaceId): void
220  {
221  $this->‪workspaceId = (int)$workspaceId;
222  }
223 
230  public function ‪setReferenceIndexUpdater(ReferenceIndexUpdater $updater): void
231  {
232  $this->referenceIndexUpdater = $updater;
233  }
234 
240  public function ‪isPurged()
241  {
242  return $this->purged;
243  }
244 
255  public function ‪start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = [])
256  {
257  $conf = (array)$conf;
258  // SECTION: MM reverse relations
259  $this->MM_is_foreign = (bool)($conf['MM_opposite_field'] ?? false);
260  $this->MM_table_where = $conf['MM_table_where'] ?? null;
261  $this->MM_hasUidField = $conf['MM_hasUidField'] ?? null;
262  $this->MM_match_fields = (isset($conf['MM_match_fields']) && is_array($conf['MM_match_fields'])) ? $conf['MM_match_fields'] : [];
263  $this->MM_insert_fields = (isset($conf['MM_insert_fields']) && is_array($conf['MM_insert_fields'])) ? $conf['MM_insert_fields'] : $this->MM_match_fields;
264  $this->currentTable = $currentTable;
265  if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) {
266  $this->MM_oppositeUsage = $conf['MM_oppositeUsage'];
267  }
268  $mmOppositeTable = '';
269  if ($this->MM_is_foreign) {
270  $allowedTableList = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
271  // Normally, $conf['allowed'] can contain a list of tables,
272  // but as we are looking at a MM relation from the foreign side,
273  // it only makes sense to allow one table in $conf['allowed'].
274  [$mmOppositeTable] = ‪GeneralUtility::trimExplode(',', $allowedTableList);
275  // Only add the current table name if there is more than one allowed
276  // field. We must be sure this has been done at least once before accessing
277  // the "columns" part of TCA for a table.
278  $mmOppositeAllowed = (string)(‪$GLOBALS['TCA'][$mmOppositeTable]['columns'][$conf['MM_opposite_field'] ?? '']['config']['allowed'] ?? '');
279  if ($mmOppositeAllowed !== '') {
280  $mmOppositeAllowedTables = explode(',', $mmOppositeAllowed);
281  if ($mmOppositeAllowed === '*' || count($mmOppositeAllowedTables) > 1) {
282  $this->MM_isMultiTableRelationship = $mmOppositeAllowedTables[0];
283  }
284  }
285  }
286  // SECTION: normal MM relations
287  // If the table list is "*" then all tables are used in the list:
288  if (trim($tablelist) === '*') {
289  $tablelist = implode(',', array_keys(‪$GLOBALS['TCA']));
290  }
291  // The tables are traversed and internal arrays are initialized:
292  $tempTableArray = ‪GeneralUtility::trimExplode(',', $tablelist, true);
293  foreach ($tempTableArray as $val) {
294  $tName = trim($val);
295  $this->tableArray[$tName] = [];
296  $deleteField = ‪$GLOBALS['TCA'][$tName]['ctrl']['delete'] ?? false;
297  if ($this->checkIfDeleted && $deleteField) {
298  $fieldN = $tName . '.' . $deleteField;
299  if (!isset($this->additionalWhere[$tName])) {
300  $this->additionalWhere[$tName] = '';
301  }
302  $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
303  }
304  }
305  if (is_array($this->tableArray)) {
306  reset($this->tableArray);
307  } else {
308  // No tables
309  return;
310  }
311  // Set first and second tables:
312  // Is the first table
313  $this->firstTable = (string)key($this->tableArray);
314  next($this->tableArray);
315  // Now, populate the internal itemArray and tableArray arrays:
316  // If MM, then call this function to do that:
317  if ($MMtable) {
318  if ($MMuid) {
319  $this->‪readMM($MMtable, $MMuid, $mmOppositeTable);
321  } else {
322  // Revert to readList() for new records in order to load possible default values from $itemlist
323  $this->‪readList($itemlist, $conf);
324  $this->‪purgeItemArray();
325  }
326  } elseif ($MMuid && ($conf['foreign_field'] ?? false)) {
327  // If not MM but foreign_field, the read the records by the foreign_field
328  $this->‪readForeignField($MMuid, $conf);
329  } else {
330  // If not MM, then explode the itemlist by "," and traverse the list:
331  $this->‪readList($itemlist, $conf);
332  // Do automatic default_sortby, if any
333  if (isset($conf['foreign_default_sortby']) && $conf['foreign_default_sortby']) {
334  $this->‪sortList($conf['foreign_default_sortby']);
335  }
336  }
337  }
338 
344  public function ‪setFetchAllFields($allFields)
345  {
346  $this->fetchAllFields = (bool)$allFields;
347  }
348 
355  public function ‪setUpdateReferenceIndex($updateReferenceIndex)
356  {
357  trigger_error(
358  'Calling RelationHandler->setUpdateReferenceIndex() is deprecated. Use setReferenceIndexUpdater() instead.',
359  E_USER_DEPRECATED
360  );
361  $this->updateReferenceIndex = (bool)$updateReferenceIndex;
362  }
363 
367  public function ‪setUseLiveParentIds($useLiveParentIds)
368  {
369  $this->useLiveParentIds = (bool)$useLiveParentIds;
370  }
371 
375  public function ‪setUseLiveReferenceIds($useLiveReferenceIds)
376  {
377  $this->useLiveReferenceIds = (bool)$useLiveReferenceIds;
378  }
379 
386  protected function ‪readList($itemlist, array $configuration)
387  {
388  if (trim((string)$itemlist) !== '') {
389  // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
390  // if there were spaces in the list... I suppose this is better overall...
391  $tempItemArray = ‪GeneralUtility::trimExplode(',', $itemlist);
392  // If the second table is set and the ID number is less than zero (later)
393  // then the record is regarded to come from the second table...
394  $secondTable = (string)(key($this->tableArray) ?? '');
395  foreach ($tempItemArray as $key => $val) {
396  // Will be set to "true" if the entry was a real table/id
397  $isSet = false;
398  // Extract table name and id. This is in the formula [tablename]_[id]
399  // where table name MIGHT contain "_", hence the reversion of the string!
400  $val = strrev($val);
401  $parts = explode('_', $val, 2);
402  $theID = strrev($parts[0]);
403  // Check that the id IS an integer:
405  // Get the table name: If a part of the exploded string, use that.
406  // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
407  $theTable = trim($parts[1] ?? '')
408  ? strrev(trim($parts[1] ?? ''))
409  : ($secondTable && $theID < 0 ? $secondTable : $this->firstTable);
410  // If the ID is not blank and the table name is among the names in the inputted tableList
411  if ((string)$theID != '' && $theID && $theTable && isset($this->tableArray[$theTable])) {
412  // Get ID as the right value:
413  $theID = $secondTable ? abs((int)$theID) : (int)$theID;
414  // Register ID/table name in internal arrays:
415  $this->itemArray[$key]['id'] = $theID;
416  $this->itemArray[$key]['table'] = $theTable;
417  $this->tableArray[$theTable][] = $theID;
418  // Set update-flag
419  $isSet = true;
420  }
421  }
422  // If it turns out that the value from the list was NOT a valid reference to a table-record,
423  // then we might still set it as a NO_TABLE value:
424  if (!$isSet && $this->registerNonTableValues) {
425  $this->itemArray[$key]['id'] = $tempItemArray[$key];
426  $this->itemArray[$key]['table'] = '_NO_TABLE';
427  $this->nonTableArray[] = $tempItemArray[$key];
428  }
429  }
430 
431  // Skip if not dealing with IRRE in a CSV list on a workspace
432  if (!isset($configuration['type']) || $configuration['type'] !== 'inline'
433  || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
434  || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
435  || $this->getWorkspaceId() === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
436  ) {
437  return;
438  }
439 
440  // Fetch live record data
441  if ($this->useLiveReferenceIds) {
442  foreach ($this->itemArray as &$item) {
443  $item['id'] = $this->‪getLiveDefaultId($item['table'], $item['id']);
444  }
445  } else {
446  // Directly overlay workspace data
447  $this->itemArray = [];
448  $foreignTable = $configuration['foreign_table'];
449  $ids = $this->‪getResolver($foreignTable, $this->tableArray[$foreignTable])->‪get();
450  foreach ($ids as $id) {
451  $this->itemArray[] = [
452  'id' => $id,
453  'table' => $foreignTable,
454  ];
455  }
456  }
457  }
458  }
459 
467  protected function ‪sortList($sortby)
468  {
469  // Sort directly without fetching additional data
470  if ($sortby === 'uid') {
471  usort(
472  $this->itemArray,
473  static function ($a, $b) {
474  return $a['id'] < $b['id'] ? -1 : 1;
475  }
476  );
477  } elseif (count($this->tableArray) === 1) {
478  reset($this->tableArray);
479  $table = (string)key($this->tableArray);
480  $connection = $this->‪getConnectionForTableName($table);
481  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
482 
483  foreach (array_chunk(current($this->tableArray), $maxBindParameters - 10, true) as $chunk) {
484  if (empty($chunk)) {
485  continue;
486  }
487  $this->itemArray = [];
488  $this->tableArray = [];
489  $queryBuilder = $connection->createQueryBuilder();
490  $queryBuilder->getRestrictions()->removeAll();
491  $queryBuilder->select('uid')
492  ->from($table)
493  ->where(
494  $queryBuilder->expr()->in(
495  'uid',
496  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
497  )
498  );
499  foreach (‪QueryHelper::parseOrderBy((string)$sortby) as $orderPair) {
500  [$fieldName, $order] = $orderPair;
501  $queryBuilder->addOrderBy($fieldName, $order);
502  }
503  $statement = $queryBuilder->executeQuery();
504  while ($row = $statement->fetchAssociative()) {
505  $this->itemArray[] = ['id' => $row['uid'], 'table' => $table];
506  $this->tableArray[$table][] = $row['uid'];
507  }
508  }
509  }
510  }
511 
524  protected function ‪readMM($tableName, $uid, $mmOppositeTable)
525  {
526  $key = 0;
527  $theTable = null;
528  $queryBuilder = $this->‪getConnectionForTableName($tableName)
530  $queryBuilder->getRestrictions()->removeAll();
531  $queryBuilder->select('*')->from($tableName);
532  // In case of a reverse relation
533  if ($this->MM_is_foreign) {
534  $uidLocal_field = 'uid_foreign';
535  $uidForeign_field = 'uid_local';
536  $sorting_field = 'sorting_foreign';
537  if ($this->MM_isMultiTableRelationship) {
538  // Be backwards compatible! When allowing more than one table after
539  // having previously allowed only one table, this case applies.
540  if ($this->currentTable == $this->MM_isMultiTableRelationship) {
541  $expression = $queryBuilder->expr()->orX(
542  $queryBuilder->expr()->eq(
543  'tablenames',
544  $queryBuilder->createNamedParameter($this->currentTable, ‪Connection::PARAM_STR)
545  ),
546  $queryBuilder->expr()->eq(
547  'tablenames',
548  $queryBuilder->createNamedParameter('', ‪Connection::PARAM_STR)
549  )
550  );
551  } else {
552  $expression = $queryBuilder->expr()->eq(
553  'tablenames',
554  $queryBuilder->createNamedParameter($this->currentTable, ‪Connection::PARAM_STR)
555  );
556  }
557  $queryBuilder->andWhere($expression);
558  }
559  $theTable = $mmOppositeTable;
560  } else {
561  // Default
562  $uidLocal_field = 'uid_local';
563  $uidForeign_field = 'uid_foreign';
564  $sorting_field = 'sorting';
565  }
566  if ($this->MM_table_where) {
567  if (GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('runtimeDbQuotingOfTcaConfiguration')) {
568  $queryBuilder->andWhere(
569  ‪QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, ‪QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), $this->MM_table_where)))
570  );
571  } else {
572  $queryBuilder->andWhere(
573  ‪QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where))
574  );
575  }
576  }
577  foreach ($this->MM_match_fields as $field => $value) {
578  $queryBuilder->andWhere(
579  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, ‪Connection::PARAM_STR))
580  );
581  }
582  $queryBuilder->andWhere(
583  $queryBuilder->expr()->eq(
584  $uidLocal_field,
585  $queryBuilder->createNamedParameter((int)$uid, ‪Connection::PARAM_INT)
586  )
587  );
588  $queryBuilder->orderBy($sorting_field);
589  $queryBuilder->addOrderBy($uidForeign_field);
590  $statement = $queryBuilder->executeQuery();
591  while ($row = $statement->fetchAssociative()) {
592  // Default
593  if (!$this->MM_is_foreign) {
594  // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
595  $theTable = !empty($row['tablenames']) ? $row['tablenames'] : $this->firstTable;
596  }
597  if (($row[$uidForeign_field] || $theTable === 'pages') && $theTable && isset($this->tableArray[$theTable])) {
598  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
599  $this->itemArray[$key]['table'] = $theTable;
600  $this->tableArray[$theTable][] = $row[$uidForeign_field];
601  } elseif ($this->registerNonTableValues) {
602  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
603  $this->itemArray[$key]['table'] = '_NO_TABLE';
604  $this->nonTableArray[] = $row[$uidForeign_field];
605  }
606  $key++;
607  }
608  }
609 
617  public function ‪writeMM($MM_tableName, $uid, $prependTableName = false)
618  {
619  $connection = $this->‪getConnectionForTableName($MM_tableName);
620  $expressionBuilder = $connection->createQueryBuilder()->expr();
621 
622  // In case of a reverse relation
623  if ($this->MM_is_foreign) {
624  $uidLocal_field = 'uid_foreign';
625  $uidForeign_field = 'uid_local';
626  $sorting_field = 'sorting_foreign';
627  } else {
628  // default
629  $uidLocal_field = 'uid_local';
630  $uidForeign_field = 'uid_foreign';
631  $sorting_field = 'sorting';
632  }
633  // If there are tables...
634  $tableC = count($this->tableArray);
635  if ($tableC) {
636  // Boolean: does the field "tablename" need to be filled?
637  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
638  $c = 0;
639  $additionalWhere_tablenames = '';
640  if ($this->MM_is_foreign && $prep) {
641  $additionalWhere_tablenames = $expressionBuilder->eq(
642  'tablenames',
643  $expressionBuilder->literal($this->currentTable)
644  );
645  }
646  $additionalWhere = $expressionBuilder->andX();
647  // Add WHERE clause if configured
648  if ($this->MM_table_where) {
649  $additionalWhere->add(
651  str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where)
652  )
653  );
654  }
655  // Select, update or delete only those relations that match the configured fields
656  foreach ($this->MM_match_fields as $field => $value) {
657  $additionalWhere->add($expressionBuilder->eq($field, $expressionBuilder->literal($value)));
658  }
659 
660  $queryBuilder = $connection->createQueryBuilder();
661  $queryBuilder->getRestrictions()->removeAll();
662  $queryBuilder->select($uidForeign_field)
663  ->from($MM_tableName)
664  ->where($queryBuilder->expr()->eq(
665  $uidLocal_field,
666  $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)
667  ))
668  ->orderBy($sorting_field);
669 
670  if ($prep) {
671  $queryBuilder->addSelect('tablenames');
672  }
673  if ($this->MM_hasUidField) {
674  $queryBuilder->addSelect('uid');
675  }
676  if ($additionalWhere_tablenames) {
677  $queryBuilder->andWhere($additionalWhere_tablenames);
678  }
679  if ($additionalWhere->count()) {
680  $queryBuilder->andWhere($additionalWhere);
681  }
682 
683  $result = $queryBuilder->executeQuery();
684  $oldMMs = [];
685  // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
686  // If the UID is present it will be used to update sorting and delete MM-records.
687  // This is necessary if the "multiple" feature is used for the MM relations.
688  // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
689  $oldMMs_inclUid = [];
690  while ($row = $result->fetchAssociative()) {
691  if (!$this->MM_is_foreign && $prep) {
692  $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]];
693  } else {
694  $oldMMs[] = $row[$uidForeign_field];
695  }
696  $oldMMs_inclUid[] = (int)($row['uid'] ?? 0);
697  }
698  // For each item, insert it:
699  foreach ($this->itemArray as $val) {
700  $c++;
701  if ($prep || $val['table'] === '_NO_TABLE') {
702  // Insert current table if needed
703  if ($this->MM_is_foreign) {
704  $tablename = $this->currentTable;
705  } else {
706  $tablename = $val['table'];
707  }
708  } else {
709  $tablename = '';
710  }
711  if (!$this->MM_is_foreign && $prep) {
712  $item = [$val['table'], $val['id']];
713  } else {
714  $item = $val['id'];
715  }
716  if (in_array($item, $oldMMs)) {
717  $oldMMs_index = array_search($item, $oldMMs);
718  // In principle, selecting on the UID is all we need to do
719  // if a uid field is available since that is unique!
720  // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
721  $queryBuilder = $connection->createQueryBuilder();
722  $queryBuilder->update($MM_tableName)
723  ->set($sorting_field, $c)
724  ->where(
725  $expressionBuilder->eq(
726  $uidLocal_field,
727  $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)
728  ),
729  $expressionBuilder->eq(
730  $uidForeign_field,
731  $queryBuilder->createNamedParameter($val['id'], ‪Connection::PARAM_INT)
732  )
733  );
734 
735  if ($additionalWhere->count()) {
736  $queryBuilder->andWhere($additionalWhere);
737  }
738  if ($this->MM_hasUidField) {
739  $queryBuilder->andWhere(
740  $expressionBuilder->eq(
741  'uid',
742  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMMs_index], ‪Connection::PARAM_INT)
743  )
744  );
745  }
746  if ($tablename) {
747  $queryBuilder->andWhere(
748  $expressionBuilder->eq(
749  'tablenames',
750  $queryBuilder->createNamedParameter($tablename, ‪Connection::PARAM_STR)
751  )
752  );
753  }
754 
755  $queryBuilder->executeStatement();
756  // Remove the item from the $oldMMs array so after this
757  // foreach loop only the ones that need to be deleted are in there.
758  unset($oldMMs[$oldMMs_index]);
759  // Remove the item from the $oldMMs_inclUid array so after this
760  // foreach loop only the ones that need to be deleted are in there.
761  unset($oldMMs_inclUid[$oldMMs_index]);
762  } else {
763  $insertFields = $this->MM_insert_fields;
764  $insertFields[$uidLocal_field] = $uid;
765  $insertFields[$uidForeign_field] = $val['id'];
766  $insertFields[$sorting_field] = $c;
767  if ($tablename) {
768  $insertFields['tablenames'] = $tablename;
769  $insertFields = $this->‪completeOppositeUsageValues($tablename, $insertFields);
770  }
771  $connection->insert($MM_tableName, $insertFields);
772  if ($this->MM_is_foreign) {
773  $this->‪updateRefIndex($val['table'], $val['id']);
774  }
775  }
776  }
777  // Delete all not-used relations:
778  if (is_array($oldMMs) && !empty($oldMMs)) {
779  $queryBuilder = $connection->createQueryBuilder();
780  $removeClauses = $queryBuilder->expr()->orX();
781  $updateRefIndex_records = [];
782  foreach ($oldMMs as $oldMM_key => $mmItem) {
783  // If UID field is present, of course we need only use that for deleting.
784  if ($this->MM_hasUidField) {
785  $removeClauses->add($queryBuilder->expr()->eq(
786  'uid',
787  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMM_key], ‪Connection::PARAM_INT)
788  ));
789  } else {
790  if (is_array($mmItem)) {
791  $removeClauses->add(
792  $queryBuilder->expr()->andX(
793  $queryBuilder->expr()->eq(
794  'tablenames',
795  $queryBuilder->createNamedParameter($mmItem[0], ‪Connection::PARAM_STR)
796  ),
797  $queryBuilder->expr()->eq(
798  $uidForeign_field,
799  $queryBuilder->createNamedParameter($mmItem[1], ‪Connection::PARAM_INT)
800  )
801  )
802  );
803  } else {
804  $removeClauses->add(
805  $queryBuilder->expr()->eq(
806  $uidForeign_field,
807  $queryBuilder->createNamedParameter($mmItem, ‪Connection::PARAM_INT)
808  )
809  );
810  }
811  }
812  if ($this->MM_is_foreign) {
813  if (is_array($mmItem)) {
814  $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]];
815  } else {
816  $updateRefIndex_records[] = [$this->firstTable, $mmItem];
817  }
818  }
819  }
820 
821  $queryBuilder->delete($MM_tableName)
822  ->where(
823  $queryBuilder->expr()->eq(
824  $uidLocal_field,
825  $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)
826  ),
827  $removeClauses
828  );
829 
830  if ($additionalWhere_tablenames) {
831  $queryBuilder->andWhere($additionalWhere_tablenames);
832  }
833  if ($additionalWhere->count()) {
834  $queryBuilder->andWhere($additionalWhere);
835  }
836 
837  $queryBuilder->executeStatement();
838 
839  // Update ref index:
840  foreach ($updateRefIndex_records as $pair) {
841  $this->‪updateRefIndex($pair[0], $pair[1]);
842  }
843  }
844  // Update ref index; In DataHandler it is not certain that this will happen because
845  // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
846  // This could also have been fixed in updateDB in DataHandler, however I decided to do it here ...
847  $this->‪updateRefIndex($this->currentTable, $uid);
848  }
849  }
850 
861  public function ‪remapMM($MM_tableName, $uid, $newUid, $prependTableName = false)
862  {
863  trigger_error(
864  'Method ' . __METHOD__ . ' of class ' . __CLASS__ . ' is deprecated since v11 and will be removed in v12.',
865  E_USER_DEPRECATED
866  );
867 
868  // In case of a reverse relation
869  if ($this->MM_is_foreign) {
870  $uidLocal_field = 'uid_foreign';
871  } else {
872  // default
873  $uidLocal_field = 'uid_local';
874  }
875  // If there are tables...
876  $tableC = count($this->tableArray);
877  if ($tableC) {
878  $queryBuilder = $this->‪getConnectionForTableName($MM_tableName)
880  $queryBuilder->update($MM_tableName)
881  ->set($uidLocal_field, (int)$newUid)
882  ->where($queryBuilder->expr()->eq(
883  $uidLocal_field,
884  $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)
885  ));
886  // Boolean: does the field "tablename" need to be filled?
887  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
888  if ($this->MM_is_foreign && $prep) {
889  $queryBuilder->andWhere(
890  $queryBuilder->expr()->eq(
891  'tablenames',
892  $queryBuilder->createNamedParameter($this->currentTable, ‪Connection::PARAM_STR)
893  )
894  );
895  }
896  // Add WHERE clause if configured
897  if ($this->MM_table_where) {
898  $queryBuilder->andWhere(
899  ‪QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where))
900  );
901  }
902  // Select, update or delete only those relations that match the configured fields
903  foreach ($this->MM_match_fields as $field => $value) {
904  $queryBuilder->andWhere(
905  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, ‪Connection::PARAM_STR))
906  );
907  }
908  $queryBuilder->execute();
909  }
910  }
911 
919  protected function ‪readForeignField($uid, $conf)
920  {
921  if ($this->useLiveParentIds) {
922  $uid = $this->‪getLiveDefaultId($this->currentTable, $uid);
923  }
924 
925  $key = 0;
926  $uid = (int)$uid;
927  // skip further processing if $uid does not
928  // point to a valid parent record
929  if ($uid === 0) {
930  return;
931  }
932 
933  $foreign_table = $conf['foreign_table'];
934  $foreign_table_field = $conf['foreign_table_field'] ?? '';
935  $useDeleteClause = !$this->undeleteRecord;
936  $foreign_match_fields = is_array($conf['foreign_match_fields'] ?? false) ? $conf['foreign_match_fields'] : [];
937  $queryBuilder = $this->‪getConnectionForTableName($foreign_table)
939  $queryBuilder->getRestrictions()
940  ->removeAll();
941  // Use the deleteClause (e.g. "deleted=0") on this table
942  if ($useDeleteClause) {
943  $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
944  }
945 
946  $queryBuilder->select('uid')
947  ->from($foreign_table);
948 
949  // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
950  if (!empty($conf['symmetric_field'])) {
951  $queryBuilder->where(
952  $queryBuilder->expr()->orX(
953  $queryBuilder->expr()->eq(
954  $conf['foreign_field'],
955  $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)
956  ),
957  $queryBuilder->expr()->eq(
958  $conf['symmetric_field'],
959  $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)
960  )
961  )
962  );
963  } else {
964  $queryBuilder->where($queryBuilder->expr()->eq(
965  $conf['foreign_field'],
966  $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)
967  ));
968  }
969  // If it's requested to look for the parent uid AND the parent table,
970  // add an additional SQL-WHERE clause
971  if ($foreign_table_field && $this->currentTable) {
972  $queryBuilder->andWhere(
973  $queryBuilder->expr()->eq(
974  $foreign_table_field,
975  $queryBuilder->createNamedParameter($this->currentTable, ‪Connection::PARAM_STR)
976  )
977  );
978  }
979  // Add additional where clause if foreign_match_fields are defined
980  foreach ($foreign_match_fields as $field => $value) {
981  $queryBuilder->andWhere(
982  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, ‪Connection::PARAM_STR))
983  );
984  }
985  // Select children from the live(!) workspace only
986  if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
987  $queryBuilder->getRestrictions()->add(
988  GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getWorkspaceId())
989  );
990  }
991  // Get the correct sorting field
992  // Specific manual sortby for data handled by this field
993  $sortby = '';
994  if (!empty($conf['foreign_sortby'])) {
995  if (!empty($conf['symmetric_sortby']) && !empty($conf['symmetric_field'])) {
996  // Sorting depends on, from which side of the relation we're looking at it
997  // This requires bypassing automatic quoting and setting of the default sort direction
998  // @TODO: Doctrine: generalize to standard SQL to guarantee database independency
999  $queryBuilder->add(
1000  'orderBy',
1001  'CASE
1002  WHEN ' . $queryBuilder->expr()->eq($conf['foreign_field'], $uid) . '
1003  THEN ' . $queryBuilder->quoteIdentifier($conf['foreign_sortby']) . '
1004  ELSE ' . $queryBuilder->quoteIdentifier($conf['symmetric_sortby']) . '
1005  END'
1006  );
1007  } else {
1008  // Regular single-side behaviour
1009  $sortby = $conf['foreign_sortby'];
1010  }
1011  } elseif (!empty($conf['foreign_default_sortby'])) {
1012  // Specific default sortby for data handled by this field
1013  $sortby = $conf['foreign_default_sortby'];
1014  } elseif (!empty(‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'])) {
1015  // Manual sortby for all table records
1016  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1017  } elseif (!empty(‪$GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'])) {
1018  // Default sortby for all table records
1019  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
1020  }
1021 
1022  if (!empty($sortby)) {
1023  foreach (‪QueryHelper::parseOrderBy($sortby) as $orderPair) {
1024  [$fieldName, $sorting] = $orderPair;
1025  $queryBuilder->addOrderBy($fieldName, $sorting);
1026  }
1027  }
1028 
1029  // Get the rows from storage
1030  $rows = [];
1031  $result = $queryBuilder->executeQuery();
1032  while ($row = $result->fetchAssociative()) {
1033  $rows[(int)$row['uid']] = $row;
1034  }
1035  if (!empty($rows)) {
1036  // Retrieve the parsed and prepared ORDER BY configuration for the resolver
1037  $sortby = $queryBuilder->getQueryPart('orderBy');
1038  $ids = $this->‪getResolver($foreign_table, array_keys($rows), $sortby)->‪get();
1039  foreach ($ids as $id) {
1040  $this->itemArray[$key]['id'] = $id;
1041  $this->itemArray[$key]['table'] = $foreign_table;
1042  $this->tableArray[$foreign_table][] = $id;
1043  $key++;
1044  }
1045  }
1046  }
1047 
1056  public function ‪writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = null)
1057  {
1058  // @deprecated since v11, will be removed with v12.
1059  if ($skipSorting !== null) {
1060  trigger_error(
1061  'Calling ' . __METHOD__ . ' with 4th argument $skipSorting is deprecated and will be removed in v12.',
1062  E_USER_DEPRECATED
1063  );
1064  }
1065  $skipSorting = (bool)$skipSorting;
1066 
1067  if ($this->useLiveParentIds) {
1068  $parentUid = $this->‪getLiveDefaultId($this->currentTable, $parentUid);
1069  if (!empty($updateToUid)) {
1070  $updateToUid = $this->‪getLiveDefaultId($this->currentTable, $updateToUid);
1071  }
1072  }
1073 
1074  // Ensure all values are set.
1075  $conf += [
1076  'foreign_table' => '',
1077  'foreign_field' => '',
1078  'symmetric_field' => '',
1079  'foreign_table_field' => '',
1080  'foreign_match_fields' => [],
1081  ];
1082 
1083  $c = 0;
1084  $foreign_table = $conf['foreign_table'];
1085  $foreign_field = $conf['foreign_field'];
1086  $symmetric_field = $conf['symmetric_field'] ?? '';
1087  $foreign_table_field = $conf['foreign_table_field'];
1088  $foreign_match_fields = $conf['foreign_match_fields'];
1089  // If there are table items and we have a proper $parentUid
1090  if (‪MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
1091  // If updateToUid is not a positive integer, set it to '0', so it will be ignored
1092  if (!(‪MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
1093  $updateToUid = 0;
1094  }
1095  $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($foreign_table);
1096  ‪$fields = 'uid,pid,' . $foreign_field;
1097  // Consider the symmetric field if defined:
1098  if ($symmetric_field) {
1099  ‪$fields .= ',' . $symmetric_field;
1100  }
1101  // Consider workspaces if defined and currently used:
1102  if ($considerWorkspaces) {
1103  ‪$fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
1104  }
1105  // Update all items
1106  foreach ($this->itemArray as $val) {
1107  $uid = $val['id'];
1108  $table = $val['table'];
1109  $row = [];
1110  // Fetch the current (not overwritten) relation record if we should handle symmetric relations
1111  if ($symmetric_field || $considerWorkspaces) {
1112  $row = BackendUtility::getRecord($table, $uid, ‪$fields, '', true);
1113  if (empty($row)) {
1114  continue;
1115  }
1116  }
1117  $isOnSymmetricSide = false;
1118  if ($symmetric_field) {
1119  $isOnSymmetricSide = ‪self::isOnSymmetricSide((string)$parentUid, $conf, $row);
1120  }
1121  $updateValues = $foreign_match_fields;
1122  // No update to the uid is requested, so this is the normal behaviour
1123  // just update the fields and care about sorting
1124  if (!$updateToUid) {
1125  // Always add the pointer to the parent uid
1126  if ($isOnSymmetricSide) {
1127  $updateValues[$symmetric_field] = $parentUid;
1128  } else {
1129  $updateValues[$foreign_field] = $parentUid;
1130  }
1131  // If it is configured in TCA also to store the parent table in the child record, just do it
1132  if ($foreign_table_field && $this->currentTable) {
1133  $updateValues[$foreign_table_field] = $this->currentTable;
1134  }
1135  // Update sorting columns if not to be skipped.
1136  // @deprecated since v11, will be removed with v12. Drop if() below, assume $skipSorting false, keep body.
1137  if (!$skipSorting) {
1138  // Get the correct sorting field
1139  // Specific manual sortby for data handled by this field
1140  $sortby = '';
1141  if ($conf['foreign_sortby'] ?? false) {
1142  $sortby = $conf['foreign_sortby'];
1143  } elseif (‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'] ?? false) {
1144  // manual sortby for all table records
1145  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1146  }
1147  // Apply sorting on the symmetric side
1148  // (it depends on who created the relation, so what uid is in the symmetric_field):
1149  if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
1150  $sortby = $conf['symmetric_sortby'];
1151  } else {
1152  $tempSortBy = [];
1153  foreach (‪QueryHelper::parseOrderBy($sortby) as $orderPair) {
1154  [$fieldName, $order] = $orderPair;
1155  if ($order !== null) {
1156  $tempSortBy[] = implode(' ', $orderPair);
1157  } else {
1158  $tempSortBy[] = $fieldName;
1159  }
1160  }
1161  $sortby = implode(',', $tempSortBy);
1162  }
1163  if ($sortby) {
1164  $updateValues[$sortby] = ++$c;
1165  }
1166  }
1167  } else {
1168  if ($isOnSymmetricSide) {
1169  $updateValues[$symmetric_field] = $updateToUid;
1170  } else {
1171  $updateValues[$foreign_field] = $updateToUid;
1172  }
1173  }
1174  // Update accordant fields in the database:
1175  if (!empty($updateValues)) {
1176  // Update tstamp if any foreign field value has changed
1177  if (!empty(‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'])) {
1178  $updateValues[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = ‪$GLOBALS['EXEC_TIME'];
1179  }
1180  $this->‪getConnectionForTableName($table)
1181  ->‪update(
1182  $table,
1183  $updateValues,
1184  ['uid' => (int)$uid]
1185  );
1186  $this->‪updateRefIndex($table, $uid);
1187  }
1188  }
1189  }
1190  }
1191 
1198  public function ‪getValueArray($prependTableName = false)
1199  {
1200  // INIT:
1201  $valueArray = [];
1202  $tableC = count($this->tableArray);
1203  // If there are tables in the table array:
1204  if ($tableC) {
1205  // If there are more than ONE table in the table array, then always prepend table names:
1206  $prep = $tableC > 1 || $prependTableName;
1207  // Traverse the array of items:
1208  foreach ($this->itemArray as $val) {
1209  $valueArray[] = ($prep && $val['table'] !== '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
1210  }
1211  }
1212  // Return the array
1213  return $valueArray;
1214  }
1215 
1224  public function ‪getFromDB()
1225  {
1226  // Traverses the tables listed:
1227  foreach ($this->tableArray as $table => $ids) {
1228  if (is_array($ids) && !empty($ids)) {
1229  $connection = $this->‪getConnectionForTableName($table);
1230  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1231 
1232  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1233  $queryBuilder = $connection->createQueryBuilder();
1234  $queryBuilder->getRestrictions()->removeAll();
1235  $queryBuilder->select('*')
1236  ->from($table)
1237  ->where($queryBuilder->expr()->in(
1238  'uid',
1239  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1240  ));
1241  if ($this->additionalWhere[$table] ?? false) {
1242  $queryBuilder->andWhere(
1243  ‪QueryHelper::stripLogicalOperatorPrefix($this->additionalWhere[$table])
1244  );
1245  }
1246  $statement = $queryBuilder->executeQuery();
1247  while ($row = $statement->fetchAssociative()) {
1248  $this->results[$table][$row['uid']] = $row;
1249  }
1250  }
1251  }
1252  }
1253  return $this->results;
1254  }
1255 
1272  public function ‪getResolvedItemArray(): array
1273  {
1274  $itemArray = [];
1275  foreach ($this->itemArray as $item) {
1276  if (isset($this->results[$item['table']][$item['id']])) {
1277  $itemArray[] = [
1278  'table' => $item['table'],
1279  'uid' => $item['id'],
1280  'record' => $this->results[$item['table']][$item['id']],
1281  ];
1282  }
1283  }
1284  return $itemArray;
1285  }
1286 
1293  public function ‪countItems($returnAsArray = true)
1294  {
1295  $count = count($this->itemArray);
1296  if ($returnAsArray) {
1297  $count = [$count];
1298  }
1299  return $count;
1300  }
1301 
1311  protected function ‪updateRefIndex($table, $uid): array
1312  {
1313  if (!$this->updateReferenceIndex) {
1314  return [];
1315  }
1316  if ($this->referenceIndexUpdater) {
1317  // Add to update registry if given
1318  $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)$uid, $this->getWorkspaceId());
1319  $statisticsArray = [];
1320  } else {
1321  // @deprecated else branch can be dropped when setUpdateReferenceIndex() is dropped.
1322  // Update reference index directly if enabled
1323  $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1324  if (BackendUtility::isTableWorkspaceEnabled($table)) {
1325  $referenceIndex->setWorkspaceId($this->getWorkspaceId());
1326  }
1327  $statisticsArray = $referenceIndex->updateRefIndexTable($table, $uid);
1328  }
1329  return $statisticsArray;
1330  }
1331 
1341  public function ‪convertItemArray()
1342  {
1343  // conversion is only required in a workspace context
1344  // (the case that version ids are submitted in a live context are rare)
1345  if ($this->getWorkspaceId() === 0) {
1346  return false;
1347  }
1348 
1349  $hasBeenConverted = false;
1350  foreach ($this->tableArray as $tableName => $ids) {
1351  if (empty($ids) || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
1352  continue;
1353  }
1354 
1355  // convert live ids to version ids if available
1356  $convertedIds = $this->‪getResolver($tableName, $ids)
1358  ->‪setKeepMovePlaceholder(false)
1359  ->‪processVersionOverlays($ids);
1360  foreach ($this->itemArray as $index => $item) {
1361  if ($item['table'] !== $tableName) {
1362  continue;
1363  }
1364  $currentItemId = $item['id'];
1365  if (
1366  !isset($convertedIds[$currentItemId])
1367  || $currentItemId === $convertedIds[$currentItemId]
1368  ) {
1369  continue;
1370  }
1371  // adjust local item to use resolved version id
1372  $this->itemArray[$index]['id'] = $convertedIds[$currentItemId];
1373  $hasBeenConverted = true;
1374  }
1375  // update per-table reference for ids
1376  if ($hasBeenConverted) {
1377  $this->tableArray[$tableName] = array_values($convertedIds);
1378  }
1379  }
1380 
1381  return $hasBeenConverted;
1382  }
1383 
1395  public function ‪purgeItemArray($workspaceId = null)
1396  {
1397  if ($workspaceId === null) {
1398  $workspaceId = $this->getWorkspaceId();
1399  } else {
1400  $workspaceId = (int)$workspaceId;
1401  }
1402 
1403  // Ensure, only live relations are in the items Array
1404  if ($workspaceId === 0) {
1405  $purgeCallback = 'purgeVersionedIds';
1406  } else {
1407  // Otherwise, ensure that live relations are purged if version exists
1408  $purgeCallback = 'purgeLiveVersionedIds';
1409  }
1410 
1411  $itemArrayHasBeenPurged = $this->‪purgeItemArrayHandler($purgeCallback);
1412  $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1413  return $itemArrayHasBeenPurged;
1414  }
1415 
1421  public function ‪processDeletePlaceholder()
1422  {
1423  if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1424  return false;
1425  }
1426 
1427  return $this->‪purgeItemArrayHandler('purgeDeletePlaceholder');
1428  }
1429 
1436  protected function ‪purgeItemArrayHandler($purgeCallback)
1437  {
1438  $itemArrayHasBeenPurged = false;
1439 
1440  foreach ($this->tableArray as $itemTableName => $itemIds) {
1441  if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1442  continue;
1443  }
1444 
1445  $purgedItemIds = [];
1446  $callable = [$this, $purgeCallback];
1447  if (is_callable($callable)) {
1448  $purgedItemIds = $callable($itemTableName, $itemIds);
1449  }
1450 
1451  $removedItemIds = array_diff($itemIds, $purgedItemIds);
1452  foreach ($removedItemIds as $removedItemId) {
1453  $this->‪removeFromItemArray($itemTableName, $removedItemId);
1454  }
1455  $this->tableArray[$itemTableName] = $purgedItemIds;
1456  if (!empty($removedItemIds)) {
1457  $itemArrayHasBeenPurged = true;
1458  }
1459  }
1460 
1461  return $itemArrayHasBeenPurged;
1462  }
1463 
1471  protected function ‪purgeVersionedIds($tableName, array $ids)
1472  {
1473  $ids = $this->‪sanitizeIds($ids);
1474  $ids = (array)array_combine($ids, $ids);
1475  $connection = $this->‪getConnectionForTableName($tableName);
1476  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1477 
1478  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1479  $queryBuilder = $connection->createQueryBuilder();
1480  $queryBuilder->getRestrictions()->removeAll();
1481  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1482  ->from($tableName)
1483  ->where(
1484  $queryBuilder->expr()->in(
1485  'uid',
1486  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1487  ),
1488  $queryBuilder->expr()->neq(
1489  't3ver_wsid',
1490  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
1491  )
1492  )
1493  ->orderBy('t3ver_state', 'DESC')
1494  ->executeQuery();
1495 
1496  while ($version = $result->fetchAssociative()) {
1497  $versionId = $version['uid'];
1498  if (isset($ids[$versionId])) {
1499  unset($ids[$versionId]);
1500  }
1501  }
1502  }
1503 
1504  return array_values($ids);
1505  }
1506 
1514  protected function ‪purgeLiveVersionedIds($tableName, array $ids)
1515  {
1516  $ids = $this->‪sanitizeIds($ids);
1517  $ids = (array)array_combine($ids, $ids);
1518  $connection = $this->‪getConnectionForTableName($tableName);
1519  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1520 
1521  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1522  $queryBuilder = $connection->createQueryBuilder();
1523  $queryBuilder->getRestrictions()->removeAll();
1524  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1525  ->from($tableName)
1526  ->where(
1527  $queryBuilder->expr()->in(
1528  't3ver_oid',
1529  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1530  ),
1531  $queryBuilder->expr()->neq(
1532  't3ver_wsid',
1533  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
1534  )
1535  )
1536  ->orderBy('t3ver_state', 'DESC')
1537  ->executeQuery();
1538 
1539  while ($version = $result->fetchAssociative()) {
1540  $versionId = $version['uid'];
1541  $liveId = $version['t3ver_oid'];
1542  if (isset($ids[$liveId]) && isset($ids[$versionId])) {
1543  unset($ids[$liveId]);
1544  }
1545  }
1546  }
1547 
1548  return array_values($ids);
1549  }
1550 
1558  protected function ‪purgeDeletePlaceholder($tableName, array $ids)
1559  {
1560  $ids = $this->‪sanitizeIds($ids);
1561  $ids = array_combine($ids, $ids) ?: [];
1562  $connection = $this->‪getConnectionForTableName($tableName);
1563  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1564 
1565  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1566  $queryBuilder = $connection->createQueryBuilder();
1567  $queryBuilder->getRestrictions()->removeAll();
1568  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1569  ->from($tableName)
1570  ->where(
1571  $queryBuilder->expr()->in(
1572  't3ver_oid',
1573  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1574  ),
1575  $queryBuilder->expr()->eq(
1576  't3ver_wsid',
1577  $queryBuilder->createNamedParameter(
1578  $this->getWorkspaceId(),
1580  )
1581  ),
1582  $queryBuilder->expr()->eq(
1583  't3ver_state',
1584  $queryBuilder->createNamedParameter(
1587  )
1588  )
1589  )
1590  ->executeQuery();
1591 
1592  while ($version = $result->fetchAssociative()) {
1593  $liveId = $version['t3ver_oid'];
1594  if (isset($ids[$liveId])) {
1595  unset($ids[$liveId]);
1596  }
1597  }
1598  }
1599 
1600  return array_values($ids);
1601  }
1602 
1603  protected function ‪removeFromItemArray($tableName, $id)
1604  {
1605  foreach ($this->itemArray as $index => $item) {
1606  if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) {
1607  unset($this->itemArray[$index]);
1608  return true;
1609  }
1610  }
1611  return false;
1612  }
1622  protected static function ‪isOnSymmetricSide($parentUid, $parentConf, $childRec)
1623  {
1624  return ‪MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1625  && $parentConf['symmetric_field']
1626  && $parentUid == $childRec[$parentConf['symmetric_field']];
1627  }
1628 
1637  protected function ‪completeOppositeUsageValues($tableName, array $referenceValues)
1638  {
1639  if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1640  // @todo: count($this->MM_oppositeUsage[$tableName]) > 1 is buggy.
1641  // Scenario: Suppose a foreign table has two (!) fields that link to a sys_category. Relations can
1642  // then be correctly set for both fields when editing the foreign records. But when editing a sys_category
1643  // record (local side) and adding a relation to a table that has two category relation fields, the 'fieldname'
1644  // entry in mm-table can not be decided and ends up empty. Neither of the foreign table fields then recognize
1645  // the relation as being set.
1646  // One simple solution is to either simply pick the *first* field, or set *both* relations, but this
1647  // is a) guesswork and b) it may be that in practice only *one* field is actually shown due to record
1648  // types "showitem".
1649  // Brain melt increases with tt_content field 'selected_category' in combination with
1650  // 'category_field' for record types 'menu_categorized_pages' and 'menu_categorized_content' next
1651  // to casual 'categories' field. However, 'selected_category' is a 'oneToMany' and not a 'manyToMany'.
1652  // Hard nut ...
1653  return $referenceValues;
1654  }
1655 
1656  $fieldName = $this->MM_oppositeUsage[$tableName][0];
1657  if (empty(‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1658  return $referenceValues;
1659  }
1660 
1661  $configuration = ‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1662  if (!empty($configuration['MM_insert_fields'])) {
1663  // @todo: MM_insert_fields does not make sense and should be probably dropped altogether.
1664  // No core usages, not even with sys_category. There is no point in having data fields that
1665  // are filled with static content, especially since the mm table can't be edited directly.
1666  $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues);
1667  } elseif (!empty($configuration['MM_match_fields'])) {
1668  // @todo: In the end, MM_match_fields does not make sense. The 'tablename' and 'fieldname' restriction
1669  // in addition to uid_local and uid_foreign used when multiple 'foreign' tables and/or multiple fields
1670  // of one table refer to a single 'local' table having an mm table with these four fields, is already
1671  // clear when looking at 'MM_oppositeUsage' of the local table. 'MM_match_fields' should thus probably
1672  // fall altogether. The only information carried here are the field names of 'tablename' and 'fieldname'
1673  // within the mm table itself, which we should hard code. This is partially assumed in DefaultTcaSchema
1674  // already.
1675  $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1676  }
1677 
1678  return $referenceValues;
1679  }
1689  protected function ‪getLiveDefaultId($tableName, $id)
1690  {
1691  $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1692  if ($liveDefaultId === null) {
1693  $liveDefaultId = $id;
1694  }
1695  return (int)$liveDefaultId;
1696  }
1697 
1704  protected function ‪sanitizeIds(array $ids): array
1705  {
1706  return array_filter($ids);
1707  }
1708 
1715  protected function ‪getResolver($tableName, array $ids, array $sortingStatement = null)
1716  {
1717  $resolver = GeneralUtility::makeInstance(
1718  PlainDataResolver::class,
1719  $tableName,
1720  $ids,
1721  $sortingStatement
1722  );
1723  $resolver->setWorkspaceId($this->getWorkspaceId());
1724  $resolver->setKeepDeletePlaceholder(true);
1725  $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1726  return $resolver;
1727  }
1728 
1733  protected function ‪getConnectionForTableName(string $tableName)
1734  {
1735  return GeneralUtility::makeInstance(ConnectionPool::class)
1736  ->getConnectionForTable($tableName);
1737  }
1738 }
‪TYPO3\CMS\Core\Database\Query\QueryHelper\parseOrderBy
‪static array array[] parseOrderBy(string $input)
Definition: QueryHelper.php:44
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:999
‪TYPO3\CMS\Core\Database\RelationHandler\purgeItemArrayHandler
‪bool purgeItemArrayHandler($purgeCallback)
Definition: RelationHandler.php:1412
‪TYPO3\CMS\Core\Database\RelationHandler\purgeItemArray
‪bool purgeItemArray($workspaceId=null)
Definition: RelationHandler.php:1371
‪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:1490
‪TYPO3\CMS\Core\DataHandling\PlainDataResolver
Definition: PlainDataResolver.php:34
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Core\Database\RelationHandler\readList
‪readList($itemlist, array $configuration)
Definition: RelationHandler.php:362
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Core\Database\RelationHandler\sortList
‪sortList($sortby)
Definition: RelationHandler.php:443
‪TYPO3\CMS\Core\Database\RelationHandler\purgeDeletePlaceholder
‪array purgeDeletePlaceholder($tableName, array $ids)
Definition: RelationHandler.php:1534
‪TYPO3\CMS\Core\Database\RelationHandler
Definition: RelationHandler.php:37
‪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\writeForeignField
‪writeForeignField($conf, $parentUid, $updateToUid=0, $skipSorting=null)
Definition: RelationHandler.php:1032
‪TYPO3\CMS\Core\Database\RelationHandler\setReferenceIndexUpdater
‪setReferenceIndexUpdater(ReferenceIndexUpdater $updater)
Definition: RelationHandler.php:206
‪TYPO3\CMS\Core\Database\RelationHandler\getConnectionForTableName
‪Connection getConnectionForTableName(string $tableName)
Definition: RelationHandler.php:1709
‪TYPO3\CMS\Core\Database\RelationHandler\$tableArray
‪array $tableArray
Definition: RelationHandler.php:55
‪TYPO3\CMS\Core\Versioning\VersionState\DELETE_PLACEHOLDER
‪const DELETE_PLACEHOLDER
Definition: VersionState.php:61
‪TYPO3\CMS\Core\Database\Query\QueryHelper\quoteDatabaseIdentifiers
‪static string quoteDatabaseIdentifiers(Connection $connection, string $sql)
Definition: QueryHelper.php:228
‪TYPO3\CMS\Core\Database\RelationHandler\getFromDB
‪array getFromDB()
Definition: RelationHandler.php:1200
‪TYPO3\CMS\Core\Database\RelationHandler\setUseLiveReferenceIds
‪setUseLiveReferenceIds($useLiveReferenceIds)
Definition: RelationHandler.php:351
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:54
‪TYPO3\CMS\Core\Database\RelationHandler\isPurged
‪bool isPurged()
Definition: RelationHandler.php:216
‪TYPO3\CMS\Core\Database\Connection\update
‪int update($tableName, array $data, array $identifier, array $types=[])
Definition: Connection.php:302
‪TYPO3\CMS\Core\Database\RelationHandler\writeMM
‪writeMM($MM_tableName, $uid, $prependTableName=false)
Definition: RelationHandler.php:593
‪TYPO3\CMS\Core\Database\RelationHandler\convertItemArray
‪bool convertItemArray()
Definition: RelationHandler.php:1317
‪TYPO3\CMS\Core\Type\Enumeration\cast
‪static static cast($value)
Definition: Enumeration.php:186
‪TYPO3\CMS\Core\Database\RelationHandler\remapMM
‪remapMM($MM_tableName, $uid, $newUid, $prependTableName=false)
Definition: RelationHandler.php:837
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation\getMaxBindParameters
‪static int getMaxBindParameters(AbstractPlatform $platform)
Definition: PlatformInformation.php:125
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Core\Database\RelationHandler\purgeVersionedIds
‪array purgeVersionedIds($tableName, array $ids)
Definition: RelationHandler.php:1447
‪TYPO3\CMS\Core\Database\RelationHandler\completeOppositeUsageValues
‪array completeOppositeUsageValues($tableName, array $referenceValues)
Definition: RelationHandler.php:1613
‪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:1174
‪TYPO3\CMS\Core\Database\RelationHandler\getResolvedItemArray
‪array getResolvedItemArray()
Definition: RelationHandler.php:1248
‪TYPO3\CMS\Core\Database\RelationHandler\start
‪start($itemlist, $tablelist, $MMtable='', $MMuid=0, $currentTable='', $conf=[])
Definition: RelationHandler.php:231
‪TYPO3\CMS\Core\Configuration\Features
Definition: Features.php:56
‪TYPO3\CMS\Core\Database\RelationHandler\getLiveDefaultId
‪int getLiveDefaultId($tableName, $id)
Definition: RelationHandler.php:1665
‪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=array();protected bool $MM_hasUidField;protected array $MM_insert_fields=array();protected string $MM_table_where='';protected array $MM_oppositeUsage;protected bool $updateReferenceIndex=true;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 int function getWorkspaceId():int { $backendUser=$GLOBALS[ 'BE_USER'] ?? null;if(!isset( $this->workspaceId)) { $this-> workspaceId
Definition: RelationHandler.php:185
‪TYPO3\CMS\Core\Database\RelationHandler\updateRefIndex
‪array updateRefIndex($table, $uid)
Definition: RelationHandler.php:1287
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Database\RelationHandler\countItems
‪mixed countItems($returnAsArray=true)
Definition: RelationHandler.php:1269
‪TYPO3\CMS\Core\Database\RelationHandler\readForeignField
‪readForeignField($uid, $conf)
Definition: RelationHandler.php:895
‪TYPO3\CMS\Core\Database\RelationHandler\$registerNonTableValues
‪bool $registerNonTableValues
Definition: RelationHandler.php:48
‪TYPO3\CMS\Core\Versioning\VersionState
Definition: VersionState.php:24
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Core\Database\RelationHandler\setFetchAllFields
‪setFetchAllFields($allFields)
Definition: RelationHandler.php:320
‪TYPO3\CMS\Core\Database\RelationHandler\readMM
‪readMM($tableName, $uid, $mmOppositeTable)
Definition: RelationHandler.php:500
‪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:343
‪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:1598
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:22
‪TYPO3\CMS\Core\Database\Connection\createQueryBuilder
‪TYPO3 CMS Core Database Query QueryBuilder createQueryBuilder()
Definition: Connection.php:117
‪TYPO3\CMS\Core\Database\RelationHandler\getResolver
‪PlainDataResolver getResolver($tableName, array $ids, array $sortingStatement=null)
Definition: RelationHandler.php:1691
‪TYPO3\CMS\Core\Database\RelationHandler\$fetchAllFields
‪bool $fetchAllFields
Definition: RelationHandler.php:42
‪TYPO3\CMS\Core\Database\RelationHandler\setWorkspaceId
‪setWorkspaceId($workspaceId)
Definition: RelationHandler.php:195
‪TYPO3\CMS\Core\Database\RelationHandler\sanitizeIds
‪array sanitizeIds(array $ids)
Definition: RelationHandler.php:1680
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Database\RelationHandler\setUpdateReferenceIndex
‪setUpdateReferenceIndex($updateReferenceIndex)
Definition: RelationHandler.php:331
‪TYPO3\CMS\Core\DataHandling\ReferenceIndexUpdater
Definition: ReferenceIndexUpdater.php:35
‪TYPO3\CMS\Core\Database\RelationHandler\removeFromItemArray
‪removeFromItemArray($tableName, $id)
Definition: RelationHandler.php:1579
‪TYPO3\CMS\Core\Database\RelationHandler\processDeletePlaceholder
‪bool processDeletePlaceholder()
Definition: RelationHandler.php:1397
‪TYPO3\CMS\Core\Database
Definition: Connection.php:18
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:40