Ich habe eine eigene Extension erstellt die anhand von Form selects Land(Liste von Ländern) und Industrie(Liste von Kategorien) beim abschicken des Formulars die entsprechenden Ansprechpartner ausgeben soll.
Dazu habe ich einerseits tt_address erweitert um das Feld tx_gedankenfolger_addressfilter_countries wo man Komma getrennt verschiedene Länderkürzel eingeben kann z.b. de,fr.
Dazu kann man hier auch eine Kategorie auswählen.
Die Extension enthält 2 Content-Blocks. 1x address-filter-form und 1x address-filter-results.
Dazu gibt es 2 Dataprocessoren.
AddressFilterFormProcessor für die Befüllung und Handhabung der Formelemente.
AddressResultsProcessor für die Ausgabe des Ergebnisses.
Jetzt habe ich das komische Verhalten das wenn man parallel im Backend eingeloggt ist alles sauber funktioniert.
Sobald ich mich auslogge funktioniert nur noch die Befüllung der Selects aber weder die Vorauswahl z.b. <option value="5" selected="selected">Consumer</option> noch bekomme ich Ansprechpartner ausgegeben.
Hat da jemand eine Idee?
Unten sind auch Screenshots die das Ganze verdeutlichen.
setup.typoscript
tt_content.gedankenfolger_addressfilter_form {
settings{
appendAnchorToSubmit = {$addressFilter.appendAnchorToSubmit}
}
dataProcessing {
200 = Gedankenfolger\GedankenfolgerAddressfilter\DataProcessing\AddressFilterFormProcessor
200.as = filter
}
}
tt_content.gedankenfolger_addressfilter_results {
dataProcessing {
200 = Gedankenfolger\GedankenfolgerAddressfilter\DataProcessing\AddressResultsProcessor
200. as = result
}
}
AddressFilterFormProcessor
<?php
declare(strict_types=1);
namespace Gedankenfolger\GedankenfolgerAddressfilter\DataProcessing;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use JsonSerializable;
use Psr\Http\Message\ServerRequestInterface;
use Traversable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
final class AddressFilterFormProcessor implements DataProcessorInterface
{
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
) {
/** @var ServerRequestInterface|null $request */
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
/** @var SiteFinder $siteFinder */
$siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
/** @var ConnectionPool $connectionPool */
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
// ContentBlocks: provide object + raw record
$dataObject = $processedData['data'] ?? null;
$record = $this->resolveRecord($cObj, $processedData);
$pid = (int)($record['pid'] ?? 0);
// Site (currently not needed, but keeps context robust)
$site = $this->getSiteFromContext($request, $siteFinder, $pid);
unset($site);
// =========================
// (A) CATEGORY ROOT from CE
// =========================
$categoryRoot = $this->resolveSingleUidFromContentBlock($dataObject, $record, 'categoryRoot');
$categories = [];
if ($categoryRoot !== null && $categoryRoot > 0) {
// Direct children of the selected root category
$qbCat = $connectionPool->getQueryBuilderForTable('sys_category');
$qbCat->select('uid', 'title')
->from('sys_category')
->where(
$qbCat->expr()->eq('deleted', 0),
$qbCat->expr()->eq('hidden', 0),
$qbCat->expr()->eq(
'parent',
$qbCat->createNamedParameter($categoryRoot, ParameterType::INTEGER)
)
)
->orderBy('sorting');
$categories = $qbCat->executeQuery()->fetchAllAssociative();
} else {
// No root set → intentionally no categories (no fallback)
$categories = [];
}
// ==============================================
// (B) COUNTRIES via static_info_tables from CE
// ==============================================
$countries = [];
if (ExtensionManagementUtility::isLoaded('static_info_tables')) {
$whitelistUids = $this->resolveRelationUidsFromContentBlock($dataObject, $record, 'countryWhitelist');
$blacklistUids = $this->resolveRelationUidsFromContentBlock($dataObject, $record, 'countryBlacklist');
// Empty whitelist ⇒ all countries (minus blacklist)
$countries = $this->loadCountriesFromStaticInfo($connectionPool, $whitelistUids, $blacklistUids);
}
// Selection from GET (prefix gf_addressfilter[...])
$queryParams = $request ? $request->getQueryParams() : [];
$gf = $queryParams['gf_addressfilter'] ?? [];
if (!\is_array($gf)) {
$gf = [];
}
$selectedCountry = strtoupper((string)($gf['country'] ?? ''));
$selectedCategory = (string)($gf['category'] ?? '');
foreach ($countries as &$c) {
$c['selected'] = ($selectedCountry !== '' && strtoupper($c['code']) === $selectedCountry);
}
foreach ($categories as &$cat) {
$cat['selected'] = ($selectedCategory !== '' && (string)$cat['uid'] === (string)$selectedCategory);
}
unset($c, $cat);
// Optional target page (relation in CE)
$targetPid = $this->resolveSingleUidFromContentBlock($dataObject, $record, 'targetPage');
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'filter');
$processedData[$targetVariableName] = [
'countries' => $countries, // ['code'=>'DE','label'=>'Germany','selected'=>bool]
'categories' => $categories, // ['uid'=>123,'title'=>'...','selected'=>bool]
'selectedCountry' => $selectedCountry,
'selectedCategory' => $selectedCategory,
'actionPid' => $targetPid,
];
return $processedData;
}
/**
* Load countries from static_info_tables.
* - Empty whitelist → load all countries (minus blacklist).
* - Whitelist set → only those (minus blacklist).
*/
private function loadCountriesFromStaticInfo(ConnectionPool $cp, array $whitelistUids, array $blacklistUids): array
{
$qb = $cp->getQueryBuilderForTable('static_countries');
$qb->select('uid', 'cn_iso_2', 'cn_short_en', 'cn_short_de', 'cn_short_local', 'cn_official_name_en')
->from('static_countries');
// static_info_tables typically does not use deleted/hidden columns
if (!empty($whitelistUids)) {
$qb->andWhere(
$qb->expr()->in(
'uid',
$qb->createNamedParameter($whitelistUids, Connection::PARAM_INT_ARRAY)
)
);
}
$qb->orderBy('cn_short_de')->addOrderBy('cn_short_en');
$rows = $qb->executeQuery()->fetchAllAssociative();
// Filter out blacklist locally
if (!empty($blacklistUids)) {
$rows = array_filter($rows, static fn($r) => !in_array((int)$r['uid'], $blacklistUids, true));
}
$out = [];
foreach ($rows as $r) {
$code = strtoupper((string)($r['cn_iso_2'] ?? ''));
if ($code === '') {
continue;
}
$label = $this->pickCountryLabel($r);
$out[] = ['code' => $code, 'label' => $label];
}
usort($out, static fn($a, $b) => strnatcasecmp($a['label'], $b['label']));
return $out;
}
private function pickCountryLabel(array $row): string
{
foreach (['cn_short_de', 'cn_short_local', 'cn_short_en', 'cn_official_name_en'] as $k) {
$v = trim((string)($row[$k] ?? ''));
if ($v !== '') {
return $v;
}
}
return strtoupper((string)($row['cn_iso_2'] ?? ''));
}
// ------------------------------------------------------------------
// Content Blocks: robust field resolution (object, arrays, relations)
// ------------------------------------------------------------------
/** Determine raw record (array); fallback to $cObj->data. */
private function resolveRecord(ContentObjectRenderer $cObj, array $processedData): array
{
$data = $processedData['data'] ?? null;
if (\is_object($data) && \method_exists($data, 'getRawRecord')) {
/** @phpstan-ignore-next-line */
return (array)$data->getRawRecord();
}
if (\is_array($data)) {
return $data;
}
return \is_array($cObj->data ?? null) ? $cObj->data : [];
}
/**
* Extract SINGLE UID for $identifier from ContentBlockData OR raw record.
* Supports camelCase/snake_case and relation objects.
*/
private function resolveSingleUidFromContentBlock(mixed $dataObject, array $record, string $identifier): ?int
{
// 1) Directly from ContentBlockData (exact keys & variants)
foreach ($this->candidateKeys($identifier) as $key) {
$value = $this->getCbFieldValue($dataObject, $key);
$uid = $this->extractSingleUid($value);
if ($uid !== null) {
return $uid;
}
}
// 2) Heuristic: scan all fields in CB and match by suffix
$value = $this->scanCbForSuffix($dataObject, $identifier);
$uid = $this->extractSingleUid($value);
if ($uid !== null) {
return $uid;
}
// 3) Fallback: raw record by suffix match
return $this->resolveSingleUidField($record, $identifier);
}
/**
* Extract UID list (n:n relation) for $identifier from ContentBlockData OR raw record.
*/
private function resolveRelationUidsFromContentBlock(mixed $dataObject, array $record, string $identifier): array
{
// 1) Directly from ContentBlockData (variants)
foreach ($this->candidateKeys($identifier) as $key) {
$value = $this->getCbFieldValue($dataObject, $key);
$uids = $this->extractUidList($value);
if (!empty($uids)) {
return $uids;
}
}
// 2) Heuristic: scan CB
$value = $this->scanCbForSuffix($dataObject, $identifier);
$uids = $this->extractUidList($value);
if (!empty($uids)) {
return $uids;
}
// 3) Fallback: raw record
return $this->resolveRelationUids($record, $identifier);
}
/** Returns value of a field from ContentBlockData (get / getField / jsonSerialize / toArray). */
private function getCbFieldValue(mixed $dataObject, string $identifier): mixed
{
if (!\is_object($dataObject)) {
return null;
}
// get()
if (\method_exists($dataObject, 'get')) {
try {
$v = $dataObject->get($identifier);
if ($v !== null) {
return $v;
}
} catch (\Throwable) {}
}
// getField()
if (\method_exists($dataObject, 'getField')) {
try {
$v = $dataObject->getField($identifier);
if ($v !== null) {
return $v;
}
} catch (\Throwable) {}
}
// jsonSerialize()
if ($dataObject instanceof JsonSerializable) {
try {
$arr = $dataObject->jsonSerialize();
if (\is_array($arr) && \array_key_exists($identifier, $arr)) {
return $arr[$identifier];
}
} catch (\Throwable) {}
}
// toArray()
if (\method_exists($dataObject, 'toArray')) {
try {
$arr = $dataObject->toArray();
if (\is_array($arr) && \array_key_exists($identifier, $arr)) {
return $arr[$identifier];
}
} catch (\Throwable) {}
}
return null;
}
/** Heuristic: serialize all CB fields and find by suffix (normalized). */
private function scanCbForSuffix(mixed $dataObject, string $identifier): mixed
{
$targetNorm = $this->normalizeIdentifier($identifier);
$candidates = [];
if (\is_object($dataObject)) {
if ($dataObject instanceof JsonSerializable) {
try {
$c = $dataObject->jsonSerialize();
if (\is_array($c)) {
$candidates[] = $c;
}
} catch (\Throwable) {}
}
if (\method_exists($dataObject, 'toArray')) {
try {
$c = $dataObject->toArray();
if (\is_array($c)) {
$candidates[] = $c;
}
} catch (\Throwable) {}
}
}
foreach ($candidates as $arr) {
foreach ($arr as $key => $val) {
$keyNorm = $this->normalizeIdentifier((string)$key);
if (\str_ends_with($keyNorm, $targetNorm)) {
return $val;
}
}
}
return null;
}
/** Possible key variants (camelCase/snake_case) for the same identifier. */
private function candidateKeys(string $identifier): array
{
$camel = $identifier;
$snake = $this->toSnakeCase($identifier);
return \array_values(array_unique([$camel, $snake]));
}
private function toSnakeCase(string $s): string
{
$s = \preg_replace('/([a-z])([A-Z])/', '$1_$2', $s);
return \strtolower((string)$s);
}
/** Extract single UID from various value shapes (recursive for objects). */
private function extractSingleUid(mixed $value): ?int
{
if ($value === null) {
return null;
}
if (\is_int($value)) {
return $value > 0 ? $value : null;
}
if (\is_string($value)) {
$i = (int)$value;
return $i > 0 ? $i : null;
}
if (\is_array($value)) {
if (isset($value['uid'])) {
$u = (int)$value['uid'];
return $u > 0 ? $u : null;
}
if (isset($value[0]['uid'])) {
$u = (int)$value[0]['uid'];
return $u > 0 ? $u : null;
}
// possibly plain ID list
foreach ($value as $v) {
$i = (int)$v;
if ($i > 0) {
return $i;
}
}
return null;
}
if ($value instanceof JsonSerializable) {
try {
$arr = $value->jsonSerialize();
return $this->extractSingleUid($arr);
} catch (\Throwable) {}
}
if (\method_exists($value, 'toArray')) {
try {
$arr = $value->toArray();
return $this->extractSingleUid($arr);
} catch (\Throwable) {}
}
if ($value instanceof Traversable) {
foreach ($value as $item) {
$uid = $this->extractSingleUid($item);
if ($uid !== null) {
return $uid;
}
}
}
if (\method_exists($value, 'getUid')) {
try {
$u = (int)$value->getUid();
return $u > 0 ? $u : null;
} catch (\Throwable) {}
}
return null;
}
/** Extract UID list from various value shapes (recursive for objects/lists). */
private function extractUidList(mixed $value): array
{
$uids = [];
if ($value === null) {
return $uids;
}
if (\is_array($value)) {
if (isset($value[0]) && \is_array($value[0]) && isset($value[0]['uid'])) {
foreach ($value as $row) {
$uids[] = (int)$row['uid'];
}
} elseif (isset($value['uid'])) {
$uids[] = (int)$value['uid'];
} else {
foreach ($value as $v) {
$v = (int)$v;
if ($v > 0) {
$uids[] = $v;
}
}
}
return array_values(array_unique(array_filter($uids, static fn($v) => $v > 0)));
}
if (\is_string($value)) {
if (strpos($value, ',') !== false) {
foreach (explode(',', $value) as $v) {
$v = (int)trim($v);
if ($v > 0) {
$uids[] = $v;
}
}
} else {
$v = (int)$value;
if ($v > 0) {
$uids[] = $v;
}
}
return array_values(array_unique($uids));
}
if (\is_int($value) && $value > 0) {
return [$value];
}
if ($value instanceof JsonSerializable) {
try {
return $this->extractUidList($value->jsonSerialize());
} catch (\Throwable) {}
}
if (\method_exists($value, 'toArray')) {
try {
return $this->extractUidList($value->toArray());
} catch (\Throwable) {}
}
if ($value instanceof Traversable) {
foreach ($value as $item) {
$uid = $this->extractSingleUid($item);
if ($uid !== null) {
$uids[] = $uid;
}
}
return array_values(array_unique(array_filter($uids, static fn($v) => $v > 0)));
}
if (\method_exists($value, 'getUid')) {
try {
$u = (int)$value->getUid();
return $u > 0 ? [$u] : [];
} catch (\Throwable) {}
}
return [];
}
// ---------------------------
// Fallbacks to raw record
// ---------------------------
private function resolveSingleUidField(array $record, string $suffix): ?int
{
$targetNorm = $this->normalizeIdentifier($suffix);
foreach ($record as $key => $value) {
$keyNorm = $this->normalizeIdentifier((string)$key);
if (\str_ends_with($keyNorm, $targetNorm)) {
if (\is_array($value)) {
if (isset($value['uid'])) {
$u = (int)$value['uid'];
return $u > 0 ? $u : null;
}
if (isset($value[0]['uid'])) {
$u = (int)$value[0]['uid'];
return $u > 0 ? $u : null;
}
return null;
}
$int = (int)$value;
return $int > 0 ? $int : null;
}
}
return null;
}
private function resolveRelationUids(array $record, string $suffix): array
{
$uids = [];
$targetNorm = $this->normalizeIdentifier($suffix);
foreach ($record as $key => $value) {
$keyNorm = $this->normalizeIdentifier((string)$key);
if (!\str_ends_with($keyNorm, $targetNorm)) {
continue;
}
if (\is_array($value)) {
if (isset($value[0]) && \is_array($value[0]) && isset($value[0]['uid'])) {
foreach ($value as $row) {
$uids[] = (int)$row['uid'];
}
} elseif (isset($value['uid'])) {
$uids[] = (int)$value['uid'];
}
} elseif (\is_string($value) && strpos($value, ',') !== false) {
foreach (explode(',', $value) as $v) {
$v = trim($v);
if ($v !== '') {
$uids[] = (int)$v;
}
}
} elseif ((int)$value > 0) {
$uids[] = (int)$value;
}
}
return array_values(array_unique(array_filter($uids, static fn($v) => $v > 0)));
}
private function normalizeIdentifier(string $s): string
{
$s = \strtolower($s);
return (string)\preg_replace('~[^a-z0-9]+~', '', $s);
}
/** Determine site from Request/TSFE/PID (robust, without throwing on empty context). */
private function getSiteFromContext(
?ServerRequestInterface $request,
SiteFinder $siteFinder,
int $pid
): ?\TYPO3\CMS\Core\Site\Entity\Site {
$site = $request ? ($request->getAttribute('site') ?? null) : null;
if ($site instanceof \TYPO3\CMS\Core\Site\Entity\Site) {
return $site;
}
$tsfe = $GLOBALS['TSFE'] ?? null;
if ($tsfe && isset($tsfe->site) && $tsfe->site instanceof \TYPO3\CMS\Core\Site\Entity\Site) {
return $tsfe->site;
}
if ($pid > 0) {
try {
return $siteFinder->getSiteByPageId($pid);
} catch (\Throwable) {}
}
return null;
}
}
AddressResultsProcessor
<?php
declare(strict_types=1);
namespace Gedankenfolger\GedankenfolgerAddressfilter\DataProcessing;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use JsonSerializable;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Resource\FileReference;
/**
* Builds the result list of tt_address records from GET filters.
* - GET keys: gf_addressfilter[country], gf_addressfilter[category]
* - Country CSV column: tt_address.tx_gedankenfolger_addressfilter_countries (lowercased ISO2 codes)
* - Category via sys_category_record_mm (fieldname "categories")
* - If no filters are given AND CE flag "showAllInitially" is FALSE => return empty result
* - No site-settings defaults enforced
*
* Supports "as" from processor configuration to choose target variable name (default: "result").
* Enriches appliedFilters with complex objects for country/category.
*/
final class AddressResultsProcessor implements DataProcessorInterface
{
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
) {
/** @var ServerRequestInterface|null $request */
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
$queryParams = $request ? $request->getQueryParams() : [];
$gf = $queryParams['gf_addressfilter'] ?? [];
if (!\is_array($gf)) {
$gf = [];
}
// Read filters (prefer namespaced keys)
$country = strtoupper(trim((string)($gf['country'] ?? ($queryParams['country'] ?? ''))));
$category = (int)($gf['category'] ?? ($queryParams['category'] ?? 0));
$hasFilters = ($country !== '' || $category > 0);
/** @var ConnectionPool $connectionPool */
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
/** @var SiteFinder $siteFinder */
$siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
// Resolve Content Block data + raw record
$dataObject = $processedData['data'] ?? null;
$record = $this->resolveRecord($cObj, $processedData);
$pid = (int)($record['pid'] ?? 0);
// Site context (optional storage PIDs; no defaults enforced)
$site = $this->getSiteFromContext($request, $siteFinder, $pid);
$settings = $site ? $site->getSettings() : null;
// CE flag: show all results initially?
$showAllInitially = $this->resolveBoolFromContentBlock($dataObject, $record, 'showAllInitially');
// If no filters and flag is FALSE -> empty result set immediately
if (!$hasFilters && !$showAllInitially) {
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'result'); // honor "as", default "result"
$processedData[$targetVariableName] = [
'addresses' => [],
'total' => 0,
'hasResults' => false,
'appliedFilters' => [],
];
return $processedData;
}
// Optional: limit search to storage PIDs via site setting (CSV or array)
$storagePids = [];
if ($settings) {
$rawStorage = $settings->get('addressFilter.storagePids');
if (\is_string($rawStorage)) {
$parts = array_map('trim', explode(',', $rawStorage));
$storagePids = array_values(array_filter(array_map('intval', $parts)));
} elseif (\is_array($rawStorage)) {
$storagePids = array_values(array_filter(array_map('intval', $rawStorage)));
}
}
$qb = $connectionPool->getQueryBuilderForTable('tt_address');
$qb->select(
't.uid','t.pid','t.company','t.first_name','t.last_name',
't.email','t.phone','t.www','t.image'
)
->from('tt_address', 't')
->where(
$qb->expr()->eq('t.deleted', 0),
$qb->expr()->eq('t.hidden', 0)
);
if (!empty($storagePids)) {
$qb->andWhere(
$qb->expr()->in(
't.pid',
$qb->createNamedParameter($storagePids, Connection::PARAM_INT_ARRAY)
)
);
}
// Country filter against CSV field (lowercased by TCA eval=lower)
if ($country !== '') {
$code = strtolower($country);
// Preferred: inSet(), fallback: 4-way LIKE (no SQL functions on the left side)
if (\method_exists($qb->expr(), 'inSet')) {
$qb->andWhere(
$qb->expr()->inSet(
't.tx_gedankenfolger_addressfilter_countries',
$qb->createNamedParameter($code, ParameterType::STRING)
)
);
} else {
$pExact = $qb->createNamedParameter($code, ParameterType::STRING);
$pPref = $qb->createNamedParameter($code . ',%', ParameterType::STRING);
$pSuff = $qb->createNamedParameter('%,' . $code, ParameterType::STRING);
$pMiddle = $qb->createNamedParameter('%,' . $code . ',%', ParameterType::STRING);
$qb->andWhere(
$qb->expr()->or(
$qb->expr()->eq('t.tx_gedankenfolger_addressfilter_countries', $pExact),
$qb->expr()->like('t.tx_gedankenfolger_addressfilter_countries', $pPref),
$qb->expr()->like('t.tx_gedankenfolger_addressfilter_countries', $pSuff),
$qb->expr()->like('t.tx_gedankenfolger_addressfilter_countries', $pMiddle)
)
);
}
}
// Category filter
if ($category > 0) {
$qb->leftJoin(
't',
'sys_category_record_mm',
'mm',
$qb->expr()->and(
$qb->expr()->eq('mm.uid_foreign', $qb->quoteIdentifier('t.uid')),
$qb->expr()->eq('mm.tablenames', $qb->createNamedParameter('tt_address')),
$qb->expr()->eq('mm.fieldname', $qb->createNamedParameter('categories'))
)
);
$qb->andWhere(
$qb->expr()->eq(
'mm.uid_local',
$qb->createNamedParameter($category, ParameterType::INTEGER)
)
);
}
$qb->orderBy('t.last_name', 'ASC')->addOrderBy('t.first_name', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
// Convert FAL relation UID(s) to usable objects for the FE (Fluid) templates
$rows = $this->enrichRowsWithImages($connectionPool, $rows);
$total = (int)\count($rows);
$hasResults = ($total > 0);
// Build complex appliedFilters for FE
$appliedFilters = [];
if ($country !== '') {
$countryObj = $this->buildCountryObject($connectionPool, strtoupper($country), $request, $site);
$appliedFilters[] = $countryObj;
}
if ($category > 0) {
$categoryObj = $this->buildCategoryObject($connectionPool, $category);
if ($categoryObj !== null) {
$appliedFilters[] = $categoryObj;
}
}
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'result'); // honor "as", default "result"
$processedData[$targetVariableName] = [
'addresses' => $rows,
'total' => $total,
'hasResults' => $hasResults,
'appliedFilters' => $appliedFilters, // list of objects
];
return $processedData;
}
// ----------------- Applied filters builders -----------------
/**
* Country object with static_info_tables enrichment where available.
* @return array{
* type: 'country',
* code: string,
* name: string,
* label: string,
* iso2: string,
* iso3?: string,
* numeric?: int
* }
*/
private function buildCountryObject(
ConnectionPool $connectionPool,
string $iso2Upper,
?ServerRequestInterface $request,
?\TYPO3\CMS\Core\Site\Entity\Site $site
): array {
$base = [
'type' => 'country',
'code' => $iso2Upper,
'iso2' => $iso2Upper,
'name' => $iso2Upper,
'label' => $iso2Upper,
];
$conn = $connectionPool->getConnectionForTable('static_countries');
// If static_info_tables not present -> return code-only object
try {
$schema = $conn->createSchemaManager();
$tables = $schema->listTableNames();
if (!\in_array('static_countries', array_map('strtolower', $tables), true)) {
return $base;
}
} catch (\Throwable) {
return $base;
}
// Preferred language column
$preferredColumn = $this->getPreferredCountryNameColumn($request, $site);
// Check available columns
try {
$columns = array_change_key_case($schema->listTableColumns('static_countries'), CASE_LOWER);
} catch (\Throwable) {
$columns = [];
}
$nameColumn = null;
foreach ([$preferredColumn, 'cn_short_en', 'cn_official_name_en'] as $cand) {
if ($cand && isset($columns[strtolower($cand)])) {
$nameColumn = $cand;
break;
}
}
if ($nameColumn === null) {
return $base;
}
// Fetch row
$qb = $connectionPool->getQueryBuilderForTable('static_countries');
$qb->select($nameColumn, 'cn_iso_2', 'cn_iso_3', 'cn_iso_nr')
->from('static_countries')
->where(
$qb->expr()->eq('cn_iso_2', $qb->createNamedParameter($iso2Upper, ParameterType::STRING))
)
->setMaxResults(1);
$row = $qb->executeQuery()->fetchAssociative();
if (!\is_array($row)) {
return $base;
}
$name = trim((string)($row[$nameColumn] ?? '')) ?: $iso2Upper;
$iso3 = (string)($row['cn_iso_3'] ?? '');
$num = (int)($row['cn_iso_nr'] ?? 0);
return [
'type' => 'country',
'code' => $iso2Upper,
'iso2' => $iso2Upper,
'iso3' => $iso3 !== '' ? $iso3 : null,
'numeric' => $num > 0 ? $num : null,
'name' => $name,
'label' => sprintf('%s (%s)', $name, $iso2Upper),
];
}
/**
* Category object from sys_category.
* @return array{
* type: 'category',
* uid: int,
* title: string,
* label: string
* }|null
*/
private function buildCategoryObject(ConnectionPool $connectionPool, int $uid): ?array
{
$qb = $connectionPool->getQueryBuilderForTable('sys_category');
$qb->select('uid', 'title')
->from('sys_category')
->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid, ParameterType::INTEGER)))
->setMaxResults(1);
$row = $qb->executeQuery()->fetchAssociative();
if (!\is_array($row)) {
return null;
}
$title = (string)($row['title'] ?? '');
return [
'type' => 'category',
'uid' => (int)$row['uid'],
'title' => $title,
'label' => $title !== '' ? $title : ('#' . (int)$row['uid']),
];
}
// ----------------- Static Info Tables helpers -----------------
/**
* Decide which language column to prefer in static_countries based on site/request language.
* Example returns: 'cn_short_de', ... otherwise 'cn_short_en'.
*/
private function getPreferredCountryNameColumn(?ServerRequestInterface $request, ?\TYPO3\CMS\Core\Site\Entity\Site $site): string
{
$lang = null;
if ($request) {
$siteLanguage = $request->getAttribute('language');
if ($siteLanguage && \method_exists($siteLanguage, 'getTwoLetterIsoCode')) {
$lang = (string)$siteLanguage->getTwoLetterIsoCode();
}
}
if (!$lang && $site) {
try {
$defaultLang = $site->getDefaultLanguage();
if ($defaultLang && \method_exists($defaultLang, 'getTwoLetterIsoCode')) {
$lang = (string)$defaultLang->getTwoLetterIsoCode();
}
} catch (\Throwable) {
// ignore
}
}
$lang = strtolower((string)$lang);
$supported = ['de','en','fr','nl','it','es','pt','pl','cs','sk','hu','da','sv','no','fi'];
return \in_array($lang, $supported, true) ? ('cn_short_' . $lang) : 'cn_short_en';
}
// ----------------- Helpers: Content Blocks & Record -----------------
/** Resolve tt_content raw record (array), fallback to $cObj->data. */
private function resolveRecord(ContentObjectRenderer $cObj, array $processedData): array
{
$data = $processedData['data'] ?? null;
if (\is_object($data) && \method_exists($data, 'getRawRecord')) {
/** @phpstan-ignore-next-line */
return (array)$data->getRawRecord();
}
if (\is_array($data)) {
return $data;
}
return \is_array($cObj->data ?? null) ? $cObj->data : [];
}
/**
* Resolve boolean field from ContentBlockData by:
* 1) exact keys (camelCase/snake_case),
* 2) suffix scan in ContentBlockData (json/toArray),
* 3) suffix scan in raw record.
*/
private function resolveBoolFromContentBlock(mixed $dataObject, array $record, string $identifier): bool
{
// 1) exact keys in ContentBlockData
foreach ($this->candidateKeys($identifier) as $key) {
$val = $this->getCbFieldValue($dataObject, $key);
if ($val !== null) {
return $this->toBool($val);
}
}
// 2) suffix scan in ContentBlockData
$val = $this->scanCbForSuffix($dataObject, $identifier);
if ($val !== null) {
return $this->toBool($val);
}
// 3) fallback: scan raw record for suffix match
$targetNorm = $this->normalizeIdentifier($identifier);
foreach ($record as $k => $v) {
if (\str_ends_with($this->normalizeIdentifier((string)$k), $targetNorm)) {
return $this->toBool($v);
}
}
return false;
}
/** Get field value from ContentBlockData via get()/getField()/jsonSerialize()/toArray(). */
private function getCbFieldValue(mixed $dataObject, string $identifier): mixed
{
if (!\is_object($dataObject)) {
return null;
}
if (\method_exists($dataObject, 'get')) {
try { $v = $dataObject->get($identifier); if ($v !== null) { return $v; } } catch (\Throwable) {}
}
if (\method_exists($dataObject, 'getField')) {
try { $v = $dataObject->getField($identifier); if ($v !== null) { return $v; } } catch (\Throwable) {}
}
if ($dataObject instanceof JsonSerializable) {
try {
$arr = $dataObject->jsonSerialize();
if (\is_array($arr) && \array_key_exists($identifier, $arr)) { return $arr[$identifier]; }
} catch (\Throwable) {}
}
if (\method_exists($dataObject, 'toArray')) {
try {
$arr = $dataObject->toArray();
if (\is_array($arr) && \array_key_exists($identifier, $arr)) { return $arr[$identifier]; }
} catch (\Throwable) {}
}
return null;
}
/** Scan ContentBlockData for any key whose normalized name ends with $identifier. */
private function scanCbForSuffix(mixed $dataObject, string $identifier): mixed
{
if (!\is_object($dataObject)) {
return null;
}
$targetNorm = $this->normalizeIdentifier($identifier);
$candidates = [];
if ($dataObject instanceof JsonSerializable) {
try { $arr = $dataObject->jsonSerialize(); if (\is_array($arr)) { $candidates[] = $arr; } } catch (\Throwable) {}
}
if (\method_exists($dataObject, 'toArray')) {
try { $arr = $dataObject->toArray(); if (\is_array($arr)) { $candidates[] = $arr; } } catch (\Throwable) {}
}
foreach ($candidates as $arr) {
foreach ($arr as $key => $val) {
if (\str_ends_with($this->normalizeIdentifier((string)$key), $targetNorm)) {
return $val;
}
}
}
return null;
}
/** Candidate keys for a CB field: original + snake_case. */
private function candidateKeys(string $identifier): array
{
$camel = $identifier;
$snake = $this->toSnakeCase($identifier);
return \array_values(array_unique([$camel, $snake]));
}
// ----------------- Generic utils -----------------
private function toSnakeCase(string $s): string
{
$s = \preg_replace('/([a-z])([A-Z])/', '$1_$2', $s);
return \strtolower((string)$s);
}
private function normalizeIdentifier(string $s): string
{
$s = \strtolower($s);
return (string)\preg_replace('~[^a-z0-9]+~', '', $s);
}
private function toBool(mixed $v): bool
{
if (\is_bool($v)) return $v;
if (\is_int($v)) return $v !== 0;
if (\is_string($v)) {
$t = \strtolower(\trim($v));
return \in_array($t, ['1','true','on','yes','ja'], true);
}
if (\is_array($v)) {
foreach ($v as $vv) { if ($this->toBool($vv)) { return true; } }
return false;
}
return false;
}
// ----------------- Site helper -----------------
/** Resolve Site entity safely (no hard exception if context is incomplete). */
private function getSiteFromContext(
?ServerRequestInterface $request,
SiteFinder $siteFinder,
int $pid
): ?\TYPO3\CMS\Core\Site\Entity\Site {
$site = $request ? ($request->getAttribute('site') ?? null) : null;
if ($site instanceof \TYPO3\CMS\Core\Site\Entity\Site) {
return $site;
}
$tsfe = $GLOBALS['TSFE'] ?? null;
if ($tsfe && isset($tsfe->site) && $tsfe->site instanceof \TYPO3\CMS\Core\Site\Entity\Site) {
return $tsfe->site;
}
if ($pid > 0) {
try { return $siteFinder->getSiteByPageId($pid); } catch (\Throwable) {}
}
return null;
}
/**
* Enrich result rows with FAL file reference information for the 'image' field.
* This will add `imagesRaw` (array of \TYPO3\CMS\Core\Resource\FileReference) to
* each row so Fluid can use FileReference objects directly (e.g. <f:image image="{imagesRaw.0}" />).
*
* @param ConnectionPool $connectionPool
* @param array<int, array<string, mixed>> $rows
* @return array<int, array<string, mixed>>
*/
private function enrichRowsWithImages(ConnectionPool $connectionPool, array $rows): array
{
if (empty($rows)) {
return $rows;
}
/** @var FileRepository $fileRepository */
$fileRepository = GeneralUtility::makeInstance(FileRepository::class);
foreach ($rows as &$row) {
$uid = (int)($row['uid'] ?? 0);
if ($uid <= 0) {
$row['images'] = [];
$row['image'] = null;
continue;
}
try {
$fileRefs = $fileRepository->findByRelation('tt_address', 'image', $uid);
} catch (\Throwable $t) {
$fileRefs = [];
}
$row['image'] = $fileRefs;
}
unset($row);
return $rows;
}
}
packages/gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-results/templates/frontend.html
{namespace gfa=Gedankenfolger\GedankenfolgerAddressfilter\ViewHelpers}
<f:if condition="{data.showAllInitially} == 0 && {result.total} == 0">
<f:else>
<f:render section="Output" arguments="{_all}" />
</f:else>
</f:if>
<f:section name="Output">
<section id="c{data.uid}" class="gedankenfolger-addressfilter gedankenfolger-addressfilter-results">
<f:if condition="{result.total} > 0">
<f:if condition="{result.appliedFilters}">
<div class="row">
<div class="col">
<strong>
<f:translate
key="LLL:EXT:gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-results/language/labels.xlf:frontend.appliedFilters"
default="Active filters:"
/>
</strong>
<f:for each="{result.appliedFilters}" as="filter" iteration="iterator">
{filter.label}
<f:if condition="{iterator.isLast}">
<f:else>, </f:else>
</f:if>
</f:for>
</div>
</div>
</f:if>
</f:if>
<div class="row">
<div class="col">
<h2 class="">
<f:translate
key="LLL:EXT:gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-results/language/labels.xlf:frontend.heading"
default="Your contacts"
/>
</h2>
</div>
</div>
<div class="row">
<f:if condition="{result.total} > 0">
<f:then>
<f:for each="{result.addresses}" as="address">
<div class="col">
<div class="row">
<f:if condition="{address.image}">
<div class="col col-image">
<f:image loading="lazy" image="{address.image.0}" class="img-fluid" />
</div>
</f:if>
<div class="col col-text">
<f:if condition="{address.company}">
<div class="">{address.company}</div>
</f:if>
<div>{address.first_name} {address.last_name}</div>
<f:if condition="{address.email}">
<div><a href="mailto:{address.email}">{address.email}</a></div>
</f:if>
<f:if condition="{address.phone}">
<div><a href="tel:{address.phone}">{address.phone}</a></div>
</f:if>
<f:if condition="{address.www}">
<div><a href="{address.www}" rel="noopener" target="_blank">{address.www}</a></div>
</f:if>
</div>
</div>
</div>
</f:for>
</f:then>
<f:else>
<div class="col">
<div class="">
<f:translate
key="LLL:EXT:gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-results/language/labels.xlf:frontend.none"
default="No results."
/>
</div>
</div>
</f:else>
</f:if>
</div>
</section>
</f:section>
packages/gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-form/templates/frontend.html
{namespace gfa=Gedankenfolger\GedankenfolgerAddressfilter\ViewHelpers}
<section id="c{data.uid}" class="gedankenfolger-addressfilter gedankenfolger-addressfilter-form">
<div class="">
<div class="">
<f:variable name="formURL"><f:uri.page
pageUid="{filter.actionPid}" /></f:variable>
<form class="" action="{formURL}" method="get">
<!-- Country -->
<div class="">
<label class="" for="gf_country">
<f:translate key="LLL:EXT:gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-form/language/labels.xlf:frontend.country.label" />
</label>
<select id="gf_country" name="gf_addressfilter[country]" class="">
<option value="">
<f:translate key="LLL:EXT:gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-form/language/labels.xlf:frontend.country.placeholder" default="Select country" />
</option>
<f:for each="{filter.countries}" as="c">
<option value="{c.code}"{f:if(condition: c.selected, then: ' selected="selected"')}>
{c.label} ({c.code})
</option>
</f:for>
</select>
</div>
<!-- Category -->
<div class="">
<label class="" for="gf_category">
<f:translate key="LLL:EXT:gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-form/language/labels.xlf:frontend.category.label" />
</label>
<select id="gf_category" name="gf_addressfilter[category]" class="">
<option value="">
<f:translate key="LLL:EXT:gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-form/language/labels.xlf:frontend.category.placeholder" default="Select industry" />
</option>
<f:for each="{filter.categories}" as="cat">
<option value="{cat.uid}" {f:if(condition: cat.selected, then:'selected="selected"')}>{cat.title}</option>
</f:for>
</select>
</div>
<!-- Submit -->
<div class="">
<label class=""> </label>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
<span class="visually-hidden">
<f:translate key="LLL:EXT:gedankenfolger_addressfilter/ContentBlocks/ContentElements/address-filter-form/language/labels.xlf:frontend.submit" default="Search" />
</span>
</button>
</div>
</form>
</div>
</div>
</section>