TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
RelationHandler.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\Database;
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
24 
31 {
37  protected $fetchAllFields = false;
38 
44  public $registerNonTableValues = false;
45 
52  public $tableArray = [];
53 
59  public $itemArray = [];
60 
66  public $nonTableArray = [];
67 
71  public $additionalWhere = [];
72 
78  public $checkIfDeleted = true;
79 
83  public $dbPaths = [];
84 
90  public $firstTable = '';
91 
97  public $secondTable = '';
98 
105  public $MM_is_foreign = false;
106 
112  public $MM_oppositeField = '';
113 
119  public $MM_oppositeTable = '';
120 
127 
134 
141 
149 
156  public $MM_match_fields = [];
157 
164 
170  public $MM_insert_fields = [];
171 
177  public $MM_table_where = '';
178 
184  protected $MM_oppositeUsage;
185 
189  protected $updateReferenceIndex = true;
190 
194  protected $useLiveParentIds = true;
195 
199  protected $useLiveReferenceIds = true;
200 
204  protected $workspaceId;
205 
209  protected $purged = false;
210 
216  public $results = [];
217 
223  public function getWorkspaceId()
224  {
225  if (!isset($this->workspaceId)) {
226  $this->workspaceId = (int)$GLOBALS['BE_USER']->workspace;
227  }
228  return $this->workspaceId;
229  }
230 
236  public function setWorkspaceId($workspaceId)
237  {
238  $this->workspaceId = (int)$workspaceId;
239  }
240 
246  public function isPurged()
247  {
248  return $this->purged;
249  }
250 
262  public function start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = [])
263  {
264  $conf = (array)$conf;
265  // SECTION: MM reverse relations
266  $this->MM_is_foreign = (bool)$conf['MM_opposite_field'];
267  $this->MM_oppositeField = $conf['MM_opposite_field'];
268  $this->MM_table_where = $conf['MM_table_where'];
269  $this->MM_hasUidField = $conf['MM_hasUidField'];
270  $this->MM_match_fields = is_array($conf['MM_match_fields']) ? $conf['MM_match_fields'] : [];
271  $this->MM_insert_fields = is_array($conf['MM_insert_fields']) ? $conf['MM_insert_fields'] : $this->MM_match_fields;
272  $this->currentTable = $currentTable;
273  if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) {
274  $this->MM_oppositeUsage = $conf['MM_oppositeUsage'];
275  }
276  if ($this->MM_is_foreign) {
277  $tmp = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
278  // Normally, $conf['allowed'] can contain a list of tables,
279  // but as we are looking at a MM relation from the foreign side,
280  // it only makes sense to allow one one table in $conf['allowed']
281  $tmp = GeneralUtility::trimExplode(',', $tmp);
282  $this->MM_oppositeTable = $tmp[0];
283  unset($tmp);
284  // Only add the current table name if there is more than one allowed field
285  // We must be sure this has been done at least once before accessing the "columns" part of TCA for a table.
286  $this->MM_oppositeFieldConf = $GLOBALS['TCA'][$this->MM_oppositeTable]['columns'][$this->MM_oppositeField]['config'];
287  if ($this->MM_oppositeFieldConf['allowed']) {
288  $oppositeFieldConf_allowed = explode(',', $this->MM_oppositeFieldConf['allowed']);
289  if (count($oppositeFieldConf_allowed) > 1 || $this->MM_oppositeFieldConf['allowed'] === '*') {
290  $this->MM_isMultiTableRelationship = $oppositeFieldConf_allowed[0];
291  }
292  }
293  }
294  // SECTION: normal MM relations
295  // If the table list is "*" then all tables are used in the list:
296  if (trim($tablelist) === '*') {
297  $tablelist = implode(',', array_keys($GLOBALS['TCA']));
298  }
299  // The tables are traversed and internal arrays are initialized:
300  $tempTableArray = GeneralUtility::trimExplode(',', $tablelist, true);
301  foreach ($tempTableArray as $val) {
302  $tName = trim($val);
303  $this->tableArray[$tName] = [];
304  if ($this->checkIfDeleted && $GLOBALS['TCA'][$tName]['ctrl']['delete']) {
305  $fieldN = $tName . '.' . $GLOBALS['TCA'][$tName]['ctrl']['delete'];
306  $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
307  }
308  }
309  if (is_array($this->tableArray)) {
310  reset($this->tableArray);
311  } else {
312  // No tables
313  return;
314  }
315  // Set first and second tables:
316  // Is the first table
317  $this->firstTable = key($this->tableArray);
318  next($this->tableArray);
319  // If the second table is set and the ID number is less than zero (later)
320  // then the record is regarded to come from the second table...
321  $this->secondTable = key($this->tableArray);
322  // Now, populate the internal itemArray and tableArray arrays:
323  // If MM, then call this function to do that:
324  if ($MMtable) {
325  if ($MMuid) {
326  $this->readMM($MMtable, $MMuid);
327  $this->purgeItemArray();
328  } else {
329  // Revert to readList() for new records in order to load possible default values from $itemlist
330  $this->readList($itemlist, $conf);
331  $this->purgeItemArray();
332  }
333  } elseif ($MMuid && $conf['foreign_field']) {
334  // If not MM but foreign_field, the read the records by the foreign_field
335  $this->readForeignField($MMuid, $conf);
336  } else {
337  // If not MM, then explode the itemlist by "," and traverse the list:
338  $this->readList($itemlist, $conf);
339  // Do automatic default_sortby, if any
340  if ($conf['foreign_default_sortby']) {
341  $this->sortList($conf['foreign_default_sortby']);
342  }
343  }
344  }
345 
351  public function setFetchAllFields($allFields)
352  {
353  $this->fetchAllFields = (bool)$allFields;
354  }
355 
363  {
364  $this->updateReferenceIndex = (bool)$updateReferenceIndex;
365  }
366 
371  {
372  $this->useLiveParentIds = (bool)$useLiveParentIds;
373  }
374 
379  {
380  $this->useLiveReferenceIds = (bool)$useLiveReferenceIds;
381  }
382 
390  public function readList($itemlist, array $configuration)
391  {
392  if ((string)trim($itemlist) != '') {
393  $tempItemArray = GeneralUtility::trimExplode(',', $itemlist);
394  // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
395  // if there were spaces in the list... I suppose this is better overall...
396  foreach ($tempItemArray as $key => $val) {
397  // Will be set to "1" if the entry was a real table/id:
398  $isSet = 0;
399  // Extract table name and id. This is un the formular [tablename]_[id]
400  // where table name MIGHT contain "_", hence the reversion of the string!
401  $val = strrev($val);
402  $parts = explode('_', $val, 2);
403  $theID = strrev($parts[0]);
404  // Check that the id IS an integer:
406  // Get the table name: If a part of the exploded string, use that.
407  // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
408  $theTable = trim($parts[1])
409  ? strrev(trim($parts[1]))
410  : ($this->secondTable && $theID < 0 ? $this->secondTable : $this->firstTable);
411  // If the ID is not blank and the table name is among the names in the inputted tableList
412  if (
413  (string)$theID != ''
414  // allow the default language '0' for the special languages configuration
415  && ($theID || ($configuration['special'] ?? null) === 'languages')
416  && $theTable && isset($this->tableArray[$theTable])
417  ) {
418  // Get ID as the right value:
419  $theID = $this->secondTable ? abs((int)$theID) : (int)$theID;
420  // Register ID/table name in internal arrays:
421  $this->itemArray[$key]['id'] = $theID;
422  $this->itemArray[$key]['table'] = $theTable;
423  $this->tableArray[$theTable][] = $theID;
424  // Set update-flag:
425  $isSet = 1;
426  }
427  }
428  // If it turns out that the value from the list was NOT a valid reference to a table-record,
429  // then we might still set it as a NO_TABLE value:
430  if (!$isSet && $this->registerNonTableValues) {
431  $this->itemArray[$key]['id'] = $tempItemArray[$key];
432  $this->itemArray[$key]['table'] = '_NO_TABLE';
433  $this->nonTableArray[] = $tempItemArray[$key];
434  }
435  }
436 
437  // Skip if not dealing with IRRE in a CSV list on a workspace
438  if ($configuration['type'] !== 'inline' || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
439  || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
440  || (int)$GLOBALS['BE_USER']->workspace === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])) {
441  return;
442  }
443 
444  // Fetch live record data
445  if ($this->useLiveReferenceIds) {
446  foreach ($this->itemArray as &$item) {
447  $item['id'] = $this->getLiveDefaultId($item['table'], $item['id']);
448  }
449  // Directly overlay workspace data
450  } else {
451  $this->itemArray = [];
452  $foreignTable = $configuration['foreign_table'];
453  $ids = $this->getResolver($foreignTable, $this->tableArray[$foreignTable])->get();
454  foreach ($ids as $id) {
455  $this->itemArray[] = [
456  'id' => $id,
457  'table' => $foreignTable,
458  ];
459  }
460  }
461  }
462  }
463 
472  public function sortList($sortby)
473  {
474  // Sort directly without fetching addional data
475  if ($sortby == 'uid') {
476  usort(
477  $this->itemArray,
478  function ($a, $b) {
479  return $a['id'] < $b['id'] ? -1 : 1;
480  }
481  );
482  } elseif (count($this->tableArray) === 1) {
483  reset($this->tableArray);
484  $table = key($this->tableArray);
485  $uidList = implode(',', current($this->tableArray));
486  if ($uidList) {
487  $this->itemArray = [];
488  $this->tableArray = [];
489  $queryBuilder = $this->getConnectionForTableName($table)
490  ->createQueryBuilder();
491  $queryBuilder->getRestrictions()->removeAll();
492  $queryBuilder->select('uid')
493  ->from($table)
494  ->where(
495  $queryBuilder->expr()->in(
496  'uid',
497  $queryBuilder->createNamedParameter(
498  GeneralUtility::intExplode(',', $uidList),
499  Connection::PARAM_INT_ARRAY
500  )
501  )
502  );
503  foreach (QueryHelper::parseOrderBy((string)$sortby) as $orderPair) {
504  list($fieldName, $order) = $orderPair;
505  $queryBuilder->addOrderBy($fieldName, $order);
506  }
507  $statement = $queryBuilder->execute();
508  while ($row = $statement->fetch()) {
509  $this->itemArray[] = ['id' => $row['uid'], 'table' => $table];
510  $this->tableArray[$table][] = $row['uid'];
511  }
512  }
513  }
514  }
515 
524  public function readMM($tableName, $uid)
525  {
526  $key = 0;
527  $theTable = null;
528  $queryBuilder = $this->getConnectionForTableName($tableName)
529  ->createQueryBuilder();
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, \PDO::PARAM_STR)
545  ),
546  $queryBuilder->expr()->eq(
547  'tablenames',
548  $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
549  )
550  );
551  } else {
552  $expression = $queryBuilder->expr()->eq(
553  'tablenames',
554  $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
555  );
556  }
557  $queryBuilder->andWhere($expression);
558  }
559  $theTable = $this->MM_oppositeTable;
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  $queryBuilder->andWhere(
568  QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where))
569  );
570  }
571  foreach ($this->MM_match_fields as $field => $value) {
572  $queryBuilder->andWhere(
573  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
574  );
575  }
576  $queryBuilder->andWhere(
577  $queryBuilder->expr()->eq(
578  $uidLocal_field,
579  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
580  )
581  );
582  $queryBuilder->orderBy($sorting_field);
583  $statement = $queryBuilder->execute();
584  while ($row = $statement->fetch()) {
585  // Default
586  if (!$this->MM_is_foreign) {
587  // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
588  $theTable = $row['tablenames'] ?: $this->firstTable;
589  }
590  if (($row[$uidForeign_field] || $theTable == 'pages') && $theTable && isset($this->tableArray[$theTable])) {
591  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
592  $this->itemArray[$key]['table'] = $theTable;
593  $this->tableArray[$theTable][] = $row[$uidForeign_field];
594  } elseif ($this->registerNonTableValues) {
595  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
596  $this->itemArray[$key]['table'] = '_NO_TABLE';
597  $this->nonTableArray[] = $row[$uidForeign_field];
598  }
599  $key++;
600  }
601  }
602 
611  public function writeMM($MM_tableName, $uid, $prependTableName = false)
612  {
613  $connection = $this->getConnectionForTableName($MM_tableName);
614  $expressionBuilder = $connection->createQueryBuilder()->expr();
615 
616  // In case of a reverse relation
617  if ($this->MM_is_foreign) {
618  $uidLocal_field = 'uid_foreign';
619  $uidForeign_field = 'uid_local';
620  $sorting_field = 'sorting_foreign';
621  } else {
622  // default
623  $uidLocal_field = 'uid_local';
624  $uidForeign_field = 'uid_foreign';
625  $sorting_field = 'sorting';
626  }
627  // If there are tables...
628  $tableC = count($this->tableArray);
629  if ($tableC) {
630  // Boolean: does the field "tablename" need to be filled?
631  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship ? 1 : 0;
632  $c = 0;
633  $additionalWhere_tablenames = '';
634  if ($this->MM_is_foreign && $prep) {
635  $additionalWhere_tablenames = $expressionBuilder->eq(
636  'tablenames',
637  $expressionBuilder->literal($this->currentTable)
638  );
639  }
640  $additionalWhere = $expressionBuilder->andX();
641  // Add WHERE clause if configured
642  if ($this->MM_table_where) {
643  $additionalWhere->add(
645  str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where)
646  )
647  );
648  }
649  // Select, update or delete only those relations that match the configured fields
650  foreach ($this->MM_match_fields as $field => $value) {
651  $additionalWhere->add($expressionBuilder->eq($field, $expressionBuilder->literal($value)));
652  }
653 
654  $queryBuilder = $connection->createQueryBuilder();
655  $queryBuilder->getRestrictions()->removeAll();
656  $queryBuilder->select($uidForeign_field)
657  ->from($MM_tableName)
658  ->where($queryBuilder->expr()->eq(
659  $uidLocal_field,
660  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
661  ))
662  ->orderBy($sorting_field);
663 
664  if ($prep) {
665  $queryBuilder->addSelect('tablenames');
666  }
667  if ($this->MM_hasUidField) {
668  $queryBuilder->addSelect('uid');
669  }
670  if ($additionalWhere_tablenames) {
671  $queryBuilder->andWhere($additionalWhere_tablenames);
672  }
673  if ($additionalWhere->count()) {
674  $queryBuilder->andWhere($additionalWhere);
675  }
676 
677  $result = $queryBuilder->execute();
678  $oldMMs = [];
679  // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
680  // If the UID is present it will be used to update sorting and delete MM-records.
681  // This is necessary if the "multiple" feature is used for the MM relations.
682  // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
683  $oldMMs_inclUid = [];
684  while ($row = $result->fetch()) {
685  if (!$this->MM_is_foreign && $prep) {
686  $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]];
687  } else {
688  $oldMMs[] = $row[$uidForeign_field];
689  }
690  $oldMMs_inclUid[] = [$row['tablenames'], $row[$uidForeign_field], $row['uid']];
691  }
692  // For each item, insert it:
693  foreach ($this->itemArray as $val) {
694  $c++;
695  if ($prep || $val['table'] == '_NO_TABLE') {
696  // Insert current table if needed
697  if ($this->MM_is_foreign) {
698  $tablename = $this->currentTable;
699  } else {
700  $tablename = $val['table'];
701  }
702  } else {
703  $tablename = '';
704  }
705  if (!$this->MM_is_foreign && $prep) {
706  $item = [$val['table'], $val['id']];
707  } else {
708  $item = $val['id'];
709  }
710  if (in_array($item, $oldMMs)) {
711  $oldMMs_index = array_search($item, $oldMMs);
712  // In principle, selecting on the UID is all we need to do
713  // if a uid field is available since that is unique!
714  // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
715  $queryBuilder = $connection->createQueryBuilder();
716  $queryBuilder->update($MM_tableName)
717  ->set($sorting_field, $c)
718  ->where(
719  $expressionBuilder->eq(
720  $uidLocal_field,
721  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
722  ),
723  $expressionBuilder->eq(
724  $uidForeign_field,
725  $queryBuilder->createNamedParameter($val['id'], \PDO::PARAM_INT)
726  )
727  );
728 
729  if ($additionalWhere->count()) {
730  $queryBuilder->andWhere($additionalWhere);
731  }
732  if ($this->MM_hasUidField) {
733  $queryBuilder->andWhere(
734  $expressionBuilder->eq(
735  'uid',
736  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMMs_index][2], \PDO::PARAM_INT)
737  )
738  );
739  }
740  if ($tablename) {
741  $queryBuilder->andWhere(
742  $expressionBuilder->eq(
743  'tablenames',
744  $queryBuilder->createNamedParameter($tablename, \PDO::PARAM_STR)
745  )
746  );
747  }
748 
749  $queryBuilder->execute();
750  // Remove the item from the $oldMMs array so after this
751  // foreach loop only the ones that need to be deleted are in there.
752  unset($oldMMs[$oldMMs_index]);
753  // Remove the item from the $oldMMs_inclUid array so after this
754  // foreach loop only the ones that need to be deleted are in there.
755  unset($oldMMs_inclUid[$oldMMs_index]);
756  } else {
757  $insertFields = $this->MM_insert_fields;
758  $insertFields[$uidLocal_field] = $uid;
759  $insertFields[$uidForeign_field] = $val['id'];
760  $insertFields[$sorting_field] = $c;
761  if ($tablename) {
762  $insertFields['tablenames'] = $tablename;
763  $insertFields = $this->completeOppositeUsageValues($tablename, $insertFields);
764  }
765  $connection->insert($MM_tableName, $insertFields);
766  if ($this->MM_is_foreign) {
767  $this->updateRefIndex($val['table'], $val['id']);
768  }
769  }
770  }
771  // Delete all not-used relations:
772  if (is_array($oldMMs) && !empty($oldMMs)) {
773  $queryBuilder = $connection->createQueryBuilder();
774  $removeClauses = $queryBuilder->expr()->orX();
775  $updateRefIndex_records = [];
776  foreach ($oldMMs as $oldMM_key => $mmItem) {
777  // If UID field is present, of course we need only use that for deleting.
778  if ($this->MM_hasUidField) {
779  $removeClauses->add($queryBuilder->expr()->eq(
780  'uid',
781  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMM_key][2], \PDO::PARAM_INT)
782  ));
783  } else {
784  if (is_array($mmItem)) {
785  $removeClauses->add(
786  $queryBuilder->expr()->andX(
787  $queryBuilder->expr()->eq(
788  'tablenames',
789  $queryBuilder->createNamedParameter($mmItem[0], \PDO::PARAM_STR)
790  ),
791  $queryBuilder->expr()->eq(
792  $uidForeign_field,
793  $queryBuilder->createNamedParameter($mmItem[1], \PDO::PARAM_INT)
794  )
795  )
796  );
797  } else {
798  $removeClauses->add(
799  $queryBuilder->expr()->eq(
800  $uidForeign_field,
801  $queryBuilder->createNamedParameter($mmItem, \PDO::PARAM_INT)
802  )
803  );
804  }
805  }
806  if ($this->MM_is_foreign) {
807  if (is_array($mmItem)) {
808  $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]];
809  } else {
810  $updateRefIndex_records[] = [$this->firstTable, $mmItem];
811  }
812  }
813  }
814 
815  $queryBuilder->delete($MM_tableName)
816  ->where(
817  $queryBuilder->expr()->eq(
818  $uidLocal_field,
819  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
820  ),
821  $removeClauses
822  );
823 
824  if ($additionalWhere_tablenames) {
825  $queryBuilder->andWhere($additionalWhere_tablenames);
826  }
827  if ($additionalWhere->count()) {
828  $queryBuilder->andWhere($additionalWhere);
829  }
830 
831  $queryBuilder->execute();
832 
833  // Update ref index:
834  foreach ($updateRefIndex_records as $pair) {
835  $this->updateRefIndex($pair[0], $pair[1]);
836  }
837  }
838  // Update ref index; In DataHandler it is not certain that this will happen because
839  // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
840  // This could also have been fixed in updateDB in DataHandler, however I decided to do it here ...
841  $this->updateRefIndex($this->currentTable, $uid);
842  }
843  }
844 
855  public function remapMM($MM_tableName, $uid, $newUid, $prependTableName = false)
856  {
857  // In case of a reverse relation
858  if ($this->MM_is_foreign) {
859  $uidLocal_field = 'uid_foreign';
860  } else {
861  // default
862  $uidLocal_field = 'uid_local';
863  }
864  // If there are tables...
865  $tableC = count($this->tableArray);
866  if ($tableC) {
867  $queryBuilder = $this->getConnectionForTableName($MM_tableName)
868  ->createQueryBuilder();
869  $queryBuilder->update($MM_tableName)
870  ->set($uidLocal_field, (int)$newUid)
871  ->where($queryBuilder->expr()->eq(
872  $uidLocal_field,
873  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
874  ));
875  // Boolean: does the field "tablename" need to be filled?
876  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
877  if ($this->MM_is_foreign && $prep) {
878  $queryBuilder->andWhere(
879  $queryBuilder->expr()->eq(
880  'tablenames',
881  $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
882  )
883  );
884  }
885  // Add WHERE clause if configured
886  if ($this->MM_table_where) {
887  $queryBuilder->andWhere(
888  QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (int)$uid, $this->MM_table_where))
889  );
890  }
891  // Select, update or delete only those relations that match the configured fields
892  foreach ($this->MM_match_fields as $field => $value) {
893  $queryBuilder->andWhere(
894  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
895  );
896  }
897  $queryBuilder->execute();
898  }
899  }
900 
909  public function readForeignField($uid, $conf)
910  {
911  if ($this->useLiveParentIds) {
912  $uid = $this->getLiveDefaultId($this->currentTable, $uid);
913  }
914 
915  $key = 0;
916  $uid = (int)$uid;
917  $foreign_table = $conf['foreign_table'];
918  $foreign_table_field = $conf['foreign_table_field'];
919  $useDeleteClause = !$this->undeleteRecord;
920  $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
921  $queryBuilder = $this->getConnectionForTableName($foreign_table)
922  ->createQueryBuilder();
923  $queryBuilder->getRestrictions()
924  ->removeAll();
925  // Use the deleteClause (e.g. "deleted=0") on this table
926  if ($useDeleteClause) {
927  $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
928  }
929 
930  $queryBuilder->select('uid')
931  ->from($foreign_table);
932 
933  // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
934  if ($conf['symmetric_field']) {
935  $queryBuilder->where(
936  $queryBuilder->expr()->orX(
937  $queryBuilder->expr()->eq(
938  $conf['foreign_field'],
939  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
940  ),
941  $queryBuilder->expr()->eq(
942  $conf['symmetric_field'],
943  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
944  )
945  )
946  );
947  } else {
948  $queryBuilder->where($queryBuilder->expr()->eq(
949  $conf['foreign_field'],
950  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
951  ));
952  }
953  // If it's requested to look for the parent uid AND the parent table,
954  // add an additional SQL-WHERE clause
955  if ($foreign_table_field && $this->currentTable) {
956  $queryBuilder->andWhere(
957  $queryBuilder->expr()->eq(
958  $foreign_table_field,
959  $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
960  )
961  );
962  }
963  // Add additional where clause if foreign_match_fields are defined
964  foreach ($foreign_match_fields as $field => $value) {
965  $queryBuilder->andWhere(
966  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
967  );
968  }
969  // Select children from the live(!) workspace only
970  if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
971  $queryBuilder->andWhere(
972  $queryBuilder->expr()->in(
973  $foreign_table . '.t3ver_wsid',
974  $queryBuilder->createNamedParameter([0, (int)$this->getWorkspaceId()], Connection::PARAM_INT_ARRAY)
975  ),
976  $queryBuilder->expr()->neq(
977  $foreign_table . '.pid',
978  $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
979  )
980  );
981  }
982  // Get the correct sorting field
983  // Specific manual sortby for data handled by this field
984  $sortby = '';
985  if ($conf['foreign_sortby']) {
986  if ($conf['symmetric_sortby'] && $conf['symmetric_field']) {
987  // Sorting depends on, from which side of the relation we're looking at it
988  // This requires bypassing automatic quoting and setting of the default sort direction
989  // @TODO: Doctrine: generalize to standard SQL to guarantee database independency
990  $queryBuilder->add(
991  'orderBy',
992  'CASE
993  WHEN ' . $queryBuilder->expr()->eq($conf['foreign_field'], $uid) . '
994  THEN ' . $queryBuilder->quoteIdentifier($conf['foreign_sortby']) . '
995  ELSE ' . $queryBuilder->quoteIdentifier($conf['symmetric_sortby']) . '
996  END'
997  );
998  } else {
999  // Regular single-side behaviour
1000  $sortby = $conf['foreign_sortby'];
1001  }
1002  } elseif ($conf['foreign_default_sortby']) {
1003  // Specific default sortby for data handled by this field
1004  $sortby = $conf['foreign_default_sortby'];
1005  } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
1006  // Manual sortby for all table records
1007  $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1008  } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby']) {
1009  // Default sortby for all table records
1010  $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
1011  }
1012 
1013  if (!empty($sortby)) {
1014  foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) {
1015  list($fieldName, $sorting) = $orderPair;
1016  $queryBuilder->addOrderBy($fieldName, $sorting);
1017  }
1018  }
1019 
1020  // Get the rows from storage
1021  $rows = [];
1022  $result = $queryBuilder->execute();
1023  while ($row = $result->fetch()) {
1024  $rows[$row['uid']] = $row;
1025  }
1026  if (!empty($rows)) {
1027  // Retrieve the parsed and prepared ORDER BY configuration for the resolver
1028  $sortby = $queryBuilder->getQueryPart('orderBy');
1029  $ids = $this->getResolver($foreign_table, array_keys($rows), $sortby)->get();
1030  foreach ($ids as $id) {
1031  $this->itemArray[$key]['id'] = $id;
1032  $this->itemArray[$key]['table'] = $foreign_table;
1033  $this->tableArray[$foreign_table][] = $id;
1034  $key++;
1035  }
1036  }
1037  }
1038 
1048  public function writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = false)
1049  {
1050  if ($this->useLiveParentIds) {
1051  $parentUid = $this->getLiveDefaultId($this->currentTable, $parentUid);
1052  if (!empty($updateToUid)) {
1053  $updateToUid = $this->getLiveDefaultId($this->currentTable, $updateToUid);
1054  }
1055  }
1056 
1057  $c = 0;
1058  $foreign_table = $conf['foreign_table'];
1059  $foreign_field = $conf['foreign_field'];
1060  $symmetric_field = $conf['symmetric_field'];
1061  $foreign_table_field = $conf['foreign_table_field'];
1062  $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
1063  // If there are table items and we have a proper $parentUid
1064  if (MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
1065  // If updateToUid is not a positive integer, set it to '0', so it will be ignored
1066  if (!(MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
1067  $updateToUid = 0;
1068  }
1069  $considerWorkspaces = ($GLOBALS['BE_USER']->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($foreign_table));
1070  $fields = 'uid,pid,' . $foreign_field;
1071  // Consider the symmetric field if defined:
1072  if ($symmetric_field) {
1073  $fields .= ',' . $symmetric_field;
1074  }
1075  // Consider workspaces if defined and currently used:
1076  if ($considerWorkspaces) {
1077  $fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
1078  }
1079  // Update all items
1080  foreach ($this->itemArray as $val) {
1081  $uid = $val['id'];
1082  $table = $val['table'];
1083  $row = [];
1084  // Fetch the current (not overwritten) relation record if we should handle symmetric relations
1085  if ($symmetric_field || $considerWorkspaces) {
1086  $row = BackendUtility::getRecord($table, $uid, $fields, '', true);
1087  if (empty($row)) {
1088  continue;
1089  }
1090  }
1091  $isOnSymmetricSide = false;
1092  if ($symmetric_field) {
1093  $isOnSymmetricSide = self::isOnSymmetricSide($parentUid, $conf, $row);
1094  }
1095  $updateValues = $foreign_match_fields;
1096  // No update to the uid is requested, so this is the normal behaviour
1097  // just update the fields and care about sorting
1098  if (!$updateToUid) {
1099  // Always add the pointer to the parent uid
1100  if ($isOnSymmetricSide) {
1101  $updateValues[$symmetric_field] = $parentUid;
1102  } else {
1103  $updateValues[$foreign_field] = $parentUid;
1104  }
1105  // If it is configured in TCA also to store the parent table in the child record, just do it
1106  if ($foreign_table_field && $this->currentTable) {
1107  $updateValues[$foreign_table_field] = $this->currentTable;
1108  }
1109  // Update sorting columns if not to be skipped
1110  if (!$skipSorting) {
1111  // Get the correct sorting field
1112  // Specific manual sortby for data handled by this field
1113  $sortby = '';
1114  if ($conf['foreign_sortby']) {
1115  $sortby = $conf['foreign_sortby'];
1116  } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
1117  // manual sortby for all table records
1118  $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1119  }
1120  // Apply sorting on the symmetric side
1121  // (it depends on who created the relation, so what uid is in the symmetric_field):
1122  if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
1123  $sortby = $conf['symmetric_sortby'];
1124  } else {
1125  $tempSortBy = [];
1126  foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) {
1127  list($fieldName, $order) = $orderPair;
1128  if ($order !== null) {
1129  $tempSortBy[] = implode(' ', $orderPair);
1130  } else {
1131  $tempSortBy[] = $fieldName;
1132  }
1133  }
1134  $sortby = implode(',', $tempSortBy);
1135  }
1136  if ($sortby) {
1137  $updateValues[$sortby] = ++$c;
1138  }
1139  }
1140  } else {
1141  if ($isOnSymmetricSide) {
1142  $updateValues[$symmetric_field] = $updateToUid;
1143  } else {
1144  $updateValues[$foreign_field] = $updateToUid;
1145  }
1146  }
1147  // Update accordant fields in the database:
1148  if (!empty($updateValues)) {
1149  // Update tstamp if any foreign field value has changed
1150  if (!empty($GLOBALS['TCA'][$table]['ctrl']['tstamp'])) {
1151  $currentRow = BackendUtility::getRecord($table, $uid, implode(',', array_keys($updateValues)), '', true);
1152  $needTstampUpdate = false;
1153  if (empty($currentRow)) {
1154  $needTstampUpdate = true;
1155  } else {
1156  foreach ($currentRow as $field => $curValue) {
1157  if ((string)$curValue !== (string)$updateValues[$field]) {
1158  $needTstampUpdate = true;
1159  break;
1160  }
1161  }
1162  }
1163  if ($needTstampUpdate) {
1164  $updateValues[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1165  }
1166  }
1167  $this->getConnectionForTableName($table)
1168  ->update(
1169  $table,
1170  $updateValues,
1171  ['uid' => (int)$uid]
1172  );
1173  $this->updateRefIndex($table, $uid);
1174  }
1175  // Update accordant fields in the database for workspaces overlays/placeholders:
1176  if ($considerWorkspaces) {
1177  // It's the specific versioned record -> update placeholder (if any)
1178  if (!empty($row['t3ver_oid']) && VersionState::cast($row['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
1179  $this->getConnectionForTableName($table)
1180  ->update(
1181  $table,
1182  $updateValues,
1183  ['uid' => (int)$row['t3ver_oid']]
1184  );
1185  }
1186  }
1187  }
1188  }
1189  }
1190 
1197  public function getValueArray($prependTableName = false)
1198  {
1199  // INIT:
1200  $valueArray = [];
1201  $tableC = count($this->tableArray);
1202  // If there are tables in the table array:
1203  if ($tableC) {
1204  // If there are more than ONE table in the table array, then always prepend table names:
1205  $prep = $tableC > 1 || $prependTableName;
1206  // Traverse the array of items:
1207  foreach ($this->itemArray as $val) {
1208  $valueArray[] = ($prep && $val['table'] != '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
1209  }
1210  }
1211  // Return the array
1212  return $valueArray;
1213  }
1214 
1223  public function getFromDB()
1224  {
1225  // Traverses the tables listed:
1226  foreach ($this->tableArray as $table => $val) {
1227  if (is_array($val)) {
1228  $itemList = implode(',', $val);
1229  if ($itemList) {
1230  if ($this->fetchAllFields) {
1231  $fields = '*';
1232  } else {
1233  $fields = 'uid,pid';
1234  if ($GLOBALS['TCA'][$table]['ctrl']['label']) {
1235  // Titel
1236  $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label'];
1237  }
1238  if ($GLOBALS['TCA'][$table]['ctrl']['label_alt']) {
1239  // Alternative Title-Fields
1240  $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label_alt'];
1241  }
1242  if ($GLOBALS['TCA'][$table]['ctrl']['thumbnail']) {
1243  // Thumbnail
1244  $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['thumbnail'];
1245  }
1246  }
1247  $queryBuilder = $this->getConnectionForTableName($table)
1248  ->createQueryBuilder();
1249  $queryBuilder->getRestrictions()->removeAll();
1250  $queryBuilder->select(...(GeneralUtility::trimExplode(',', $fields, true)))
1251  ->from($table)
1252  ->where($queryBuilder->expr()->in(
1253  'uid',
1254  $queryBuilder->createNamedParameter(
1255  GeneralUtility::intExplode(',', $itemList),
1256  Connection::PARAM_INT_ARRAY
1257  )
1258  ));
1259  if ($this->additionalWhere[$table]) {
1260  $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($this->additionalWhere[$table]));
1261  }
1262  $statement = $queryBuilder->execute();
1263  while ($row = $statement->fetch()) {
1264  $this->results[$table][$row['uid']] = $row;
1265  }
1266  }
1267  }
1268  }
1269  return $this->results;
1270  }
1271 
1277  public function readyForInterface()
1278  {
1279  if (!is_array($this->itemArray)) {
1280  return false;
1281  }
1282  $output = [];
1283  $titleLen = (int)$GLOBALS['BE_USER']->uc['titleLen'];
1284  foreach ($this->itemArray as $val) {
1285  $theRow = $this->results[$val['table']][$val['id']];
1286  if ($theRow && is_array($GLOBALS['TCA'][$val['table']])) {
1287  $label = GeneralUtility::fixed_lgd_cs(strip_tags(
1288  BackendUtility::getRecordTitle($val['table'], $theRow)), $titleLen);
1289  $label = $label ? $label : '[...]';
1290  $output[] = str_replace(',', '', $val['table'] . '_' . $val['id'] . '|' . rawurlencode($label));
1291  }
1292  }
1293  return implode(',', $output);
1294  }
1295 
1302  public function countItems($returnAsArray = true)
1303  {
1304  $count = count($this->itemArray);
1305  if ($returnAsArray) {
1306  $count = [$count];
1307  }
1308  return $count;
1309  }
1310 
1320  public function updateRefIndex($table, $id)
1321  {
1322  $statisticsArray = [];
1323  if ($this->updateReferenceIndex) {
1325  $refIndexObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ReferenceIndex::class);
1327  $refIndexObj->setWorkspaceId($this->getWorkspaceId());
1328  }
1329  $statisticsArray = $refIndexObj->updateRefIndexTable($table, $id);
1330  }
1331  return $statisticsArray;
1332  }
1333 
1343  public function convertItemArray()
1344  {
1345  $hasBeenConverted = false;
1346 
1347  // conversion is only required in a workspace context
1348  // (the case that version ids are submitted in a live context are rare)
1349  if ($this->getWorkspaceId() === 0) {
1350  return $hasBeenConverted;
1351  }
1352 
1353  foreach ($this->tableArray as $tableName => $ids) {
1354  if (empty($ids) || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
1355  continue;
1356  }
1357 
1358  // convert live ids to version ids if available
1359  $convertedIds = $this->getResolver($tableName, $ids)
1360  ->setKeepDeletePlaceholder(false)
1361  ->setKeepMovePlaceholder(false)
1362  ->processVersionOverlays($ids);
1363  foreach ($this->itemArray as $index => $item) {
1364  if ($item['table'] !== $tableName) {
1365  continue;
1366  }
1367  $currentItemId = $item['id'];
1368  if (
1369  !isset($convertedIds[$currentItemId])
1370  || $currentItemId === $convertedIds[$currentItemId]
1371  ) {
1372  continue;
1373  }
1374  // adjust local item to use resolved version id
1375  $this->itemArray[$index]['id'] = $convertedIds[$currentItemId];
1376  $hasBeenConverted = true;
1377  }
1378  // update per-table reference for ids
1379  if ($hasBeenConverted) {
1380  $this->tableArray[$tableName] = array_values($convertedIds);
1381  }
1382  }
1383 
1384  return $hasBeenConverted;
1385  }
1386 
1391  public function purgeItemArray($workspaceId = null)
1392  {
1393  if ($workspaceId === null) {
1394  $workspaceId = $this->getWorkspaceId();
1395  } else {
1396  $workspaceId = (int)$workspaceId;
1397  }
1398 
1399  // Ensure, only live relations are in the items Array
1400  if ($workspaceId === 0) {
1401  $purgeCallback = 'purgeVersionedIds';
1402  // Otherwise, ensure that live relations are purged if version exists
1403  } else {
1404  $purgeCallback = 'purgeLiveVersionedIds';
1405  }
1406 
1407  $itemArrayHasBeenPurged = $this->purgeItemArrayHandler($purgeCallback);
1408  $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1409  return $itemArrayHasBeenPurged;
1410  }
1411 
1417  public function processDeletePlaceholder()
1418  {
1419  if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1420  return false;
1421  }
1422 
1423  return $this->purgeItemArrayHandler('purgeDeletePlaceholder');
1424  }
1425 
1432  protected function purgeItemArrayHandler($purgeCallback)
1433  {
1434  $itemArrayHasBeenPurged = false;
1435 
1436  foreach ($this->tableArray as $itemTableName => $itemIds) {
1437  if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1438  continue;
1439  }
1440 
1441  $purgedItemIds = call_user_func([$this, $purgeCallback], $itemTableName, $itemIds);
1442  $removedItemIds = array_diff($itemIds, $purgedItemIds);
1443  foreach ($removedItemIds as $removedItemId) {
1444  $this->removeFromItemArray($itemTableName, $removedItemId);
1445  }
1446  $this->tableArray[$itemTableName] = $purgedItemIds;
1447  if (!empty($removedItemIds)) {
1448  $itemArrayHasBeenPurged = true;
1449  }
1450  }
1451 
1452  return $itemArrayHasBeenPurged;
1453  }
1454 
1462  protected function purgeVersionedIds($tableName, array $ids)
1463  {
1464  $ids = array_combine($ids, $ids);
1465 
1466  $queryBuilder = $this->getConnectionForTableName($tableName)
1467  ->createQueryBuilder();
1468  $queryBuilder->getRestrictions()->removeAll();
1469  $versions = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1470  ->from($tableName)
1471  ->where(
1472  $queryBuilder->expr()->eq(
1473  'pid',
1474  $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1475  ),
1476  $queryBuilder->expr()->in(
1477  't3ver_oid',
1478  $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
1479  ),
1480  $queryBuilder->expr()->neq(
1481  't3ver_wsid',
1482  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1483  )
1484  )
1485  ->orderBy('t3ver_state', 'DESC')
1486  ->execute()
1487  ->fetchAll();
1488 
1489  if (!empty($versions)) {
1490  foreach ($versions as $version) {
1491  $versionId = $version['uid'];
1492  if (isset($ids[$versionId])) {
1493  unset($ids[$versionId]);
1494  }
1495  }
1496  }
1497 
1498  return array_values($ids);
1499  }
1500 
1508  protected function purgeLiveVersionedIds($tableName, array $ids)
1509  {
1510  $ids = array_combine($ids, $ids);
1511 
1512  $queryBuilder = $this->getConnectionForTableName($tableName)
1513  ->createQueryBuilder();
1514  $queryBuilder->getRestrictions()->removeAll();
1515  $versions = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1516  ->from($tableName)
1517  ->where(
1518  $queryBuilder->expr()->eq(
1519  'pid',
1520  $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1521  ),
1522  $queryBuilder->expr()->in(
1523  't3ver_oid',
1524  $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
1525  ),
1526  $queryBuilder->expr()->neq(
1527  't3ver_wsid',
1528  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1529  )
1530  )
1531  ->orderBy('t3ver_state', 'DESC')
1532  ->execute()
1533  ->fetchAll();
1534 
1535  if (!empty($versions)) {
1536  foreach ($versions as $version) {
1537  $versionId = $version['uid'];
1538  $liveId = $version['t3ver_oid'];
1539  if (isset($ids[$liveId]) && isset($ids[$versionId])) {
1540  unset($ids[$liveId]);
1541  }
1542  }
1543  }
1544 
1545  return array_values($ids);
1546  }
1547 
1555  protected function purgeDeletePlaceholder($tableName, array $ids)
1556  {
1557  $ids = array_combine($ids, $ids);
1558 
1559  $queryBuilder = $this->getConnectionForTableName($tableName)
1560  ->createQueryBuilder();
1561  $queryBuilder->getRestrictions()->removeAll();
1562  $versions = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1563  ->from($tableName)
1564  ->where(
1565  $queryBuilder->expr()->eq(
1566  'pid',
1567  $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
1568  ),
1569  $queryBuilder->expr()->in(
1570  't3ver_oid',
1571  $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
1572  ),
1573  $queryBuilder->expr()->neq(
1574  't3ver_wsid',
1575  $queryBuilder->createNamedParameter(
1576  $this->getWorkspaceId(),
1577  \PDO::PARAM_INT
1578  )
1579  ),
1580  $queryBuilder->expr()->eq(
1581  't3ver_state',
1582  $queryBuilder->createNamedParameter(
1584  \PDO::PARAM_INT
1585  )
1586  )
1587  )
1588  ->execute()
1589  ->fetchAll();
1590 
1591  if (!empty($versions)) {
1592  foreach ($versions as $version) {
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  }
1613 
1622  public 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  return $referenceValues;
1641  }
1642 
1643  $fieldName = $this->MM_oppositeUsage[$tableName][0];
1644  if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1645  return $referenceValues;
1646  }
1647 
1648  $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1649  if (!empty($configuration['MM_insert_fields'])) {
1650  $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues);
1651  } elseif (!empty($configuration['MM_match_fields'])) {
1652  $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1653  }
1654 
1655  return $referenceValues;
1656  }
1657 
1666  protected function getLiveDefaultId($tableName, $id)
1667  {
1668  $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1669  if ($liveDefaultId === null) {
1670  $liveDefaultId = $id;
1671  }
1672  return (int)$liveDefaultId;
1673  }
1674 
1681  protected function getResolver($tableName, array $ids, array $sortingStatement = null)
1682  {
1684  $resolver = GeneralUtility::makeInstance(
1685  PlainDataResolver::class,
1686  $tableName,
1687  $ids,
1688  $sortingStatement
1689  );
1690  $resolver->setWorkspaceId($this->getWorkspaceId());
1691  $resolver->setKeepDeletePlaceholder(true);
1692  $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1693  return $resolver;
1694  }
1695 
1700  protected function getConnectionForTableName(string $tableName)
1701  {
1702  return GeneralUtility::makeInstance(ConnectionPool::class)
1703  ->getConnectionForTable($tableName);
1704  }
1705 }
setUpdateReferenceIndex($updateReferenceIndex)
writeForeignField($conf, $parentUid, $updateToUid=0, $skipSorting=false)
purgeLiveVersionedIds($tableName, array $ids)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static getRecordTitle($table, $row, $prep=false, $forceResult=true)
start($itemlist, $tablelist, $MMtable= '', $MMuid=0, $currentTable= '', $conf=[])
purgeDeletePlaceholder($tableName, array $ids)
static getRecord($table, $uid, $fields= '*', $where= '', $useDeleteClause=true)
readList($itemlist, array $configuration)
completeOppositeUsageValues($tableName, array $referenceValues)
static isOnSymmetricSide($parentUid, $parentConf, $childRec)
writeMM($MM_tableName, $uid, $prependTableName=false)
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)
static stripLogicalOperatorPrefix(string $constraint)
remapMM($MM_tableName, $uid, $newUid, $prependTableName=false)
static intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)