/ and * attaches them (review/crop stays editorial - a failed download is a * warning, never an abort). * * Dry run by default; --apply writes. Idempotent: re-importing an unchanged * workbook updates fields to the same values. Flip-card icons are NOTED but * not attached yet - the icon set is still open with the front-end team. * * vendor/bin/typo3 vitec:import-submarket-workbooks --dir=migrations/submarkets_xlsx_in * vendor/bin/typo3 vitec:import-submarket-workbooks --dir=... --only=broadcast --apply */ #[AsCommand( name: 'vitec:import-submarket-workbooks', description: 'Import filled sub-market workbooks into layout-21 pages (dry-run by default)' )] final class ImportSubmarketWorkbooksCommand extends Command { private const LAYOUT = 21; private const COLPOS_HERO = 110; private const COLPOS_INTRO = 120; private const COLPOS_FLOW = 130; private const COLPOS_TESTI = 140; private const COLPOS_PROD = 150; private const COLPOS_INSIGHTS = 160; private const CARD_ROWS = 8; private const STORY_ROWS = 6; private const PRODUCT_ROWS = 9; /** Story detail page; mirrors the site setting vitec.storyDetailPid. */ private const STORY_SINGLE_PID = 26; /** @var string[] */ private array $report = []; /** @var array>> */ private array $datamap = []; /** @var array>> */ private array $cmdmap = []; private int $newCounter = 0; protected function configure(): void { $this->addOption('dir', null, InputOption::VALUE_REQUIRED, 'Directory with filled workbooks', 'migrations/submarkets_xlsx_in'); $this->addOption('only', null, InputOption::VALUE_REQUIRED, 'Only files whose name contains this substring'); $this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)'); } protected function execute(InputInterface $input, OutputInterface $output): int { Bootstrap::initializeBackendAuthentication(); $apply = (bool)$input->getOption('apply'); $only = mb_strtolower((string)($input->getOption('only') ?? '')); $dir = rtrim((string)$input->getOption('dir'), '/'); if (!is_dir($dir)) { $output->writeln('Directory not found: ' . $dir . ''); return Command::FAILURE; } $files = []; $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)); foreach ($iterator as $fileInfo) { $name = $fileInfo->getFilename(); if ($fileInfo->isFile() && str_ends_with($name, '.xlsx') && !str_starts_with($name, '~$')) { if ($only === '' || str_contains(mb_strtolower($name), $only)) { $files[] = $fileInfo->getPathname(); } } } sort($files); if ($files === []) { $output->writeln('No workbooks found in ' . $dir); return Command::SUCCESS; } $output->writeln(count($files) . ' workbook(s) · ' . ($apply ? 'APPLY' : 'DRY-RUN')); $errors = 0; foreach ($files as $file) { $this->report = []; $this->datamap = []; $this->cmdmap = []; try { $this->importOne($file, $apply, $output); } catch (\Throwable $e) { $errors++; $output->writeln('' . basename($file) . ': ' . $e->getMessage() . ''); } } if (!$apply) { $output->writeln("\nDRY RUN - nothing written. Add --apply to write."); } return $errors > 0 ? Command::FAILURE : Command::SUCCESS; } private function importOne(string $file, bool $apply, OutputInterface $output): void { $spreadsheet = IOFactory::load($file); $values = $this->readNamedValues($spreadsheet); $submarket = trim((string)($values['meta_submarket'] ?? '')); $group = trim((string)($values['meta_group'] ?? '')); $recordUid = (int)($values['meta_record_uid'] ?? 0); $spreadsheet->disconnectWorksheets(); if ($submarket === '' || $group === '') { throw new \RuntimeException('Meta sheet missing - not a generated sub-market workbook?'); } $output->writeln(sprintf("\n%s (%s / %s)", basename($file), $group, $submarket)); // --- resolve pages --------------------------------------------------- $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages'); $marketsRoot = $connection->fetchAssociative("SELECT uid FROM pages WHERE deleted = 0 AND slug = '/markets'"); if (!$marketsRoot) { throw new \RuntimeException('Page /markets not found.'); } $mainPage = $this->childByTitle((int)$marketsRoot['uid'], $group); if ($mainPage === null) { throw new \RuntimeException('Main market page "' . $group . '" not found below /markets.'); } $subPage = $this->childByTitle((int)$mainPage['uid'], $submarket); $pageId = null; if ($subPage === null) { $pageId = $this->newId(); $this->datamap['pages'][$pageId] = [ 'pid' => (int)$mainPage['uid'], 'title' => $submarket, 'doktype' => 1, 'layout' => self::LAYOUT, 'hidden' => 1, 'backend_layout' => 'vitec__' . self::LAYOUT, ]; $this->report[] = 'page: CREATE (hidden, review & publish editorially)'; } else { $pageId = (string)(int)$subPage['uid']; $updates = []; if ((int)$subPage['layout'] !== self::LAYOUT) { $updates['layout'] = self::LAYOUT; $updates['backend_layout'] = 'vitec__' . self::LAYOUT; $this->report[] = 'page: switch layout -> 21 (was ' . (int)$subPage['layout'] . ')'; } if ($updates !== []) { $this->datamap['pages'][$pageId] = $updates; } } // --- SEO on the page -------------------------------------------------- $seoTitle = trim((string)($values['seo_title'] ?? '')); $seoDescription = trim((string)($values['seo_description'] ?? '')); if ($seoTitle !== '') { $this->datamap['pages'][$pageId]['seo_title'] = $seoTitle; } if ($seoDescription !== '') { $this->datamap['pages'][$pageId]['description'] = $seoDescription; } // --- slot 110: hero --------------------------------------------------- $heroId = $this->slotElement($pageId, $subPage, self::COLPOS_HERO, 'vitec_markethero'); $this->setFields('tt_content', $heroId, [ 'vitec_subline' => (string)($values['hero_subline'] ?? ''), 'vitec_cta_label' => (string)($values['hero_cta_label'] ?? ''), 'vitec_cta_link' => (string)($values['hero_cta_url'] ?? ''), 'vitec_video_title' => (string)($values['hero_video_title'] ?? ''), 'vitec_video_url' => (string)($values['hero_video_url'] ?? ''), ]); $this->attachUrlFile($heroId, 'vitec_bgimage', (string)($values['hero_bgimage_url'] ?? ''), $submarket, $apply, $this->pidValue($pageId)); $this->attachUrlFile($heroId, 'vitec_video_cover', (string)($values['hero_video_cover_url'] ?? ''), $submarket, $apply, $this->pidValue($pageId)); // --- slot 120: intro ---------------------------------------------------- $introId = $this->slotElement($pageId, $subPage, self::COLPOS_INTRO, 'vitec_marketintro'); $this->setFields('tt_content', $introId, [ 'vitec_intro_text' => (string)($values['intro_text'] ?? ''), 'vitec_learnmore_label' => (string)($values['intro_learnmore_label'] ?? ''), 'vitec_learnmore_target' => (string)($values['intro_learnmore_target'] ?? ''), 'vitec_show_portfolio' => strcasecmp(trim((string)($values['intro_portfolio'] ?? 'Show')), 'Hide') === 0 ? 0 : 1, ]); $this->attachUrlFile($introId, 'vitec_image', (string)($values['intro_image_url'] ?? ''), $submarket, $apply, $this->pidValue($pageId)); // --- slot 130: workflow (intro-paragraph + flip cards) ------------------ $flowHeaderId = $this->slotElement($pageId, $subPage, self::COLPOS_FLOW, 'vitec_introparagraph'); $this->setFields('tt_content', $flowHeaderId, [ 'header' => (string)($values['flow_headline'] ?? ''), 'bodytext' => (string)($values['flow_subtext'] ?? ''), ]); $existingCards = $subPage ? $this->slotElements((int)$subPage['uid'], self::COLPOS_FLOW, 'vitec_card') : []; $cardIndex = 0; for ($i = 1; $i <= self::CARD_ROWS; $i++) { $title = trim((string)($values['flow_card' . $i . '_title'] ?? '')); $front = trim((string)($values['flow_card' . $i . '_front'] ?? '')); $back = trim((string)($values['flow_card' . $i . '_back'] ?? '')); $icon = trim((string)($values['flow_card' . $i . '_icon'] ?? '')); if ($title === '' && $front === '') { continue; } $cardId = $existingCards[$cardIndex] ?? $this->newId(); if (!isset($existingCards[$cardIndex])) { $this->datamap['tt_content'][$cardId]['pid'] = $this->pidValue($pageId); $this->datamap['tt_content'][$cardId]['colPos'] = self::COLPOS_FLOW; $this->datamap['tt_content'][$cardId]['CType'] = 'vitec_card'; } $this->setFields('tt_content', (string)$cardId, [ 'vitec_layout_variant' => 'flipcard', 'header' => $title, 'bodytext' => $front, 'vitec_back_headline' => $title, 'vitec_back_text' => $back, ]); if ($icon !== '') { $this->report[] = 'card ' . $i . ': icon "' . $icon . '" noted - icon set still pending, attach later'; } $cardIndex++; } if (count($existingCards) > $cardIndex) { $this->report[] = 'workflow: ' . (count($existingCards) - $cardIndex) . ' existing card(s) beyond the workbook - left untouched, review editorially'; } // --- slot 140: testimonials -------------------------------------------- $storyUids = []; foreach (range(1, self::STORY_ROWS) as $i) { $storyTitle = trim((string)($values['testi_story_' . $i] ?? '')); if ($storyTitle === '') { continue; } $story = $this->recordByTitle('tx_vitec_domain_model_usecase', $storyTitle); if ($story === null) { $this->report[] = 'testimonial ' . $i . ': story "' . $storyTitle . '" NOT FOUND - skipped'; continue; } if (trim((string)$story['quote']) === '') { $this->report[] = 'testimonial: "' . $storyTitle . '" has NO QUOTE yet - maintain on the story record'; } $storyUids[] = (int)$story['uid']; } $testiId = $this->slotElement($pageId, $subPage, self::COLPOS_TESTI, 'vitec_usecaselist'); $this->setFields('tt_content', $testiId, [ 'header' => (string)($values['testi_headline'] ?? ''), 'pi_flexform' => $this->flex([ 'settings.layout' => 'testimonials', 'settings.usecases' => implode(',', $storyUids), 'settings.singlePid' => (string)self::STORY_SINGLE_PID, 'settings.showtoolbar' => '0', ]), ]); // --- slot 150: products -------------------------------------------------- $featuredTitle = trim((string)($values['prod_featured'] ?? '')); $featuredUid = 0; if ($featuredTitle !== '') { $featured = $this->recordByTitle('tx_vitec_domain_model_product', $featuredTitle); $featured === null ? $this->report[] = 'featured product "' . $featuredTitle . '" NOT FOUND' : $featuredUid = (int)$featured['uid']; } $featureId = $this->slotElement($pageId, $subPage, self::COLPOS_PROD, 'vitec_productfeature'); $this->setFields('tt_content', $featureId, [ 'pi_flexform' => $this->flex(['settings.product' => (string)$featuredUid]), ]); $productUids = []; foreach (range(1, self::PRODUCT_ROWS) as $i) { $productTitle = trim((string)($values['prod_card_' . $i] ?? '')); if ($productTitle === '') { continue; } $product = $this->recordByTitle('tx_vitec_domain_model_product', $productTitle); $product === null ? $this->report[] = 'product card ' . $i . ': "' . $productTitle . '" NOT FOUND - skipped' : $productUids[] = (int)$product['uid']; } $listId = $this->slotElement($pageId, $subPage, self::COLPOS_PROD, 'vitec_productlist'); $this->setFields('tt_content', $listId, [ 'pi_flexform' => $this->flex([ 'settings.selectedproducts' => implode(',', $productUids), 'settings.layout' => '0', 'settings.showtoolbar' => '0', ]), ]); // --- slot 160: insights --------------------------------------------------- $insightsId = $this->slotElement($pageId, $subPage, self::COLPOS_INSIGHTS, 'vitec_marketinsights'); $this->setFields('tt_content', $insightsId, [ 'vitec_text1' => (string)($values['insights_text1'] ?? ''), 'vitec_text2' => (string)($values['insights_text2'] ?? ''), 'vitec_cta_label' => (string)($values['insights_cta_label'] ?? ''), 'vitec_cta_target' => (string)($values['insights_cta_target'] ?? ''), 'vitec_video_person' => (string)($values['insights_video_person'] ?? ''), 'vitec_video_role' => (string)($values['insights_video_role'] ?? ''), 'vitec_video_title' => (string)($values['insights_video_title'] ?? ''), 'vitec_video_url' => (string)($values['insights_video_url'] ?? ''), ]); $this->attachUrlFile($insightsId, 'vitec_video_cover', (string)($values['insights_video_cover_url'] ?? ''), $submarket, $apply, $this->pidValue($pageId)); // --- market record link ----------------------------------------------------- if ($recordUid > 0 && !str_starts_with($pageId, 'NEW')) { $this->datamap['tx_vitec_domain_model_market'][$recordUid]['detail_page'] = (int)$pageId; } elseif ($recordUid > 0) { $this->datamap['tx_vitec_domain_model_market'][$recordUid]['detail_page'] = $pageId; } else { $this->report[] = 'no market record for this sub-market - detail_page not set'; } foreach ($this->report as $line) { $output->writeln(' ' . $line . ''); } $elementCount = count($this->datamap['tt_content'] ?? []); $output->writeln(sprintf(' page %s · %d element(s) touched · %s', str_starts_with($pageId, 'NEW') ? 'NEW' : '#' . $pageId, $elementCount, $apply ? 'writing...' : 'dry run')); if (!$apply) { return; } $dataHandler = GeneralUtility::makeInstance(DataHandler::class); $dataHandler->start($this->datamap, $this->cmdmap); $dataHandler->process_datamap(); if ($this->cmdmap !== []) { $dataHandler->process_cmdmap(); } if ($dataHandler->errorLog !== []) { throw new \RuntimeException('DataHandler: ' . implode(' | ', $dataHandler->errorLog)); } $output->writeln(' written.'); } // ---- helpers ------------------------------------------------------------- /** @return array name => first-cell value */ private function readNamedValues(Spreadsheet $spreadsheet): array { $values = []; foreach ($spreadsheet->getNamedRanges() as $namedRange) { $name = $namedRange->getName(); $sheet = $namedRange->getWorksheet(); if ($sheet === null) { continue; } $range = str_replace('$', '', (string)$namedRange->getValue()); $range = str_contains($range, '!') ? substr($range, strpos($range, '!') + 1) : $range; $cell = str_contains($range, ':') ? substr($range, 0, strpos($range, ':')) : $range; $values[$name] = $sheet->getCell($cell)->getValue(); } if (isset($values['meta_submarket'])) { return $values; } // Names are gone - scan by the LOCKED labels in column A instead. // Fixed cell addresses were tried first and failed the same day: the // Excel round-trip that strips the names may ALSO shift rows, so the // only thing left to trust is the label text we generated ourselves. $meta = $spreadsheet->getSheetByName('Meta'); $content = $spreadsheet->getSheetByName('Content'); if ($meta === null || $content === null) { return $values; // caller reports "Meta sheet missing" } $this->report[] = 'NOTE: defined names were stripped (Excel save) - reading by column-A labels instead'; $values['meta_submarket'] = $meta->getCell('B1')->getValue(); $values['meta_group'] = $meta->getCell('B2')->getValue(); $values['meta_record_uid'] = $meta->getCell('B3')->getValue(); $section = 0; $inCardTable = false; $cardIndex = 0; $highestRow = min(200, (int)$content->getHighestRow()); for ($rowNum = 1; $rowNum <= $highestRow; $rowNum++) { $label = mb_strtolower(trim((string)$content->getCell('A' . $rowNum)->getValue())); if (preg_match('/^section (\d)/', $label, $m)) { $section = (int)$m[1]; $inCardTable = false; continue; } // Inside the card table every row is a card until the next // section header - icons are optional, so rows with an empty // column A still count when B-D carry content. if ($inCardTable && $section === 3) { $icon = trim((string)$content->getCell('A' . $rowNum)->getValue()); $title = trim((string)$content->getCell('B' . $rowNum)->getValue()); $front = trim((string)$content->getCell('C' . $rowNum)->getValue()); $back = trim((string)$content->getCell('D' . $rowNum)->getValue()); if ($icon === '' && $title === '' && $front === '' && $back === '') { continue; } $cardIndex++; if ($cardIndex <= self::CARD_ROWS) { $values['flow_card' . $cardIndex . '_icon'] = $icon; $values['flow_card' . $cardIndex . '_title'] = $title; $values['flow_card' . $cardIndex . '_front'] = $front; $values['flow_card' . $cardIndex . '_back'] = $back; } continue; } if ($label === '') { continue; } if ($section === 3 && $label === 'icon') { $inCardTable = true; continue; } $label = rtrim($label, ' *'); $b = $content->getCell('B' . $rowNum)->getValue(); $name = match (true) { $label === 'subline' => 'hero_subline', $label === 'cta button label' => 'hero_cta_label', $label === 'cta button link (url)' => 'hero_cta_url', $label === 'background image url' => 'hero_bgimage_url', $label === 'video teaser: title' => 'hero_video_title', $label === 'video teaser: video url' => 'hero_video_url', $label === 'video teaser: cover image url' => 'hero_video_cover_url', $label === 'intro text' => 'intro_text', $label === 'image url (right)' => 'intro_image_url', $label === 'learn-more label' => 'intro_learnmore_label', $label === 'learn-more target' => 'intro_learnmore_target', str_starts_with($label, 'show "product portfolio"') => 'intro_portfolio', $label === 'headline' && $section === 3 => 'flow_headline', $label === 'subtext' => 'flow_subtext', $label === 'headline' && $section === 4 => 'testi_headline', str_starts_with($label, 'case study ') => 'testi_story_' . (int)substr($label, 11), $label === 'featured product' => 'prod_featured', str_starts_with($label, 'product card ') => 'prod_card_' . (int)substr($label, 13), $label === 'paragraph 1' => 'insights_text1', $label === 'paragraph 2' => 'insights_text2', $label === 'cta label' => 'insights_cta_label', $label === 'cta target' => 'insights_cta_target', $label === 'video card: person' => 'insights_video_person', $label === 'video card: role / title' => 'insights_video_role', $label === 'video card: video title' => 'insights_video_title', $label === 'video card: video url' => 'insights_video_url', $label === 'video card: cover image url' => 'insights_video_cover_url', $label === 'seo title' => 'seo_title', $label === 'seo description' => 'seo_description', default => null, }; if ($name !== null && !array_key_exists($name, $values)) { $values[$name] = $b; } } return $values; } /** @return array{uid:int,layout:int}|null */ private function childByTitle(int $parentUid, string $title): ?array { $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages'); foreach ($connection->fetchAllAssociative( 'SELECT uid, title, layout FROM pages WHERE deleted = 0 AND pid = ? AND sys_language_uid IN (0,-1)', [$parentUid] ) as $row) { if ($this->norm((string)$row['title']) === $this->norm($title)) { return ['uid' => (int)$row['uid'], 'layout' => (int)$row['layout']]; } } return null; } /** Existing element uid for a single-element slot, or a NEW id. */ private function slotElement(string $pageId, ?array $subPage, int $colPos, string $ctype): string { if ($subPage !== null) { $existing = $this->slotElements((int)$subPage['uid'], $colPos, $ctype); if ($existing !== []) { return (string)$existing[0]; } } $id = $this->newId(); $this->datamap['tt_content'][$id] = [ 'pid' => $this->pidValue($pageId), 'colPos' => $colPos, 'CType' => $ctype, ]; return $id; } /** @return int[] uids in sorting order */ private function slotElements(int $pageUid, int $colPos, string $ctype): array { $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content'); return array_map('intval', $connection->fetchFirstColumn( 'SELECT uid FROM tt_content WHERE deleted = 0 AND pid = ? AND colPos = ? AND CType = ? ORDER BY sorting', [$pageUid, $colPos, $ctype] )); } /** @param array $fields */ private function setFields(string $table, string $id, array $fields): void { foreach ($fields as $field => $value) { $this->datamap[$table][$id][$field] = $value; } } /** NEW page ids must stay strings in datamap pid references. */ private function pidValue(string $pageId): int|string { return str_starts_with($pageId, 'NEW') ? $pageId : (int)$pageId; } /** @return array|null */ private function recordByTitle(string $table, string $title): ?array { $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); foreach ($connection->fetchAllAssociative( 'SELECT * FROM ' . $table . ' WHERE deleted = 0', [] ) as $row) { if ($this->norm((string)$row['title']) === $this->norm($title)) { return $row; } } return null; } /** * Download a URL into fileadmin/submarket_import// and attach it as * the element's file field. Editorial review happens in the backend; a * failed download is a warning. Skipped entirely in dry runs. */ private function attachUrlFile(string $elementId, string $fieldName, string $url, string $submarket, bool $apply, int|string $pid = 0): void { $url = trim($url); if ($url === '' || !str_starts_with($url, 'http')) { return; } if (!$apply) { $this->report[] = $fieldName . ': would download ' . $url; return; } try { // RequestFactory instead of GeneralUtility::getUrl(): the latter // swallows the failure reason, which cost a debugging round on // 2026-10-02 (the server could not fetch the URL, the report // could not say why). $response = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\RequestFactory::class) ->request($url, 'GET', ['timeout' => 20]); if ($response->getStatusCode() !== 200) { $this->report[] = $fieldName . ': DOWNLOAD FAILED (HTTP ' . $response->getStatusCode() . ' for ' . $url . ') - attach editorially'; return; } $content = (string)$response->getBody(); if ($content === '') { $this->report[] = $fieldName . ': DOWNLOAD FAILED (empty response from ' . $url . ') - attach editorially'; return; } $storage = GeneralUtility::makeInstance(StorageRepository::class)->getDefaultStorage(); $folderPath = 'submarket_import/' . mb_strtolower((string)preg_replace('/[^A-Za-z0-9]+/', '-', $submarket)); $folder = $storage->hasFolder($folderPath) ? $storage->getFolder($folderPath) : $storage->createFolder($folderPath); $fileName = basename(parse_url($url, PHP_URL_PATH) ?: '') ?: ($fieldName . '.bin'); $temp = GeneralUtility::tempnam('submarket_'); file_put_contents($temp, $content); $file = $storage->addFile($temp, $folder, $fileName, DuplicationBehavior::REPLACE); // Re-import idempotency BY CONTENT (lesson 2026-10-02: passing a // fresh NEW reference every run made DataHandler STACK live // references - two visible images after two runs). An existing // element therefore reuses its first live reference; surplus // live references on the same field - our own earlier duplicates - // are deleted via cmdmap. if (!str_starts_with($elementId, 'NEW')) { $existingRefs = $this->liveReferences((int)$elementId, $fieldName); if ($existingRefs !== []) { $first = array_shift($existingRefs); if ((int)$first['uid_local'] === $file->getUid()) { $this->report[] = $fieldName . ': ' . $fileName . ' already attached - unchanged'; } else { $this->datamap['sys_file_reference'][(int)$first['uid']] = ['uid_local' => $file->getUid()]; $this->report[] = $fieldName . ': ' . $fileName . ' swapped onto the existing reference'; } foreach ($existingRefs as $surplus) { $this->cmdmap['sys_file_reference'][(int)$surplus['uid']]['delete'] = 1; $this->report[] = $fieldName . ': removed duplicate reference #' . $surplus['uid']; } return; } } $refId = $this->newId(); // pid, tablenames and uid_foreign are REQUIRED - without them // DataHandler rejects the reference (found 2026-10-02: downloads // succeeded, attachments silently did not). $this->datamap['sys_file_reference'][$refId] = [ 'pid' => $pid, 'table_local' => 'sys_file', 'uid_local' => $file->getUid(), 'tablenames' => 'tt_content', 'fieldname' => $fieldName, 'uid_foreign' => str_starts_with($elementId, 'NEW') ? $elementId : (int)$elementId, ]; $this->datamap['tt_content'][$elementId][$fieldName] = $refId; $this->report[] = $fieldName . ': downloaded ' . $fileName . ' - review editorially'; } catch (\Throwable $e) { $this->report[] = $fieldName . ': DOWNLOAD FAILED (' . $e->getMessage() . ') - attach editorially'; } } /** @return array live refs on one field, oldest first */ private function liveReferences(int $elementUid, string $fieldName): array { $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_file_reference'); return array_map( static fn(array $r): array => ['uid' => (int)$r['uid'], 'uid_local' => (int)$r['uid_local']], $connection->fetchAllAssociative( "SELECT uid, uid_local FROM sys_file_reference WHERE deleted = 0 AND hidden = 0 AND tablenames = 'tt_content' AND fieldname = ? AND uid_foreign = ? ORDER BY uid", [$fieldName, $elementUid] ) ); } /** * DataHandler flexform array for sheet sDEF. * * @param array $settings * @return array */ private function flex(array $settings): array { $fields = []; foreach ($settings as $key => $value) { $fields[$key] = ['vDEF' => $value]; } return ['data' => ['sDEF' => ['lDEF' => $fields]]]; } private function newId(): string { return 'NEW' . str_pad((string)(++$this->newCounter), 6, '0', STR_PAD_LEFT) . substr(md5(uniqid('', true)), 0, 6); } private function norm(string $value): string { return (string)preg_replace('/[^a-z0-9]+/', '', str_replace('+', 'plus', mb_strtolower($value))); } }