Files
wp-multi-city/docs/superpowers/specs/2026-05-12-s2-taxonomy-cpt-acf-design.md
T
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

212 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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\PostType``includes/class-post-type.php`, etc.
### Plugin::boot()
После проверки `dependencies_satisfied()`:
```php
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`
```php
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/<slug>/ архива; роутер /{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`
```php
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`
```php
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=id``get_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_town**`register_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_group**`acf_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_registrations**`Plugin::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
Нет. Все решения зафиксированы выше.