‪TYPO3CMS  10.4
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 
28 
35 {
41  protected ‪$fetchAllFields = false;
42 
48  public ‪$registerNonTableValues = false;
49 
56  public ‪$tableArray = [];
57 
63  public $itemArray = [];
64 
70  public $nonTableArray = [];
71 
75  public $additionalWhere = [];
76 
82  public $checkIfDeleted = true;
83 
89  public $firstTable = '';
90 
96  public $secondTable = '';
97 
104  public $MM_is_foreign = false;
105 
111  public $MM_oppositeField = '';
112 
118  public $MM_oppositeTable = '';
119 
125  public $MM_oppositeFieldConf = [];
126 
132  public $MM_isMultiTableRelationship = '';
133 
139  public $currentTable;
140 
147  public $undeleteRecord;
148 
155  public $MM_match_fields = [];
156 
162  public $MM_hasUidField;
163 
169  public $MM_insert_fields = [];
170 
176  public $MM_table_where = '';
177 
183  protected $MM_oppositeUsage;
184 
190  protected $updateReferenceIndex = true;
191 
195  protected $referenceIndexUpdater;
196 
200  protected $useLiveParentIds = true;
201 
205  protected $useLiveReferenceIds = true;
206 
210  protected $workspaceId;
211 
215  protected $purged = false;
216 
222  public $results = [];
223 
229  public function getWorkspaceId(): int
230  {
231  if (!isset($this->‪workspaceId)) {
232  $this->‪workspaceId = (int)‪$GLOBALS['BE_USER']->workspace;
233  }
234  return $this->workspaceId;
235  }
236 
242  public function ‪setWorkspaceId($workspaceId): void
243  {
244  $this->‪workspaceId = (int)$workspaceId;
245  }
246 
253  public function ‪setReferenceIndexUpdater(ReferenceIndexUpdater $updater): void
254  {
255  $this->referenceIndexUpdater = $updater;
256  }
257 
263  public function ‪isPurged()
264  {
265  return $this->purged;
266  }
267 
278  public function ‪start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = [])
279  {
280  $conf = (array)$conf;
281  // SECTION: MM reverse relations
282  $this->MM_is_foreign = (bool)($conf['MM_opposite_field'] ?? false);
283  $this->MM_oppositeField = $conf['MM_opposite_field'] ?? null;
284  $this->MM_table_where = $conf['MM_table_where'] ?? null;
285  $this->MM_hasUidField = $conf['MM_hasUidField'] ?? null;
286  $this->MM_match_fields = (isset($conf['MM_match_fields']) && is_array($conf['MM_match_fields'])) ? $conf['MM_match_fields'] : [];
287  $this->MM_insert_fields = (isset($conf['MM_insert_fields']) && is_array($conf['MM_insert_fields'])) ? $conf['MM_insert_fields'] : $this->MM_match_fields;
288  $this->currentTable = $currentTable;
289  if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) {
290  $this->MM_oppositeUsage = $conf['MM_oppositeUsage'];
291  }
292  if ($this->MM_is_foreign) {
293  $allowedTableList = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
294  // Normally, $conf['allowed'] can contain a list of tables,
295  // but as we are looking at a MM relation from the foreign side,
296  // it only makes sense to allow one one table in $conf['allowed']
297  [$this->MM_oppositeTable] = ‪GeneralUtility::trimExplode(',', $allowedTableList);
298  // Only add the current table name if there is more than one allowed field
299  // We must be sure this has been done at least once before accessing the "columns" part of TCA for a table.
300  $this->MM_oppositeFieldConf = ‪$GLOBALS['TCA'][$this->MM_oppositeTable]['columns'][$this->MM_oppositeField]['config'];
301  if ($this->MM_oppositeFieldConf['allowed']) {
302  $oppositeFieldConf_allowed = explode(',', $this->MM_oppositeFieldConf['allowed']);
303  if (count($oppositeFieldConf_allowed) > 1 || $this->MM_oppositeFieldConf['allowed'] === '*') {
304  $this->MM_isMultiTableRelationship = $oppositeFieldConf_allowed[0];
305  }
306  }
307  }
308  // SECTION: normal MM relations
309  // If the table list is "*" then all tables are used in the list:
310  if (trim($tablelist) === '*') {
311  $tablelist = implode(',', array_keys(‪$GLOBALS['TCA']));
312  }
313  // The tables are traversed and internal arrays are initialized:
314  $tempTableArray = ‪GeneralUtility::trimExplode(',', $tablelist, true);
315  foreach ($tempTableArray as $val) {
316  $tName = trim($val);
317  $this->tableArray[$tName] = [];
318  $deleteField = ‪$GLOBALS['TCA'][$tName]['ctrl']['delete'] ?? false;
319  if ($this->checkIfDeleted && $deleteField) {
320  $fieldN = $tName . '.' . $deleteField;
321  $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
322  }
323  }
324  if (is_array($this->tableArray)) {
325  reset($this->tableArray);
326  } else {
327  // No tables
328  return;
329  }
330  // Set first and second tables:
331  // Is the first table
332  $this->firstTable = key($this->tableArray);
333  next($this->tableArray);
334  // If the second table is set and the ID number is less than zero (later)
335  // then the record is regarded to come from the second table...
336  $this->secondTable = key($this->tableArray);
337  // Now, populate the internal itemArray and tableArray arrays:
338  // If MM, then call this function to do that:
339  if ($MMtable) {
340  if ($MMuid) {
341  $this->‪readMM($MMtable, $MMuid);
342  $this->‪purgeItemArray();
343  } else {
344  // Revert to readList() for new records in order to load possible default values from $itemlist
345  $this->‪readList($itemlist, $conf);
346  $this->‪purgeItemArray();
347  }
348  } elseif ($MMuid && isset($conf['foreign_field']) && (bool)$conf['foreign_field']) {
349  // If not MM but foreign_field, the read the records by the foreign_field
350  $this->‪readForeignField($MMuid, $conf);
351  } else {
352  // If not MM, then explode the itemlist by "," and traverse the list:
353  $this->‪readList($itemlist, $conf);
354  // Do automatic default_sortby, if any
355  if (isset($conf['foreign_default_sortby']) && $conf['foreign_default_sortby']) {
356  $this->‪sortList($conf['foreign_default_sortby']);
357  }
358  }
359  }
360 
366  public function ‪setFetchAllFields($allFields)
367  {
368  $this->fetchAllFields = (bool)$allFields;
369  }
370 
377  public function ‪setUpdateReferenceIndex($updateReferenceIndex)
378  {
379  $this->updateReferenceIndex = (bool)$updateReferenceIndex;
380  }
381 
385  public function ‪setUseLiveParentIds($useLiveParentIds)
386  {
387  $this->useLiveParentIds = (bool)$useLiveParentIds;
388  }
389 
393  public function ‪setUseLiveReferenceIds($useLiveReferenceIds)
394  {
395  $this->useLiveReferenceIds = (bool)$useLiveReferenceIds;
396  }
397 
404  public function ‪readList($itemlist, array $configuration)
405  {
406  if ((string)trim($itemlist) != '') {
407  $tempItemArray = ‪GeneralUtility::trimExplode(',', $itemlist);
408  // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
409  // if there were spaces in the list... I suppose this is better overall...
410  foreach ($tempItemArray as $key => $val) {
411  // Will be set to "true" if the entry was a real table/id
412  $isSet = false;
413  // Extract table name and id. This is in the formula [tablename]_[id]
414  // where table name MIGHT contain "_", hence the reversion of the string!
415  $val = strrev($val);
416  $parts = explode('_', $val, 2);
417  $theID = strrev($parts[0]);
418  // Check that the id IS an integer:
420  // Get the table name: If a part of the exploded string, use that.
421  // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
422  $theTable = trim($parts[1] ?? '')
423  ? strrev(trim($parts[1] ?? ''))
424  : ($this->secondTable && $theID < 0 ? $this->secondTable : $this->firstTable);
425  // If the ID is not blank and the table name is among the names in the inputted tableList
426  if (
427  (string)$theID != ''
428  // allow the default language '0' for the special languages configuration
429  && ($theID || ($configuration['special'] ?? null) === 'languages')
430  && $theTable && isset($this->tableArray[$theTable])
431  ) {
432  // Get ID as the right value:
433  $theID = $this->secondTable ? abs((int)$theID) : (int)$theID;
434  // Register ID/table name in internal arrays:
435  $this->itemArray[$key]['id'] = $theID;
436  $this->itemArray[$key]['table'] = $theTable;
437  $this->tableArray[$theTable][] = $theID;
438  // Set update-flag
439  $isSet = true;
440  }
441  }
442  // If it turns out that the value from the list was NOT a valid reference to a table-record,
443  // then we might still set it as a NO_TABLE value:
444  if (!$isSet && $this->registerNonTableValues) {
445  $this->itemArray[$key]['id'] = $tempItemArray[$key];
446  $this->itemArray[$key]['table'] = '_NO_TABLE';
447  $this->nonTableArray[] = $tempItemArray[$key];
448  }
449  }
450 
451  // Skip if not dealing with IRRE in a CSV list on a workspace
452  if ($configuration['type'] !== 'inline' || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
453  || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
454  || $this->getWorkspaceId() === 0 || !‪BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])) {
455  return;
456  }
457 
458  // Fetch live record data
459  if ($this->useLiveReferenceIds) {
460  foreach ($this->itemArray as &$item) {
461  $item['id'] = $this->‪getLiveDefaultId($item['table'], $item['id']);
462  }
463  } else {
464  // Directly overlay workspace data
465  $this->itemArray = [];
466  $foreignTable = $configuration['foreign_table'];
467  $ids = $this->‪getResolver($foreignTable, $this->tableArray[$foreignTable])->‪get();
468  foreach ($ids as $id) {
469  $this->itemArray[] = [
470  'id' => $id,
471  'table' => $foreignTable,
472  ];
473  }
474  }
475  }
476  }
477 
485  public function ‪sortList($sortby)
486  {
487  // Sort directly without fetching additional data
488  if ($sortby === 'uid') {
489  usort(
490  $this->itemArray,
491  function ($a, $b) {
492  return $a['id'] < $b['id'] ? -1 : 1;
493  }
494  );
495  } elseif (count($this->tableArray) === 1) {
496  reset($this->tableArray);
497  $table = (string)key($this->tableArray);
498  $connection = $this->‪getConnectionForTableName($table);
499  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
500 
501  foreach (array_chunk(current($this->tableArray), $maxBindParameters - 10, true) as $chunk) {
502  if (empty($chunk)) {
503  continue;
504  }
505  $this->itemArray = [];
506  $this->tableArray = [];
507  $queryBuilder = $connection->createQueryBuilder();
508  $queryBuilder->getRestrictions()->removeAll();
509  $queryBuilder->select('uid')
510  ->from($table)
511  ->where(
512  $queryBuilder->expr()->in(
513  'uid',
514  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
515  )
516  );
517  foreach (‪QueryHelper::parseOrderBy((string)$sortby) as $orderPair) {
518  [$fieldName, $order] = $orderPair;
519  $queryBuilder->addOrderBy($fieldName, $order);
520  }
521  $statement = $queryBuilder->execute();
522  while ($row = $statement->fetch()) {
523  $this->itemArray[] = ['id' => $row['uid'], 'table' => $table];
524  $this->tableArray[$table][] = $row['uid'];
525  }
526  }
527  }
528  }
529 
537  public function ‪readMM($tableName, $uid)
538  {
539  $key = 0;
540  $theTable = null;
541  $queryBuilder = $this->‪getConnectionForTableName($tableName)
543  $queryBuilder->‪getRestrictions()->‪removeAll();
544  $queryBuilder->select('*')->from($tableName);
545  // In case of a reverse relation
546  if ($this->MM_is_foreign) {
547  $uidLocal_field = 'uid_foreign';
548  $uidForeign_field = 'uid_local';
549  $sorting_field = 'sorting_foreign';
550  if ($this->MM_isMultiTableRelationship) {
551  // Be backwards compatible! When allowing more than one table after
552  // having previously allowed only one table, this case applies.
553  if ($this->currentTable == $this->MM_isMultiTableRelationship) {
554  $expression = $queryBuilder->expr()->orX(
555  $queryBuilder->expr()->eq(
556  'tablenames',
557  $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
558  ),
559  $queryBuilder->expr()->eq(
560  'tablenames',
561  $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
562  )
563  );
564  } else {
565  $expression = $queryBuilder->expr()->eq(
566  'tablenames',
567  $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
568  );
569  }
570  $queryBuilder->andWhere($expression);
571  }
572  $theTable = $this->MM_oppositeTable;
573  } else {
574  // Default
575  $uidLocal_field = 'uid_local';
576  $uidForeign_field = 'uid_foreign';
577  $sorting_field = 'sorting';
578  }
579  if ($this->MM_table_where) {
580  $queryBuilder->andWhere(
581  ‪QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where))
582  );
583  }
584  foreach ($this->MM_match_fields as $field => $value) {
585  $queryBuilder->andWhere(
586  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
587  );
588  }
589  $queryBuilder->andWhere(
590  $queryBuilder->expr()->eq(
591  $uidLocal_field,
592  $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)
593  )
594  );
595  $queryBuilder->orderBy($sorting_field);
596  $queryBuilder->addOrderBy($uidForeign_field);
597  $statement = $queryBuilder->execute();
598  while ($row = $statement->fetch()) {
599  // Default
600  if (!$this->MM_is_foreign) {
601  // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
602  $theTable = $row['tablenames'] ?: $this->firstTable;
603  }
604  if (($row[$uidForeign_field] || $theTable === 'pages') && $theTable && isset($this->tableArray[$theTable])) {
605  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
606  $this->itemArray[$key]['table'] = $theTable;
607  $this->tableArray[$theTable][] = $row[$uidForeign_field];
608  } elseif ($this->registerNonTableValues) {
609  $this->itemArray[$key]['id'] = $row[$uidForeign_field];
610  $this->itemArray[$key]['table'] = '_NO_TABLE';
611  $this->nonTableArray[] = $row[$uidForeign_field];
612  }
613  $key++;
614  }
615  }
616 
624  public function ‪writeMM($MM_tableName, $uid, $prependTableName = false)
625  {
626  $connection = $this->‪getConnectionForTableName($MM_tableName);
627  $expressionBuilder = $connection->createQueryBuilder()->expr();
628 
629  // In case of a reverse relation
630  if ($this->MM_is_foreign) {
631  $uidLocal_field = 'uid_foreign';
632  $uidForeign_field = 'uid_local';
633  $sorting_field = 'sorting_foreign';
634  } else {
635  // default
636  $uidLocal_field = 'uid_local';
637  $uidForeign_field = 'uid_foreign';
638  $sorting_field = 'sorting';
639  }
640  // If there are tables...
641  $tableC = count($this->tableArray);
642  if ($tableC) {
643  // Boolean: does the field "tablename" need to be filled?
644  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
645  $c = 0;
646  $additionalWhere_tablenames = '';
647  if ($this->MM_is_foreign && $prep) {
648  $additionalWhere_tablenames = $expressionBuilder->eq(
649  'tablenames',
650  $expressionBuilder->literal($this->currentTable)
651  );
652  }
653  $additionalWhere = $expressionBuilder->andX();
654  // Add WHERE clause if configured
655  if ($this->MM_table_where) {
656  $additionalWhere->add(
658  str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where)
659  )
660  );
661  }
662  // Select, update or delete only those relations that match the configured fields
663  foreach ($this->MM_match_fields as $field => $value) {
664  $additionalWhere->add($expressionBuilder->eq($field, $expressionBuilder->literal($value)));
665  }
666 
667  $queryBuilder = $connection->createQueryBuilder();
668  $queryBuilder->getRestrictions()->removeAll();
669  $queryBuilder->select($uidForeign_field)
670  ->from($MM_tableName)
671  ->where($queryBuilder->expr()->eq(
672  $uidLocal_field,
673  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
674  ))
675  ->orderBy($sorting_field);
676 
677  if ($prep) {
678  $queryBuilder->addSelect('tablenames');
679  }
680  if ($this->MM_hasUidField) {
681  $queryBuilder->addSelect('uid');
682  }
683  if ($additionalWhere_tablenames) {
684  $queryBuilder->andWhere($additionalWhere_tablenames);
685  }
686  if ($additionalWhere->count()) {
687  $queryBuilder->andWhere($additionalWhere);
688  }
689 
690  $result = $queryBuilder->execute();
691  $oldMMs = [];
692  // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
693  // If the UID is present it will be used to update sorting and delete MM-records.
694  // This is necessary if the "multiple" feature is used for the MM relations.
695  // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
696  $oldMMs_inclUid = [];
697  while ($row = $result->fetch()) {
698  if (!$this->MM_is_foreign && $prep) {
699  $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]];
700  } else {
701  $oldMMs[] = $row[$uidForeign_field];
702  }
703  $oldMMs_inclUid[] = [$row['tablenames'], $row[$uidForeign_field], $row['uid']];
704  }
705  // For each item, insert it:
706  foreach ($this->itemArray as $val) {
707  $c++;
708  if ($prep || $val['table'] === '_NO_TABLE') {
709  // Insert current table if needed
710  if ($this->MM_is_foreign) {
711  $tablename = $this->currentTable;
712  } else {
713  $tablename = $val['table'];
714  }
715  } else {
716  $tablename = '';
717  }
718  if (!$this->MM_is_foreign && $prep) {
719  $item = [$val['table'], $val['id']];
720  } else {
721  $item = $val['id'];
722  }
723  if (in_array($item, $oldMMs)) {
724  $oldMMs_index = array_search($item, $oldMMs);
725  // In principle, selecting on the UID is all we need to do
726  // if a uid field is available since that is unique!
727  // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
728  $queryBuilder = $connection->createQueryBuilder();
729  $queryBuilder->update($MM_tableName)
730  ->set($sorting_field, $c)
731  ->where(
732  $expressionBuilder->eq(
733  $uidLocal_field,
734  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
735  ),
736  $expressionBuilder->eq(
737  $uidForeign_field,
738  $queryBuilder->createNamedParameter($val['id'], \PDO::PARAM_INT)
739  )
740  );
741 
742  if ($additionalWhere->count()) {
743  $queryBuilder->andWhere($additionalWhere);
744  }
745  if ($this->MM_hasUidField) {
746  $queryBuilder->andWhere(
747  $expressionBuilder->eq(
748  'uid',
749  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMMs_index][2], \PDO::PARAM_INT)
750  )
751  );
752  }
753  if ($tablename) {
754  $queryBuilder->andWhere(
755  $expressionBuilder->eq(
756  'tablenames',
757  $queryBuilder->createNamedParameter($tablename, \PDO::PARAM_STR)
758  )
759  );
760  }
761 
762  $queryBuilder->execute();
763  // Remove the item from the $oldMMs array so after this
764  // foreach loop only the ones that need to be deleted are in there.
765  unset($oldMMs[$oldMMs_index]);
766  // Remove the item from the $oldMMs_inclUid array so after this
767  // foreach loop only the ones that need to be deleted are in there.
768  unset($oldMMs_inclUid[$oldMMs_index]);
769  } else {
770  $insertFields = $this->MM_insert_fields;
771  $insertFields[$uidLocal_field] = $uid;
772  $insertFields[$uidForeign_field] = $val['id'];
773  $insertFields[$sorting_field] = $c;
774  if ($tablename) {
775  $insertFields['tablenames'] = $tablename;
776  $insertFields = $this->‪completeOppositeUsageValues($tablename, $insertFields);
777  }
778  $connection->insert($MM_tableName, $insertFields);
779  if ($this->MM_is_foreign) {
780  $this->‪updateRefIndex($val['table'], $val['id']);
781  }
782  }
783  }
784  // Delete all not-used relations:
785  if (is_array($oldMMs) && !empty($oldMMs)) {
786  $queryBuilder = $connection->createQueryBuilder();
787  $removeClauses = $queryBuilder->expr()->orX();
788  $updateRefIndex_records = [];
789  foreach ($oldMMs as $oldMM_key => $mmItem) {
790  // If UID field is present, of course we need only use that for deleting.
791  if ($this->MM_hasUidField) {
792  $removeClauses->add($queryBuilder->expr()->eq(
793  'uid',
794  $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMM_key][2], \PDO::PARAM_INT)
795  ));
796  } else {
797  if (is_array($mmItem)) {
798  $removeClauses->add(
799  $queryBuilder->expr()->andX(
800  $queryBuilder->expr()->eq(
801  'tablenames',
802  $queryBuilder->createNamedParameter($mmItem[0], \PDO::PARAM_STR)
803  ),
804  $queryBuilder->expr()->eq(
805  $uidForeign_field,
806  $queryBuilder->createNamedParameter($mmItem[1], \PDO::PARAM_INT)
807  )
808  )
809  );
810  } else {
811  $removeClauses->add(
812  $queryBuilder->expr()->eq(
813  $uidForeign_field,
814  $queryBuilder->createNamedParameter($mmItem, \PDO::PARAM_INT)
815  )
816  );
817  }
818  }
819  if ($this->MM_is_foreign) {
820  if (is_array($mmItem)) {
821  $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]];
822  } else {
823  $updateRefIndex_records[] = [$this->firstTable, $mmItem];
824  }
825  }
826  }
827 
828  $queryBuilder->delete($MM_tableName)
829  ->where(
830  $queryBuilder->expr()->eq(
831  $uidLocal_field,
832  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
833  ),
834  $removeClauses
835  );
836 
837  if ($additionalWhere_tablenames) {
838  $queryBuilder->andWhere($additionalWhere_tablenames);
839  }
840  if ($additionalWhere->count()) {
841  $queryBuilder->andWhere($additionalWhere);
842  }
843 
844  $queryBuilder->execute();
845 
846  // Update ref index:
847  foreach ($updateRefIndex_records as $pair) {
848  $this->‪updateRefIndex($pair[0], $pair[1]);
849  }
850  }
851  // Update ref index; In DataHandler it is not certain that this will happen because
852  // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
853  // This could also have been fixed in updateDB in DataHandler, however I decided to do it here ...
854  $this->‪updateRefIndex($this->currentTable, $uid);
855  }
856  }
857 
867  public function ‪remapMM($MM_tableName, $uid, $newUid, $prependTableName = false)
868  {
869  // In case of a reverse relation
870  if ($this->MM_is_foreign) {
871  $uidLocal_field = 'uid_foreign';
872  } else {
873  // default
874  $uidLocal_field = 'uid_local';
875  }
876  // If there are tables...
877  $tableC = count($this->tableArray);
878  if ($tableC) {
879  $queryBuilder = $this->‪getConnectionForTableName($MM_tableName)
881  $queryBuilder->‪update($MM_tableName)
882  ->‪set($uidLocal_field, (int)$newUid)
883  ->‪where($queryBuilder->expr()->eq(
884  $uidLocal_field,
885  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
886  ));
887  // Boolean: does the field "tablename" need to be filled?
888  $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
889  if ($this->MM_is_foreign && $prep) {
890  $queryBuilder->‪andWhere(
891  $queryBuilder->expr()->eq(
892  'tablenames',
893  $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
894  )
895  );
896  }
897  // Add WHERE clause if configured
898  if ($this->MM_table_where) {
899  $queryBuilder->‪andWhere(
900  ‪QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where))
901  );
902  }
903  // Select, update or delete only those relations that match the configured fields
904  foreach ($this->MM_match_fields as $field => $value) {
905  $queryBuilder->‪andWhere(
906  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
907  );
908  }
909  $queryBuilder->‪execute();
910  }
911  }
912 
920  public function ‪readForeignField($uid, $conf)
921  {
922  if ($this->useLiveParentIds) {
923  $uid = $this->‪getLiveDefaultId($this->currentTable, $uid);
924  }
925 
926  $key = 0;
927  $uid = (int)$uid;
928  // skip further processing if $uid does not
929  // point to a valid parent record
930  if ($uid === 0) {
931  return;
932  }
933 
934  $foreign_table = $conf['foreign_table'];
935  $foreign_table_field = $conf['foreign_table_field'];
936  $useDeleteClause = !$this->undeleteRecord;
937  $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
938  $queryBuilder = $this->‪getConnectionForTableName($foreign_table)
940  $queryBuilder->‪getRestrictions()
941  ->‪removeAll();
942  // Use the deleteClause (e.g. "deleted=0") on this table
943  if ($useDeleteClause) {
944  $queryBuilder->getRestrictions()->‪add(GeneralUtility::makeInstance(DeletedRestriction::class));
945  }
946 
947  $queryBuilder->select('uid')
948  ->from($foreign_table);
949 
950  // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
951  if ($conf['symmetric_field']) {
952  $queryBuilder->where(
953  $queryBuilder->expr()->orX(
954  $queryBuilder->expr()->eq(
955  $conf['foreign_field'],
956  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
957  ),
958  $queryBuilder->expr()->eq(
959  $conf['symmetric_field'],
960  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
961  )
962  )
963  );
964  } else {
965  $queryBuilder->where($queryBuilder->expr()->eq(
966  $conf['foreign_field'],
967  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
968  ));
969  }
970  // If it's requested to look for the parent uid AND the parent table,
971  // add an additional SQL-WHERE clause
972  if ($foreign_table_field && $this->currentTable) {
973  $queryBuilder->andWhere(
974  $queryBuilder->expr()->eq(
975  $foreign_table_field,
976  $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
977  )
978  );
979  }
980  // Add additional where clause if foreign_match_fields are defined
981  foreach ($foreign_match_fields as $field => $value) {
982  $queryBuilder->andWhere(
983  $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
984  );
985  }
986  // Select children from the live(!) workspace only
987  if (‪BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
988  $queryBuilder->getRestrictions()->‪add(
989  GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getWorkspaceId())
990  );
991  }
992  // Get the correct sorting field
993  // Specific manual sortby for data handled by this field
994  $sortby = '';
995  if ($conf['foreign_sortby']) {
996  if ($conf['symmetric_sortby'] && $conf['symmetric_field']) {
997  // Sorting depends on, from which side of the relation we're looking at it
998  // This requires bypassing automatic quoting and setting of the default sort direction
999  // @TODO: Doctrine: generalize to standard SQL to guarantee database independency
1000  $queryBuilder->‪add(
1001  'orderBy',
1002  'CASE
1003  WHEN ' . $queryBuilder->expr()->eq($conf['foreign_field'], $uid) . '
1004  THEN ' . $queryBuilder->quoteIdentifier($conf['foreign_sortby']) . '
1005  ELSE ' . $queryBuilder->quoteIdentifier($conf['symmetric_sortby']) . '
1006  END'
1007  );
1008  } else {
1009  // Regular single-side behaviour
1010  $sortby = $conf['foreign_sortby'];
1011  }
1012  } elseif ($conf['foreign_default_sortby']) {
1013  // Specific default sortby for data handled by this field
1014  $sortby = $conf['foreign_default_sortby'];
1015  } elseif (‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
1016  // Manual sortby for all table records
1017  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1018  } elseif (‪$GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby']) {
1019  // Default sortby for all table records
1020  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
1021  }
1022 
1023  if (!empty($sortby)) {
1024  foreach (‪QueryHelper::parseOrderBy($sortby) as $orderPair) {
1025  [$fieldName, $sorting] = $orderPair;
1026  $queryBuilder->addOrderBy($fieldName, $sorting);
1027  }
1028  }
1030  // Get the rows from storage
1031  $rows = [];
1032  $result = $queryBuilder->execute();
1033  while ($row = $result->fetch()) {
1034  $rows[(int)$row['uid']] = $row;
1035  }
1036  if (!empty($rows)) {
1037  // Retrieve the parsed and prepared ORDER BY configuration for the resolver
1038  $sortby = $queryBuilder->getQueryPart('orderBy');
1039  $ids = $this->‪getResolver($foreign_table, array_keys($rows), $sortby)->‪get();
1040  foreach ($ids as $id) {
1041  $this->itemArray[$key]['id'] = $id;
1042  $this->itemArray[$key]['table'] = $foreign_table;
1043  $this->tableArray[$foreign_table][] = $id;
1044  $key++;
1045  }
1046  }
1047  }
1048 
1057  public function ‪writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = false)
1058  {
1059  if ($this->useLiveParentIds) {
1060  $parentUid = $this->‪getLiveDefaultId($this->currentTable, $parentUid);
1061  if (!empty($updateToUid)) {
1062  $updateToUid = $this->‪getLiveDefaultId($this->currentTable, $updateToUid);
1063  }
1064  }
1065 
1066  $c = 0;
1067  $foreign_table = $conf['foreign_table'];
1068  $foreign_field = $conf['foreign_field'];
1069  $symmetric_field = $conf['symmetric_field'];
1070  $foreign_table_field = $conf['foreign_table_field'];
1071  $foreign_match_fields = is_array($conf['foreign_match_fields']) ? $conf['foreign_match_fields'] : [];
1072  // If there are table items and we have a proper $parentUid
1073  if (‪MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
1074  // If updateToUid is not a positive integer, set it to '0', so it will be ignored
1075  if (!(‪MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
1076  $updateToUid = 0;
1077  }
1078  $considerWorkspaces = ‪BackendUtility::isTableWorkspaceEnabled($foreign_table);
1079  ‪$fields = 'uid,pid,' . $foreign_field;
1080  // Consider the symmetric field if defined:
1081  if ($symmetric_field) {
1082  ‪$fields .= ',' . $symmetric_field;
1083  }
1084  // Consider workspaces if defined and currently used:
1085  if ($considerWorkspaces) {
1086  ‪$fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
1087  }
1088  // Update all items
1089  foreach ($this->itemArray as $val) {
1090  $uid = $val['id'];
1091  $table = $val['table'];
1092  $row = [];
1093  // Fetch the current (not overwritten) relation record if we should handle symmetric relations
1094  if ($symmetric_field || $considerWorkspaces) {
1095  $row = ‪BackendUtility::getRecord($table, $uid, ‪$fields, '', true);
1096  if (empty($row)) {
1097  continue;
1098  }
1099  }
1100  $isOnSymmetricSide = false;
1101  if ($symmetric_field) {
1102  $isOnSymmetricSide = ‪self::isOnSymmetricSide((string)$parentUid, $conf, $row);
1103  }
1104  $updateValues = $foreign_match_fields;
1105  // No update to the uid is requested, so this is the normal behaviour
1106  // just update the fields and care about sorting
1107  if (!$updateToUid) {
1108  // Always add the pointer to the parent uid
1109  if ($isOnSymmetricSide) {
1110  $updateValues[$symmetric_field] = $parentUid;
1111  } else {
1112  $updateValues[$foreign_field] = $parentUid;
1113  }
1114  // If it is configured in TCA also to store the parent table in the child record, just do it
1115  if ($foreign_table_field && $this->currentTable) {
1116  $updateValues[$foreign_table_field] = $this->currentTable;
1117  }
1118  // Update sorting columns if not to be skipped
1119  if (!$skipSorting) {
1120  // Get the correct sorting field
1121  // Specific manual sortby for data handled by this field
1122  $sortby = '';
1123  if ($conf['foreign_sortby']) {
1124  $sortby = $conf['foreign_sortby'];
1125  } elseif (‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']) {
1126  // manual sortby for all table records
1127  $sortby = ‪$GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1128  }
1129  // Apply sorting on the symmetric side
1130  // (it depends on who created the relation, so what uid is in the symmetric_field):
1131  if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
1132  $sortby = $conf['symmetric_sortby'];
1133  } else {
1134  $tempSortBy = [];
1135  foreach (‪QueryHelper::parseOrderBy($sortby) as $orderPair) {
1136  [$fieldName, $order] = $orderPair;
1137  if ($order !== null) {
1138  $tempSortBy[] = implode(' ', $orderPair);
1139  } else {
1140  $tempSortBy[] = $fieldName;
1141  }
1142  }
1143  $sortby = implode(',', $tempSortBy);
1144  }
1145  if ($sortby) {
1146  $updateValues[$sortby] = ++$c;
1147  }
1148  }
1149  } else {
1150  if ($isOnSymmetricSide) {
1151  $updateValues[$symmetric_field] = $updateToUid;
1152  } else {
1153  $updateValues[$foreign_field] = $updateToUid;
1154  }
1155  }
1156  // Update accordant fields in the database:
1157  if (!empty($updateValues)) {
1158  // Update tstamp if any foreign field value has changed
1159  if (!empty(‪$GLOBALS['TCA'][$table]['ctrl']['tstamp'])) {
1160  $updateValues[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = ‪$GLOBALS['EXEC_TIME'];
1161  }
1162  $this->‪getConnectionForTableName($table)
1163  ->‪update(
1164  $table,
1165  $updateValues,
1166  ['uid' => (int)$uid]
1167  );
1168  $this->‪updateRefIndex($table, $uid);
1169  }
1170  // Update accordant fields in the database for workspaces overlays/placeholders:
1171  if ($considerWorkspaces) {
1172  // It's the specific versioned record -> update placeholder (if any)
1173  if (!empty($row['t3ver_oid']) && ‪VersionState::cast($row['t3ver_state'])->equals(‪VersionState::NEW_PLACEHOLDER_VERSION)) {
1174  $this->‪getConnectionForTableName($table)
1175  ->‪update(
1176  $table,
1177  $updateValues,
1178  ['uid' => (int)$row['t3ver_oid']]
1179  );
1180  }
1181  }
1182  }
1183  }
1184  }
1185 
1192  public function ‪getValueArray($prependTableName = false)
1193  {
1194  // INIT:
1195  $valueArray = [];
1196  $tableC = count($this->tableArray);
1197  // If there are tables in the table array:
1198  if ($tableC) {
1199  // If there are more than ONE table in the table array, then always prepend table names:
1200  $prep = $tableC > 1 || $prependTableName;
1201  // Traverse the array of items:
1202  foreach ($this->itemArray as $val) {
1203  $valueArray[] = ($prep && $val['table'] !== '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
1204  }
1205  }
1206  // Return the array
1207  return $valueArray;
1208  }
1209 
1218  public function ‪getFromDB()
1219  {
1220  // Traverses the tables listed:
1221  foreach ($this->tableArray as $table => $ids) {
1222  if (is_array($ids) && !empty($ids)) {
1223  $connection = $this->‪getConnectionForTableName($table);
1224  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1225 
1226  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1227  if ($this->fetchAllFields) {
1228  ‪$fields = '*';
1229  } else {
1230  ‪$fields = 'uid,pid';
1231  if (‪$GLOBALS['TCA'][$table]['ctrl']['label']) {
1232  // Title
1233  ‪$fields .= ',' . ‪$GLOBALS['TCA'][$table]['ctrl']['label'];
1234  }
1235  if (‪$GLOBALS['TCA'][$table]['ctrl']['label_alt']) {
1236  // Alternative Title-Fields
1237  ‪$fields .= ',' . ‪$GLOBALS['TCA'][$table]['ctrl']['label_alt'];
1238  }
1239  if (‪$GLOBALS['TCA'][$table]['ctrl']['thumbnail']) {
1240  // Thumbnail
1241  ‪$fields .= ',' . ‪$GLOBALS['TCA'][$table]['ctrl']['thumbnail'];
1242  }
1243  }
1244  $queryBuilder = $connection->createQueryBuilder();
1245  $queryBuilder->getRestrictions()->removeAll();
1246  $queryBuilder->select(...‪GeneralUtility::trimExplode(',', ‪$fields, true))
1247  ->from($table)
1248  ->where($queryBuilder->expr()->in(
1249  'uid',
1250  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1251  ));
1252  if ($this->additionalWhere[$table]) {
1253  $queryBuilder->andWhere(
1254  ‪QueryHelper::stripLogicalOperatorPrefix($this->additionalWhere[$table])
1255  );
1256  }
1257  $statement = $queryBuilder->execute();
1258  while ($row = $statement->fetch()) {
1259  $this->results[$table][$row['uid']] = $row;
1260  }
1261  }
1262  }
1263  }
1264  return $this->results;
1265  }
1266 
1280  public function ‪getResolvedItemArray(): array
1281  {
1282  $itemArray = [];
1283  foreach ($this->itemArray as $item) {
1284  if (isset($this->results[$item['table']][$item['id']])) {
1285  $itemArray[] = [
1286  'table' => $item['table'],
1287  'uid' => $item['id'],
1288  ];
1289  }
1290  }
1291  return $itemArray;
1292  }
1293 
1300  public function ‪countItems($returnAsArray = true)
1301  {
1302  $count = count($this->itemArray);
1303  if ($returnAsArray) {
1304  $count = [$count];
1305  }
1306  return $count;
1307  }
1308 
1319  public function ‪updateRefIndex($table, $uid): array
1320  {
1321  if (!$this->updateReferenceIndex) {
1322  return [];
1323  }
1324  if ($this->referenceIndexUpdater) {
1325  // Add to update registry if given
1326  $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)$uid, $this->getWorkspaceId());
1327  $statisticsArray = [];
1328  } else {
1329  // Update reference index directly if enabled
1330  $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1332  $referenceIndex->setWorkspaceId($this->getWorkspaceId());
1333  }
1334  $referenceIndex->enableRuntimeCache();
1335  $statisticsArray = $referenceIndex->updateRefIndexTable($table, $uid);
1336  }
1337  return $statisticsArray;
1338  }
1339 
1349  public function ‪convertItemArray()
1350  {
1351  // conversion is only required in a workspace context
1352  // (the case that version ids are submitted in a live context are rare)
1353  if ($this->getWorkspaceId() === 0) {
1354  return false;
1355  }
1356 
1357  $hasBeenConverted = false;
1358  foreach ($this->tableArray as $tableName => $ids) {
1359  if (empty($ids) || !‪BackendUtility::isTableWorkspaceEnabled($tableName)) {
1360  continue;
1361  }
1362 
1363  // convert live ids to version ids if available
1364  $convertedIds = $this->‪getResolver($tableName, $ids)
1366  ->‪setKeepMovePlaceholder(false)
1367  ->‪processVersionOverlays($ids);
1368  foreach ($this->itemArray as $index => $item) {
1369  if ($item['table'] !== $tableName) {
1370  continue;
1371  }
1372  $currentItemId = $item['id'];
1373  if (
1374  !isset($convertedIds[$currentItemId])
1375  || $currentItemId === $convertedIds[$currentItemId]
1376  ) {
1377  continue;
1378  }
1379  // adjust local item to use resolved version id
1380  $this->itemArray[$index]['id'] = $convertedIds[$currentItemId];
1381  $hasBeenConverted = true;
1382  }
1383  // update per-table reference for ids
1384  if ($hasBeenConverted) {
1385  $this->tableArray[$tableName] = array_values($convertedIds);
1386  }
1387  }
1388 
1389  return $hasBeenConverted;
1390  }
1391 
1396  public function ‪purgeItemArray($workspaceId = null)
1397  {
1398  if ($workspaceId === null) {
1399  $workspaceId = $this->getWorkspaceId();
1400  } else {
1401  $workspaceId = (int)$workspaceId;
1402  }
1403 
1404  // Ensure, only live relations are in the items Array
1405  if ($workspaceId === 0) {
1406  $purgeCallback = 'purgeVersionedIds';
1407  } else {
1408  // Otherwise, ensure that live relations are purged if version exists
1409  $purgeCallback = 'purgeLiveVersionedIds';
1410  }
1411 
1412  $itemArrayHasBeenPurged = $this->‪purgeItemArrayHandler($purgeCallback);
1413  $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1414  return $itemArrayHasBeenPurged;
1415  }
1416 
1422  public function ‪processDeletePlaceholder()
1423  {
1424  if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1425  return false;
1426  }
1427 
1428  return $this->‪purgeItemArrayHandler('purgeDeletePlaceholder');
1429  }
1430 
1437  protected function ‪purgeItemArrayHandler($purgeCallback)
1438  {
1439  $itemArrayHasBeenPurged = false;
1440 
1441  foreach ($this->tableArray as $itemTableName => $itemIds) {
1442  if (empty($itemIds) || !‪BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1443  continue;
1444  }
1445 
1446  $purgedItemIds = [];
1447  $callable =[$this, $purgeCallback];
1448  if (is_callable($callable)) {
1449  $purgedItemIds = call_user_func($callable, $itemTableName, $itemIds);
1450  }
1451 
1452  $removedItemIds = array_diff($itemIds, $purgedItemIds);
1453  foreach ($removedItemIds as $removedItemId) {
1454  $this->‪removeFromItemArray($itemTableName, $removedItemId);
1455  }
1456  $this->tableArray[$itemTableName] = $purgedItemIds;
1457  if (!empty($removedItemIds)) {
1458  $itemArrayHasBeenPurged = true;
1459  }
1460  }
1461 
1462  return $itemArrayHasBeenPurged;
1463  }
1464 
1472  protected function ‪purgeVersionedIds($tableName, array $ids)
1473  {
1474  $ids = $this->‪sanitizeIds($ids);
1475  $ids = (array)array_combine($ids, $ids);
1476  $connection = $this->‪getConnectionForTableName($tableName);
1477  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1478 
1479  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1480  $queryBuilder = $connection->createQueryBuilder();
1481  $queryBuilder->getRestrictions()->removeAll();
1482  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1483  ->from($tableName)
1484  ->where(
1485  $queryBuilder->expr()->in(
1486  't3ver_oid',
1487  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1488  ),
1489  $queryBuilder->expr()->neq(
1490  't3ver_wsid',
1491  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1492  )
1493  )
1494  ->orderBy('t3ver_state', 'DESC')
1495  ->execute();
1496 
1497  while ($version = $result->fetch()) {
1498  $versionId = $version['uid'];
1499  if (isset($ids[$versionId])) {
1500  unset($ids[$versionId]);
1501  }
1502  }
1503  }
1504 
1505  return array_values($ids);
1506  }
1507 
1515  protected function ‪purgeLiveVersionedIds($tableName, array $ids)
1516  {
1517  $ids = $this->‪sanitizeIds($ids);
1518  $ids = (array)array_combine($ids, $ids);
1519  $connection = $this->‪getConnectionForTableName($tableName);
1520  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1521 
1522  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1523  $queryBuilder = $connection->createQueryBuilder();
1524  $queryBuilder->getRestrictions()->removeAll();
1525  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1526  ->from($tableName)
1527  ->where(
1528  $queryBuilder->expr()->in(
1529  't3ver_oid',
1530  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1531  ),
1532  $queryBuilder->expr()->neq(
1533  't3ver_wsid',
1534  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1535  )
1536  )
1537  ->orderBy('t3ver_state', 'DESC')
1538  ->execute();
1539 
1540  while ($version = $result->fetch()) {
1541  $versionId = $version['uid'];
1542  $liveId = $version['t3ver_oid'];
1543  if (isset($ids[$liveId]) && isset($ids[$versionId])) {
1544  unset($ids[$liveId]);
1545  }
1546  }
1547  }
1548 
1549  return array_values($ids);
1550  }
1551 
1559  protected function ‪purgeDeletePlaceholder($tableName, array $ids)
1560  {
1561  $ids = $this->‪sanitizeIds($ids);
1562  $ids = array_combine($ids, $ids) ?: [];
1563  $connection = $this->‪getConnectionForTableName($tableName);
1564  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1565 
1566  foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1567  $queryBuilder = $connection->createQueryBuilder();
1568  $queryBuilder->getRestrictions()->removeAll();
1569  $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1570  ->from($tableName)
1571  ->where(
1572  $queryBuilder->expr()->in(
1573  't3ver_oid',
1574  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1575  ),
1576  $queryBuilder->expr()->eq(
1577  't3ver_wsid',
1578  $queryBuilder->createNamedParameter(
1579  $this->getWorkspaceId(),
1580  \PDO::PARAM_INT
1581  )
1582  ),
1583  $queryBuilder->expr()->eq(
1584  't3ver_state',
1585  $queryBuilder->createNamedParameter(
1587  \PDO::PARAM_INT
1588  )
1589  )
1590  )
1591  ->execute();
1592 
1593  while ($version = $result->fetch()) {
1594  $liveId = $version['t3ver_oid'];
1595  if (isset($ids[$liveId])) {
1596  unset($ids[$liveId]);
1597  }
1598  }
1599  }
1600 
1601  return array_values($ids);
1602  }
1603 
1604  protected function ‪removeFromItemArray($tableName, $id)
1605  {
1606  foreach ($this->itemArray as $index => $item) {
1607  if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) {
1608  unset($this->itemArray[$index]);
1609  return true;
1610  }
1611  }
1612  return false;
1613  }
1614 
1623  public static function ‪isOnSymmetricSide($parentUid, $parentConf, $childRec)
1624  {
1625  return ‪MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1626  && $parentConf['symmetric_field']
1627  && $parentUid == $childRec[$parentConf['symmetric_field']];
1628  }
1629 
1638  protected function ‪completeOppositeUsageValues($tableName, array $referenceValues)
1639  {
1640  if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1641  return $referenceValues;
1642  }
1643 
1644  $fieldName = $this->MM_oppositeUsage[$tableName][0];
1645  if (empty(‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1646  return $referenceValues;
1647  }
1648 
1649  $configuration = ‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1650  if (!empty($configuration['MM_insert_fields'])) {
1651  $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues);
1652  } elseif (!empty($configuration['MM_match_fields'])) {
1653  $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1654  }
1655 
1656  return $referenceValues;
1657  }
1658 
1667  protected function ‪getLiveDefaultId($tableName, $id)
1668  {
1669  $liveDefaultId = ‪BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1670  if ($liveDefaultId === null) {
1671  $liveDefaultId = $id;
1672  }
1673  return (int)$liveDefaultId;
1674  }
1675 
1682  protected function ‪sanitizeIds(array $ids): array
1683  {
1684  return array_filter($ids);
1685  }
1686 
1693  protected function ‪getResolver($tableName, array $ids, array $sortingStatement = null)
1694  {
1696  $resolver = GeneralUtility::makeInstance(
1697  PlainDataResolver::class,
1698  $tableName,
1699  $ids,
1700  $sortingStatement
1701  );
1702  $resolver->setWorkspaceId($this->getWorkspaceId());
1703  $resolver->setKeepDeletePlaceholder(true);
1704  $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1705  return $resolver;
1706  }
1707 
1712  protected function ‪getConnectionForTableName(string $tableName)
1713  {
1714  return GeneralUtility::makeInstance(ConnectionPool::class)
1715  ->getConnectionForTable($tableName);
1716  }
1717 }
‪TYPO3\CMS\Core\Database\RelationHandler\workspaceId
‪array< int, $itemArray=array();public array $nonTableArray=array();public array $additionalWhere=array();public bool $checkIfDeleted=true;public string $firstTable='';public string $secondTable='';public bool $MM_is_foreign=false;public string $MM_oppositeField='';public string $MM_oppositeTable='';public array $MM_oppositeFieldConf=array();public string $MM_isMultiTableRelationship='';public string $currentTable;public bool $undeleteRecord;public array $MM_match_fields=array();public bool $MM_hasUidField;public array $MM_insert_fields=array();public 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();public int function getWorkspaceId():int { if(!isset( $this->workspaceId)) { $this-> workspaceId
Definition: RelationHandler.php:204
‪TYPO3\CMS\Core\Database\Query\QueryHelper\parseOrderBy
‪static array array[] parseOrderBy(string $input)
Definition: QueryHelper.php:44
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\set
‪QueryBuilder set(string $key, $value, bool $createNamedParameter=true, int $type=\PDO::PARAM_STR)
Definition: QueryBuilder.php:660
‪TYPO3\CMS\Core\Database\RelationHandler\purgeItemArrayHandler
‪bool purgeItemArrayHandler($purgeCallback)
Definition: RelationHandler.php:1409
‪TYPO3\CMS\Core\Database\RelationHandler\purgeItemArray
‪bool purgeItemArray($workspaceId=null)
Definition: RelationHandler.php:1368
‪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:1487
‪TYPO3\CMS\Core\DataHandling\PlainDataResolver
Definition: PlainDataResolver.php:34
‪TYPO3\CMS\Core\Database\RelationHandler\readList
‪readList($itemlist, array $configuration)
Definition: RelationHandler.php:376
‪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:457
‪TYPO3\CMS\Core\Database\RelationHandler\purgeDeletePlaceholder
‪array purgeDeletePlaceholder($tableName, array $ids)
Definition: RelationHandler.php:1531
‪TYPO3\CMS\Core\Database\RelationHandler
Definition: RelationHandler.php:35
‪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\Query\Restriction\QueryRestrictionContainerInterface\removeAll
‪QueryRestrictionContainerInterface removeAll()
‪TYPO3\CMS\Core\Database\RelationHandler\setReferenceIndexUpdater
‪setReferenceIndexUpdater(ReferenceIndexUpdater $updater)
Definition: RelationHandler.php:225
‪TYPO3\CMS\Core\Database\RelationHandler\getConnectionForTableName
‪Connection getConnectionForTableName(string $tableName)
Definition: RelationHandler.php:1684
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\getRestrictions
‪QueryRestrictionContainerInterface getRestrictions()
Definition: QueryBuilder.php:104
‪TYPO3\CMS\Backend\Utility\BackendUtility\getLiveVersionIdOfRecord
‪static int null getLiveVersionIdOfRecord($table, $uid)
Definition: BackendUtility.php:3765
‪TYPO3\CMS\Core\Database\RelationHandler\$tableArray
‪array $tableArray
Definition: RelationHandler.php:53
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\update
‪QueryBuilder update(string $update, string $alias=null)
Definition: QueryBuilder.php:497
‪TYPO3\CMS\Core\Versioning\VersionState\DELETE_PLACEHOLDER
‪const DELETE_PLACEHOLDER
Definition: VersionState.php:55
‪TYPO3\CMS\Core\Database\RelationHandler\getFromDB
‪array getFromDB()
Definition: RelationHandler.php:1190
‪TYPO3\CMS\Core\Database\RelationHandler\setUseLiveReferenceIds
‪setUseLiveReferenceIds($useLiveReferenceIds)
Definition: RelationHandler.php:365
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Core\Database\RelationHandler\isPurged
‪bool isPurged()
Definition: RelationHandler.php:235
‪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:596
‪TYPO3\CMS\Core\Database\RelationHandler\convertItemArray
‪bool convertItemArray()
Definition: RelationHandler.php:1321
‪TYPO3\CMS\Backend\Utility\BackendUtility\isTableWorkspaceEnabled
‪static bool isTableWorkspaceEnabled($table)
Definition: BackendUtility.php:4021
‪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:839
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\andWhere
‪QueryBuilder andWhere(... $where)
Definition: QueryBuilder.php:694
‪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\Query\QueryBuilder\execute
‪Statement Doctrine DBAL ForwardCompatibility Result Doctrine DBAL Driver ResultStatement int execute()
Definition: QueryBuilder.php:204
‪TYPO3\CMS\Core\Database\RelationHandler\purgeVersionedIds
‪array purgeVersionedIds($tableName, array $ids)
Definition: RelationHandler.php:1444
‪TYPO3\CMS\Core\Database\RelationHandler\completeOppositeUsageValues
‪array completeOppositeUsageValues($tableName, array $referenceValues)
Definition: RelationHandler.php:1610
‪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:1164
‪TYPO3\CMS\Core\Database\RelationHandler\getResolvedItemArray
‪array getResolvedItemArray()
Definition: RelationHandler.php:1252
‪TYPO3\CMS\Core\Database\RelationHandler\start
‪start($itemlist, $tablelist, $MMtable='', $MMuid=0, $currentTable='', $conf=[])
Definition: RelationHandler.php:250
‪TYPO3\CMS\Core\Database\RelationHandler\getLiveDefaultId
‪int getLiveDefaultId($tableName, $id)
Definition: RelationHandler.php:1639
‪TYPO3\CMS\Core\Database\RelationHandler\writeForeignField
‪writeForeignField($conf, $parentUid, $updateToUid=0, $skipSorting=false)
Definition: RelationHandler.php:1029
‪TYPO3\CMS\Core\Database\RelationHandler\updateRefIndex
‪array updateRefIndex($table, $uid)
Definition: RelationHandler.php:1291
‪TYPO3\CMS\Core\Database\RelationHandler\readMM
‪readMM($tableName, $uid)
Definition: RelationHandler.php:509
‪TYPO3\CMS\Core\Database\RelationHandler\countItems
‪mixed countItems($returnAsArray=true)
Definition: RelationHandler.php:1272
‪TYPO3\CMS\Core\Database\RelationHandler\readForeignField
‪readForeignField($uid, $conf)
Definition: RelationHandler.php:892
‪TYPO3\CMS\Backend\Utility\BackendUtility
Definition: BackendUtility.php:75
‪TYPO3\CMS\Core\Database\RelationHandler\$registerNonTableValues
‪bool $registerNonTableValues
Definition: RelationHandler.php:46
‪TYPO3\CMS\Backend\Utility\BackendUtility\getRecord
‪static array null getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
Definition: BackendUtility.php:95
‪TYPO3\CMS\Core\Versioning\VersionState
Definition: VersionState.php:24
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static string[] trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:1059
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:36
‪TYPO3\CMS\Core\Versioning\VersionState\NEW_PLACEHOLDER_VERSION
‪const NEW_PLACEHOLDER_VERSION
Definition: VersionState.php:33
‪TYPO3\CMS\Core\Database\RelationHandler\setFetchAllFields
‪setFetchAllFields($allFields)
Definition: RelationHandler.php:338
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:165
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\Database\RelationHandler\setUseLiveParentIds
‪setUseLiveParentIds($useLiveParentIds)
Definition: RelationHandler.php:357
‪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:1595
‪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:1665
‪TYPO3\CMS\Core\Database\RelationHandler\$fetchAllFields
‪bool $fetchAllFields
Definition: RelationHandler.php:40
‪TYPO3\CMS\Core\Database\RelationHandler\setWorkspaceId
‪setWorkspaceId($workspaceId)
Definition: RelationHandler.php:214
‪TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface\add
‪QueryRestrictionContainerInterface add(QueryRestrictionInterface $restriction)
‪TYPO3\CMS\Core\Database\RelationHandler\sanitizeIds
‪array sanitizeIds(array $ids)
Definition: RelationHandler.php:1654
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\Database\RelationHandler\setUpdateReferenceIndex
‪setUpdateReferenceIndex($updateReferenceIndex)
Definition: RelationHandler.php:349
‪TYPO3\CMS\Core\DataHandling\ReferenceIndexUpdater
Definition: ReferenceIndexUpdater.php:34
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\where
‪QueryBuilder where(... $predicates)
Definition: QueryBuilder.php:677
‪TYPO3\CMS\Core\Database\RelationHandler\removeFromItemArray
‪removeFromItemArray($tableName, $id)
Definition: RelationHandler.php:1576
‪TYPO3\CMS\Core\Database\RelationHandler\processDeletePlaceholder
‪bool processDeletePlaceholder()
Definition: RelationHandler.php:1394
‪TYPO3\CMS\Core\Database
Definition: Connection.php:18
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39