Files
wp-multi-city/docs/superpowers/specs/2026-05-12-s2-taxonomy-cpt-acf-design.md
Vladimir Bryzgalov fad5a7d03b chore: init wp-multi-city v0.1.0 with S1 skeleton + S2 design spec
S1 skeleton (cp-8ci, already done):
- wp-multi-city.php with header, constants, hooks bootstrap
- includes/class-plugin.php with PSR-4-ish autoloader, ACF dependency guard
- includes/class-activator.php placeholder
- composer.json (PHP 8.0, phpunit dev)
- README.md with roadmap
- uninstall.php placeholder

S2 design (cp-q7g):
- docs/superpowers/specs/2026-05-12-s2-taxonomy-cpt-acf-design.md
2026-05-12 17:21:08 +05:00

10 KiB
Raw Permalink Blame History

S2 — Taxonomy town + CPT shortcode-town + ACF list_shortcode

  • Beads: cp-q7g (parent epic cp-cw4)
  • Depends on: cp-8ci (S1 — скелет плагина, закрыт)
  • Blocks: cp-arq (S3 — [shortcode_dmr] handler)
  • Дата: 2026-05-12

Цель

Подсистема хранения для мультигородского контента: справочник городов (taxonomy), внутренний CPT-контейнер «шорткодов», и ACF-репитер строк подстановок «город → значение». Воспроизводит структуру темы washanyanya, но регистрируется только в коде (не через ACF UI) — чтобы плагин активировался на новых сайтах без миграции БД.

Архитектура

Три независимых класса в includes/, каждый со статическим register():

includes/
├── class-plugin.php          ← Plugin::boot() вызывает три register()
├── class-activator.php       ← без изменений
├── class-taxonomy.php        ← WPMultiCity\Taxonomy
├── class-post-type.php       ← WPMultiCity\PostType
└── class-field-group.php     ← WPMultiCity\FieldGroup

Имена классов совместимы с S1 autoloader (Plugin::register_autoloader): WPMultiCity\PostTypeincludes/class-post-type.php, etc.

Plugin::boot()

После проверки dependencies_satisfied():

Taxonomy::register();    // add_action('init', [self::class, 'do_register'])
PostType::register();    // add_action('init', [self::class, 'do_register'])
FieldGroup::register();  // add_action('acf/init', [self::class, 'do_register'])

Каждый register() идемпотентен (вешает свой add_action один раз) и сам по себе ничего не регистрирует — регистрация происходит в hook-callback. Это даёт чистое разделение «когда вешаем хук» (boot) и «что делаем в хуке» (do_register) для тестируемости.

Регистрационные параметры

Taxonomy town

register_taxonomy('town', ['shortcode-town'], [
    'labels'             => [
        'name'          => __('Города', 'wp-multi-city'),
        'singular_name' => __('Город', 'wp-multi-city'),
        'menu_name'     => __('Города', 'wp-multi-city'),
        'add_new_item'  => __('Добавить', 'wp-multi-city'),
    ],
    'public'             => false,
    'publicly_queryable' => false,
    'hierarchical'       => false,
    'show_ui'            => true,
    'show_in_menu'       => true,
    'show_in_nav_menus'  => false,
    'show_admin_column'  => false,
    'show_in_rest'       => true,
    'query_var'          => false,
    'rewrite'            => false,
    'meta_box_cb'        => false,
]);

Решения:

  • hierarchical=false — города плоские (acceptance cp-q7g).
  • public=false + publicly_queryable=false — нет publik /town// архива; роутер /{city}/{path}/ строится отдельно в S4.
  • show_in_rest=true — на случай Gutenberg-блоков и REST API клиентов.
  • meta_box_cb=false — на CPT edit-screen мета-бокс таксы не нужен (term выбирается в ACF repeater подстановок).
  • rewrite=false — никаких URL-конфликтов с роутером S4.

CPT shortcode-town

register_post_type('shortcode-town', [
    'labels'              => [
        'name'               => __('Шорткоды', 'wp-multi-city'),
        'singular_name'      => __('Шорткод', 'wp-multi-city'),
        'menu_name'          => __('Шорткоды', 'wp-multi-city'),
        'add_new'            => __('Добавить', 'wp-multi-city'),
        'add_new_item'       => __('Добавить шорткод', 'wp-multi-city'),
        'edit_item'          => __('Редактировать', 'wp-multi-city'),
        'new_item'           => __('Новый', 'wp-multi-city'),
        'view_item'          => __('Смотреть', 'wp-multi-city'),
        'search_items'       => __('Поиск', 'wp-multi-city'),
        'not_found'          => __('Не найдено', 'wp-multi-city'),
    ],
    'public'              => false,
    'exclude_from_search' => true,
    'publicly_queryable'  => false,
    'show_ui'             => true,
    'show_in_menu'        => true,
    'show_in_nav_menus'   => false,
    'show_in_rest'        => false,
    'query_var'           => false,
    'has_archive'         => false,
    'rewrite'             => false,
    'hierarchical'        => false,
    'capability_type'     => 'post',
    'supports'            => ['title'],
    'menu_position'       => 8,
]);

