11 KiB
S3 — [shortcode_dmr] + хелперы wpmc_current_town / wpmc_alt_get_field / wpmc_remove_domain_link / wpmc_get_term_by_slug
- Beads:
cp-arq(parent epiccp-cw4) - Depends on:
cp-q7g(S2 — taxonomytown+ CPTshortcode-town+ ACFlist_shortcode, закрыт) - Blocks:
cp-83m(S4 — URL-роутер/{city}/{path}/) - Дата: 2026-05-12
Цель
Ядро подстановки контента по городу. Тема вызывает четыре публичные wpmc-функции и шорткод [shortcode_dmr id=N]; плагин разрешает значение для текущего города из ACF-репитера list_shortcode поста-контейнера.
Версия S3 не зависит от URL-роутера (S4) — current_town() работает через парсинг REQUEST_URI напрямую. S4 потом подключит свою query-var логику через фильтр wpmc_current_town_slug.
Архитектура
includes/
├── class-helpers.php ← WPMultiCity\Helpers
├── class-shortcode-dmr.php ← WPMultiCity\ShortcodeDmr
└── wpmc-functions.php ← процедурный API (тонкие обёртки)
wp-multi-city.php ← require_once wpmc-functions.php (unconditional)
includes/class-plugin.php ← boot() добавляет ShortcodeDmr::register()
Helpers — статический класс с методами реализации. ShortcodeDmr — статический класс с register() (вешает add_shortcode на init) и handler-методом. wpmc-functions.php — глобальные функции wpmc_*, каждая в одну строку вызывает соответствующий метод Helpers.
Загрузка процедурного API — безусловно в главном файле плагина (до проверки ACF), чтобы function_exists('wpmc_current_town') в шаблонах темы всегда возвращал true.
Публичный API
function wpmc_current_town(): ?string;
function wpmc_get_term_by_slug(string $slug, string $taxonomy = 'town'): ?WP_Term;
function wpmc_alt_get_field(string $selector, $post_id = false, bool $format_value = true);
function wpmc_remove_domain_link(string $url): string;
Shortcode: [shortcode_dmr id=N].
Helpers::current_town(): ?string
Логика:
- Per-request memoization — если уже определено, вернуть закэшированное.
- Распарсить
$_SERVER['REQUEST_URI']: разбить по/, отбросить пустые сегменты, взять первый. - Если первый сегмент совпадает со slug существующего term'а
town(черезget_terms('town', ['fields' => 'id=>slug', 'hide_empty' => false])) → resolved = этот slug. - Иначе → resolved =
apply_filters('wpmc_main_town_slug', null)(тема выставляет дефолт). resolved = apply_filters('wpmc_current_town_slug', $resolved)— extension-point для S4.- Сохранить в memo, вернуть.
Возвращает null если ни один из шагов 3-5 не дал значение (т.е. URI не совпадает с термом, фильтры пусты).
Helpers::get_term_by_slug(string $slug, string $taxonomy = 'town'): ?WP_Term
private static array $term_cache = [];
public static function get_term_by_slug(string $slug, string $taxonomy = 'town'): ?WP_Term
{
$key = $taxonomy . '|' . $slug;
if (array_key_exists($key, self::$term_cache)) {
return self::$term_cache[$key];
}
$term = get_term_by('slug', $slug, $taxonomy);
return self::$term_cache[$key] = ($term instanceof \WP_Term) ? $term : null;
}
ShortcodeDmr — handler [shortcode_dmr id=N]
1. shortcode_atts(['id' => 0], $atts); $id = (int) $atts['id']; if ($id <= 0) return '';
2. $town = Helpers::current_town(); if ($town === null) return '';
3. $term = Helpers::get_term_by_slug($town); if ($term === null) return '';
4. Per-request cache key = $id . '|' . $town. Если есть — вернуть.
5. $rows = get_field('list_shortcode', $id); if (empty($rows) || !is_array($rows)) cache and return ''.
6. foreach ($rows as $row): if ((int) $row['town'] === $term->term_id) { result = (string) $row['text']; break; }
7. Save to cache and return.
Регистрация: add_action('init', [self::class, 'do_register']) → внутри add_shortcode('shortcode_dmr', [self::class, 'render']).
Helpers::alt_get_field($selector, $post_id, $format_value)
public static function alt_get_field(string $selector, $post_id = false, bool $format_value = true)
{
$post_id = apply_filters('wpmc_alt_get_field_post_id', $post_id, $selector);
if (!function_exists('get_field')) {
return null;
}
$value = get_field($selector, $post_id, $format_value);
return is_string($value) ? do_shortcode($value) : $value;
}
S4 подключит свой фильтр wpmc_alt_get_field_post_id для clone→original маппинга. В S3 фильтр без подписчиков — $post_id передаётся через get_field без изменений.
function_exists('get_field') guard — на случай если ACF деактивирован после загрузки темы.
Helpers::remove_domain_link(string $url): string
public static function remove_domain_link(string $url): string
{
$parsed = parse_url($url);
if ($parsed === false || empty($parsed['scheme'])) {
return $url;
}
$scheme = strtolower($parsed['scheme']);
if ($scheme !== 'http' && $scheme !== 'https') {
return $url;
}
$path = $parsed['path'] ?? '';
$query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
$fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
return $path . $query . $fragment;
}
Поведение:
https://site.com/foo/bar/→/foo/bar/http://site.com/foo?q=1#a→/foo?q=1#a/foo/bar/(без schemе) →/foo/bar/(no-op)mailto:x@y.com→mailto:x@y.com(no-op)tel:+79991234567→tel:+79991234567(no-op)
Plugin::boot() — изменение
После S2-регистраций (Taxonomy/PostType/FieldGroup) добавить:
ShortcodeDmr::register();
Helpers сам по себе не регистрирует никаких хуков — это статический utility-класс.
wp-multi-city.php — изменение
Добавить:
require_once WPMC_PLUGIN_DIR . 'includes/wpmc-functions.php';
После строки с register_autoloader() и до plugins_loaded add_action. Безусловно — функции должны быть доступны теме даже если ACF отсутствует.
Тестовый стек
PHPUnit 9.6 + Brain Monkey 2.7 + Mockery 1.6 + Patchwork (уже настроены в S2).
Дополнительно в tests/TestCase.php::setUp() после Monkey\setUp():
\WPMultiCity\Helpers::reset_cache();
\WPMultiCity\ShortcodeDmr::reset_cache();
Каждый класс выставит публичный reset_cache() метод (только для тестов; не используется в production-коде, но не помечается @internal — это легитимная точка очистки).
patchwork.json уже разрешает редефайн function_exists (нужен для теста alt_get_field без ACF).
Тестовые сценарии
tests/HelpersTest.php:
current_town: REQUEST_URI=/tyumen/, termtyumenexists →'tyumen'.current_town: REQUEST_URI=/something-not-a-city/, нет main_town filter →null.current_town: REQUEST_URI=/, filterwpmc_main_town_slug→'spb'→'spb'.current_town: REQUEST_URI=/tyumen/, termtyumenexists, main_town='spb'→'tyumen'(URI выигрывает).current_town: фильтрwpmc_current_town_slugперекрывает результат →'override'.current_town: per-request memo — второй вызов не зовётget_termsповторно.get_term_by_slug: первый вызов вызываетget_term_by, второй — нет (cache).get_term_by_slug: term не найден →null(и null закэширован).alt_get_field: string value →do_shortcodeприменён.alt_get_field: array value → возвращён безdo_shortcode.alt_get_field: фильтрwpmc_alt_get_field_post_idподменяет $post_id.alt_get_field:function_exists('get_field')false →null.remove_domain_link:https://site.com/foo/→/foo/.remove_domain_link:https://site.com/foo?q=1#a→/foo?q=1#a.remove_domain_link:/foo/→/foo/(no-op).remove_domain_link:mailto:x@y→mailto:x@y(no-op).remove_domain_link: невалидный URL → возвращает как есть.
tests/ShortcodeDmrTest.php:
registerвешаетinitaction сdo_register.do_registerвызываетadd_shortcode('shortcode_dmr', ...).renderс id=0 →''.renderкогдаcurrent_town()= null →''.renderкогда term не найден →''.renderкогда rows = [] →''.renderкогда matching row → возвращаетtext.renderкогда no matching row →''.render— второй вызов с теми же $atts → не зовётget_fieldповторно (cache).
tests/PluginBootShortcodeDmrTest.php:
Plugin::boot()вешаетShortcodeDmr::do_registerна init.
Финал: 27 новых тестов + 9 предыдущих = 36 тестов суммарно.
Acceptance verification (после green-тестов)
vendor/bin/phpunit→ 36 тестов зелёные, 0 risky/warning.- Активировать плагин на чистом WP с ACF Pro + темой, в которой шаблон вызывает
wpmc_current_town()и[shortcode_dmr id=42]. - Создать таксон
townсо slug'амиspb,tyumen. - Создать
shortcode-townпост ID=42 с одной строкойlist_shortcode: town=tyumen, text=Тюмень-текст. - Открыть
/tyumen/→ шорткод должен отдатьТюмень-текст. Открыть/→''(без filter'а). Сadd_filter('wpmc_main_town_slug', fn() => 'spb')и второй row town=spb, text=СПб-текст→/отдаётСПб-текст.
Что НЕ входит в S3
- query-var
townи URL-роутер/{city}/{path}/— S4 (cp-83m). page_unic/ clone-to-original mapping — S4.- Yoast/CF7/sitemap фильтры — out of MVP.
- Admin UI / settings page для main_town slug — используем фильтр.
Open questions
Нет. Все решения зафиксированы.