<?php
declare(strict_types=1);
namespace MeinWeimar;
final class ContentTypes
{
private const PLACE_CATEGORY_MIGRATION_VERSION = '2';
public static function hooks(): void
{
add_action('init', [self::class, 'register']);
add_action('init', [self::class, 'register_meta']);
add_action('admin_init', [self::class, 'maybe_repair_place_category_hierarchy']);
}
/**
* Stable public resource names shared by WordPress, the website and Flutter.
* Internal post type names stay an implementation detail of the plugin.
*/
public static function definitions(): array
{
return [
'places' => ['post_type' => 'mw_place', 'label' => __('Places', 'meinweimar-core'), 'singular' => __('Place', 'meinweimar-core'), 'add_new' => __('Place hinzufügen', 'meinweimar-core'), 'edit' => __('Place bearbeiten', 'meinweimar-core'), 'slug' => 'places', 'icon' => 'dashicons-location-alt', 'category' => 'mw_place_category', 'geo' => true],
'events' => ['post_type' => 'mw_event', 'label' => __('Events', 'meinweimar-core'), 'singular' => __('Event', 'meinweimar-core'), 'add_new' => __('Event hinzufügen', 'meinweimar-core'), 'edit' => __('Event bearbeiten', 'meinweimar-core'), 'slug' => 'events', 'icon' => 'dashicons-calendar-alt', 'category' => 'mw_event_category', 'geo' => true],
'services' => ['post_type' => 'mw_service', 'label' => __('Services und Einrichtungen', 'meinweimar-core'), 'singular' => __('Service oder Einrichtung', 'meinweimar-core'), 'add_new' => __('Service hinzufügen', 'meinweimar-core'), 'edit' => __('Service bearbeiten', 'meinweimar-core'), 'slug' => 'services', 'icon' => 'dashicons-building', 'category' => 'mw_service_category', 'geo' => true],
'classifieds' => ['post_type' => 'mw_classified', 'label' => __('Kleinanzeigen', 'meinweimar-core'), 'singular' => __('Kleinanzeige', 'meinweimar-core'), 'add_new' => __('Kleinanzeige hinzufügen', 'meinweimar-core'), 'edit' => __('Kleinanzeige bearbeiten', 'meinweimar-core'), 'slug' => 'kleinanzeigen', 'icon' => 'dashicons-megaphone', 'category' => 'mw_classified_category', 'geo' => true],
'jobs' => ['post_type' => 'mw_job', 'label' => __('Jobs', 'meinweimar-core'), 'singular' => __('Job', 'meinweimar-core'), 'add_new' => __('Job hinzufügen', 'meinweimar-core'), 'edit' => __('Job bearbeiten', 'meinweimar-core'), 'slug' => 'jobs', 'icon' => 'dashicons-businessperson', 'category' => 'mw_job_category', 'geo' => true],
'real-estate' => ['post_type' => 'mw_property', 'label' => __('Immobilien', 'meinweimar-core'), 'singular' => __('Immobilie', 'meinweimar-core'), 'add_new' => __('Immobilie hinzufügen', 'meinweimar-core'), 'edit' => __('Immobilie bearbeiten', 'meinweimar-core'), 'slug' => 'immobilien', 'icon' => 'dashicons-admin-home', 'category' => 'mw_property_category', 'geo' => true],
'news' => ['post_type' => 'mw_news', 'label' => __('News', 'meinweimar-core'), 'singular' => __('News-Beitrag', 'meinweimar-core'), 'add_new' => __('News-Beitrag hinzufügen', 'meinweimar-core'), 'edit' => __('News-Beitrag bearbeiten', 'meinweimar-core'), 'slug' => 'news', 'icon' => 'dashicons-media-document', 'category' => 'mw_news_category', 'geo' => false],
'guides' => ['post_type' => 'mw_guide', 'label' => __('Ratgeber', 'meinweimar-core'), 'singular' => __('Ratgeber', 'meinweimar-core'), 'add_new' => __('Ratgeber hinzufügen', 'meinweimar-core'), 'edit' => __('Ratgeber bearbeiten', 'meinweimar-core'), 'slug' => 'guides', 'icon' => 'dashicons-book-alt', 'category' => 'mw_guide_category', 'geo' => false],
];
}
public static function public_post_types(): array
{
$post_types = [];
foreach (self::definitions() as $route => $definition) {
$post_types[$route] = $definition['post_type'];
}
return $post_types;
}
/**
* Legacy Place CPT labels become the first level of the shared Place
* taxonomy. The legacy post types themselves are intentionally not
* recreated in the target installation.
*
* @return array<string, array{name:string, source_post_type:string}>
*/
public static function place_category_roots(): array
{
return [
'essen-trinken' => [
'name' => 'Essen & Trinken',
'source_post_type' => 'gd_place',
],
'kunst-kultur' => [
'name' => 'Kunst & Kultur',
'source_post_type' => 'gd_kunstkultur',
],
'freizeit-erleben' => [
'name' => 'Freizeit & Erleben',
'source_post_type' => 'gd_freizeiterlebe',
],
];
}
/**
* Canonical Kleinanzeigen category tree used by the local site and fresh
* installations. Labels are German presentation values; slugs remain
* stable machine identifiers for REST, Flutter and conditional fields.
*
* @return array<string, array<string, string>>
*/
public static function classified_category_tree(): array
{
return [
'auto-rad-boot' => [
'name' => 'Auto, Rad & Boot',
'children' => [
'autos' => 'Autos',
'autoteile-reifen' => 'Autoteile & Reifen',
'boote-bootszubehoer' => 'Boote & Bootszubehör',
'fahrrad-zubehoer' => 'Fahrräder & Zubehör',
'motorraeder-motorroller' => 'Motorräder & Motorroller',
'motorradteile-zubehoer' => 'Motorradteile & Zubehör',
'nutzfahrzeuge-anhaenger' => 'Nutzfahrzeuge & Anhänger',
'reparaturen-dienstleistungen' => 'Reparaturen & Dienstleistungen',
'wohnwagen-wohnmobile' => 'Wohnwagen & -mobile',
'weiteres-auto-rad-boot' => 'Weiteres Auto, Rad & Boot',
],
],
'elektronik' => [
'name' => 'Elektronik',
'children' => [
'audio-hifi' => 'Audio & HiFi',
'dienstleistungen-elektronik' => 'Dienstleistungen Elektronik',
'foto' => 'Foto',
'handy-telefon' => 'Handy & Telefon',
'haushaltsgeraete' => 'Haushaltsgeräte',
'konsolen' => 'Konsolen',
'pc-zubehoer-software' => 'PC-Zubehör & Software',
'tablets-reader' => 'Tablets & Reader',
'tv-video' => 'TV & Video',
'videospiele' => 'Videospiele',
'weiteres-elektronik' => 'Weiteres Elektronik',
],
],
'haus-garten' => [
'name' => 'Haus & Garten',
'children' => [
'badezimmer' => 'Badezimmer',
'buero' => 'Büro',
'dekoration' => 'Dekoration',
'dienstleistungen-haus-garten' => 'Dienstleistungen Haus & Garten',
'gartenzubehoer-pflanzen' => 'Gartenzubehör & Pflanzen',
'heimtextilien' => 'Heimtextilien',
'heimwerken' => 'Heimwerken',
'kueche-esszimmer' => 'Küche & Esszimmer',
'lampen-licht' => 'Lampen & Licht',
'schlafzimmer' => 'Schlafzimmer',
'wohnzimmer' => 'Wohnzimmer',
'weiteres-haus-garten' => 'Weiteres Haus & Garten',
],
],
'familie-kind-baby' => [
'name' => 'Familie, Kind & Baby',
'children' => [
'baby-kinderschuhe' => 'Baby- & Kinderschuhe',
'baby-kinderkleidung' => 'Baby- & Kinderkleidung',
'babyausstattung' => 'Babyausstattung',
'kinderwagen-tragen' => 'Kinderwagen & Tragen',
'kinderzimmer' => 'Kinderzimmer',
'spielzeug' => 'Spielzeug',
'weiteres-familie-kind-baby' => 'Weiteres Familie, Kind & Baby',
],
],
'mode-beauty' => [
'name' => 'Mode & Beauty',
'children' => [
'beauty-gesundheit' => 'Beauty & Gesundheit',
'damenschuhe' => 'Damenschuhe',
'damenkleidung' => 'Damenkleidung',
'herrenschuhe' => 'Herrenschuhe',
'herrenkleidung' => 'Herrenkleidung',
'schmuck-uhren' => 'Schmuck & Uhren',
'taschen-accessoires' => 'Taschen & Accessoires',
'weiteres-mode-beauty' => 'Weiteres Mode & Beauty',
],
],
'dienstleistungen' => [
'name' => 'Dienstleistungen',
'children' => [
'dienstleistung-altenpflege' => 'Altenpflege',
'dienstleistung-auto-rad-boot' => 'Auto, Rad & Boot',
'dienstleistung-babysitter' => 'Babysitter/-in',
'dienstleistung-elektronik' => 'Elektronik',
'dienstleistung-haus-garten' => 'Haus & Garten',
'dienstleistung-kuenstler-musiker' => 'Künstler & Musiker',
'dienstleistung-reise-event' => 'Reise & Event',
'dienstleistung-tierbetreuung-training' => 'Tierbetreuung & Training',
'dienstleistung-umzug-transport' => 'Umzug & Transport',
'weiteres-dienstleistungen' => 'Weitere Dienstleistungen',
],
],
'musik-filme-buecher' => [
'name' => 'Musik, Filme & Bücher',
'children' => [
'buecher-zeitschriften' => 'Bücher & Zeitschriften',
'filme-dvds' => 'Filme & DVDs',
'musik-cds' => 'Musik & CDs',
'musikinstrumente' => 'Musikinstrumente',
'weiteres-musik-filme-buecher' => 'Weiteres Musik, Filme & Bücher',
],
],
'freizeit-hobby-nachbarschaft' => [
'name' => 'Freizeit, Hobby & Nachbarschaft',
'children' => [
'esoterik-spirituelles' => 'Esoterik & Spirituelles',
'essen-trinken' => 'Essen & Trinken',
'handarbeit' => 'Handarbeit',
'basteln-kunsthandwerk' => 'Basteln & Kunsthandwerk',
'kunst-antiquitaeten' => 'Kunst & Antiquitäten',
'modellbau' => 'Modellbau',
'reise-urlaub' => 'Reise & Urlaub',
'sammeln' => 'Sammeln',
'sport-camping' => 'Sport & Camping',
'weiteres-freizeit-hobby-nachbarschaft' => 'Weiteres Freizeit, Hobby & Nachbarschaft',
],
],
'haustiere' => [
'name' => 'Haustiere',
'children' => [
'hunde' => 'Hunde',
'katzen' => 'Katzen',
'pferde-zubehoer' => 'Pferde & Zubehör',
'kleintiere' => 'Kleintiere',
'voegel' => 'Vögel',
'fische' => 'Fische',
'reptilien' => 'Reptilien',
'tierzubehoer' => 'Tierzubehör',
'vermisste-tiere' => 'Vermisste Tiere',
'weitere-haustiere' => 'Weitere Haustiere',
],
],
'unterricht-kurse' => [
'name' => 'Unterricht & Kurse',
'children' => [
'unterricht-beauty-gesundheit' => 'Beauty & Gesundheit',
'computerkurse' => 'Computerkurse',
'nachhilfe' => 'Nachhilfe',
'musikunterricht' => 'Musikunterricht',
'sportkurse' => 'Sportkurse',
'sprachkurse' => 'Sprachkurse',
'weitere-unterricht-kurse' => 'Weitere Unterricht & Kurse',
],
],
'verschenken-tauschen' => [
'name' => 'Verschenken & Tauschen',
'children' => [
'verschenken' => 'Verschenken',
'tauschen' => 'Tauschen',
'verleihen' => 'Verleihen',
],
],
];
}
/**
* Canonical German job-field hierarchy shared by WordPress, the website
* and Flutter. Employment type, work mode and qualification remain fields
* and filters; they are deliberately not mixed into this taxonomy.
*
* @return array<string, array{name:string, children:array<string,string>}>
*/
public static function job_category_tree(): array
{
return [
'vertrieb-verkauf' => [
'name' => 'Vertrieb & Verkauf',
'children' => [
'einzelhandel' => 'Einzelhandel',
'aussendienst' => 'Außendienst',
'account-management' => 'Account Management',
'weiteres-vertrieb-verkauf' => 'Weitere Tätigkeiten im Vertrieb & Verkauf',
],
],
'verwaltung-buero' => [
'name' => 'Verwaltung & Büro',
'children' => [
'sekretariat' => 'Sekretariat',
'sachbearbeitung' => 'Sachbearbeitung',
'assistenz' => 'Assistenz',
'office-management' => 'Office Management',
'weiteres-verwaltung-buero' => 'Weitere Tätigkeiten in Verwaltung & Büro',
],
],
'it-softwareentwicklung' => [
'name' => 'IT & Softwareentwicklung',
'children' => [
'programmierung' => 'Programmierung',
'systemadministration' => 'Systemadministration',
'it-support' => 'IT-Support',
'daten-ki' => 'Daten & KI',
'webentwicklung' => 'Webentwicklung',
'weiteres-it-softwareentwicklung' => 'Weitere Tätigkeiten in IT & Softwareentwicklung',
],
],
'gesundheit-pflege' => [
'name' => 'Gesundheitswesen & Pflege',
'children' => [
'medizin' => 'Medizin',
'krankenpflege' => 'Krankenpflege',
'therapie' => 'Therapie',
'medizinische-verwaltung' => 'Medizinische Verwaltung',
'weiteres-gesundheitswesen-pflege' => 'Weitere Tätigkeiten im Gesundheitswesen & in der Pflege',
],
],
'handwerk-produktion' => [
'name' => 'Handwerk & Produktion',
'children' => [
'anlagenmechanik' => 'Anlagenmechanik',
'elektro-mechatronik' => 'Elektro & Mechatronik',
'fertigung' => 'Fertigung',
'qualitaetssicherung' => 'Qualitätssicherung',
'weiteres-handwerk-produktion' => 'Weitere Tätigkeiten in Handwerk & Produktion',
],
],
'gastronomie-hotellerie-tourismus' => [
'name' => 'Gastronomie, Hotellerie & Tourismus',
'children' => [
'kueche' => 'Küche',
'service' => 'Service',
'hotellerie' => 'Hotellerie',
'tourismus-reise' => 'Tourismus & Reise',
'weiteres-gastronomie-hotellerie-tourismus' => 'Weitere Tätigkeiten in Gastronomie, Hotellerie & Tourismus',
],
],
'marketing-pr-design' => [
'name' => 'Marketing, PR & Design',
'children' => [
'marketing' => 'Marketing',
'pr-kommunikation' => 'PR & Kommunikation',
'grafikdesign' => 'Grafik & Design',
'content-redaktion' => 'Content & Redaktion',
'weiteres-marketing-pr-design' => 'Weitere Tätigkeiten in Marketing, PR & Design',
],
],
'logistik-transport' => [
'name' => 'Logistik & Transport',
'children' => [
'fahrer' => 'Fahrer',
'disposition' => 'Disposition',
'spedition' => 'Spedition',
'lager-versand' => 'Lager & Versand',
'weiteres-logistik-transport' => 'Weitere Tätigkeiten in Logistik & Transport',
],
],
'finanzen-controlling-rechnungswesen' => [
'name' => 'Finanzen, Controlling & Rechnungswesen',
'children' => [
'buchhaltung' => 'Buchhaltung',
'controlling' => 'Controlling',
'banken-versicherungen' => 'Banken & Versicherungen',
'steuern' => 'Steuern',
'weiteres-finanzen-controlling-rechnungswesen' => 'Weitere Tätigkeiten in Finanzen, Controlling & Rechnungswesen',
],
],
'personalwesen-recruiting' => [
'name' => 'Personalwesen & Recruiting',
'children' => [
'recruiting' => 'Recruiting',
'personalbetreuung' => 'Personalbetreuung',
'personalentwicklung' => 'Personalentwicklung',
'lohn-gehaltsabrechnung' => 'Lohn- & Gehaltsabrechnung',
'weiteres-personalwesen-recruiting' => 'Weitere Tätigkeiten im Personalwesen & Recruiting',
],
],
'bildung-erziehung-wissenschaft' => [
'name' => 'Bildung, Erziehung & Wissenschaft',
'children' => [
'kita-schule' => 'Kita & Schule',
'hochschule' => 'Hochschule',
'forschung' => 'Forschung',
'nachhilfe-unterricht' => 'Nachhilfe & Unterricht',
'weiteres-bildung-erziehung-wissenschaft' => 'Weitere Tätigkeiten in Bildung, Erziehung & Wissenschaft',
],
],
'kultur-medien-veranstaltungen' => [
'name' => 'Kultur, Medien & Veranstaltungen',
'children' => [
'kultur' => 'Kultur',
'medien' => 'Medien',
'veranstaltungsmanagement' => 'Veranstaltungsmanagement',
'weiteres-kultur-medien-veranstaltungen' => 'Weitere Tätigkeiten in Kultur, Medien & Veranstaltungen',
],
],
'bau-architektur-immobilien' => [
'name' => 'Bau, Architektur & Immobilien',
'children' => [
'bau' => 'Bau',
'architektur' => 'Architektur',
'bauleitung' => 'Bauleitung',
'immobilienverwaltung' => 'Immobilienverwaltung',
'weiteres-bau-architektur-immobilien' => 'Weitere Tätigkeiten in Bau, Architektur & Immobilien',
],
],
'recht-versicherungen' => [
'name' => 'Recht & Versicherungen',
'children' => [
'juristische-taetigkeiten' => 'Juristische Tätigkeiten',
'notariat' => 'Notariat',
'versicherungsberatung' => 'Versicherungsberatung',
'weiteres-recht-versicherungen' => 'Weitere Tätigkeiten in Recht & Versicherungen',
],
],
'soziales-paedagogik' => [
'name' => 'Soziales & Pädagogik',
'children' => [
'sozialarbeit' => 'Sozialarbeit',
'beratung' => 'Beratung',
'jugendhilfe' => 'Jugendhilfe',
'weiteres-soziales-paedagogik' => 'Weitere Tätigkeiten in Soziales & Pädagogik',
],
],
'naturwissenschaften-umwelt-landwirtschaft' => [
'name' => 'Naturwissenschaften, Umwelt & Landwirtschaft',
'children' => [
'naturwissenschaften' => 'Naturwissenschaften',
'umwelt-nachhaltigkeit' => 'Umwelt & Nachhaltigkeit',
'landwirtschaft' => 'Landwirtschaft',
'weiteres-naturwissenschaften-umwelt-landwirtschaft' => 'Weitere Tätigkeiten in Naturwissenschaften, Umwelt & Landwirtschaft',
],
],
'reinigung-facility-hauswirtschaft' => [
'name' => 'Reinigung, Facility Management & Hauswirtschaft',
'children' => [
'reinigung' => 'Reinigung',
'facility-management' => 'Facility Management',
'hauswirtschaft' => 'Hauswirtschaft',
'weiteres-reinigung-facility-hauswirtschaft' => 'Weitere Tätigkeiten in Reinigung, Facility Management & Hauswirtschaft',
],
],
'sicherheit-bewachung' => [
'name' => 'Sicherheit & Bewachung',
'children' => [
'objektschutz' => 'Objektschutz',
'veranstaltungsschutz' => 'Veranstaltungsschutz',
'sicherheitsmanagement' => 'Sicherheitsmanagement',
'weiteres-sicherheit-bewachung' => 'Weitere Tätigkeiten in Sicherheit & Bewachung',
],
],
'oeffentlicher-dienst' => [
'name' => 'Öffentlicher Dienst & Verwaltung',
'children' => [
'kommunalverwaltung' => 'Kommunalverwaltung',
'landesbehoerden' => 'Landesbehörden',
'oeffentliche-betriebe' => 'Öffentliche Betriebe',
'weiteres-oeffentlicher-dienst' => 'Weitere Tätigkeiten im öffentlichen Dienst',
],
],
];
}
/**
* Create or repair the canonical job-field hierarchy.
*
* @return array{created:int,updated:int}
*/
public static function seed_job_categories(): array
{
$taxonomy = 'mw_job_category';
$created = 0;
$updated = 0;
foreach (self::job_category_tree() as $parent_slug => $parent_definition) {
$parent = term_exists($parent_slug, $taxonomy);
if (is_array($parent)) {
$parent_id = (int) $parent['term_id'];
wp_update_term($parent_id, $taxonomy, ['name' => $parent_definition['name']]);
$updated++;
} elseif (is_int($parent)) {
$parent_id = $parent;
$updated++;
} else {
$inserted = wp_insert_term($parent_definition['name'], $taxonomy, ['slug' => $parent_slug]);
if (is_wp_error($inserted)) {
continue;
}
$parent_id = (int) $inserted['term_id'];
$created++;
}
foreach ($parent_definition['children'] as $child_slug => $child_name) {
$child = term_exists($child_slug, $taxonomy);
if (is_array($child)) {
wp_update_term((int) $child['term_id'], $taxonomy, ['parent' => $parent_id, 'name' => $child_name]);
$updated++;
continue;
}
if (is_int($child)) {
wp_update_term($child, $taxonomy, ['parent' => $parent_id, 'name' => $child_name]);
$updated++;
continue;
}
$inserted = wp_insert_term($child_name, $taxonomy, ['slug' => $child_slug, 'parent' => $parent_id]);
if (!is_wp_error($inserted)) {
$created++;
}
}
}
return ['created' => $created, 'updated' => $updated];
}
/**
* Canonical event hierarchy for local culture, leisure, student and
* visitor discovery. Ticketing remains a separate event field.
*
* @return array<string, array{name:string, children:array<string,string> }>
*/
public static function event_category_tree(): array
{
return [
'konzerte' => [
'name' => 'Konzerte',
'children' => [
'rock-pop' => 'Rock & Pop',
'hard-heavy' => 'Hard & Heavy',
'hiphop-rnb' => "HipHop & R'n'B",
'electronic-dance' => 'Electronic & Dance',
'festivals' => 'Festivals',
'schlager-volksmusik' => 'Schlager & Volksmusik',
'clubkonzerte' => 'Clubkonzerte',
'country-folk' => 'Country & Folk',
'jazz-blues' => 'Jazz & Blues',
'weitere-konzerte' => 'Weitere Konzerte',
],
],
'kultur' => [
'name' => 'Kultur',
'children' => [
'theater' => 'Theater',
'klassische-konzerte' => 'Klassische Konzerte',
'oper-operette' => 'Oper & Operette',
'tanz-ballett' => 'Tanz / Ballett',
'festspiele' => 'Festspiele',
'kleinkunst' => 'Kleinkunst',
'lesungen-vortraege' => 'Lesungen & Vorträge',
'weitere-kultur' => 'Weitere Kultur',
],
],
'freizeit-events' => [
'name' => 'Freizeit & Events',
'children' => [
'zirkus' => 'Zirkus',
'ausstellungen' => 'Ausstellungen',
'fuehrungen-rundfahrten' => 'Führungen & Rundfahrten',
'kinder-familie' => 'Kinder & Familie',
'partys-silvester' => 'Partys & Silvester',
'karneval-oktoberfeste' => 'Karneval & Oktoberfeste',
'kino' => 'Kino',
'weitere-events' => 'Weitere Events',
],
],
'comedy' => [
'name' => 'Comedy',
'children' => [
'stand-up-comedy' => 'Stand-up-Comedy',
'kabarett' => 'Kabarett',
'comedy-shows' => 'Comedy-Shows',
],
],
'musicals-shows' => [
'name' => 'Musicals & Shows',
'children' => [
'musicals' => 'Musicals',
'show-events' => 'Show-Events',
],
],
'sport' => [
'name' => 'Sport',
'children' => [
'fussball' => 'Fußball',
'motorsport' => 'Motorsport',
'handball-basketball-eishockey' => 'Handball, Basketball & Eishockey',
'wintersport' => 'Wintersport',
'weitere-sportevents' => 'Weitere Sportevents',
],
],
'maerkte-feste-stadtleben' => [
'name' => 'Märkte, Feste & Stadtleben',
'children' => [
'wochenmaerkte' => 'Wochenmärkte',
'stadtfeste' => 'Stadtfeste',
'weihnachtsmaerkte' => 'Weihnachtsmärkte',
'flohmarkt' => 'Flohmärkte',
'weitere-maerkte-feste' => 'Weitere Märkte & Feste',
],
],
'workshops-kurse-bildung' => [
'name' => 'Workshops, Kurse & Bildung',
'children' => [
'workshops' => 'Workshops',
'kurse' => 'Kurse',
'seminare' => 'Seminare',
'hochschule-studierendenleben' => 'Hochschule & Studierendenleben',
'weitere-bildung' => 'Weitere Workshops, Kurse & Bildung',
],
],
];
}
/**
* Canonical real-estate hierarchy. Transaction and commission values are
* still stored in FieldSchemas; this taxonomy controls discovery and the
* progressive property-type choices only.
*
* @return array<string, array{name:string, children:array<string,mixed> }>
*/
public static function property_category_tree(): array
{
return [
'wohnen-zur-miete' => [
'name' => 'Wohnen zur Miete',
'children' => [
'mietwohnungen' => [
'name' => 'Mietwohnungen',
'children' => [
'etagenwohnung' => 'Etagenwohnung',
'dachgeschosswohnung' => 'Dachgeschosswohnung',
'souterrainwohnung' => 'Souterrainwohnung',
'penthouse' => 'Penthouse',
'maisonette' => 'Maisonette',
],
],
'haeuser-zur-miete' => [
'name' => 'Häuser zur Miete',
'children' => [
'einfamilienhaus-miete' => 'Einfamilienhaus',
'reihenhaus-miete' => 'Reihenhaus',
'doppelhaushaelfte-miete' => 'Doppelhaushälfte',
],
],
'wohnen-auf-zeit' => 'Wohnen auf Zeit',
'moebliert' => 'Möbliert',
'wg-zimmer' => 'WG-Zimmer',
],
],
'wohnen-zum-kauf' => [
'name' => 'Wohnen zum Kauf',
'children' => [
'eigentumswohnungen' => [
'name' => 'Eigentumswohnungen',
'children' => [
'etagenwohnung-kauf' => 'Etagenwohnung',
'dachgeschosswohnung-kauf' => 'Dachgeschosswohnung',
'penthouse-kauf' => 'Penthouse',
'maisonette-kauf' => 'Maisonette',
],
],
'haeuser-zum-kauf' => [
'name' => 'Häuser zum Kauf',
'children' => [
'bungalow' => 'Bungalow',
'villa' => 'Villa',
'mehrfamilienhaus' => 'Mehrfamilienhaus',
'massiv-fertighaus' => 'Massiv- & Fertighaus',
],
],
'grundstuecke' => [
'name' => 'Grundstücke',
'children' => [
'grundstueck-kauf' => 'Grundstück zum Kauf',
'grundstueck-pacht' => 'Grundstück zur Pacht',
],
],
'zwangsversteigerungen' => 'Zwangsversteigerungen',
'anlageobjekte-zinshaeuser' => 'Anlageobjekte / Zinshäuser',
],
],
'gewerbeimmobilien' => [
'name' => 'Gewerbeimmobilien',
'children' => [
'buero-praxen' => 'Büros & Praxen',
'einzelhandel-gastronomie' => 'Einzelhandel & Gastronomie',
'hallen-logistik-produktion' => 'Hallen, Logistik & Produktion',
'gewerbegrundstuecke' => 'Gewerbegrundstücke',
'spezialimmobilien' => 'Spezialimmobilien',
],
],
'stellplaetze-sonstiges' => [
'name' => 'Stellplätze & Sonstiges',
'children' => [
'garagen-stellplaetze' => 'Garagen & Stellplätze',
],
],
];
}
/**
* Seed a hierarchical tree with optional nested children.
*
* @param array<string,mixed> $tree
* @return array{created:int,updated:int}
*/
private static function seed_taxonomy_tree(string $taxonomy, array $tree): array
{
$created = 0;
$updated = 0;
$visit = function (array $nodes, int $parent_id = 0) use (&$visit, &$created, &$updated, $taxonomy): void {
foreach ($nodes as $slug => $definition) {
$name = is_array($definition) ? (string) ($definition['name'] ?? $slug) : (string) $definition;
$children = is_array($definition) ? (array) ($definition['children'] ?? []) : [];
$term = term_exists((string) $slug, $taxonomy);
if (is_array($term)) {
$term_id = (int) $term['term_id'];
wp_update_term($term_id, $taxonomy, ['name' => $name, 'parent' => $parent_id]);
$updated++;
} elseif (is_int($term)) {
$term_id = $term;
wp_update_term($term_id, $taxonomy, ['name' => $name, 'parent' => $parent_id]);
$updated++;
} else {
$inserted = wp_insert_term($name, $taxonomy, ['slug' => (string) $slug, 'parent' => $parent_id]);
if (is_wp_error($inserted)) {
continue;
}
$term_id = (int) $inserted['term_id'];
$created++;
}
if ($children) {
$visit($children, $term_id);
}
}
};
$visit($tree);
return ['created' => $created, 'updated' => $updated];
}
/**
* Create the shared Place roots used by fresh installations and imports.
* Legacy child categories are added below these roots by the importer.
*
* @return array{created:int,updated:int}
*/
public static function seed_place_categories(): array
{
$taxonomy = 'mw_place_category';
$created = 0;
$updated = 0;
foreach (self::place_category_roots() as $slug => $definition) {
$term = term_exists($slug, $taxonomy);
if (is_array($term)) {
$term_id = (int) ($term['term_id'] ?? 0);
if ($term_id) {
$result = wp_update_term($term_id, $taxonomy, [
'name' => $definition['name'],
'parent' => 0,
]);
if (!is_wp_error($result)) {
$updated++;
}
}
continue;
}
if (is_int($term)) {
$result = wp_update_term($term, $taxonomy, [
'name' => $definition['name'],
'parent' => 0,
]);
if (!is_wp_error($result)) {
$updated++;
}
continue;
}
$inserted = wp_insert_term($definition['name'], $taxonomy, [
'slug' => $slug,
'parent' => 0,
]);
if (!is_wp_error($inserted)) {
$created++;
}
}
return ['created' => $created, 'updated' => $updated];
}
/**
* Repair the already imported Place posts without touching unrelated
* hand-created Places. A legacy source CPT determines the root; imported
* source categories remain as children below that root.
*
* @return array{posts_repaired:int,terms_reparented:int,errors:list<string>}
*/
public static function repair_place_category_hierarchy(): array
{
$taxonomy = 'mw_place_category';
$roots = self::place_category_roots();
$root_ids = [];
foreach ($roots as $slug => $definition) {
$term = term_exists($slug, $taxonomy);
$root_ids[$definition['source_post_type']] = is_array($term)
? (int) ($term['term_id'] ?? 0)
: (is_int($term) ? $term : 0);
}
$posts = get_posts([
'post_type' => 'mw_place',
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_key' => '_mw_legacy_source_key',
]);
$stats = [
'posts_repaired' => 0,
'terms_reparented' => 0,
'errors' => [],
];
foreach (array_map('absint', (array) $posts) as $post_id) {
$source_key = (string) get_post_meta($post_id, '_mw_legacy_source_key', true);
if (preg_match('/:(gd_place|gd_kunstkultur|gd_freizeiterlebe):\d+$/', $source_key, $matches) !== 1) {
continue;
}
$root_id = (int) ($root_ids[$matches[1]] ?? 0);
if (!$root_id) {
$stats['errors'][] = 'Place ' . $post_id . ': root category missing';
continue;
}
$term_ids = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
if (is_wp_error($term_ids)) {
$stats['errors'][] = 'Place ' . $post_id . ': categories could not be read';
continue;
}
$assigned = [$root_id];
foreach (array_map('absint', (array) $term_ids) as $term_id) {
if (!$term_id || $term_id === $root_id) {
continue;
}
$updated = wp_update_term($term_id, $taxonomy, ['parent' => $root_id]);
if (is_wp_error($updated)) {
$stats['errors'][] = 'Place ' . $post_id . ': category ' . $term_id . ' could not be nested';
continue;
}
$stats['terms_reparented']++;
$assigned[] = $term_id;
}
$assigned = array_values(array_unique(array_filter(array_map('absint', $assigned))));
$result = wp_set_post_terms($post_id, $assigned, $taxonomy, false);
if (is_wp_error($result)) {
$stats['errors'][] = 'Place ' . $post_id . ': categories could not be assigned';
continue;
}
$stats['posts_repaired']++;
}
return $stats;
}
/** Run the one-time repair for existing local imports. */
public static function maybe_repair_place_category_hierarchy(): void
{
$option = 'meinweimar_place_category_hierarchy_version';
if (self::PLACE_CATEGORY_MIGRATION_VERSION === (string) get_option($option, '')) {
return;
}
self::seed_place_categories();
$stats = self::repair_place_category_hierarchy();
if (empty($stats['errors'])) {
update_option($option, self::PLACE_CATEGORY_MIGRATION_VERSION, false);
}
}
public static function seed_event_categories(): array
{
return self::seed_taxonomy_tree('mw_event_category', self::event_category_tree());
}
public static function seed_property_categories(): array
{
return self::seed_taxonomy_tree('mw_property_category', self::property_category_tree());
}
/**
* Create or repair the canonical Kleinanzeigen hierarchy.
*
* @return array{created:int,updated:int}
*/
public static function seed_classified_categories(): array
{
$taxonomy = 'mw_classified_category';
$created = 0;
$updated = 0;
foreach (self::classified_category_tree() as $parent_slug => $parent_definition) {
$parent = term_exists($parent_slug, $taxonomy);
if (is_array($parent)) {
$parent_id = (int) $parent['term_id'];
$updated++;
} elseif (is_int($parent)) {
$parent_id = $parent;
$updated++;
} else {
$inserted = wp_insert_term($parent_definition['name'], $taxonomy, ['slug' => $parent_slug]);
if (is_wp_error($inserted)) {
continue;
}
$parent_id = (int) $inserted['term_id'];
$created++;
}
foreach ($parent_definition['children'] as $child_slug => $child_name) {
$child = term_exists($child_slug, $taxonomy);
if (is_array($child)) {
$child_id = (int) $child['term_id'];
wp_update_term($child_id, $taxonomy, ['parent' => $parent_id, 'name' => $child_name]);
$updated++;
continue;
}
if (is_int($child)) {
wp_update_term($child, $taxonomy, ['parent' => $parent_id, 'name' => $child_name]);
$updated++;
continue;
}
$inserted = wp_insert_term($child_name, $taxonomy, ['slug' => $child_slug, 'parent' => $parent_id]);
if (!is_wp_error($inserted)) {
$created++;
}
}
}
return ['created' => $created, 'updated' => $updated];
}
public static function post_type_for_route(string $route): ?string
{
$definition = self::definitions()[$route] ?? null;
return is_array($definition) ? $definition['post_type'] : null;
}
public static function route_for_post_type(string $post_type): string
{
foreach (self::definitions() as $route => $definition) {
if ($definition['post_type'] === $post_type) {
return $route;
}
}
return $post_type;
}
public static function category_taxonomy(string $post_type): ?string
{
foreach (self::definitions() as $definition) {
if ($definition['post_type'] === $post_type) {
return $definition['category'];
}
}
return null;
}
public static function geo_post_types(): array
{
$post_types = [];
foreach (self::definitions() as $definition) {
if (!empty($definition['geo'])) {
$post_types[] = $definition['post_type'];
}
}
return $post_types;
}
/**
* Public resource ids that belong to the shared map/discovery surface.
* Editorial resources such as News and Guides deliberately stay outside
* this list even when legacy posts happen to contain location metadata.
*
* @return list<string>
*/
public static function geo_routes(): array
{
$routes = [];
foreach (self::definitions() as $route => $definition) {
if (!empty($definition['geo'])) {
$routes[] = $route;
}
}
return $routes;
}
public static function register(): void
{
register_post_type('mw_place', [
'labels' => [
'name' => __('Places', 'meinweimar-core'),
'singular_name' => __('Place', 'meinweimar-core'),
'add_new_item' => __('Place hinzufügen', 'meinweimar-core'),
'edit_item' => __('Place bearbeiten', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'menu_icon' => 'dashicons-location-alt',
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'revisions'],
'has_archive' => true,
'rewrite' => ['slug' => 'places', 'with_front' => false],
'map_meta_cap' => true,
]);
register_post_type('mw_event', [
'labels' => [
'name' => __('Events', 'meinweimar-core'),
'singular_name' => __('Event', 'meinweimar-core'),
'add_new_item' => __('Event hinzufügen', 'meinweimar-core'),
'edit_item' => __('Event bearbeiten', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'menu_icon' => 'dashicons-calendar-alt',
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'revisions'],
'has_archive' => true,
'rewrite' => ['slug' => 'events', 'with_front' => false],
'map_meta_cap' => true,
]);
foreach (['services', 'classifieds', 'jobs', 'real-estate', 'news', 'guides'] as $route) {
$definition = self::definitions()[$route];
register_post_type($definition['post_type'], [
'labels' => [
'name' => $definition['label'],
'singular_name' => $definition['singular'],
'add_new_item' => $definition['add_new'],
'edit_item' => $definition['edit'],
],
'public' => true,
'show_in_rest' => true,
'menu_icon' => $definition['icon'],
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'revisions', 'author'],
'has_archive' => true,
'rewrite' => ['slug' => $definition['slug'], 'with_front' => false],
'map_meta_cap' => true,
]);
}
register_taxonomy('mw_place_category', ['mw_place'], [
'labels' => [
'name' => __('Place-Kategorien', 'meinweimar-core'),
'singular_name' => __('Place-Kategorie', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => true,
'rewrite' => ['slug' => 'place-kategorie', 'with_front' => false],
]);
register_taxonomy('mw_event_category', ['mw_event'], [
'labels' => [
'name' => __('Event-Kategorien', 'meinweimar-core'),
'singular_name' => __('Event-Kategorie', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => true,
'rewrite' => ['slug' => 'event-kategorie', 'with_front' => false],
]);
foreach (['services', 'classifieds', 'jobs', 'real-estate', 'news', 'guides'] as $route) {
$definition = self::definitions()[$route];
register_taxonomy($definition['category'], [$definition['post_type']], [
'labels' => [
'name' => sprintf(__('%s-Kategorien', 'meinweimar-core'), $definition['label']),
'singular_name' => sprintf(__('%s-Kategorie', 'meinweimar-core'), $definition['singular']),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => true,
'rewrite' => ['slug' => sanitize_title($definition['label']) . '-kategorie', 'with_front' => false],
]);
}
register_taxonomy('mw_tag', array_values(self::public_post_types()), [
'labels' => [
'name' => __('Gemeinsame Tags', 'meinweimar-core'),
'singular_name' => __('Tag', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => false,
'rewrite' => ['slug' => 'tag', 'with_front' => false],
]);
register_taxonomy('mw_region', self::geo_post_types(), [
'labels' => [
'name' => __('Regionen', 'meinweimar-core'),
'singular_name' => __('Region', 'meinweimar-core'),
],
'public' => true,
'show_in_rest' => true,
'hierarchical' => true,
'rewrite' => ['slug' => 'region', 'with_front' => false],
]);
}
public static function register_meta(): void
{
EventOccurrences::register_meta();
foreach (self::geo_post_types() as $post_type) {
$route = self::route_for_post_type($post_type);
foreach (FieldSchemas::meta_for_route($route) as $key => $schema) {
register_post_meta($post_type, $key, [
'single' => true,
'type' => $schema['wp_type'],
'show_in_rest' => $schema['show_in_rest'],
'sanitize_callback' => FieldSchemas::sanitize_callback($key, $route),
'auth_callback' => static fn (): bool => current_user_can('edit_posts'),
]);
}
}
foreach (array_values(self::public_post_types()) as $post_type) {
register_post_meta($post_type, 'mw_content_locale', [
'single' => true,
'type' => 'string',
'show_in_rest' => true,
'sanitize_callback' => [Settings::class, 'sanitize_locale'],
'auth_callback' => static fn (): bool => current_user_can('edit_posts'),
]);
register_post_meta($post_type, 'mw_gallery_ids', [
'single' => true,
'type' => 'array',
'show_in_rest' => [
'schema' => [
'type' => 'array',
'items' => ['type' => 'integer'],
],
],
'sanitize_callback' => [self::class, 'sanitize_gallery_ids'],
'auth_callback' => static fn (): bool => current_user_can('edit_posts'),
]);
}
}
public static function sanitize_gallery_ids($value): array
{
if (!is_array($value)) {
$value = is_scalar($value) ? preg_split('/[,\s]+/', (string) $value) : [];
}
$ids = [];
foreach (array_slice((array) $value, 0, 20) as $item) {
$attachment_id = absint($item);
if ($attachment_id && wp_attachment_is_image($attachment_id)) {
$ids[] = $attachment_id;
}
}
return array_values(array_unique($ids));
}
}
