| Total Complexity | 260 |
| Total Lines | 1702 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like RelationHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use RelationHandler, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 36 | class RelationHandler |
||
| 37 | { |
||
| 38 | /** |
||
| 39 | * $fetchAllFields if false getFromDB() fetches only uid, pid, thumbnail and label fields (as defined in TCA) |
||
| 40 | * |
||
| 41 | * @var bool |
||
| 42 | */ |
||
| 43 | protected $fetchAllFields = true; |
||
| 44 | |||
| 45 | /** |
||
| 46 | * If set, values that are not ids in tables are normally discarded. By this options they will be preserved. |
||
| 47 | * |
||
| 48 | * @var bool |
||
| 49 | */ |
||
| 50 | public $registerNonTableValues = false; |
||
| 51 | |||
| 52 | /** |
||
| 53 | * Contains the table names as keys. The values are the id-values for each table. |
||
| 54 | * Should ONLY contain proper table names. |
||
| 55 | * |
||
| 56 | * @var array |
||
| 57 | */ |
||
| 58 | public $tableArray = []; |
||
| 59 | |||
| 60 | /** |
||
| 61 | * Contains items in a numeric array (table/id for each). Tablenames here might be "_NO_TABLE". Keeps |
||
| 62 | * the sorting of thee retrieved items. |
||
| 63 | * |
||
| 64 | * @var array<int, array<string, mixed>> |
||
| 65 | */ |
||
| 66 | public $itemArray = []; |
||
| 67 | |||
| 68 | /** |
||
| 69 | * Array for NON-table elements |
||
| 70 | * |
||
| 71 | * @var array |
||
| 72 | */ |
||
| 73 | public $nonTableArray = []; |
||
| 74 | |||
| 75 | /** |
||
| 76 | * @var array |
||
| 77 | */ |
||
| 78 | public $additionalWhere = []; |
||
| 79 | |||
| 80 | /** |
||
| 81 | * Deleted-column is added to additionalWhere... if this is set... |
||
| 82 | * |
||
| 83 | * @var bool |
||
| 84 | */ |
||
| 85 | public $checkIfDeleted = true; |
||
| 86 | |||
| 87 | /** |
||
| 88 | * Will contain the first table name in the $tablelist (for positive ids) |
||
| 89 | * |
||
| 90 | * @var string |
||
| 91 | */ |
||
| 92 | protected $firstTable = ''; |
||
| 93 | |||
| 94 | /** |
||
| 95 | * If TRUE, uid_local and uid_foreign are switched, and the current table |
||
| 96 | * is inserted as tablename - this means you display a foreign relation "from the opposite side" |
||
| 97 | * |
||
| 98 | * @var bool |
||
| 99 | */ |
||
| 100 | protected $MM_is_foreign = false; |
||
| 101 | |||
| 102 | /** |
||
| 103 | * Is empty by default; if MM_is_foreign is set and there is more than one table |
||
| 104 | * allowed (on the "local" side), then it contains the first table (as a fallback) |
||
| 105 | * @var string |
||
| 106 | */ |
||
| 107 | protected $MM_isMultiTableRelationship = ''; |
||
| 108 | |||
| 109 | /** |
||
| 110 | * Current table => Only needed for reverse relations |
||
| 111 | * |
||
| 112 | * @var string |
||
| 113 | */ |
||
| 114 | protected $currentTable; |
||
| 115 | |||
| 116 | /** |
||
| 117 | * If a record should be undeleted |
||
| 118 | * (so do not use the $useDeleteClause on \TYPO3\CMS\Backend\Utility\BackendUtility) |
||
| 119 | * |
||
| 120 | * @var bool |
||
| 121 | */ |
||
| 122 | public $undeleteRecord; |
||
| 123 | |||
| 124 | /** |
||
| 125 | * Array of fields value pairs that should match while SELECT |
||
| 126 | * and will be written into MM table if $MM_insert_fields is not set |
||
| 127 | * |
||
| 128 | * @var array |
||
| 129 | */ |
||
| 130 | protected $MM_match_fields = []; |
||
| 131 | |||
| 132 | /** |
||
| 133 | * This is set to TRUE if the MM table has a UID field. |
||
| 134 | * |
||
| 135 | * @var bool |
||
| 136 | */ |
||
| 137 | protected $MM_hasUidField; |
||
| 138 | |||
| 139 | /** |
||
| 140 | * Array of fields and value pairs used for insert in MM table |
||
| 141 | * |
||
| 142 | * @var array |
||
| 143 | */ |
||
| 144 | protected $MM_insert_fields = []; |
||
| 145 | |||
| 146 | /** |
||
| 147 | * Extra MM table where |
||
| 148 | * |
||
| 149 | * @var string |
||
| 150 | */ |
||
| 151 | protected $MM_table_where = ''; |
||
| 152 | |||
| 153 | /** |
||
| 154 | * Usage of an MM field on the opposite relation. |
||
| 155 | * |
||
| 156 | * @var array |
||
| 157 | */ |
||
| 158 | protected $MM_oppositeUsage; |
||
| 159 | |||
| 160 | /** |
||
| 161 | * If false, reference index is not updated. |
||
| 162 | * |
||
| 163 | * @var bool |
||
| 164 | * @deprecated since v11, will be removed in v12 |
||
| 165 | */ |
||
| 166 | protected $updateReferenceIndex = true; |
||
| 167 | |||
| 168 | /** |
||
| 169 | * @var ReferenceIndexUpdater|null |
||
| 170 | */ |
||
| 171 | protected $referenceIndexUpdater; |
||
| 172 | |||
| 173 | /** |
||
| 174 | * @var bool |
||
| 175 | */ |
||
| 176 | protected $useLiveParentIds = true; |
||
| 177 | |||
| 178 | /** |
||
| 179 | * @var bool |
||
| 180 | */ |
||
| 181 | protected $useLiveReferenceIds = true; |
||
| 182 | |||
| 183 | /** |
||
| 184 | * @var int|null |
||
| 185 | */ |
||
| 186 | protected $workspaceId; |
||
| 187 | |||
| 188 | /** |
||
| 189 | * @var bool |
||
| 190 | */ |
||
| 191 | protected $purged = false; |
||
| 192 | |||
| 193 | /** |
||
| 194 | * This array will be filled by getFromDB(). |
||
| 195 | * |
||
| 196 | * @var array |
||
| 197 | */ |
||
| 198 | public $results = []; |
||
| 199 | |||
| 200 | /** |
||
| 201 | * Gets the current workspace id. |
||
| 202 | * |
||
| 203 | * @return int |
||
| 204 | */ |
||
| 205 | protected function getWorkspaceId(): int |
||
| 206 | { |
||
| 207 | $backendUser = $GLOBALS['BE_USER'] ?? null; |
||
| 208 | if (!isset($this->workspaceId)) { |
||
| 209 | $this->workspaceId = $backendUser instanceof BackendUserAuthentication ? (int)($backendUser->workspace) : 0; |
||
| 210 | } |
||
| 211 | return $this->workspaceId; |
||
| 212 | } |
||
| 213 | |||
| 214 | /** |
||
| 215 | * Sets the current workspace id. |
||
| 216 | * |
||
| 217 | * @param int $workspaceId |
||
| 218 | */ |
||
| 219 | public function setWorkspaceId($workspaceId): void |
||
| 220 | { |
||
| 221 | $this->workspaceId = (int)$workspaceId; |
||
| 222 | } |
||
| 223 | |||
| 224 | /** |
||
| 225 | * Setter to carry the 'deferred' reference index updater registry around. |
||
| 226 | * |
||
| 227 | * @param ReferenceIndexUpdater $updater |
||
| 228 | * @internal Used internally within DataHandler only |
||
| 229 | */ |
||
| 230 | public function setReferenceIndexUpdater(ReferenceIndexUpdater $updater): void |
||
| 231 | { |
||
| 232 | $this->referenceIndexUpdater = $updater; |
||
| 233 | } |
||
| 234 | |||
| 235 | /** |
||
| 236 | * Whether item array has been purged in this instance. |
||
| 237 | * |
||
| 238 | * @return bool |
||
| 239 | */ |
||
| 240 | public function isPurged() |
||
| 241 | { |
||
| 242 | return $this->purged; |
||
| 243 | } |
||
| 244 | |||
| 245 | /** |
||
| 246 | * Initialization of the class. |
||
| 247 | * |
||
| 248 | * @param string $itemlist List of group/select items |
||
| 249 | * @param string $tablelist Comma list of tables, first table takes priority if no table is set for an entry in the list. |
||
| 250 | * @param string $MMtable Name of a MM table. |
||
| 251 | * @param int|string $MMuid Local UID for MM lookup. May be a string for newly created elements. |
||
| 252 | * @param string $currentTable Current table name |
||
| 253 | * @param array $conf TCA configuration for current field |
||
| 254 | */ |
||
| 255 | public function start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = []) |
||
| 256 | { |
||
| 257 | $conf = (array)$conf; |
||
| 258 | // SECTION: MM reverse relations |
||
| 259 | $this->MM_is_foreign = (bool)($conf['MM_opposite_field'] ?? false); |
||
| 260 | $this->MM_table_where = $conf['MM_table_where'] ?? null; |
||
| 261 | $this->MM_hasUidField = $conf['MM_hasUidField'] ?? null; |
||
| 262 | $this->MM_match_fields = (isset($conf['MM_match_fields']) && is_array($conf['MM_match_fields'])) ? $conf['MM_match_fields'] : []; |
||
| 263 | $this->MM_insert_fields = (isset($conf['MM_insert_fields']) && is_array($conf['MM_insert_fields'])) ? $conf['MM_insert_fields'] : $this->MM_match_fields; |
||
| 264 | $this->currentTable = $currentTable; |
||
| 265 | if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) { |
||
| 266 | $this->MM_oppositeUsage = $conf['MM_oppositeUsage']; |
||
| 267 | } |
||
| 268 | $mmOppositeTable = ''; |
||
| 269 | if ($this->MM_is_foreign) { |
||
| 270 | $allowedTableList = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table']; |
||
| 271 | // Normally, $conf['allowed'] can contain a list of tables, |
||
| 272 | // but as we are looking at a MM relation from the foreign side, |
||
| 273 | // it only makes sense to allow one table in $conf['allowed']. |
||
| 274 | [$mmOppositeTable] = GeneralUtility::trimExplode(',', $allowedTableList); |
||
| 275 | // Only add the current table name if there is more than one allowed |
||
| 276 | // field. We must be sure this has been done at least once before accessing |
||
| 277 | // the "columns" part of TCA for a table. |
||
| 278 | $mmOppositeAllowed = (string)($GLOBALS['TCA'][$mmOppositeTable]['columns'][$conf['MM_opposite_field'] ?? '']['config']['allowed'] ?? ''); |
||
| 279 | if ($mmOppositeAllowed !== '') { |
||
| 280 | $mmOppositeAllowedTables = explode(',', $mmOppositeAllowed); |
||
| 281 | if ($mmOppositeAllowed === '*' || count($mmOppositeAllowedTables) > 1) { |
||
| 282 | $this->MM_isMultiTableRelationship = $mmOppositeAllowedTables[0]; |
||
| 283 | } |
||
| 284 | } |
||
| 285 | } |
||
| 286 | // SECTION: normal MM relations |
||
| 287 | // If the table list is "*" then all tables are used in the list: |
||
| 288 | if (trim($tablelist) === '*') { |
||
| 289 | $tablelist = implode(',', array_keys($GLOBALS['TCA'])); |
||
| 290 | } |
||
| 291 | // The tables are traversed and internal arrays are initialized: |
||
| 292 | $tempTableArray = GeneralUtility::trimExplode(',', $tablelist, true); |
||
| 293 | foreach ($tempTableArray as $val) { |
||
| 294 | $tName = trim($val); |
||
| 295 | $this->tableArray[$tName] = []; |
||
| 296 | $deleteField = $GLOBALS['TCA'][$tName]['ctrl']['delete'] ?? false; |
||
| 297 | if ($this->checkIfDeleted && $deleteField) { |
||
| 298 | $fieldN = $tName . '.' . $deleteField; |
||
| 299 | if (!isset($this->additionalWhere[$tName])) { |
||
| 300 | $this->additionalWhere[$tName] = ''; |
||
| 301 | } |
||
| 302 | $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0'; |
||
| 303 | } |
||
| 304 | } |
||
| 305 | if (is_array($this->tableArray)) { |
||
|
|
|||
| 306 | reset($this->tableArray); |
||
| 307 | } else { |
||
| 308 | // No tables |
||
| 309 | return; |
||
| 310 | } |
||
| 311 | // Set first and second tables: |
||
| 312 | // Is the first table |
||
| 313 | $this->firstTable = (string)key($this->tableArray); |
||
| 314 | next($this->tableArray); |
||
| 315 | // Now, populate the internal itemArray and tableArray arrays: |
||
| 316 | // If MM, then call this function to do that: |
||
| 317 | if ($MMtable) { |
||
| 318 | if ($MMuid) { |
||
| 319 | $this->readMM($MMtable, $MMuid, $mmOppositeTable); |
||
| 320 | $this->purgeItemArray(); |
||
| 321 | } else { |
||
| 322 | // Revert to readList() for new records in order to load possible default values from $itemlist |
||
| 323 | $this->readList($itemlist, $conf); |
||
| 324 | $this->purgeItemArray(); |
||
| 325 | } |
||
| 326 | } elseif ($MMuid && ($conf['foreign_field'] ?? false)) { |
||
| 327 | // If not MM but foreign_field, the read the records by the foreign_field |
||
| 328 | $this->readForeignField($MMuid, $conf); |
||
| 329 | } else { |
||
| 330 | // If not MM, then explode the itemlist by "," and traverse the list: |
||
| 331 | $this->readList($itemlist, $conf); |
||
| 332 | // Do automatic default_sortby, if any |
||
| 333 | if (isset($conf['foreign_default_sortby']) && $conf['foreign_default_sortby']) { |
||
| 334 | $this->sortList($conf['foreign_default_sortby']); |
||
| 335 | } |
||
| 336 | } |
||
| 337 | } |
||
| 338 | |||
| 339 | /** |
||
| 340 | * Sets $fetchAllFields |
||
| 341 | * |
||
| 342 | * @param bool $allFields enables fetching of all fields in getFromDB() |
||
| 343 | */ |
||
| 344 | public function setFetchAllFields($allFields) |
||
| 345 | { |
||
| 346 | $this->fetchAllFields = (bool)$allFields; |
||
| 347 | } |
||
| 348 | |||
| 349 | /** |
||
| 350 | * Sets whether the reference index shall be updated. |
||
| 351 | * |
||
| 352 | * @param bool $updateReferenceIndex Whether the reference index shall be updated |
||
| 353 | * @deprecated since v11, will be removed in v12 |
||
| 354 | */ |
||
| 355 | public function setUpdateReferenceIndex($updateReferenceIndex) |
||
| 356 | { |
||
| 357 | trigger_error( |
||
| 358 | 'Calling RelationHandler->setUpdateReferenceIndex() is deprecated. Use setReferenceIndexUpdater() instead.', |
||
| 359 | E_USER_DEPRECATED |
||
| 360 | ); |
||
| 361 | $this->updateReferenceIndex = (bool)$updateReferenceIndex; |
||
| 362 | } |
||
| 363 | |||
| 364 | /** |
||
| 365 | * @param bool $useLiveParentIds |
||
| 366 | */ |
||
| 367 | public function setUseLiveParentIds($useLiveParentIds) |
||
| 368 | { |
||
| 369 | $this->useLiveParentIds = (bool)$useLiveParentIds; |
||
| 370 | } |
||
| 371 | |||
| 372 | /** |
||
| 373 | * @param bool $useLiveReferenceIds |
||
| 374 | */ |
||
| 375 | public function setUseLiveReferenceIds($useLiveReferenceIds) |
||
| 376 | { |
||
| 377 | $this->useLiveReferenceIds = (bool)$useLiveReferenceIds; |
||
| 378 | } |
||
| 379 | |||
| 380 | /** |
||
| 381 | * Explodes the item list and stores the parts in the internal arrays itemArray and tableArray from MM records. |
||
| 382 | * |
||
| 383 | * @param string $itemlist Item list |
||
| 384 | * @param array $configuration Parent field configuration |
||
| 385 | */ |
||
| 386 | protected function readList($itemlist, array $configuration) |
||
| 387 | { |
||
| 388 | if (trim((string)$itemlist) !== '') { |
||
| 389 | // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work |
||
| 390 | // if there were spaces in the list... I suppose this is better overall... |
||
| 391 | $tempItemArray = GeneralUtility::trimExplode(',', $itemlist); |
||
| 392 | // If the second table is set and the ID number is less than zero (later) |
||
| 393 | // then the record is regarded to come from the second table... |
||
| 394 | $secondTable = (string)(key($this->tableArray) ?? ''); |
||
| 395 | foreach ($tempItemArray as $key => $val) { |
||
| 396 | // Will be set to "true" if the entry was a real table/id |
||
| 397 | $isSet = false; |
||
| 398 | // Extract table name and id. This is in the formula [tablename]_[id] |
||
| 399 | // where table name MIGHT contain "_", hence the reversion of the string! |
||
| 400 | $val = strrev($val); |
||
| 401 | $parts = explode('_', $val, 2); |
||
| 402 | $theID = strrev($parts[0]); |
||
| 403 | // Check that the id IS an integer: |
||
| 404 | if (MathUtility::canBeInterpretedAsInteger($theID)) { |
||
| 405 | // Get the table name: If a part of the exploded string, use that. |
||
| 406 | // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table |
||
| 407 | $theTable = trim($parts[1] ?? '') |
||
| 408 | ? strrev(trim($parts[1] ?? '')) |
||
| 409 | : ($secondTable && $theID < 0 ? $secondTable : $this->firstTable); |
||
| 410 | // If the ID is not blank and the table name is among the names in the inputted tableList |
||
| 411 | if ((string)$theID != '' && $theID && $theTable && isset($this->tableArray[$theTable])) { |
||
| 412 | // Get ID as the right value: |
||
| 413 | $theID = $secondTable ? abs((int)$theID) : (int)$theID; |
||
| 414 | // Register ID/table name in internal arrays: |
||
| 415 | $this->itemArray[$key]['id'] = $theID; |
||
| 416 | $this->itemArray[$key]['table'] = $theTable; |
||
| 417 | $this->tableArray[$theTable][] = $theID; |
||
| 418 | // Set update-flag |
||
| 419 | $isSet = true; |
||
| 420 | } |
||
| 421 | } |
||
| 422 | // If it turns out that the value from the list was NOT a valid reference to a table-record, |
||
| 423 | // then we might still set it as a NO_TABLE value: |
||
| 424 | if (!$isSet && $this->registerNonTableValues) { |
||
| 425 | $this->itemArray[$key]['id'] = $tempItemArray[$key]; |
||
| 426 | $this->itemArray[$key]['table'] = '_NO_TABLE'; |
||
| 427 | $this->nonTableArray[] = $tempItemArray[$key]; |
||
| 428 | } |
||
| 429 | } |
||
| 430 | |||
| 431 | // Skip if not dealing with IRRE in a CSV list on a workspace |
||
| 432 | if (!isset($configuration['type']) || $configuration['type'] !== 'inline' |
||
| 433 | || empty($configuration['foreign_table']) || !empty($configuration['foreign_field']) |
||
| 434 | || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']]) |
||
| 435 | || $this->getWorkspaceId() === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table']) |
||
| 436 | ) { |
||
| 437 | return; |
||
| 438 | } |
||
| 439 | |||
| 440 | // Fetch live record data |
||
| 441 | if ($this->useLiveReferenceIds) { |
||
| 442 | foreach ($this->itemArray as &$item) { |
||
| 443 | $item['id'] = $this->getLiveDefaultId($item['table'], $item['id']); |
||
| 444 | } |
||
| 445 | } else { |
||
| 446 | // Directly overlay workspace data |
||
| 447 | $this->itemArray = []; |
||
| 448 | $foreignTable = $configuration['foreign_table']; |
||
| 449 | $ids = $this->getResolver($foreignTable, $this->tableArray[$foreignTable])->get(); |
||
| 450 | foreach ($ids as $id) { |
||
| 451 | $this->itemArray[] = [ |
||
| 452 | 'id' => $id, |
||
| 453 | 'table' => $foreignTable, |
||
| 454 | ]; |
||
| 455 | } |
||
| 456 | } |
||
| 457 | } |
||
| 458 | } |
||
| 459 | |||
| 460 | /** |
||
| 461 | * Does a sorting on $this->itemArray depending on a default sortby field. |
||
| 462 | * This is only used for automatic sorting of comma separated lists. |
||
| 463 | * This function is only relevant for data that is stored in comma separated lists! |
||
| 464 | * |
||
| 465 | * @param string $sortby The default_sortby field/command (e.g. 'price DESC') |
||
| 466 | */ |
||
| 467 | protected function sortList($sortby) |
||
| 468 | { |
||
| 469 | // Sort directly without fetching additional data |
||
| 470 | if ($sortby === 'uid') { |
||
| 471 | usort( |
||
| 472 | $this->itemArray, |
||
| 473 | static function ($a, $b) { |
||
| 474 | return $a['id'] < $b['id'] ? -1 : 1; |
||
| 475 | } |
||
| 476 | ); |
||
| 477 | } elseif (count($this->tableArray) === 1) { |
||
| 478 | reset($this->tableArray); |
||
| 479 | $table = (string)key($this->tableArray); |
||
| 480 | $connection = $this->getConnectionForTableName($table); |
||
| 481 | $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform()); |
||
| 482 | |||
| 483 | foreach (array_chunk(current($this->tableArray), $maxBindParameters - 10, true) as $chunk) { |
||
| 484 | if (empty($chunk)) { |
||
| 485 | continue; |
||
| 486 | } |
||
| 487 | $this->itemArray = []; |
||
| 488 | $this->tableArray = []; |
||
| 489 | $queryBuilder = $connection->createQueryBuilder(); |
||
| 490 | $queryBuilder->getRestrictions()->removeAll(); |
||
| 491 | $queryBuilder->select('uid') |
||
| 492 | ->from($table) |
||
| 493 | ->where( |
||
| 494 | $queryBuilder->expr()->in( |
||
| 495 | 'uid', |
||
| 496 | $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY) |
||
| 497 | ) |
||
| 498 | ); |
||
| 499 | foreach (QueryHelper::parseOrderBy((string)$sortby) as $orderPair) { |
||
| 500 | [$fieldName, $order] = $orderPair; |
||
| 501 | $queryBuilder->addOrderBy($fieldName, $order); |
||
| 502 | } |
||
| 503 | $statement = $queryBuilder->execute(); |
||
| 504 | while ($row = $statement->fetchAssociative()) { |
||
| 505 | $this->itemArray[] = ['id' => $row['uid'], 'table' => $table]; |
||
| 506 | $this->tableArray[$table][] = $row['uid']; |
||
| 507 | } |
||
| 508 | } |
||
| 509 | } |
||
| 510 | } |
||
| 511 | |||
| 512 | /** |
||
| 513 | * Reads the record tablename/id into the internal arrays itemArray and tableArray from MM records. |
||
| 514 | * |
||
| 515 | * @todo: The source record is not checked for correct workspace. Say there is a category 5 in |
||
| 516 | * workspace 1. setWorkspace(0) is called, after that readMM('sys_category_record_mm', 5 ...). |
||
| 517 | * readMM will *still* return the list of records connected to this workspace 1 item, |
||
| 518 | * even though workspace 0 has been set. |
||
| 519 | * |
||
| 520 | * @param string $tableName MM Tablename |
||
| 521 | * @param int|string $uid Local UID |
||
| 522 | * @param string $mmOppositeTable Opposite table name |
||
| 523 | */ |
||
| 524 | protected function readMM($tableName, $uid, $mmOppositeTable) |
||
| 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 = $mmOppositeTable; |
||
| 560 | } else { |
||
| 561 | // Default |
||
| 562 | $uidLocal_field = 'uid_local'; |
||
| 563 | $uidForeign_field = 'uid_foreign'; |
||
| 564 | $sorting_field = 'sorting'; |
||
| 565 | } |
||
| 566 | if ($this->MM_table_where) { |
||
| 567 | if (GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('runtimeDbQuotingOfTcaConfiguration')) { |
||
| 568 | $queryBuilder->andWhere( |
||
| 569 | QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), $this->MM_table_where))) |
||
| 570 | ); |
||
| 571 | } else { |
||
| 572 | $queryBuilder->andWhere( |
||
| 573 | QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where)) |
||
| 574 | ); |
||
| 575 | } |
||
| 576 | } |
||
| 577 | foreach ($this->MM_match_fields as $field => $value) { |
||
| 578 | $queryBuilder->andWhere( |
||
| 579 | $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)) |
||
| 580 | ); |
||
| 581 | } |
||
| 582 | $queryBuilder->andWhere( |
||
| 583 | $queryBuilder->expr()->eq( |
||
| 584 | $uidLocal_field, |
||
| 585 | $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT) |
||
| 586 | ) |
||
| 587 | ); |
||
| 588 | $queryBuilder->orderBy($sorting_field); |
||
| 589 | $queryBuilder->addOrderBy($uidForeign_field); |
||
| 590 | $statement = $queryBuilder->execute(); |
||
| 591 | while ($row = $statement->fetchAssociative()) { |
||
| 592 | // Default |
||
| 593 | if (!$this->MM_is_foreign) { |
||
| 594 | // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable... |
||
| 595 | $theTable = !empty($row['tablenames']) ? $row['tablenames'] : $this->firstTable; |
||
| 596 | } |
||
| 597 | if (($row[$uidForeign_field] || $theTable === 'pages') && $theTable && isset($this->tableArray[$theTable])) { |
||
| 598 | $this->itemArray[$key]['id'] = $row[$uidForeign_field]; |
||
| 599 | $this->itemArray[$key]['table'] = $theTable; |
||
| 600 | $this->tableArray[$theTable][] = $row[$uidForeign_field]; |
||
| 601 | } elseif ($this->registerNonTableValues) { |
||
| 602 | $this->itemArray[$key]['id'] = $row[$uidForeign_field]; |
||
| 603 | $this->itemArray[$key]['table'] = '_NO_TABLE'; |
||
| 604 | $this->nonTableArray[] = $row[$uidForeign_field]; |
||
| 605 | } |
||
| 606 | $key++; |
||
| 607 | } |
||
| 608 | } |
||
| 609 | |||
| 610 | /** |
||
| 611 | * Writes the internal itemArray to MM table: |
||
| 612 | * |
||
| 613 | * @param string $MM_tableName MM table name |
||
| 614 | * @param int $uid Local UID |
||
| 615 | * @param bool $prependTableName If set, then table names will always be written. |
||
| 616 | */ |
||
| 617 | public function writeMM($MM_tableName, $uid, $prependTableName = false) |
||
| 618 | { |
||
| 619 | $connection = $this->getConnectionForTableName($MM_tableName); |
||
| 620 | $expressionBuilder = $connection->createQueryBuilder()->expr(); |
||
| 621 | |||
| 622 | // In case of a reverse relation |
||
| 623 | if ($this->MM_is_foreign) { |
||
| 624 | $uidLocal_field = 'uid_foreign'; |
||
| 625 | $uidForeign_field = 'uid_local'; |
||
| 626 | $sorting_field = 'sorting_foreign'; |
||
| 627 | } else { |
||
| 628 | // default |
||
| 629 | $uidLocal_field = 'uid_local'; |
||
| 630 | $uidForeign_field = 'uid_foreign'; |
||
| 631 | $sorting_field = 'sorting'; |
||
| 632 | } |
||
| 633 | // If there are tables... |
||
| 634 | $tableC = count($this->tableArray); |
||
| 635 | if ($tableC) { |
||
| 636 | // Boolean: does the field "tablename" need to be filled? |
||
| 637 | $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship; |
||
| 638 | $c = 0; |
||
| 639 | $additionalWhere_tablenames = ''; |
||
| 640 | if ($this->MM_is_foreign && $prep) { |
||
| 641 | $additionalWhere_tablenames = $expressionBuilder->eq( |
||
| 642 | 'tablenames', |
||
| 643 | $expressionBuilder->literal($this->currentTable) |
||
| 644 | ); |
||
| 645 | } |
||
| 646 | $additionalWhere = $expressionBuilder->andX(); |
||
| 647 | // Add WHERE clause if configured |
||
| 648 | if ($this->MM_table_where) { |
||
| 649 | $additionalWhere->add( |
||
| 650 | QueryHelper::stripLogicalOperatorPrefix( |
||
| 651 | str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where) |
||
| 652 | ) |
||
| 653 | ); |
||
| 654 | } |
||
| 655 | // Select, update or delete only those relations that match the configured fields |
||
| 656 | foreach ($this->MM_match_fields as $field => $value) { |
||
| 657 | $additionalWhere->add($expressionBuilder->eq($field, $expressionBuilder->literal($value))); |
||
| 658 | } |
||
| 659 | |||
| 660 | $queryBuilder = $connection->createQueryBuilder(); |
||
| 661 | $queryBuilder->getRestrictions()->removeAll(); |
||
| 662 | $queryBuilder->select($uidForeign_field) |
||
| 663 | ->from($MM_tableName) |
||
| 664 | ->where($queryBuilder->expr()->eq( |
||
| 665 | $uidLocal_field, |
||
| 666 | $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) |
||
| 667 | )) |
||
| 668 | ->orderBy($sorting_field); |
||
| 669 | |||
| 670 | if ($prep) { |
||
| 671 | $queryBuilder->addSelect('tablenames'); |
||
| 672 | } |
||
| 673 | if ($this->MM_hasUidField) { |
||
| 674 | $queryBuilder->addSelect('uid'); |
||
| 675 | } |
||
| 676 | if ($additionalWhere_tablenames) { |
||
| 677 | $queryBuilder->andWhere($additionalWhere_tablenames); |
||
| 678 | } |
||
| 679 | if ($additionalWhere->count()) { |
||
| 680 | $queryBuilder->andWhere($additionalWhere); |
||
| 681 | } |
||
| 682 | |||
| 683 | $result = $queryBuilder->execute(); |
||
| 684 | $oldMMs = []; |
||
| 685 | // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField). |
||
| 686 | // If the UID is present it will be used to update sorting and delete MM-records. |
||
| 687 | // This is necessary if the "multiple" feature is used for the MM relations. |
||
| 688 | // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs |
||
| 689 | $oldMMs_inclUid = []; |
||
| 690 | while ($row = $result->fetchAssociative()) { |
||
| 691 | if (!$this->MM_is_foreign && $prep) { |
||
| 692 | $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]]; |
||
| 693 | } else { |
||
| 694 | $oldMMs[] = $row[$uidForeign_field]; |
||
| 695 | } |
||
| 696 | $oldMMs_inclUid[] = (int)($row['uid'] ?? 0); |
||
| 697 | } |
||
| 698 | // For each item, insert it: |
||
| 699 | foreach ($this->itemArray as $val) { |
||
| 700 | $c++; |
||
| 701 | if ($prep || $val['table'] === '_NO_TABLE') { |
||
| 702 | // Insert current table if needed |
||
| 703 | if ($this->MM_is_foreign) { |
||
| 704 | $tablename = $this->currentTable; |
||
| 705 | } else { |
||
| 706 | $tablename = $val['table']; |
||
| 707 | } |
||
| 708 | } else { |
||
| 709 | $tablename = ''; |
||
| 710 | } |
||
| 711 | if (!$this->MM_is_foreign && $prep) { |
||
| 712 | $item = [$val['table'], $val['id']]; |
||
| 713 | } else { |
||
| 714 | $item = $val['id']; |
||
| 715 | } |
||
| 716 | if (in_array($item, $oldMMs)) { |
||
| 717 | $oldMMs_index = array_search($item, $oldMMs); |
||
| 718 | // In principle, selecting on the UID is all we need to do |
||
| 719 | // if a uid field is available since that is unique! |
||
| 720 | // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up. |
||
| 721 | $queryBuilder = $connection->createQueryBuilder(); |
||
| 722 | $queryBuilder->update($MM_tableName) |
||
| 723 | ->set($sorting_field, $c) |
||
| 724 | ->where( |
||
| 725 | $expressionBuilder->eq( |
||
| 726 | $uidLocal_field, |
||
| 727 | $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) |
||
| 728 | ), |
||
| 729 | $expressionBuilder->eq( |
||
| 730 | $uidForeign_field, |
||
| 731 | $queryBuilder->createNamedParameter($val['id'], \PDO::PARAM_INT) |
||
| 732 | ) |
||
| 733 | ); |
||
| 734 | |||
| 735 | if ($additionalWhere->count()) { |
||
| 736 | $queryBuilder->andWhere($additionalWhere); |
||
| 737 | } |
||
| 738 | if ($this->MM_hasUidField) { |
||
| 739 | $queryBuilder->andWhere( |
||
| 740 | $expressionBuilder->eq( |
||
| 741 | 'uid', |
||
| 742 | $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMMs_index], \PDO::PARAM_INT) |
||
| 743 | ) |
||
| 744 | ); |
||
| 745 | } |
||
| 746 | if ($tablename) { |
||
| 747 | $queryBuilder->andWhere( |
||
| 748 | $expressionBuilder->eq( |
||
| 749 | 'tablenames', |
||
| 750 | $queryBuilder->createNamedParameter($tablename, \PDO::PARAM_STR) |
||
| 751 | ) |
||
| 752 | ); |
||
| 753 | } |
||
| 754 | |||
| 755 | $queryBuilder->execute(); |
||
| 756 | // Remove the item from the $oldMMs array so after this |
||
| 757 | // foreach loop only the ones that need to be deleted are in there. |
||
| 758 | unset($oldMMs[$oldMMs_index]); |
||
| 759 | // Remove the item from the $oldMMs_inclUid array so after this |
||
| 760 | // foreach loop only the ones that need to be deleted are in there. |
||
| 761 | unset($oldMMs_inclUid[$oldMMs_index]); |
||
| 762 | } else { |
||
| 763 | $insertFields = $this->MM_insert_fields; |
||
| 764 | $insertFields[$uidLocal_field] = $uid; |
||
| 765 | $insertFields[$uidForeign_field] = $val['id']; |
||
| 766 | $insertFields[$sorting_field] = $c; |
||
| 767 | if ($tablename) { |
||
| 768 | $insertFields['tablenames'] = $tablename; |
||
| 769 | $insertFields = $this->completeOppositeUsageValues($tablename, $insertFields); |
||
| 770 | } |
||
| 771 | $connection->insert($MM_tableName, $insertFields); |
||
| 772 | if ($this->MM_is_foreign) { |
||
| 773 | $this->updateRefIndex($val['table'], $val['id']); |
||
| 774 | } |
||
| 775 | } |
||
| 776 | } |
||
| 777 | // Delete all not-used relations: |
||
| 778 | if (is_array($oldMMs) && !empty($oldMMs)) { |
||
| 779 | $queryBuilder = $connection->createQueryBuilder(); |
||
| 780 | $removeClauses = $queryBuilder->expr()->orX(); |
||
| 781 | $updateRefIndex_records = []; |
||
| 782 | foreach ($oldMMs as $oldMM_key => $mmItem) { |
||
| 783 | // If UID field is present, of course we need only use that for deleting. |
||
| 784 | if ($this->MM_hasUidField) { |
||
| 785 | $removeClauses->add($queryBuilder->expr()->eq( |
||
| 786 | 'uid', |
||
| 787 | $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMM_key], \PDO::PARAM_INT) |
||
| 788 | )); |
||
| 789 | } else { |
||
| 790 | if (is_array($mmItem)) { |
||
| 791 | $removeClauses->add( |
||
| 792 | $queryBuilder->expr()->andX( |
||
| 793 | $queryBuilder->expr()->eq( |
||
| 794 | 'tablenames', |
||
| 795 | $queryBuilder->createNamedParameter($mmItem[0], \PDO::PARAM_STR) |
||
| 796 | ), |
||
| 797 | $queryBuilder->expr()->eq( |
||
| 798 | $uidForeign_field, |
||
| 799 | $queryBuilder->createNamedParameter($mmItem[1], \PDO::PARAM_INT) |
||
| 800 | ) |
||
| 801 | ) |
||
| 802 | ); |
||
| 803 | } else { |
||
| 804 | $removeClauses->add( |
||
| 805 | $queryBuilder->expr()->eq( |
||
| 806 | $uidForeign_field, |
||
| 807 | $queryBuilder->createNamedParameter($mmItem, \PDO::PARAM_INT) |
||
| 808 | ) |
||
| 809 | ); |
||
| 810 | } |
||
| 811 | } |
||
| 812 | if ($this->MM_is_foreign) { |
||
| 813 | if (is_array($mmItem)) { |
||
| 814 | $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]]; |
||
| 815 | } else { |
||
| 816 | $updateRefIndex_records[] = [$this->firstTable, $mmItem]; |
||
| 817 | } |
||
| 818 | } |
||
| 819 | } |
||
| 820 | |||
| 821 | $queryBuilder->delete($MM_tableName) |
||
| 822 | ->where( |
||
| 823 | $queryBuilder->expr()->eq( |
||
| 824 | $uidLocal_field, |
||
| 825 | $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) |
||
| 826 | ), |
||
| 827 | $removeClauses |
||
| 828 | ); |
||
| 829 | |||
| 830 | if ($additionalWhere_tablenames) { |
||
| 831 | $queryBuilder->andWhere($additionalWhere_tablenames); |
||
| 832 | } |
||
| 833 | if ($additionalWhere->count()) { |
||
| 834 | $queryBuilder->andWhere($additionalWhere); |
||
| 835 | } |
||
| 836 | |||
| 837 | $queryBuilder->execute(); |
||
| 838 | |||
| 839 | // Update ref index: |
||
| 840 | foreach ($updateRefIndex_records as $pair) { |
||
| 841 | $this->updateRefIndex($pair[0], $pair[1]); |
||
| 842 | } |
||
| 843 | } |
||
| 844 | // Update ref index; In DataHandler it is not certain that this will happen because |
||
| 845 | // if only the MM field is changed the record itself is not updated and so the ref-index is not either. |
||
| 846 | // This could also have been fixed in updateDB in DataHandler, however I decided to do it here ... |
||
| 847 | $this->updateRefIndex($this->currentTable, $uid); |
||
| 848 | } |
||
| 849 | } |
||
| 850 | |||
| 851 | /** |
||
| 852 | * Remaps MM table elements from one local uid to another |
||
| 853 | * Does NOT update the reference index for you, must be called subsequently to do that! |
||
| 854 | * |
||
| 855 | * @param string $MM_tableName MM table name |
||
| 856 | * @param int $uid Local, current UID |
||
| 857 | * @param int $newUid Local, new UID |
||
| 858 | * @param bool $prependTableName If set, then table names will always be written. |
||
| 859 | * @deprecated since v11, will be removed with v12. |
||
| 860 | */ |
||
| 861 | public function remapMM($MM_tableName, $uid, $newUid, $prependTableName = false) |
||
| 862 | { |
||
| 863 | trigger_error( |
||
| 864 | 'Method ' . __METHOD__ . ' of class ' . __CLASS__ . ' is deprecated since v11 and will be removed in v12.', |
||
| 865 | E_USER_DEPRECATED |
||
| 866 | ); |
||
| 867 | |||
| 868 | // In case of a reverse relation |
||
| 869 | if ($this->MM_is_foreign) { |
||
| 870 | $uidLocal_field = 'uid_foreign'; |
||
| 871 | } else { |
||
| 872 | // default |
||
| 873 | $uidLocal_field = 'uid_local'; |
||
| 874 | } |
||
| 875 | // If there are tables... |
||
| 876 | $tableC = count($this->tableArray); |
||
| 877 | if ($tableC) { |
||
| 878 | $queryBuilder = $this->getConnectionForTableName($MM_tableName) |
||
| 879 | ->createQueryBuilder(); |
||
| 880 | $queryBuilder->update($MM_tableName) |
||
| 881 | ->set($uidLocal_field, (int)$newUid) |
||
| 882 | ->where($queryBuilder->expr()->eq( |
||
| 883 | $uidLocal_field, |
||
| 884 | $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) |
||
| 885 | )); |
||
| 886 | // Boolean: does the field "tablename" need to be filled? |
||
| 887 | $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship; |
||
| 888 | if ($this->MM_is_foreign && $prep) { |
||
| 889 | $queryBuilder->andWhere( |
||
| 890 | $queryBuilder->expr()->eq( |
||
| 891 | 'tablenames', |
||
| 892 | $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR) |
||
| 893 | ) |
||
| 894 | ); |
||
| 895 | } |
||
| 896 | // Add WHERE clause if configured |
||
| 897 | if ($this->MM_table_where) { |
||
| 898 | $queryBuilder->andWhere( |
||
| 899 | QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where)) |
||
| 900 | ); |
||
| 901 | } |
||
| 902 | // Select, update or delete only those relations that match the configured fields |
||
| 903 | foreach ($this->MM_match_fields as $field => $value) { |
||
| 904 | $queryBuilder->andWhere( |
||
| 905 | $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)) |
||
| 906 | ); |
||
| 907 | } |
||
| 908 | $queryBuilder->execute(); |
||
| 909 | } |
||
| 910 | } |
||
| 911 | |||
| 912 | /** |
||
| 913 | * Reads items from a foreign_table, that has a foreign_field (uid of the parent record) and |
||
| 914 | * stores the parts in the internal array itemArray and tableArray. |
||
| 915 | * |
||
| 916 | * @param int|string $uid The uid of the parent record (this value is also on the foreign_table in the foreign_field) |
||
| 917 | * @param array $conf TCA configuration for current field |
||
| 918 | */ |
||
| 919 | protected function readForeignField($uid, $conf) |
||
| 920 | { |
||
| 921 | if ($this->useLiveParentIds) { |
||
| 922 | $uid = $this->getLiveDefaultId($this->currentTable, $uid); |
||
| 923 | } |
||
| 924 | |||
| 925 | $key = 0; |
||
| 926 | $uid = (int)$uid; |
||
| 927 | // skip further processing if $uid does not |
||
| 928 | // point to a valid parent record |
||
| 929 | if ($uid === 0) { |
||
| 930 | return; |
||
| 931 | } |
||
| 932 | |||
| 933 | $foreign_table = $conf['foreign_table']; |
||
| 934 | $foreign_table_field = $conf['foreign_table_field'] ?? ''; |
||
| 935 | $useDeleteClause = !$this->undeleteRecord; |
||
| 936 | $foreign_match_fields = is_array($conf['foreign_match_fields'] ?? false) ? $conf['foreign_match_fields'] : []; |
||
| 937 | $queryBuilder = $this->getConnectionForTableName($foreign_table) |
||
| 938 | ->createQueryBuilder(); |
||
| 939 | $queryBuilder->getRestrictions() |
||
| 940 | ->removeAll(); |
||
| 941 | // Use the deleteClause (e.g. "deleted=0") on this table |
||
| 942 | if ($useDeleteClause) { |
||
| 943 | $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
||
| 944 | } |
||
| 945 | |||
| 946 | $queryBuilder->select('uid') |
||
| 947 | ->from($foreign_table); |
||
| 948 | |||
| 949 | // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field |
||
| 950 | if (!empty($conf['symmetric_field'])) { |
||
| 951 | $queryBuilder->where( |
||
| 952 | $queryBuilder->expr()->orX( |
||
| 953 | $queryBuilder->expr()->eq( |
||
| 954 | $conf['foreign_field'], |
||
| 955 | $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) |
||
| 956 | ), |
||
| 957 | $queryBuilder->expr()->eq( |
||
| 958 | $conf['symmetric_field'], |
||
| 959 | $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) |
||
| 960 | ) |
||
| 961 | ) |
||
| 962 | ); |
||
| 963 | } else { |
||
| 964 | $queryBuilder->where($queryBuilder->expr()->eq( |
||
| 965 | $conf['foreign_field'], |
||
| 966 | $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) |
||
| 967 | )); |
||
| 968 | } |
||
| 969 | // If it's requested to look for the parent uid AND the parent table, |
||
| 970 | // add an additional SQL-WHERE clause |
||
| 971 | if ($foreign_table_field && $this->currentTable) { |
||
| 972 | $queryBuilder->andWhere( |
||
| 973 | $queryBuilder->expr()->eq( |
||
| 974 | $foreign_table_field, |
||
| 975 | $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR) |
||
| 976 | ) |
||
| 977 | ); |
||
| 978 | } |
||
| 979 | // Add additional where clause if foreign_match_fields are defined |
||
| 980 | foreach ($foreign_match_fields as $field => $value) { |
||
| 981 | $queryBuilder->andWhere( |
||
| 982 | $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)) |
||
| 983 | ); |
||
| 984 | } |
||
| 985 | // Select children from the live(!) workspace only |
||
| 986 | if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) { |
||
| 987 | $queryBuilder->getRestrictions()->add( |
||
| 988 | GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getWorkspaceId()) |
||
| 989 | ); |
||
| 990 | } |
||
| 991 | // Get the correct sorting field |
||
| 992 | // Specific manual sortby for data handled by this field |
||
| 993 | $sortby = ''; |
||
| 994 | if (!empty($conf['foreign_sortby'])) { |
||
| 995 | if (!empty($conf['symmetric_sortby']) && !empty($conf['symmetric_field'])) { |
||
| 996 | // Sorting depends on, from which side of the relation we're looking at it |
||
| 997 | // This requires bypassing automatic quoting and setting of the default sort direction |
||
| 998 | // @TODO: Doctrine: generalize to standard SQL to guarantee database independency |
||
| 999 | $queryBuilder->add( |
||
| 1000 | 'orderBy', |
||
| 1001 | 'CASE |
||
| 1002 | WHEN ' . $queryBuilder->expr()->eq($conf['foreign_field'], $uid) . ' |
||
| 1003 | THEN ' . $queryBuilder->quoteIdentifier($conf['foreign_sortby']) . ' |
||
| 1004 | ELSE ' . $queryBuilder->quoteIdentifier($conf['symmetric_sortby']) . ' |
||
| 1005 | END' |
||
| 1006 | ); |
||
| 1007 | } else { |
||
| 1008 | // Regular single-side behaviour |
||
| 1009 | $sortby = $conf['foreign_sortby']; |
||
| 1010 | } |
||
| 1011 | } elseif (!empty($conf['foreign_default_sortby'])) { |
||
| 1012 | // Specific default sortby for data handled by this field |
||
| 1013 | $sortby = $conf['foreign_default_sortby']; |
||
| 1014 | } elseif (!empty($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'])) { |
||
| 1015 | // Manual sortby for all table records |
||
| 1016 | $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']; |
||
| 1017 | } elseif (!empty($GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'])) { |
||
| 1018 | // Default sortby for all table records |
||
| 1019 | $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby']; |
||
| 1020 | } |
||
| 1021 | |||
| 1022 | if (!empty($sortby)) { |
||
| 1023 | foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) { |
||
| 1024 | [$fieldName, $sorting] = $orderPair; |
||
| 1025 | $queryBuilder->addOrderBy($fieldName, $sorting); |
||
| 1026 | } |
||
| 1027 | } |
||
| 1028 | |||
| 1029 | // Get the rows from storage |
||
| 1030 | $rows = []; |
||
| 1031 | $result = $queryBuilder->execute(); |
||
| 1032 | while ($row = $result->fetchAssociative()) { |
||
| 1033 | $rows[(int)$row['uid']] = $row; |
||
| 1034 | } |
||
| 1035 | if (!empty($rows)) { |
||
| 1036 | // Retrieve the parsed and prepared ORDER BY configuration for the resolver |
||
| 1037 | $sortby = $queryBuilder->getQueryPart('orderBy'); |
||
| 1038 | $ids = $this->getResolver($foreign_table, array_keys($rows), $sortby)->get(); |
||
| 1039 | foreach ($ids as $id) { |
||
| 1040 | $this->itemArray[$key]['id'] = $id; |
||
| 1041 | $this->itemArray[$key]['table'] = $foreign_table; |
||
| 1042 | $this->tableArray[$foreign_table][] = $id; |
||
| 1043 | $key++; |
||
| 1044 | } |
||
| 1045 | } |
||
| 1046 | } |
||
| 1047 | |||
| 1048 | /** |
||
| 1049 | * Write the sorting values to a foreign_table, that has a foreign_field (uid of the parent record) |
||
| 1050 | * |
||
| 1051 | * @param array $conf TCA configuration for current field |
||
| 1052 | * @param int $parentUid The uid of the parent record |
||
| 1053 | * @param int $updateToUid If this is larger than zero it will be used as foreign UID instead of the given $parentUid (on Copy) |
||
| 1054 | * @param bool $skipSorting @deprecated since v11, will be dropped with v12. Simplify the if below when removing argument. |
||
| 1055 | */ |
||
| 1056 | public function writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = null) |
||
| 1057 | { |
||
| 1058 | // @deprecated since v11, will be removed with v12. |
||
| 1059 | if ($skipSorting !== null) { |
||
| 1060 | trigger_error( |
||
| 1061 | 'Calling ' . __METHOD__ . ' with 4th argument $skipSorting is deprecated and will be removed in v12.', |
||
| 1062 | E_USER_DEPRECATED |
||
| 1063 | ); |
||
| 1064 | } |
||
| 1065 | $skipSorting = (bool)$skipSorting; |
||
| 1066 | |||
| 1067 | if ($this->useLiveParentIds) { |
||
| 1068 | $parentUid = $this->getLiveDefaultId($this->currentTable, $parentUid); |
||
| 1069 | if (!empty($updateToUid)) { |
||
| 1070 | $updateToUid = $this->getLiveDefaultId($this->currentTable, $updateToUid); |
||
| 1071 | } |
||
| 1072 | } |
||
| 1073 | |||
| 1074 | // Ensure all values are set. |
||
| 1075 | $conf += [ |
||
| 1076 | 'foreign_table' => '', |
||
| 1077 | 'foreign_field' => '', |
||
| 1078 | 'symmetric_field' => '', |
||
| 1079 | 'foreign_table_field' => '', |
||
| 1080 | 'foreign_match_fields' => [], |
||
| 1081 | ]; |
||
| 1082 | |||
| 1083 | $c = 0; |
||
| 1084 | $foreign_table = $conf['foreign_table']; |
||
| 1085 | $foreign_field = $conf['foreign_field']; |
||
| 1086 | $symmetric_field = $conf['symmetric_field'] ?? ''; |
||
| 1087 | $foreign_table_field = $conf['foreign_table_field']; |
||
| 1088 | $foreign_match_fields = $conf['foreign_match_fields']; |
||
| 1089 | // If there are table items and we have a proper $parentUid |
||
| 1090 | if (MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) { |
||
| 1091 | // If updateToUid is not a positive integer, set it to '0', so it will be ignored |
||
| 1092 | if (!(MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) { |
||
| 1093 | $updateToUid = 0; |
||
| 1094 | } |
||
| 1095 | $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($foreign_table); |
||
| 1096 | $fields = 'uid,pid,' . $foreign_field; |
||
| 1097 | // Consider the symmetric field if defined: |
||
| 1098 | if ($symmetric_field) { |
||
| 1099 | $fields .= ',' . $symmetric_field; |
||
| 1100 | } |
||
| 1101 | // Consider workspaces if defined and currently used: |
||
| 1102 | if ($considerWorkspaces) { |
||
| 1103 | $fields .= ',t3ver_wsid,t3ver_state,t3ver_oid'; |
||
| 1104 | } |
||
| 1105 | // Update all items |
||
| 1106 | foreach ($this->itemArray as $val) { |
||
| 1107 | $uid = $val['id']; |
||
| 1108 | $table = $val['table']; |
||
| 1109 | $row = []; |
||
| 1110 | // Fetch the current (not overwritten) relation record if we should handle symmetric relations |
||
| 1111 | if ($symmetric_field || $considerWorkspaces) { |
||
| 1112 | $row = BackendUtility::getRecord($table, $uid, $fields, '', true); |
||
| 1113 | if (empty($row)) { |
||
| 1114 | continue; |
||
| 1115 | } |
||
| 1116 | } |
||
| 1117 | $isOnSymmetricSide = false; |
||
| 1118 | if ($symmetric_field) { |
||
| 1119 | $isOnSymmetricSide = self::isOnSymmetricSide((string)$parentUid, $conf, $row); |
||
| 1120 | } |
||
| 1121 | $updateValues = $foreign_match_fields; |
||
| 1122 | // No update to the uid is requested, so this is the normal behaviour |
||
| 1123 | // just update the fields and care about sorting |
||
| 1124 | if (!$updateToUid) { |
||
| 1125 | // Always add the pointer to the parent uid |
||
| 1126 | if ($isOnSymmetricSide) { |
||
| 1127 | $updateValues[$symmetric_field] = $parentUid; |
||
| 1128 | } else { |
||
| 1129 | $updateValues[$foreign_field] = $parentUid; |
||
| 1130 | } |
||
| 1131 | // If it is configured in TCA also to store the parent table in the child record, just do it |
||
| 1132 | if ($foreign_table_field && $this->currentTable) { |
||
| 1133 | $updateValues[$foreign_table_field] = $this->currentTable; |
||
| 1134 | } |
||
| 1135 | // Update sorting columns if not to be skipped. |
||
| 1136 | // @deprecated since v11, will be removed with v12. Drop if() below, assume $skipSorting false, keep body. |
||
| 1137 | if (!$skipSorting) { |
||
| 1138 | // Get the correct sorting field |
||
| 1139 | // Specific manual sortby for data handled by this field |
||
| 1140 | $sortby = ''; |
||
| 1141 | if ($conf['foreign_sortby'] ?? false) { |
||
| 1142 | $sortby = $conf['foreign_sortby']; |
||
| 1143 | } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'] ?? false) { |
||
| 1144 | // manual sortby for all table records |
||
| 1145 | $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby']; |
||
| 1146 | } |
||
| 1147 | // Apply sorting on the symmetric side |
||
| 1148 | // (it depends on who created the relation, so what uid is in the symmetric_field): |
||
| 1149 | if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) { |
||
| 1150 | $sortby = $conf['symmetric_sortby']; |
||
| 1151 | } else { |
||
| 1152 | $tempSortBy = []; |
||
| 1153 | foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) { |
||
| 1154 | [$fieldName, $order] = $orderPair; |
||
| 1155 | if ($order !== null) { |
||
| 1156 | $tempSortBy[] = implode(' ', $orderPair); |
||
| 1157 | } else { |
||
| 1158 | $tempSortBy[] = $fieldName; |
||
| 1159 | } |
||
| 1160 | } |
||
| 1161 | $sortby = implode(',', $tempSortBy); |
||
| 1162 | } |
||
| 1163 | if ($sortby) { |
||
| 1164 | $updateValues[$sortby] = ++$c; |
||
| 1165 | } |
||
| 1166 | } |
||
| 1167 | } else { |
||
| 1168 | if ($isOnSymmetricSide) { |
||
| 1169 | $updateValues[$symmetric_field] = $updateToUid; |
||
| 1170 | } else { |
||
| 1171 | $updateValues[$foreign_field] = $updateToUid; |
||
| 1172 | } |
||
| 1173 | } |
||
| 1174 | // Update accordant fields in the database: |
||
| 1175 | if (!empty($updateValues)) { |
||
| 1176 | // Update tstamp if any foreign field value has changed |
||
| 1177 | if (!empty($GLOBALS['TCA'][$table]['ctrl']['tstamp'])) { |
||
| 1178 | $updateValues[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME']; |
||
| 1179 | } |
||
| 1180 | $this->getConnectionForTableName($table) |
||
| 1181 | ->update( |
||
| 1182 | $table, |
||
| 1183 | $updateValues, |
||
| 1184 | ['uid' => (int)$uid] |
||
| 1185 | ); |
||
| 1186 | $this->updateRefIndex($table, $uid); |
||
| 1187 | } |
||
| 1188 | } |
||
| 1189 | } |
||
| 1190 | } |
||
| 1191 | |||
| 1192 | /** |
||
| 1193 | * After initialization you can extract an array of the elements from the object. Use this function for that. |
||
| 1194 | * |
||
| 1195 | * @param bool $prependTableName If set, then table names will ALWAYS be prepended (unless its a _NO_TABLE value) |
||
| 1196 | * @return array A numeric array. |
||
| 1197 | */ |
||
| 1198 | public function getValueArray($prependTableName = false) |
||
| 1199 | { |
||
| 1200 | // INIT: |
||
| 1201 | $valueArray = []; |
||
| 1202 | $tableC = count($this->tableArray); |
||
| 1203 | // If there are tables in the table array: |
||
| 1204 | if ($tableC) { |
||
| 1205 | // If there are more than ONE table in the table array, then always prepend table names: |
||
| 1206 | $prep = $tableC > 1 || $prependTableName; |
||
| 1207 | // Traverse the array of items: |
||
| 1208 | foreach ($this->itemArray as $val) { |
||
| 1209 | $valueArray[] = ($prep && $val['table'] !== '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id']; |
||
| 1210 | } |
||
| 1211 | } |
||
| 1212 | // Return the array |
||
| 1213 | return $valueArray; |
||
| 1214 | } |
||
| 1215 | |||
| 1216 | /** |
||
| 1217 | * Reads all records from internal tableArray into the internal ->results array |
||
| 1218 | * where keys are table names and for each table, records are stored with uids as their keys. |
||
| 1219 | * If $this->fetchAllFields is false you can save a little memory |
||
| 1220 | * since only uid,pid and a few other fields are selected. |
||
| 1221 | * |
||
| 1222 | * @return array |
||
| 1223 | */ |
||
| 1224 | public function getFromDB() |
||
| 1225 | { |
||
| 1226 | // Traverses the tables listed: |
||
| 1227 | foreach ($this->tableArray as $table => $ids) { |
||
| 1228 | if (is_array($ids) && !empty($ids)) { |
||
| 1229 | $connection = $this->getConnectionForTableName($table); |
||
| 1230 | $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform()); |
||
| 1231 | |||
| 1232 | foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) { |
||
| 1233 | $queryBuilder = $connection->createQueryBuilder(); |
||
| 1234 | $queryBuilder->getRestrictions()->removeAll(); |
||
| 1235 | $queryBuilder->select('*') |
||
| 1236 | ->from($table) |
||
| 1237 | ->where($queryBuilder->expr()->in( |
||
| 1238 | 'uid', |
||
| 1239 | $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY) |
||
| 1240 | )); |
||
| 1241 | if ($this->additionalWhere[$table] ?? false) { |
||
| 1242 | $queryBuilder->andWhere( |
||
| 1243 | QueryHelper::stripLogicalOperatorPrefix($this->additionalWhere[$table]) |
||
| 1244 | ); |
||
| 1245 | } |
||
| 1246 | $statement = $queryBuilder->execute(); |
||
| 1247 | while ($row = $statement->fetchAssociative()) { |
||
| 1248 | $this->results[$table][$row['uid']] = $row; |
||
| 1249 | } |
||
| 1250 | } |
||
| 1251 | } |
||
| 1252 | } |
||
| 1253 | return $this->results; |
||
| 1254 | } |
||
| 1255 | |||
| 1256 | /** |
||
| 1257 | * This method is typically called after getFromDB(). |
||
| 1258 | * $this->results holds a list of resolved and valid relations, |
||
| 1259 | * $this->itemArray hold a list of "selected" relations from the incoming selection array. |
||
| 1260 | * The difference is that "itemArray" may hold a single table/uid combination multiple times, |
||
| 1261 | * for instance in a type=group relation having multiple=true, while "results" hold each |
||
| 1262 | * resolved relation only once. |
||
| 1263 | * The methods creates a sanitized "itemArray" from resolved "results" list, normalized |
||
| 1264 | * the return array to always contain both table name and uid, and keep incoming |
||
| 1265 | * "itemArray" sort order and keeps "multiple" selections. |
||
| 1266 | * |
||
| 1267 | * In addition, the item array contains the full record to be used later-on and save database queries. |
||
| 1268 | * This method keeps the ordering intact. |
||
| 1269 | * |
||
| 1270 | * @return array |
||
| 1271 | */ |
||
| 1272 | public function getResolvedItemArray(): array |
||
| 1273 | { |
||
| 1274 | $itemArray = []; |
||
| 1275 | foreach ($this->itemArray as $item) { |
||
| 1276 | if (isset($this->results[$item['table']][$item['id']])) { |
||
| 1277 | $itemArray[] = [ |
||
| 1278 | 'table' => $item['table'], |
||
| 1279 | 'uid' => $item['id'], |
||
| 1280 | 'record' => $this->results[$item['table']][$item['id']], |
||
| 1281 | ]; |
||
| 1282 | } |
||
| 1283 | } |
||
| 1284 | return $itemArray; |
||
| 1285 | } |
||
| 1286 | |||
| 1287 | /** |
||
| 1288 | * Counts the items in $this->itemArray and puts this value in an array by default. |
||
| 1289 | * |
||
| 1290 | * @param bool $returnAsArray Whether to put the count value in an array |
||
| 1291 | * @return mixed The plain count as integer or the same inside an array |
||
| 1292 | */ |
||
| 1293 | public function countItems($returnAsArray = true) |
||
| 1294 | { |
||
| 1295 | $count = count($this->itemArray); |
||
| 1296 | if ($returnAsArray) { |
||
| 1297 | $count = [$count]; |
||
| 1298 | } |
||
| 1299 | return $count; |
||
| 1300 | } |
||
| 1301 | |||
| 1302 | /** |
||
| 1303 | * Update Reference Index (sys_refindex) for a record. |
||
| 1304 | * Should be called any almost any update to a record which could affect references inside the record. |
||
| 1305 | * If used from within DataHandler, only registers a row for update for later processing. |
||
| 1306 | * |
||
| 1307 | * @param string $table Table name |
||
| 1308 | * @param int $uid Record uid |
||
| 1309 | * @return array Result from ReferenceIndex->updateRefIndexTable() updated directly, else empty array |
||
| 1310 | */ |
||
| 1311 | protected function updateRefIndex($table, $uid): array |
||
| 1312 | { |
||
| 1313 | if (!$this->updateReferenceIndex) { |
||
| 1314 | return []; |
||
| 1315 | } |
||
| 1316 | if ($this->referenceIndexUpdater) { |
||
| 1317 | // Add to update registry if given |
||
| 1318 | $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)$uid, $this->getWorkspaceId()); |
||
| 1319 | $statisticsArray = []; |
||
| 1320 | } else { |
||
| 1321 | // @deprecated else branch can be dropped when setUpdateReferenceIndex() is dropped. |
||
| 1322 | // Update reference index directly if enabled |
||
| 1323 | $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class); |
||
| 1324 | if (BackendUtility::isTableWorkspaceEnabled($table)) { |
||
| 1325 | $referenceIndex->setWorkspaceId($this->getWorkspaceId()); |
||
| 1326 | } |
||
| 1327 | $statisticsArray = $referenceIndex->updateRefIndexTable($table, $uid); |
||
| 1328 | } |
||
| 1329 | return $statisticsArray; |
||
| 1330 | } |
||
| 1331 | |||
| 1332 | /** |
||
| 1333 | * Converts elements in the local item array to use version ids instead of |
||
| 1334 | * live ids, if possible. The most common use case is, to call that prior |
||
| 1335 | * to processing with MM relations in a workspace context. For tha special |
||
| 1336 | * case, ids on both side of the MM relation must use version ids if |
||
| 1337 | * available. |
||
| 1338 | * |
||
| 1339 | * @return bool Whether items have been converted |
||
| 1340 | */ |
||
| 1341 | public function convertItemArray() |
||
| 1342 | { |
||
| 1343 | // conversion is only required in a workspace context |
||
| 1344 | // (the case that version ids are submitted in a live context are rare) |
||
| 1345 | if ($this->getWorkspaceId() === 0) { |
||
| 1346 | return false; |
||
| 1347 | } |
||
| 1348 | |||
| 1349 | $hasBeenConverted = false; |
||
| 1350 | foreach ($this->tableArray as $tableName => $ids) { |
||
| 1351 | if (empty($ids) || !BackendUtility::isTableWorkspaceEnabled($tableName)) { |
||
| 1352 | continue; |
||
| 1353 | } |
||
| 1354 | |||
| 1355 | // convert live ids to version ids if available |
||
| 1356 | $convertedIds = $this->getResolver($tableName, $ids) |
||
| 1357 | ->setKeepDeletePlaceholder(false) |
||
| 1358 | ->setKeepMovePlaceholder(false) |
||
| 1359 | ->processVersionOverlays($ids); |
||
| 1360 | foreach ($this->itemArray as $index => $item) { |
||
| 1361 | if ($item['table'] !== $tableName) { |
||
| 1362 | continue; |
||
| 1363 | } |
||
| 1364 | $currentItemId = $item['id']; |
||
| 1365 | if ( |
||
| 1366 | !isset($convertedIds[$currentItemId]) |
||
| 1367 | || $currentItemId === $convertedIds[$currentItemId] |
||
| 1368 | ) { |
||
| 1369 | continue; |
||
| 1370 | } |
||
| 1371 | // adjust local item to use resolved version id |
||
| 1372 | $this->itemArray[$index]['id'] = $convertedIds[$currentItemId]; |
||
| 1373 | $hasBeenConverted = true; |
||
| 1374 | } |
||
| 1375 | // update per-table reference for ids |
||
| 1376 | if ($hasBeenConverted) { |
||
| 1377 | $this->tableArray[$tableName] = array_values($convertedIds); |
||
| 1378 | } |
||
| 1379 | } |
||
| 1380 | |||
| 1381 | return $hasBeenConverted; |
||
| 1382 | } |
||
| 1383 | |||
| 1384 | /** |
||
| 1385 | * @todo: It *should* be possible to drop all three 'purge' methods by using |
||
| 1386 | * a clever join within readMM - that sounds doable now with pid -1 and |
||
| 1387 | * ws-pair records being gone since v11. It would resolve this indirect |
||
| 1388 | * callback logic and would reduce some queries. The (workspace) mm tests |
||
| 1389 | * should be complete enough now to verify if a change like that would do. |
||
| 1390 | * |
||
| 1391 | * @param int|null $workspaceId |
||
| 1392 | * @return bool Whether items have been purged |
||
| 1393 | * @internal |
||
| 1394 | */ |
||
| 1395 | public function purgeItemArray($workspaceId = null) |
||
| 1396 | { |
||
| 1397 | if ($workspaceId === null) { |
||
| 1398 | $workspaceId = $this->getWorkspaceId(); |
||
| 1399 | } else { |
||
| 1400 | $workspaceId = (int)$workspaceId; |
||
| 1401 | } |
||
| 1402 | |||
| 1403 | // Ensure, only live relations are in the items Array |
||
| 1404 | if ($workspaceId === 0) { |
||
| 1405 | $purgeCallback = 'purgeVersionedIds'; |
||
| 1406 | } else { |
||
| 1407 | // Otherwise, ensure that live relations are purged if version exists |
||
| 1408 | $purgeCallback = 'purgeLiveVersionedIds'; |
||
| 1409 | } |
||
| 1410 | |||
| 1411 | $itemArrayHasBeenPurged = $this->purgeItemArrayHandler($purgeCallback); |
||
| 1412 | $this->purged = ($this->purged || $itemArrayHasBeenPurged); |
||
| 1413 | return $itemArrayHasBeenPurged; |
||
| 1414 | } |
||
| 1415 | |||
| 1416 | /** |
||
| 1417 | * Removes items having a delete placeholder from $this->itemArray |
||
| 1418 | * |
||
| 1419 | * @return bool Whether items have been purged |
||
| 1420 | */ |
||
| 1421 | public function processDeletePlaceholder() |
||
| 1422 | { |
||
| 1423 | if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) { |
||
| 1424 | return false; |
||
| 1425 | } |
||
| 1426 | |||
| 1427 | return $this->purgeItemArrayHandler('purgeDeletePlaceholder'); |
||
| 1428 | } |
||
| 1429 | |||
| 1430 | /** |
||
| 1431 | * Handles a purge callback on $this->itemArray |
||
| 1432 | * |
||
| 1433 | * @param string $purgeCallback |
||
| 1434 | * @return bool Whether items have been purged |
||
| 1435 | */ |
||
| 1436 | protected function purgeItemArrayHandler($purgeCallback) |
||
| 1437 | { |
||
| 1438 | $itemArrayHasBeenPurged = false; |
||
| 1439 | |||
| 1440 | foreach ($this->tableArray as $itemTableName => $itemIds) { |
||
| 1441 | if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) { |
||
| 1442 | continue; |
||
| 1443 | } |
||
| 1444 | |||
| 1445 | $purgedItemIds = []; |
||
| 1446 | $callable =[$this, $purgeCallback]; |
||
| 1447 | if (is_callable($callable)) { |
||
| 1448 | $purgedItemIds = $callable($itemTableName, $itemIds); |
||
| 1449 | } |
||
| 1450 | |||
| 1451 | $removedItemIds = array_diff($itemIds, $purgedItemIds); |
||
| 1452 | foreach ($removedItemIds as $removedItemId) { |
||
| 1453 | $this->removeFromItemArray($itemTableName, $removedItemId); |
||
| 1454 | } |
||
| 1455 | $this->tableArray[$itemTableName] = $purgedItemIds; |
||
| 1456 | if (!empty($removedItemIds)) { |
||
| 1457 | $itemArrayHasBeenPurged = true; |
||
| 1458 | } |
||
| 1459 | } |
||
| 1460 | |||
| 1461 | return $itemArrayHasBeenPurged; |
||
| 1462 | } |
||
| 1463 | |||
| 1464 | /** |
||
| 1465 | * Purges ids that are versioned. |
||
| 1466 | * |
||
| 1467 | * @param string $tableName |
||
| 1468 | * @param array $ids |
||
| 1469 | * @return array |
||
| 1470 | */ |
||
| 1471 | protected function purgeVersionedIds($tableName, array $ids) |
||
| 1472 | { |
||
| 1473 | $ids = $this->sanitizeIds($ids); |
||
| 1474 | $ids = (array)array_combine($ids, $ids); |
||
| 1475 | $connection = $this->getConnectionForTableName($tableName); |
||
| 1476 | $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform()); |
||
| 1477 | |||
| 1478 | foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) { |
||
| 1479 | $queryBuilder = $connection->createQueryBuilder(); |
||
| 1480 | $queryBuilder->getRestrictions()->removeAll(); |
||
| 1481 | $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state') |
||
| 1482 | ->from($tableName) |
||
| 1483 | ->where( |
||
| 1484 | $queryBuilder->expr()->in( |
||
| 1485 | 'uid', |
||
| 1486 | $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY) |
||
| 1487 | ), |
||
| 1488 | $queryBuilder->expr()->neq( |
||
| 1489 | 't3ver_wsid', |
||
| 1490 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
| 1491 | ) |
||
| 1492 | ) |
||
| 1493 | ->orderBy('t3ver_state', 'DESC') |
||
| 1494 | ->execute(); |
||
| 1495 | |||
| 1496 | while ($version = $result->fetchAssociative()) { |
||
| 1497 | $versionId = $version['uid']; |
||
| 1498 | if (isset($ids[$versionId])) { |
||
| 1499 | unset($ids[$versionId]); |
||
| 1500 | } |
||
| 1501 | } |
||
| 1502 | } |
||
| 1503 | |||
| 1504 | return array_values($ids); |
||
| 1505 | } |
||
| 1506 | |||
| 1507 | /** |
||
| 1508 | * Purges ids that are live but have an accordant version. |
||
| 1509 | * |
||
| 1510 | * @param string $tableName |
||
| 1511 | * @param array $ids |
||
| 1512 | * @return array |
||
| 1513 | */ |
||
| 1514 | protected function purgeLiveVersionedIds($tableName, array $ids) |
||
| 1515 | { |
||
| 1516 | $ids = $this->sanitizeIds($ids); |
||
| 1517 | $ids = (array)array_combine($ids, $ids); |
||
| 1518 | $connection = $this->getConnectionForTableName($tableName); |
||
| 1519 | $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform()); |
||
| 1520 | |||
| 1521 | foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) { |
||
| 1522 | $queryBuilder = $connection->createQueryBuilder(); |
||
| 1523 | $queryBuilder->getRestrictions()->removeAll(); |
||
| 1524 | $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state') |
||
| 1525 | ->from($tableName) |
||
| 1526 | ->where( |
||
| 1527 | $queryBuilder->expr()->in( |
||
| 1528 | 't3ver_oid', |
||
| 1529 | $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY) |
||
| 1530 | ), |
||
| 1531 | $queryBuilder->expr()->neq( |
||
| 1532 | 't3ver_wsid', |
||
| 1533 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
| 1534 | ) |
||
| 1535 | ) |
||
| 1536 | ->orderBy('t3ver_state', 'DESC') |
||
| 1537 | ->execute(); |
||
| 1538 | |||
| 1539 | while ($version = $result->fetchAssociative()) { |
||
| 1540 | $versionId = $version['uid']; |
||
| 1541 | $liveId = $version['t3ver_oid']; |
||
| 1542 | if (isset($ids[$liveId]) && isset($ids[$versionId])) { |
||
| 1543 | unset($ids[$liveId]); |
||
| 1544 | } |
||
| 1545 | } |
||
| 1546 | } |
||
| 1547 | |||
| 1548 | return array_values($ids); |
||
| 1549 | } |
||
| 1550 | |||
| 1551 | /** |
||
| 1552 | * Purges ids that have a delete placeholder |
||
| 1553 | * |
||
| 1554 | * @param string $tableName |
||
| 1555 | * @param array $ids |
||
| 1556 | * @return array |
||
| 1557 | */ |
||
| 1558 | protected function purgeDeletePlaceholder($tableName, array $ids) |
||
| 1559 | { |
||
| 1560 | $ids = $this->sanitizeIds($ids); |
||
| 1561 | $ids = array_combine($ids, $ids) ?: []; |
||
| 1562 | $connection = $this->getConnectionForTableName($tableName); |
||
| 1563 | $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform()); |
||
| 1564 | |||
| 1565 | foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) { |
||
| 1566 | $queryBuilder = $connection->createQueryBuilder(); |
||
| 1567 | $queryBuilder->getRestrictions()->removeAll(); |
||
| 1568 | $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state') |
||
| 1569 | ->from($tableName) |
||
| 1570 | ->where( |
||
| 1571 | $queryBuilder->expr()->in( |
||
| 1572 | 't3ver_oid', |
||
| 1573 | $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY) |
||
| 1574 | ), |
||
| 1575 | $queryBuilder->expr()->eq( |
||
| 1576 | 't3ver_wsid', |
||
| 1577 | $queryBuilder->createNamedParameter( |
||
| 1578 | $this->getWorkspaceId(), |
||
| 1579 | \PDO::PARAM_INT |
||
| 1580 | ) |
||
| 1581 | ), |
||
| 1582 | $queryBuilder->expr()->eq( |
||
| 1583 | 't3ver_state', |
||
| 1584 | $queryBuilder->createNamedParameter( |
||
| 1585 | (string)VersionState::cast(VersionState::DELETE_PLACEHOLDER), |
||
| 1586 | \PDO::PARAM_INT |
||
| 1587 | ) |
||
| 1588 | ) |
||
| 1589 | ) |
||
| 1590 | ->execute(); |
||
| 1591 | |||
| 1592 | while ($version = $result->fetchAssociative()) { |
||
| 1593 | $liveId = $version['t3ver_oid']; |
||
| 1594 | if (isset($ids[$liveId])) { |
||
| 1595 | unset($ids[$liveId]); |
||
| 1596 | } |
||
| 1597 | } |
||
| 1598 | } |
||
| 1599 | |||
| 1600 | return array_values($ids); |
||
| 1601 | } |
||
| 1602 | |||
| 1603 | protected function removeFromItemArray($tableName, $id) |
||
| 1604 | { |
||
| 1605 | foreach ($this->itemArray as $index => $item) { |
||
| 1606 | if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) { |
||
| 1607 | unset($this->itemArray[$index]); |
||
| 1608 | return true; |
||
| 1609 | } |
||
| 1610 | } |
||
| 1611 | return false; |
||
| 1612 | } |
||
| 1613 | |||
| 1614 | /** |
||
| 1615 | * Checks, if we're looking from the "other" side, the symmetric side, to a symmetric relation. |
||
| 1616 | * |
||
| 1617 | * @param string $parentUid The uid of the parent record |
||
| 1618 | * @param array $parentConf The TCA configuration of the parent field embedding the child records |
||
| 1619 | * @param array $childRec The record row of the child record |
||
| 1620 | * @return bool Returns TRUE if looking from the symmetric ("other") side to the relation. |
||
| 1621 | */ |
||
| 1622 | protected static function isOnSymmetricSide($parentUid, $parentConf, $childRec) |
||
| 1623 | { |
||
| 1624 | return MathUtility::canBeInterpretedAsInteger($childRec['uid']) |
||
| 1625 | && $parentConf['symmetric_field'] |
||
| 1626 | && $parentUid == $childRec[$parentConf['symmetric_field']]; |
||
| 1627 | } |
||
| 1628 | |||
| 1629 | /** |
||
| 1630 | * Completes MM values to be written by values from the opposite relation. |
||
| 1631 | * This method used MM insert field or MM match fields if defined. |
||
| 1632 | * |
||
| 1633 | * @param string $tableName Name of the opposite table |
||
| 1634 | * @param array $referenceValues Values to be written |
||
| 1635 | * @return array Values to be written, possibly modified |
||
| 1636 | */ |
||
| 1637 | protected function completeOppositeUsageValues($tableName, array $referenceValues) |
||
| 1638 | { |
||
| 1639 | if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) { |
||
| 1640 | // @todo: count($this->MM_oppositeUsage[$tableName]) > 1 is buggy. |
||
| 1641 | // Scenario: Suppose a foreign table has two (!) fields that link to a sys_category. Relations can |
||
| 1642 | // then be correctly set for both fields when editing the foreign records. But when editing a sys_category |
||
| 1643 | // record (local side) and adding a relation to a table that has two category relation fields, the 'fieldname' |
||
| 1644 | // entry in mm-table can not be decided and ends up empty. Neither of the foreign table fields then recognize |
||
| 1645 | // the relation as being set. |
||
| 1646 | // One simple solution is to either simply pick the *first* field, or set *both* relations, but this |
||
| 1647 | // is a) guesswork and b) it may be that in practice only *one* field is actually shown due to record |
||
| 1648 | // types "showitem". |
||
| 1649 | // Brain melt increases with tt_content field 'selected_category' in combination with |
||
| 1650 | // 'category_field' for record types 'menu_categorized_pages' and 'menu_categorized_content' next |
||
| 1651 | // to casual 'categories' field. However, 'selected_category' is a 'oneToMany' and not a 'manyToMany'. |
||
| 1652 | // Hard nut ... |
||
| 1653 | return $referenceValues; |
||
| 1654 | } |
||
| 1655 | |||
| 1656 | $fieldName = $this->MM_oppositeUsage[$tableName][0]; |
||
| 1657 | if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) { |
||
| 1658 | return $referenceValues; |
||
| 1659 | } |
||
| 1660 | |||
| 1661 | $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']; |
||
| 1662 | if (!empty($configuration['MM_insert_fields'])) { |
||
| 1663 | // @todo: MM_insert_fields does not make sense and should be probably dropped altogether. |
||
| 1664 | // No core usages, not even with sys_category. There is no point in having data fields that |
||
| 1665 | // are filled with static content, especially since the mm table can't be edited directly. |
||
| 1666 | $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues); |
||
| 1667 | } elseif (!empty($configuration['MM_match_fields'])) { |
||
| 1668 | // @todo: In the end, MM_match_fields does not make sense. The 'tablename' and 'fieldname' restriction |
||
| 1669 | // in addition to uid_local and uid_foreign used when multiple 'foreign' tables and/or multiple fields |
||
| 1670 | // of one table refer to a single 'local' table having an mm table with these four fields, is already |
||
| 1671 | // clear when looking at 'MM_oppositeUsage' of the local table. 'MM_match_fields' should thus probably |
||
| 1672 | // fall altogether. The only information carried here are the field names of 'tablename' and 'fieldname' |
||
| 1673 | // within the mm table itself, which we should hard code. This is partially assumed in DefaultTcaSchema |
||
| 1674 | // already. |
||
| 1675 | $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues); |
||
| 1676 | } |
||
| 1677 | |||
| 1678 | return $referenceValues; |
||
| 1679 | } |
||
| 1680 | |||
| 1681 | /** |
||
| 1682 | * Gets the record uid of the live default record. If already |
||
| 1683 | * pointing to the live record, the submitted record uid is returned. |
||
| 1684 | * |
||
| 1685 | * @param string $tableName |
||
| 1686 | * @param int|string $id |
||
| 1687 | * @return int |
||
| 1688 | */ |
||
| 1689 | protected function getLiveDefaultId($tableName, $id) |
||
| 1696 | } |
||
| 1697 | |||
| 1698 | /** |
||
| 1699 | * Removes empty values (null, '0', 0, false). |
||
| 1700 | * |
||
| 1701 | * @param int[] $ids |
||
| 1702 | * @return array |
||
| 1703 | */ |
||
| 1704 | protected function sanitizeIds(array $ids): array |
||
| 1707 | } |
||
| 1708 | |||
| 1709 | /** |
||
| 1710 | * @param string $tableName |
||
| 1711 | * @param int[] $ids |
||
| 1712 | * @param array $sortingStatement |
||
| 1713 | * @return PlainDataResolver |
||
| 1714 | */ |
||
| 1715 | protected function getResolver($tableName, array $ids, array $sortingStatement = null) |
||
| 1728 | } |
||
| 1729 | |||
| 1730 | /** |
||
| 1731 | * @param string $tableName |
||
| 1732 | * @return Connection |
||
| 1733 | */ |
||
| 1734 | protected function getConnectionForTableName(string $tableName) |
||
| 1735 | { |
||
| 1736 | return GeneralUtility::makeInstance(ConnectionPool::class) |
||
| 1737 | ->getConnectionForTable($tableName); |
||
| 1738 | } |
||
| 1739 | } |
||
| 1740 |