CPT — служебный контейнер. show_in_rest=false (acceptance молчит, держим minimal scope).

ACF group WPMultiCity / Shortcode rows

acf_add_local_field_group([
    'key'        => 'group_wpmc_shortcode_rows',
    'title'      => 'WPMultiCity / Shortcode rows',
    'fields'     => [
        [
            'key'          => 'field_wpmc_list_shortcode',
            'label'        => __('Подстановки по городам', 'wp-multi-city'),
            'name'         => 'list_shortcode',
            'type'         => 'repeater',
            'layout'       => 'table',
            'button_label' => __('Добавить город', 'wp-multi-city'),
            'sub_fields'   => [
                [
                    'key'           => 'field_wpmc_list_shortcode_town',
                    'label'         => __('Город', 'wp-multi-city'),
                    'name'          => 'town',
                    'type'          => 'taxonomy',
                    'taxonomy'      => 'town',
                    'field_type'    => 'select',
                    'return_format' => 'id',
                    'allow_null'    => 0,
                    'multiple'      => 0,
                    'add_term'      => 0,
                ],
                [
                    'key'   => 'field_wpmc_list_shortcode_text',
                    'label' => __('Значение', 'wp-multi-city'),
                    'name'  => 'text',
                    'type'  => 'textarea',
                    'rows'  => 4,
                ],
            ],
        ],
    ],
    'location'   => [
        [
            ['param' => 'post_type', 'operator' => '==', 'value' => 'shortcode-town'],
        ],
    ],
    'menu_order' => 0,
    'position'   => 'normal',
    'style'      => 'default',
    'active'     => true,
]);

Под-поле town:

  • return_format=idget_field('list_shortcode', $id) отдаёт массив со строками вида ['town' => 42, 'text' => '...']. Это то, что ждёт S3 (add_shortcode_dmr на washanyanya читает $item['town'] как ID и затем get_term($item['town'], 'town')).
  • allow_null=0 — пустой город в строке репитера бессмыслен.

Тестирование (Brain Monkey + Mockery)

Composer dev-deps:

  • brain/monkey: ^2.6
  • mockery/mockery: ^1.6
  • phpunit/phpunit: ^9.6 (уже есть)

Структура:

tests/
├── bootstrap.php       ← require vendor/autoload.php
├── TestCase.php        ← Brain\Monkey\setUp/tearDown + Mockery\close()
├── TaxonomyTest.php
├── PostTypeTest.php
├── FieldGroupTest.php
└── PluginBootTest.php
phpunit.xml.dist

Тестовые сценарии (RED → GREEN порядок):

  1. TaxonomyTest::test_register_adds_init_action — после Taxonomy::register() Brain Monkey подтверждает что add_action('init', ...) был вызван.
  2. TaxonomyTest::test_init_callback_registers_taxonomy_town — выполнение callback вызывает register_taxonomy('town', ['shortcode-town'], $args) с обязательными ключами (hierarchical=false, show_in_rest=true, public=false).
  3. PostTypeTest::test_register_adds_init_action — аналогично.
  4. PostTypeTest::test_init_callback_registers_post_type_shortcode_townregister_post_type('shortcode-town', $args) с public=false, show_ui=true, supports=['title'].
  5. FieldGroupTest::test_register_adds_acf_init_action — после FieldGroup::register() Brain Monkey подтверждает add_action('acf/init', ...).
  6. FieldGroupTest::test_acf_init_callback_registers_local_field_groupacf_add_local_field_group() вызывается с key=group_wpmc_shortcode_rows, наличием repeater list_shortcode и двух sub-fields с правильными name/type/taxonomy/return_format.
  7. FieldGroupTest::test_acf_init_callback_noop_when_acf_missing — если function_exists('acf_add_local_field_group') возвращает false, callback не падает.
  8. PluginBootTest::test_boot_invokes_three_registrationsPlugin::instance()->boot() (когда зависимости удовлетворены) вызывает Taxonomy::register, PostType::register, FieldGroup::register. Реализуется через Brain Monkey Actions\expectAdded.

Каждый тест — отдельный RED → GREEN коммит.

Acceptance verification (после green-тестов)

  1. composer install && vendor/bin/phpunit — все тесты зелёные.
  2. Активировать плагин на чистом WP с ACF Pro (можно washanyanya local-копии не нужен — wp-cli + новый WP):
    • admin menu показывает «Шорткоды» (CPT) и «Города» (taxonomy в его подменю);
    • открыть «Шорткоды → Добавить» — виден repeater «Подстановки по городам» с двумя sub-fields.
  3. Деактивация плагина → пункты меню исчезают.

Что НЕ входит в S2

  • Term-meta main_page_alt на term'е town — S4 (cp-83m).
  • Shortcode [shortcode_dmr] handler + хелперы current_town, _alt_get_field, remove_domain_link — S3 (cp-arq).
  • Term ACF procent (цены) — out of MVP.
  • Полный WP integration test suite — S6 (cp-la7).

Open questions

Нет. Все решения зафиксированы выше.