7 Commits

Author SHA1 Message Date
Vladimir Bryzgalov d0a7b8eb9d wp-plugin-info.json: update for 1.0.1
tests / phpunit (push) Has been cancelled
2026-05-17 17:02:11 +05:00
Vladimir Bryzgalov d36408a4a6 release: 1.0.1
tests / phpunit (push) Has been cancelled
2026-05-17 17:02:11 +05:00
Vladimir Bryzgalov c002b08275 docs(wpmc 1.0.1): changelog entries in readme.txt + CHANGELOG.md
tests / phpunit (push) Has been cancelled
Add 1.0.1 release notes documenting the three hotfixes: procedural wrapper,
WP 6.7 textdomain notice fix, and ACF early-call guard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:01:23 +05:00
Vladimir Bryzgalov 82d2345d12 fix(wpmc 1.0.1): guard ACF helpers with did_action('acf/init')
Add private Helpers::acf_ready() that checks did_action('acf/init') > 0,
then guard the five ACF-dependent methods (alt_get_field, index_town_alt_page_id,
main_town_slug, page_unic_swap, is_routable_path) so they return safe defaults
(null/true) before ACF initialises. Fixes the acf_get_value "called too early"
notice triggered when home_url filter fires before acf/init.

Update existing tests in HelpersTest, LinkFilterTest, RedirectTest, RouterTest
to stub did_action => 1 where ACF-ready behaviour is expected. Add four new
guard tests covering before/after acf/init for main_town_slug and is_routable_path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:01:19 +05:00
Vladimir Bryzgalov 949fb19e95 fix(wpmc 1.0.1): remove __() from labels (no translations, fixes WP 6.7 notice)
Replace all __('...', 'wp-multi-city') calls in CPT, taxonomy, ACF field group,
and options page label arrays with literal Russian strings. The plugin ships no
.mo translation files, so wrapping labels in __() was YAGNI and triggered the
WP 6.7+ _load_textdomain_just_in_time notice.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:01:11 +05:00
Vladimir Bryzgalov 19409ac062 feat(wpmc 1.0.1): wpmc_main_town_slug procedural wrapper
Add wpmc_main_town_slug() wrapper function to wpmc-functions.php, delegating
to Helpers::main_town_slug(). The function was documented in the Hook API
README but was missing from the codebase.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:01:05 +05:00
Vladimir Bryzgalov 4c6173619b wp-plugin-info.json: update for 1.0.0
tests / phpunit (push) Has been cancelled
2026-05-17 12:49:33 +05:00
14 changed files with 162 additions and 36 deletions
+9
View File
@@ -2,6 +2,15 @@
All notable changes to wp-multi-city. Format follows [Keep a Changelog](https://keepachangelog.com/). All notable changes to wp-multi-city. Format follows [Keep a Changelog](https://keepachangelog.com/).
## [1.0.1] — 2026-05-17
### Added
- Процедурный wrapper `wpmc_main_town_slug()` (документирован в README, отсутствовал в коде).
### Fixed
- WP 6.7+ notice `_load_textdomain_just_in_time`: убраны `__()` из labels CPT/таксономии/ACF field group.
- ACF `acf_get_value` "called too early": `did_action('acf/init')` guard в 5 Helpers методах.
## [1.0.0] — 2026-05-17 ## [1.0.0] — 2026-05-17
### Added ### Added
+4 -4
View File
@@ -25,15 +25,15 @@ final class FieldGroup
'fields' => [ 'fields' => [
[ [
'key' => self::REPEATER_KEY, 'key' => self::REPEATER_KEY,
'label' => __('Подстановки по городам', 'wp-multi-city'), 'label' => 'Подстановки по городам',
'name' => 'list_shortcode', 'name' => 'list_shortcode',
'type' => 'repeater', 'type' => 'repeater',
'layout' => 'table', 'layout' => 'table',
'button_label' => __('Добавить город', 'wp-multi-city'), 'button_label' => 'Добавить город',
'sub_fields' => [ 'sub_fields' => [
[ [
'key' => 'field_wpmc_list_shortcode_town', 'key' => 'field_wpmc_list_shortcode_town',
'label' => __('Город', 'wp-multi-city'), 'label' => 'Город',
'name' => 'town', 'name' => 'town',
'type' => 'taxonomy', 'type' => 'taxonomy',
'taxonomy' => 'town', 'taxonomy' => 'town',
@@ -45,7 +45,7 @@ final class FieldGroup
], ],
[ [
'key' => 'field_wpmc_list_shortcode_text', 'key' => 'field_wpmc_list_shortcode_text',
'label' => __('Значение', 'wp-multi-city'), 'label' => 'Значение',
'name' => 'text', 'name' => 'text',
'type' => 'textarea', 'type' => 'textarea',
'rows' => 4, 'rows' => 4,
+20
View File
@@ -11,6 +11,11 @@ final class Helpers
private static array $page_unic_cache = []; private static array $page_unic_cache = [];
private static int $bypass_depth = 0; private static int $bypass_depth = 0;
private static function acf_ready(): bool
{
return \did_action('acf/init') > 0;
}
public static function reset_cache(): void public static function reset_cache(): void
{ {
self::$current_town_memo = null; self::$current_town_memo = null;
@@ -80,6 +85,9 @@ final class Helpers
public static function alt_get_field(string $selector, $post_id = false, bool $format_value = true) public static function alt_get_field(string $selector, $post_id = false, bool $format_value = true)
{ {
if (!self::acf_ready()) {
return null;
}
$post_id = \apply_filters('wpmc_alt_get_field_post_id', $post_id, $selector); $post_id = \apply_filters('wpmc_alt_get_field_post_id', $post_id, $selector);
if (!\function_exists('get_field')) { if (!\function_exists('get_field')) {
return null; return null;
@@ -106,6 +114,9 @@ final class Helpers
public static function index_town_alt_page_id(): ?int public static function index_town_alt_page_id(): ?int
{ {
if (!self::acf_ready()) {
return null;
}
if (!\function_exists('get_field')) { if (!\function_exists('get_field')) {
return null; return null;
} }
@@ -116,6 +127,9 @@ final class Helpers
public static function main_town_slug(): ?string public static function main_town_slug(): ?string
{ {
if (!self::acf_ready()) {
return null;
}
if (!\function_exists('get_field')) { if (!\function_exists('get_field')) {
return null; return null;
} }
@@ -132,6 +146,9 @@ final class Helpers
if (array_key_exists($key, self::$page_unic_cache)) { if (array_key_exists($key, self::$page_unic_cache)) {
return self::$page_unic_cache[$key]; return self::$page_unic_cache[$key];
} }
if (!self::acf_ready()) {
return null;
}
$term = self::get_term_by_slug($town_slug); $term = self::get_term_by_slug($town_slug);
if ($term === null) { if ($term === null) {
return self::$page_unic_cache[$key] = null; return self::$page_unic_cache[$key] = null;
@@ -161,6 +178,9 @@ final class Helpers
if ($path === null) { if ($path === null) {
$path = $_SERVER['REQUEST_URI'] ?? '/'; $path = $_SERVER['REQUEST_URI'] ?? '/';
} }
if (!self::acf_ready()) {
return true;
}
if (!\function_exists('get_field')) { if (!\function_exists('get_field')) {
return true; return true;
} }
+11 -11
View File
@@ -35,7 +35,7 @@ final class OptionsPage
'fields' => [ 'fields' => [
[ [
'key' => 'field_wpmc_main_town_slug', 'key' => 'field_wpmc_main_town_slug',
'label' => __('Главный город (без префикса URL)', 'wp-multi-city'), 'label' => 'Главный город (без префикса URL)',
'name' => 'main_town_slug', 'name' => 'main_town_slug',
'type' => 'taxonomy', 'type' => 'taxonomy',
'taxonomy' => 'town', 'taxonomy' => 'town',
@@ -47,7 +47,7 @@ final class OptionsPage
], ],
[ [
'key' => 'field_wpmc_index_town_alt', 'key' => 'field_wpmc_index_town_alt',
'label' => __('Главная для других городов', 'wp-multi-city'), 'label' => 'Главная для других городов',
'name' => 'index_town_alt', 'name' => 'index_town_alt',
'type' => 'post_object', 'type' => 'post_object',
'post_type' => ['page'], 'post_type' => ['page'],
@@ -57,15 +57,15 @@ final class OptionsPage
], ],
[ [
'key' => 'field_wpmc_page_unic', 'key' => 'field_wpmc_page_unic',
'label' => __('Клоны страниц', 'wp-multi-city'), 'label' => 'Клоны страниц',
'name' => 'page_unic', 'name' => 'page_unic',
'type' => 'repeater', 'type' => 'repeater',
'layout' => 'table', 'layout' => 'table',
'button_label' => __('Добавить клон', 'wp-multi-city'), 'button_label' => 'Добавить клон',
'sub_fields' => [ 'sub_fields' => [
[ [
'key' => 'field_wpmc_page_unic_page_old', 'key' => 'field_wpmc_page_unic_page_old',
'label' => __('Оригинал', 'wp-multi-city'), 'label' => 'Оригинал',
'name' => 'page_old', 'name' => 'page_old',
'type' => 'post_object', 'type' => 'post_object',
'post_type' => ['page'], 'post_type' => ['page'],
@@ -75,7 +75,7 @@ final class OptionsPage
], ],
[ [
'key' => 'field_wpmc_page_unic_page_new', 'key' => 'field_wpmc_page_unic_page_new',
'label' => __('Клон', 'wp-multi-city'), 'label' => 'Клон',
'name' => 'page_new', 'name' => 'page_new',
'type' => 'post_object', 'type' => 'post_object',
'post_type' => ['page'], 'post_type' => ['page'],
@@ -85,7 +85,7 @@ final class OptionsPage
], ],
[ [
'key' => 'field_wpmc_page_unic_town', 'key' => 'field_wpmc_page_unic_town',
'label' => __('Город', 'wp-multi-city'), 'label' => 'Город',
'name' => 'town', 'name' => 'town',
'type' => 'taxonomy', 'type' => 'taxonomy',
'taxonomy' => 'town', 'taxonomy' => 'town',
@@ -99,18 +99,18 @@ final class OptionsPage
], ],
[ [
'key' => 'field_wpmc_page_no_rep', 'key' => 'field_wpmc_page_no_rep',
'label' => __('Исключения из роутинга', 'wp-multi-city'), 'label' => 'Исключения из роутинга',
'name' => 'page_no_rep', 'name' => 'page_no_rep',
'type' => 'repeater', 'type' => 'repeater',
'layout' => 'table', 'layout' => 'table',
'button_label' => __('Добавить исключение', 'wp-multi-city'), 'button_label' => 'Добавить исключение',
'sub_fields' => [ 'sub_fields' => [
[ [
'key' => 'field_wpmc_page_no_rep_link', 'key' => 'field_wpmc_page_no_rep_link',
'label' => __('Путь', 'wp-multi-city'), 'label' => 'Путь',
'name' => 'link', 'name' => 'link',
'type' => 'text', 'type' => 'text',
'instructions' => __('Префикс URL, например /privacy/', 'wp-multi-city'), 'instructions' => 'Префикс URL, например /privacy/',
], ],
], ],
], ],
+10 -10
View File
@@ -16,16 +16,16 @@ final class PostType
{ {
register_post_type(self::SLUG, [ register_post_type(self::SLUG, [
'labels' => [ 'labels' => [
'name' => __('Шорткоды', 'wp-multi-city'), 'name' => 'Шорткоды',
'singular_name' => __('Шорткод', 'wp-multi-city'), 'singular_name' => 'Шорткод',
'menu_name' => __('Шорткоды', 'wp-multi-city'), 'menu_name' => 'Шорткоды',
'add_new' => __('Добавить', 'wp-multi-city'), 'add_new' => 'Добавить',
'add_new_item' => __('Добавить шорткод', 'wp-multi-city'), 'add_new_item' => 'Добавить шорткод',
'edit_item' => __('Редактировать', 'wp-multi-city'), 'edit_item' => 'Редактировать',
'new_item' => __('Новый', 'wp-multi-city'), 'new_item' => 'Новый',
'view_item' => __('Смотреть', 'wp-multi-city'), 'view_item' => 'Смотреть',
'search_items' => __('Поиск', 'wp-multi-city'), 'search_items' => 'Поиск',
'not_found' => __('Не найдено', 'wp-multi-city'), 'not_found' => 'Не найдено',
], ],
'public' => false, 'public' => false,
'exclude_from_search' => true, 'exclude_from_search' => true,
+4 -4
View File
@@ -16,10 +16,10 @@ final class Taxonomy
{ {
register_taxonomy(self::SLUG, [PostType::SLUG], [ register_taxonomy(self::SLUG, [PostType::SLUG], [
'labels' => [ 'labels' => [
'name' => __('Города', 'wp-multi-city'), 'name' => 'Города',
'singular_name' => __('Город', 'wp-multi-city'), 'singular_name' => 'Город',
'menu_name' => __('Города', 'wp-multi-city'), 'menu_name' => 'Города',
'add_new_item' => __('Добавить', 'wp-multi-city'), 'add_new_item' => 'Добавить',
], ],
'public' => false, 'public' => false,
'publicly_queryable' => false, 'publicly_queryable' => false,
+7
View File
@@ -30,3 +30,10 @@ if (!function_exists('wpmc_remove_domain_link')) {
return Helpers::remove_domain_link($url); return Helpers::remove_domain_link($url);
} }
} }
if (!function_exists('wpmc_main_town_slug')) {
function wpmc_main_town_slug(): ?string
{
return Helpers::main_town_slug();
}
}
+6 -1
View File
@@ -4,7 +4,7 @@ Tags: multisite, multi-city, taxonomy, acf, shortcode
Requires at least: 6.0 Requires at least: 6.0
Tested up to: 6.9 Tested up to: 6.9
Requires PHP: 8.0 Requires PHP: 8.0
Stable tag: 1.0.0 Stable tag: 1.0.1
License: GPLv2 or later License: GPLv2 or later
Мультигородская подсистема: одна WP-инсталляция обслуживает несколько городов по схеме /{city}/{path}/ с подстановками контента и URL. Мультигородская подсистема: одна WP-инсталляция обслуживает несколько городов по схеме /{city}/{path}/ с подстановками контента и URL.
@@ -23,6 +23,11 @@ License: GPLv2 or later
== Changelog == == Changelog ==
= 1.0.1 =
* Add: процедурный wrapper `wpmc_main_town_slug()` (был задокументирован в README, но отсутствовал в коде).
* Fix: WP 6.7+ notice `_load_textdomain_just_in_time` — убраны `__()` обёртки из labels CPT/таксономии/ACF field group (плагин не поставляется с переводами).
* Fix: ACF `acf_get_value` "called too early" notice — добавлен `did_action('acf/init')` guard в `Helpers::main_town_slug()`, `index_town_alt_page_id`, `page_unic_swap`, `is_routable_path`, `alt_get_field`. До инициализации ACF возвращают безопасные defaults (null/true).
= 1.0.0 = = 1.0.0 =
* Первый релиз. MVP-ядро завершено (S1–S7 эпика cp-cw4). * Первый релиз. MVP-ядро завершено (S1–S7 эпика cp-cw4).
* taxonomy town + CPT shortcode-town + ACF list_shortcode repeater. * taxonomy town + CPT shortcode-town + ACF list_shortcode repeater.
+69
View File
@@ -133,6 +133,7 @@ final class HelpersTest extends TestCase
public function test_alt_get_field_applies_do_shortcode_to_string_values(): void public function test_alt_get_field_applies_do_shortcode_to_string_values(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
// expect() must come before when('function_exists') so Brain Monkey's // expect() must come before when('function_exists') so Brain Monkey's
// FunctionStub::__construct can eval-define get_field() before the shim // FunctionStub::__construct can eval-define get_field() before the shim
// fakes function_exists('get_field') === true. // fakes function_exists('get_field') === true.
@@ -153,6 +154,7 @@ final class HelpersTest extends TestCase
public function test_alt_get_field_returns_arrays_as_is(): void public function test_alt_get_field_returns_arrays_as_is(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
// expect() before when('function_exists') to avoid FunctionStub seeing // expect() before when('function_exists') to avoid FunctionStub seeing
// the shim and skipping the eval-declaration of get_field. // the shim and skipping the eval-declaration of get_field.
// do_shortcode is intentionally NOT registered: if the implementation // do_shortcode is intentionally NOT registered: if the implementation
@@ -167,6 +169,7 @@ final class HelpersTest extends TestCase
public function test_alt_get_field_post_id_filter_overrides_caller_id(): void public function test_alt_get_field_post_id_filter_overrides_caller_id(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field') \Brain\Monkey\Functions\expect('get_field')
->once() ->once()
->with('title', 99, true) ->with('title', 99, true)
@@ -207,13 +210,27 @@ final class HelpersTest extends TestCase
self::assertTrue(function_exists('wpmc_get_term_by_slug')); self::assertTrue(function_exists('wpmc_get_term_by_slug'));
self::assertTrue(function_exists('wpmc_alt_get_field')); self::assertTrue(function_exists('wpmc_alt_get_field'));
self::assertTrue(function_exists('wpmc_remove_domain_link')); self::assertTrue(function_exists('wpmc_remove_domain_link'));
self::assertTrue(function_exists('wpmc_main_town_slug'));
// remove_domain_link is pure (no WP function dependencies) — exercise the proxy. // remove_domain_link is pure (no WP function dependencies) — exercise the proxy.
self::assertSame('/foo/', wpmc_remove_domain_link('https://site.com/foo/')); self::assertSame('/foo/', wpmc_remove_domain_link('https://site.com/foo/'));
} }
public function test_wpmc_main_town_slug_delegates_to_helpers(): void
{
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field')
->once()
->with('main_town_slug', 'option')
->andReturn('spb');
require_once dirname(__DIR__) . '/includes/wpmc-functions.php';
self::assertSame('spb', wpmc_main_town_slug());
}
public function test_main_town_slug_reads_option_via_acf(): void public function test_main_town_slug_reads_option_via_acf(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field') \Brain\Monkey\Functions\expect('get_field')
->once() ->once()
->with('main_town_slug', 'option') ->with('main_town_slug', 'option')
@@ -224,6 +241,7 @@ final class HelpersTest extends TestCase
public function test_main_town_slug_returns_null_when_option_empty(): void public function test_main_town_slug_returns_null_when_option_empty(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field') \Brain\Monkey\Functions\expect('get_field')
->once() ->once()
->with('main_town_slug', 'option') ->with('main_town_slug', 'option')
@@ -234,6 +252,7 @@ final class HelpersTest extends TestCase
public function test_is_routable_path_returns_true_when_no_exclusions(): void public function test_is_routable_path_returns_true_when_no_exclusions(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field') \Brain\Monkey\Functions\expect('get_field')
->once() ->once()
->with('page_no_rep', 'option') ->with('page_no_rep', 'option')
@@ -244,6 +263,7 @@ final class HelpersTest extends TestCase
public function test_is_routable_path_returns_false_when_path_matches_exclusion(): void public function test_is_routable_path_returns_false_when_path_matches_exclusion(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field') \Brain\Monkey\Functions\expect('get_field')
->once() ->once()
->with('page_no_rep', 'option') ->with('page_no_rep', 'option')
@@ -257,6 +277,7 @@ final class HelpersTest extends TestCase
public function test_is_routable_path_uses_request_uri_when_path_null(): void public function test_is_routable_path_uses_request_uri_when_path_null(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
$_SERVER['REQUEST_URI'] = '/terms/'; $_SERVER['REQUEST_URI'] = '/terms/';
\Brain\Monkey\Functions\expect('get_field') \Brain\Monkey\Functions\expect('get_field')
->once() ->once()
@@ -268,6 +289,7 @@ final class HelpersTest extends TestCase
public function test_page_unic_swap_returns_clone_id_when_row_matches(): void public function test_page_unic_swap_returns_clone_id_when_row_matches(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
$term = new \WP_Term(); $term = new \WP_Term();
$term->term_id = 11; $term->term_id = 11;
$term->slug = 'tyumen'; $term->slug = 'tyumen';
@@ -285,6 +307,7 @@ final class HelpersTest extends TestCase
public function test_page_unic_swap_returns_null_when_no_row_matches(): void public function test_page_unic_swap_returns_null_when_no_row_matches(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
$term = new \WP_Term(); $term = new \WP_Term();
$term->term_id = 11; $term->term_id = 11;
$term->slug = 'tyumen'; $term->slug = 'tyumen';
@@ -298,6 +321,7 @@ final class HelpersTest extends TestCase
public function test_page_unic_swap_caches_within_request(): void public function test_page_unic_swap_caches_within_request(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
$term = new \WP_Term(); $term = new \WP_Term();
$term->term_id = 11; $term->term_id = 11;
\Brain\Monkey\Functions\when('get_term_by')->justReturn($term); \Brain\Monkey\Functions\when('get_term_by')->justReturn($term);
@@ -314,6 +338,7 @@ final class HelpersTest extends TestCase
public function test_index_town_alt_page_id_returns_int_when_option_set(): void public function test_index_town_alt_page_id_returns_int_when_option_set(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field') \Brain\Monkey\Functions\expect('get_field')
->once() ->once()
->with('index_town_alt', 'option') ->with('index_town_alt', 'option')
@@ -324,6 +349,7 @@ final class HelpersTest extends TestCase
public function test_index_town_alt_page_id_returns_null_when_zero(): void public function test_index_town_alt_page_id_returns_null_when_zero(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field') \Brain\Monkey\Functions\expect('get_field')
->once() ->once()
->with('index_town_alt', 'option') ->with('index_town_alt', 'option')
@@ -334,6 +360,7 @@ final class HelpersTest extends TestCase
public function test_reset_cache_clears_page_unic_cache(): void public function test_reset_cache_clears_page_unic_cache(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
$term = new \WP_Term(); $term = new \WP_Term();
$term->term_id = 11; $term->term_id = 11;
\Brain\Monkey\Functions\when('get_term_by')->justReturn($term); \Brain\Monkey\Functions\when('get_term_by')->justReturn($term);
@@ -348,6 +375,48 @@ final class HelpersTest extends TestCase
self::assertTrue(true); self::assertTrue(true);
} }
public function test_main_town_slug_returns_null_before_acf_init(): void
{
\Brain\Monkey\Functions\expect('did_action')
->with('acf/init')
->andReturn(0);
// get_field should NOT be called — no expectation means Brain Monkey throws if it is.
self::assertNull(Helpers::main_town_slug());
}
public function test_main_town_slug_calls_get_field_after_acf_init(): void
{
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field')
->once()
->with('main_town_slug', 'option')
->andReturn('spb');
self::assertSame('spb', Helpers::main_town_slug());
}
public function test_is_routable_path_returns_true_before_acf_init(): void
{
\Brain\Monkey\Functions\expect('did_action')
->with('acf/init')
->andReturn(0);
// get_field should NOT be called.
self::assertTrue(Helpers::is_routable_path('/some/path/'));
}
public function test_is_routable_path_checks_exclusions_after_acf_init(): void
{
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_field')
->once()
->with('page_no_rep', 'option')
->andReturn([['link' => '/excluded/']]);
self::assertFalse(Helpers::is_routable_path('/excluded/page/'));
}
public function test_is_bypassed_returns_false_by_default(): void public function test_is_bypassed_returns_false_by_default(): void
{ {
self::assertFalse(Helpers::is_bypassed()); self::assertFalse(Helpers::is_bypassed());
+3
View File
@@ -44,6 +44,7 @@ final class LinkFilterTest extends TestCase
{ {
$_SERVER['REQUEST_URI'] = '/' . $town_slug . '/'; $_SERVER['REQUEST_URI'] = '/' . $town_slug . '/';
$_SERVER['HTTP_HOST'] = $host; $_SERVER['HTTP_HOST'] = $host;
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => $town_slug]); \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => $town_slug]);
\Brain\Monkey\Functions\when('get_field')->alias(function ($name) use ($main) { \Brain\Monkey\Functions\when('get_field')->alias(function ($name) use ($main) {
if ($name === 'main_town_slug') return $main; if ($name === 'main_town_slug') return $main;
@@ -62,6 +63,7 @@ final class LinkFilterTest extends TestCase
{ {
$_SERVER['REQUEST_URI'] = '/random/'; $_SERVER['REQUEST_URI'] = '/random/';
$_SERVER['HTTP_HOST'] = 'site.com'; $_SERVER['HTTP_HOST'] = 'site.com';
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\when('get_terms')->justReturn([10 => 'spb']); \Brain\Monkey\Functions\when('get_terms')->justReturn([10 => 'spb']);
\Brain\Monkey\Functions\when('get_field')->justReturn(false); \Brain\Monkey\Functions\when('get_field')->justReturn(false);
@@ -129,6 +131,7 @@ final class LinkFilterTest extends TestCase
{ {
$_SERVER['REQUEST_URI'] = '/tyumen/'; $_SERVER['REQUEST_URI'] = '/tyumen/';
$_SERVER['HTTP_HOST'] = 'site.com'; $_SERVER['HTTP_HOST'] = 'site.com';
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']); \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
\Brain\Monkey\Functions\when('get_field')->alias(function ($name) { \Brain\Monkey\Functions\when('get_field')->alias(function ($name) {
if ($name === 'main_town_slug') return 'spb'; if ($name === 'main_town_slug') return 'spb';
+4
View File
@@ -20,6 +20,7 @@ final class RedirectTest extends TestCase
public function test_handle_skips_when_current_town_equals_main(): void public function test_handle_skips_when_current_town_equals_main(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); \Brain\Monkey\Functions\expect('wp_safe_redirect')->never();
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/spb/about/'; $_SERVER['REQUEST_URI'] = '/spb/about/';
@@ -36,6 +37,7 @@ final class RedirectTest extends TestCase
public function test_handle_skips_when_post_not_set(): void public function test_handle_skips_when_post_not_set(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); \Brain\Monkey\Functions\expect('wp_safe_redirect')->never();
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/about/'; $_SERVER['REQUEST_URI'] = '/about/';
@@ -53,6 +55,7 @@ final class RedirectTest extends TestCase
public function test_handle_skips_when_url_already_has_town_prefix(): void public function test_handle_skips_when_url_already_has_town_prefix(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('wp_safe_redirect')->never(); \Brain\Monkey\Functions\expect('wp_safe_redirect')->never();
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/tyumen/about/'; $_SERVER['REQUEST_URI'] = '/tyumen/about/';
@@ -70,6 +73,7 @@ final class RedirectTest extends TestCase
public function test_handle_redirects_when_post_is_page_unic_clone_without_prefix(): void public function test_handle_redirects_when_post_is_page_unic_clone_without_prefix(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/about/'; $_SERVER['REQUEST_URI'] = '/about/';
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']); \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
+9
View File
@@ -48,6 +48,7 @@ final class RouterTest extends TestCase
public function test_route_skips_when_current_town_is_null(): void public function test_route_skips_when_current_town_is_null(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('url_to_postid')->never(); \Brain\Monkey\Functions\expect('url_to_postid')->never();
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/something-not-a-city/'; $_SERVER['REQUEST_URI'] = '/something-not-a-city/';
@@ -61,6 +62,7 @@ final class RouterTest extends TestCase
public function test_route_skips_when_current_town_equals_main_town(): void public function test_route_skips_when_current_town_equals_main_town(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('url_to_postid')->never(); \Brain\Monkey\Functions\expect('url_to_postid')->never();
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/spb/'; $_SERVER['REQUEST_URI'] = '/spb/';
@@ -77,6 +79,7 @@ final class RouterTest extends TestCase
public function test_route_skips_when_path_not_routable(): void public function test_route_skips_when_path_not_routable(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('url_to_postid')->never(); \Brain\Monkey\Functions\expect('url_to_postid')->never();
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/tyumen/privacy/'; $_SERVER['REQUEST_URI'] = '/tyumen/privacy/';
@@ -94,6 +97,7 @@ final class RouterTest extends TestCase
public function test_route_uses_main_page_alt_term_meta_for_root_url(): void public function test_route_uses_main_page_alt_term_meta_for_root_url(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/tyumen/'; $_SERVER['REQUEST_URI'] = '/tyumen/';
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']); \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
@@ -123,6 +127,7 @@ final class RouterTest extends TestCase
public function test_route_falls_back_to_index_town_alt_option(): void public function test_route_falls_back_to_index_town_alt_option(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/tyumen/'; $_SERVER['REQUEST_URI'] = '/tyumen/';
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']); \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
@@ -148,6 +153,7 @@ final class RouterTest extends TestCase
public function test_route_does_not_install_query_when_neither_main_nor_fallback_set(): void public function test_route_does_not_install_query_when_neither_main_nor_fallback_set(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_post')->never(); \Brain\Monkey\Functions\expect('get_post')->never();
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/tyumen/'; $_SERVER['REQUEST_URI'] = '/tyumen/';
@@ -169,6 +175,7 @@ final class RouterTest extends TestCase
public function test_route_resolves_internal_page_via_url_to_postid(): void public function test_route_resolves_internal_page_via_url_to_postid(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/tyumen/about/'; $_SERVER['REQUEST_URI'] = '/tyumen/about/';
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']); \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
@@ -193,6 +200,7 @@ final class RouterTest extends TestCase
public function test_route_applies_page_unic_swap_when_clone_exists(): void public function test_route_applies_page_unic_swap_when_clone_exists(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/tyumen/about/'; $_SERVER['REQUEST_URI'] = '/tyumen/about/';
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']); \Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
@@ -220,6 +228,7 @@ final class RouterTest extends TestCase
public function test_route_does_not_install_query_when_url_to_postid_returns_zero(): void public function test_route_does_not_install_query_when_url_to_postid_returns_zero(): void
{ {
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
\Brain\Monkey\Functions\expect('get_post')->never(); \Brain\Monkey\Functions\expect('get_post')->never();
\Brain\Monkey\Functions\when('is_admin')->justReturn(false); \Brain\Monkey\Functions\when('is_admin')->justReturn(false);
$_SERVER['REQUEST_URI'] = '/tyumen/unknown/'; $_SERVER['REQUEST_URI'] = '/tyumen/unknown/';
+2 -2
View File
@@ -2,7 +2,7 @@
/** /**
* Plugin Name: WP Multi City * Plugin Name: WP Multi City
* Description: Мультигородская подсистема: taxonomy town, шорткоды для подстановки по городу, роутер /{city}/{path}/, page-clones по городам. * Description: Мультигородская подсистема: taxonomy town, шорткоды для подстановки по городу, роутер /{city}/{path}/, page-clones по городам.
* Version: 1.0.0 * Version: 1.0.1
* Author: Vladimir Bryzgalov * Author: Vladimir Bryzgalov
* Requires PHP: 8.0 * Requires PHP: 8.0
* Requires at least: 6.0 * Requires at least: 6.0
@@ -18,7 +18,7 @@ if (!defined('ABSPATH')) exit;
defined('WPMC_PLUGIN_FILE') || define('WPMC_PLUGIN_FILE', __FILE__); defined('WPMC_PLUGIN_FILE') || define('WPMC_PLUGIN_FILE', __FILE__);
defined('WPMC_PLUGIN_DIR') || define('WPMC_PLUGIN_DIR', plugin_dir_path(__FILE__)); defined('WPMC_PLUGIN_DIR') || define('WPMC_PLUGIN_DIR', plugin_dir_path(__FILE__));
defined('WPMC_PLUGIN_URL') || define('WPMC_PLUGIN_URL', plugin_dir_url(__FILE__)); defined('WPMC_PLUGIN_URL') || define('WPMC_PLUGIN_URL', plugin_dir_url(__FILE__));
defined('WPMC_VERSION') || define('WPMC_VERSION', '1.0.0'); defined('WPMC_VERSION') || define('WPMC_VERSION', '1.0.1');
require_once WPMC_PLUGIN_DIR . 'includes/class-plugin.php'; require_once WPMC_PLUGIN_DIR . 'includes/class-plugin.php';
\WPMultiCity\Plugin::register_autoloader(); \WPMultiCity\Plugin::register_autoloader();
+4 -4
View File
@@ -1,16 +1,16 @@
{ {
"name": "WP Multi City", "name": "WP Multi City",
"slug": "wp-multi-city", "slug": "wp-multi-city",
"version": "1.0.0", "version": "1.0.1",
"download_url": "https://git.netranking.ru/bryzgalov/wp-multi-city/releases/download/v1.0.0/wp-multi-city-1.0.0.zip", "download_url": "https://git.netranking.ru/bryzgalov/wp-multi-city/releases/download/v1.0.1/wp-multi-city-1.0.1.zip",
"homepage": "https://git.netranking.ru/bryzgalov/wp-multi-city", "homepage": "https://git.netranking.ru/bryzgalov/wp-multi-city",
"requires": "6.0", "requires": "6.0",
"tested": "6.9", "tested": "6.9",
"requires_php": "8.0", "requires_php": "8.0",
"last_updated": "2026-05-17 00:00:00", "last_updated": "2026-05-17 12:02:11",
"author": "Vladimir Bryzgalov", "author": "Vladimir Bryzgalov",
"sections": { "sections": {
"description": "Мультигородская подсистема: taxonomy town, [shortcode_dmr] для подстановок, URL-роутер /{city}/{path}/, ACF-driven page clones.", "description": "Мультигородская подсистема: taxonomy town, [shortcode_dmr] для подстановок, URL-роутер /{city}/{path}/, ACF-driven page clones.",
"changelog": "<h4>1.0.0</h4><ul><li>Первый релиз. MVP-ядро S1–S7.</li></ul>" "changelog": "<h4>1.0.1</h4><ul><li>Add: процедурный wrapper `wpmc_main_town_slug()` (был задокументирован в README, но отсутствовал в коде).</li><li>Fix: WP 6.7+ notice `_load_textdomain_just_in_time` — убраны `__()` обёртки из labels CPT/таксономии/ACF field group (плагин не поставляется с переводами).</li><li>Fix: ACF `acf_get_value` \"called too early\" notice — добавлен `did_action('acf/init')` guard в `Helpers::main_town_slug()`, `index_town_alt_page_id`, `page_unic_swap`, `is_routable_path`, `alt_get_field`. До инициализации ACF возвращают безопасные defaults (null/true).</li></ul><h4>1.0.0</h4><ul><li>Первый релиз. MVP-ядро завершено (S1–S7 эпика cp-cw4).</li><li>taxonomy town + CPT shortcode-town + ACF list_shortcode repeater.</li><li>[shortcode_dmr id=N] для подстановок по городу.</li><li>URL-роутер /{city}/{path}/ с поддержкой page_unic / main_page_alt / page_no_rep.</li><li>Глобальные фильтры the_title, home_url, post_link, acf/load_value, rest_url.</li><li>Self-hosted updates через Gitea + plugin-update-checker v5.6.</li></ul>"
} }
} }