35 KiB
S2 — Taxonomy town + CPT shortcode-town + ACF list_shortcode Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Register WordPress taxonomy town, CPT shortcode-town, and ACF local field group list_shortcode inside the WP Multi City plugin, fully tested via Brain Monkey unit tests.
Architecture: Three independent classes (Taxonomy, PostType, FieldGroup) each with a static register() that hooks WP init (or acf/init) and a static do-method that performs the registration. Plugin::boot() invokes the three register() calls when ACF is active.
Tech Stack: PHP 8.0+, WordPress 6.0+, ACF Pro, Composer, PHPUnit 9.6, Brain Monkey 2.6, Mockery 1.6.
Spec: docs/superpowers/specs/2026-05-12-s2-taxonomy-cpt-acf-design.md
Beads task: cp-q7g (epic cp-cw4).
File Structure
wp-multi-city/
├── wp-multi-city.php # (existing, unchanged)
├── composer.json # MODIFY — add brain/monkey, mockery
├── phpunit.xml.dist # CREATE — PHPUnit config
├── includes/
│ ├── class-plugin.php # MODIFY — boot() invokes three register()
│ ├── class-activator.php # (existing, unchanged)
│ ├── class-taxonomy.php # CREATE — WPMultiCity\Taxonomy
│ ├── class-post-type.php # CREATE — WPMultiCity\PostType
│ └── class-field-group.php # CREATE — WPMultiCity\FieldGroup
└── tests/
├── bootstrap.php # CREATE — Composer autoload
├── TestCase.php # CREATE — Brain Monkey setUp/tearDown
├── TaxonomyTest.php # CREATE
├── PostTypeTest.php # CREATE
├── FieldGroupTest.php # CREATE
└── PluginBootTest.php # CREATE
Boundary rules:
- One class per WP-registration concern, ≤80 lines each.
- Static methods only — no instance state. Easier to mock + assert.
- All Russian-language strings wrapped in
__('…', 'wp-multi-city').
Working Directory
All paths in this plan are relative to:
/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city
For shell snippets, run them from that directory (the path is shell-quoted in commands).
Task 1: Composer dev dependencies + PHPUnit config
Files:
-
Modify:
composer.json -
Create:
phpunit.xml.dist -
Create:
tests/bootstrap.php -
Create:
tests/TestCase.php -
Step 1.1: Update composer.json — add brain/monkey + mockery, autoload-dev path
Final composer.json:
{
"name": "bryzgalov/wp-multi-city",
"description": "Multi-city subsystem for WordPress: taxonomy town, per-city shortcodes, /{city}/{path}/ router, page clones.",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=8.0"
},
"require-dev": {
"brain/monkey": "^2.6",
"mockery/mockery": "^1.6",
"phpunit/phpunit": "^9.6"
},
"autoload-dev": {
"psr-4": {
"WPMultiCity\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit"
},
"config": {
"sort-packages": true
}
}
- Step 1.2: Install dependencies
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
composer install
Expected: composer.lock created, vendor/ populated, brain/monkey + mockery + phpunit downloaded. No errors.
- Step 1.3: Create phpunit.xml.dist
phpunit.xml.dist:
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true"
cacheResultFile=".phpunit.result.cache">
<testsuites>
<testsuite name="unit">
<directory>tests</directory>
<exclude>tests/bootstrap.php</exclude>
<exclude>tests/TestCase.php</exclude>
</testsuite>
</testsuites>
</phpunit>
- Step 1.4: Create tests/bootstrap.php
tests/bootstrap.php:
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
if (!defined('ABSPATH')) {
define('ABSPATH', __DIR__ . '/');
}
require_once __DIR__ . '/../includes/class-plugin.php';
\WPMultiCity\Plugin::register_autoloader();
The ABSPATH define lets the plugin's wp-multi-city.php style guards pass when included from tests (we don't include the main file here — only autoload classes via the plugin's own loader, registered after include).
- Step 1.5: Create tests/TestCase.php — base class with Brain Monkey lifecycle
tests/TestCase.php:
<?php
declare(strict_types=1);
namespace WPMultiCity\Tests;
use Brain\Monkey;
use Mockery;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
protected function setUp(): void
{
parent::setUp();
Monkey\setUp();
}
protected function tearDown(): void
{
Monkey\tearDown();
Mockery::close();
parent::tearDown();
}
}
- Step 1.6: Verify PHPUnit boots (no tests yet — runs zero)
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit
Expected: No tests executed! and exit code 0 (or warning about empty suite). No fatal errors, no autoload failures.
- Step 1.7: Commit
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add composer.json composer.lock phpunit.xml.dist tests/bootstrap.php tests/TestCase.php
git commit -m "test(wpmc S2): setup PHPUnit + Brain Monkey + Mockery"
Task 2: Taxonomy town — RED test (init hook attached)
Files:
-
Create:
tests/TaxonomyTest.php -
Step 2.1: Write failing test that Taxonomy::register() adds init action
tests/TaxonomyTest.php:
<?php
declare(strict_types=1);
namespace WPMultiCity\Tests;
use Brain\Monkey\Actions;
use WPMultiCity\Taxonomy;
final class TaxonomyTest extends TestCase
{
public function test_register_attaches_init_action(): void
{
Taxonomy::register();
self::assertNotFalse(
has_action('init', [Taxonomy::class, 'do_register']),
'Taxonomy::register() must attach init action with do_register handler'
);
}
}
- Step 2.2: Run — expect failure (Taxonomy class does not exist)
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter test_register_attaches_init_action
Expected: FAIL — Error: Class "WPMultiCity\Taxonomy" not found.
Task 3: Taxonomy town — GREEN (minimal Taxonomy::register + do_register stub)
Files:
-
Create:
includes/class-taxonomy.php -
Step 3.1: Create Taxonomy class with register() that hooks init
includes/class-taxonomy.php:
<?php
declare(strict_types=1);
namespace WPMultiCity;
final class Taxonomy
{
public const SLUG = 'town';
public static function register(): void
{
add_action('init', [self::class, 'do_register']);
}
public static function do_register(): void
{
// Implementation filled in Task 5.
}
}
- Step 3.2: Run — expect PASS
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter test_register_attaches_init_action
Expected: PASS (1 test, 1 assertion).
- Step 3.3: Commit
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add tests/TaxonomyTest.php includes/class-taxonomy.php
git commit -m "feat(wpmc S2): Taxonomy::register() attaches init hook"
Task 4: Taxonomy town — RED test (register_taxonomy called with correct args)
Files:
-
Modify:
tests/TaxonomyTest.php -
Step 4.1: Add test that do_register() calls register_taxonomy with the right args
Append to tests/TaxonomyTest.php (inside the class, after the previous method):
public function test_do_register_calls_register_taxonomy_town_with_required_args(): void
{
$captured = null;
\Brain\Monkey\Functions\expect('register_taxonomy')
->once()
->with('town', ['shortcode-town'], \Mockery::on(function ($args) use (&$captured) {
$captured = $args;
return true;
}));
Taxonomy::do_register();
self::assertFalse($captured['hierarchical'], 'town must be flat');
self::assertFalse($captured['public'], 'town must not be public');
self::assertTrue($captured['show_ui'], 'town must show in admin UI');
self::assertTrue($captured['show_in_rest'], 'town must be REST-exposed');
self::assertFalse($captured['publicly_queryable']);
self::assertFalse($captured['query_var']);
self::assertFalse($captured['rewrite']);
self::assertFalse($captured['meta_box_cb']);
self::assertSame('Города', $captured['labels']['name']);
}
- Step 4.2: Run — expect failure (do_register is empty, register_taxonomy never called)
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter test_do_register_calls_register_taxonomy_town_with_required_args
Expected: FAIL — Mockery exception "expected register_taxonomy to be called 1 time, called 0 times".
Task 5: Taxonomy town — GREEN (real registration body)
Files:
-
Modify:
includes/class-taxonomy.php -
Step 5.1: Replace empty do_register() with full registration
Replace the do_register() body in includes/class-taxonomy.php:
public static function do_register(): void
{
register_taxonomy(self::SLUG, ['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,
]);
}
- Step 5.2: Run — expect both Taxonomy tests pass
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit tests/TaxonomyTest.php
Expected: OK (2 tests, ≥10 assertions).
- Step 5.3: Commit
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add tests/TaxonomyTest.php includes/class-taxonomy.php
git commit -m "feat(wpmc S2): register taxonomy town with required args"
Task 6: PostType shortcode-town — RED test (init hook attached)
Files:
-
Create:
tests/PostTypeTest.php -
Step 6.1: Write failing test
tests/PostTypeTest.php:
<?php
declare(strict_types=1);
namespace WPMultiCity\Tests;
use WPMultiCity\PostType;
final class PostTypeTest extends TestCase
{
public function test_register_attaches_init_action(): void
{
PostType::register();
self::assertNotFalse(
has_action('init', [PostType::class, 'do_register']),
'PostType::register() must attach init action with do_register handler'
);
}
}
- Step 6.2: Run — expect failure (class does not exist)
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter "PostTypeTest::test_register_attaches_init_action"
Expected: FAIL — class WPMultiCity\PostType not found.
Task 7: PostType shortcode-town — GREEN stub
Files:
-
Create:
includes/class-post-type.php -
Step 7.1: Create PostType class with register() and empty do_register()
includes/class-post-type.php:
<?php
declare(strict_types=1);
namespace WPMultiCity;
final class PostType
{
public const SLUG = 'shortcode-town';
public static function register(): void
{
add_action('init', [self::class, 'do_register']);
}
public static function do_register(): void
{
// Implementation filled in Task 9.
}
}
- Step 7.2: Run — expect PASS
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter "PostTypeTest::test_register_attaches_init_action"
Expected: PASS.
- Step 7.3: Commit
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add tests/PostTypeTest.php includes/class-post-type.php
git commit -m "feat(wpmc S2): PostType::register() attaches init hook"
Task 8: PostType shortcode-town — RED test (register_post_type called with correct args)
Files:
-
Modify:
tests/PostTypeTest.php -
Step 8.1: Add second test
Append to tests/PostTypeTest.php (inside the class):
public function test_do_register_calls_register_post_type_shortcode_town_with_required_args(): void
{
$captured = null;
\Brain\Monkey\Functions\expect('register_post_type')
->once()
->with('shortcode-town', \Mockery::on(function ($args) use (&$captured) {
$captured = $args;
return true;
}));
PostType::do_register();
self::assertFalse($captured['public'], 'CPT must be non-public');
self::assertTrue($captured['show_ui'], 'CPT must show in admin');
self::assertSame(['title'], $captured['supports']);
self::assertTrue($captured['exclude_from_search']);
self::assertFalse($captured['publicly_queryable']);
self::assertFalse($captured['has_archive']);
self::assertFalse($captured['rewrite']);
self::assertFalse($captured['hierarchical']);
self::assertSame(8, $captured['menu_position']);
self::assertSame('Шорткоды', $captured['labels']['name']);
}
- Step 8.2: Run — expect failure (register_post_type not called)
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter "PostTypeTest::test_do_register_calls_register_post_type_shortcode_town_with_required_args"
Expected: FAIL — register_post_type expected 1 time, called 0 times.
Task 9: PostType shortcode-town — GREEN (real registration body)
Files:
-
Modify:
includes/class-post-type.php -
Step 9.1: Replace empty do_register() with full registration
Replace do_register() body:
public static function do_register(): void
{
register_post_type(self::SLUG, [
'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,
]);
}
- Step 9.2: Run — expect all PostType tests pass
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit tests/PostTypeTest.php
Expected: OK (2 tests, ≥11 assertions).
- Step 9.3: Commit
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add tests/PostTypeTest.php includes/class-post-type.php
git commit -m "feat(wpmc S2): register CPT shortcode-town with required args"
Task 10: FieldGroup — RED test (acf/init hook attached)
Files:
-
Create:
tests/FieldGroupTest.php -
Step 10.1: Write failing test
tests/FieldGroupTest.php:
<?php
declare(strict_types=1);
namespace WPMultiCity\Tests;
use WPMultiCity\FieldGroup;
final class FieldGroupTest extends TestCase
{
public function test_register_attaches_acf_init_action(): void
{
FieldGroup::register();
self::assertNotFalse(
has_action('acf/init', [FieldGroup::class, 'do_register']),
'FieldGroup::register() must hook acf/init'
);
}
}
- Step 10.2: Run — expect failure
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter "FieldGroupTest::test_register_attaches_acf_init_action"
Expected: FAIL — class WPMultiCity\FieldGroup not found.
Task 11: FieldGroup — GREEN stub
Files:
-
Create:
includes/class-field-group.php -
Step 11.1: Create FieldGroup class with register() and empty do_register()
includes/class-field-group.php:
<?php
declare(strict_types=1);
namespace WPMultiCity;
final class FieldGroup
{
public const GROUP_KEY = 'group_wpmc_shortcode_rows';
public const REPEATER_KEY = 'field_wpmc_list_shortcode';
public static function register(): void
{
add_action('acf/init', [self::class, 'do_register']);
}
public static function do_register(): void
{
// Implementation filled in Tasks 13 and 15.
}
}
- Step 11.2: Run — expect PASS
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter "FieldGroupTest::test_register_attaches_acf_init_action"
Expected: PASS.
- Step 11.3: Commit
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add tests/FieldGroupTest.php includes/class-field-group.php
git commit -m "feat(wpmc S2): FieldGroup::register() attaches acf/init hook"
Task 12: FieldGroup — RED test (do_register is no-op when ACF missing)
Files:
-
Modify:
tests/FieldGroupTest.php -
Step 12.1: Add test ensuring graceful exit when acf_add_local_field_group is undefined
Append to tests/FieldGroupTest.php:
public function test_do_register_is_noop_when_acf_missing(): void
{
\Brain\Monkey\Functions\when('function_exists')->alias(function ($name) {
return $name !== 'acf_add_local_field_group';
});
\Brain\Monkey\Functions\expect('acf_add_local_field_group')->never();
FieldGroup::do_register();
// No assertion needed beyond Mockery's verification of ->never().
self::assertTrue(true);
}
- Step 12.2: Run — expect failure
Currently do_register() is empty — function_exists() is mocked but never consulted. The test passes by accident. Add the active assertion that forces an early-return path:
Refine the test (replace the body added in 12.1) so that the assertion verifies the actual code path. Use this version instead:
public function test_do_register_is_noop_when_acf_missing(): void
{
\Brain\Monkey\Functions\when('function_exists')->justReturn(false);
\Brain\Monkey\Functions\expect('acf_add_local_field_group')->never();
FieldGroup::do_register();
self::assertTrue(true, 'do_register must not call acf_add_local_field_group when ACF is missing');
}
Run:
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter "FieldGroupTest::test_do_register_is_noop_when_acf_missing"
Expected: PASS (current empty do_register satisfies the never() expectation). This test exists to guard regression once we add the real body in Task 13 — it will then fail if we forget the guard.
(Note: this is the rare TDD case where the RED step is implicit — the real RED for this contract surfaces only after Task 13 adds the registration call. Step 12 locks the invariant in.)
- Step 12.3: Commit (test-only)
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add tests/FieldGroupTest.php
git commit -m "test(wpmc S2): lock ACF-missing guard for FieldGroup"
Task 13: FieldGroup — RED test (acf_add_local_field_group called with correct structure)
Files:
-
Modify:
tests/FieldGroupTest.php -
Step 13.1: Add test verifying the registered group structure
Append to tests/FieldGroupTest.php:
public function test_do_register_calls_acf_add_local_field_group_with_expected_structure(): void
{
\Brain\Monkey\Functions\when('function_exists')->alias(function ($name) {
return $name === 'acf_add_local_field_group';
});
$captured = null;
\Brain\Monkey\Functions\expect('acf_add_local_field_group')
->once()
->with(\Mockery::on(function ($group) use (&$captured) {
$captured = $group;
return true;
}));
FieldGroup::do_register();
self::assertSame('group_wpmc_shortcode_rows', $captured['key']);
self::assertSame('WPMultiCity / Shortcode rows', $captured['title']);
self::assertCount(1, $captured['fields']);
$repeater = $captured['fields'][0];
self::assertSame('field_wpmc_list_shortcode', $repeater['key']);
self::assertSame('list_shortcode', $repeater['name']);
self::assertSame('repeater', $repeater['type']);
self::assertCount(2, $repeater['sub_fields']);
[$town, $text] = $repeater['sub_fields'];
self::assertSame('town', $town['name']);
self::assertSame('taxonomy', $town['type']);
self::assertSame('town', $town['taxonomy']);
self::assertSame('id', $town['return_format']);
self::assertSame(0, $town['allow_null']);
self::assertSame('text', $text['name']);
self::assertSame('textarea', $text['type']);
self::assertSame(
[[['param' => 'post_type', 'operator' => '==', 'value' => 'shortcode-town']]],
$captured['location']
);
}
- Step 13.2: Run — expect failure (do_register is empty, acf_add_local_field_group never called)
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter "FieldGroupTest::test_do_register_calls_acf_add_local_field_group_with_expected_structure"
Expected: FAIL — acf_add_local_field_group expected 1 time, called 0 times.
Task 14: FieldGroup — GREEN (real registration body)
Files:
-
Modify:
includes/class-field-group.php -
Step 14.1: Replace empty do_register() with full ACF group registration
Replace do_register() body in includes/class-field-group.php:
public static function do_register(): void
{
if (!function_exists('acf_add_local_field_group')) {
return;
}
acf_add_local_field_group([
'key' => self::GROUP_KEY,
'title' => 'WPMultiCity / Shortcode rows',
'fields' => [
[
'key' => self::REPEATER_KEY,
'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,
]);
}
- Step 14.2: Run all FieldGroup tests — expect all pass
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit tests/FieldGroupTest.php
Expected: OK (3 tests, ≥14 assertions).
- Step 14.3: Commit
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add tests/FieldGroupTest.php includes/class-field-group.php
git commit -m "feat(wpmc S2): register ACF group list_shortcode for shortcode-town"
Task 15: Plugin::boot() — RED test (boot wires up the three registrations)
Files:
-
Create:
tests/PluginBootTest.php -
Step 15.1: Write failing test
tests/PluginBootTest.php:
<?php
declare(strict_types=1);
namespace WPMultiCity\Tests;
use WPMultiCity\Plugin;
final class PluginBootTest extends TestCase
{
public function test_boot_attaches_init_and_acf_init_hooks_when_acf_present(): void
{
// Simulate ACF being active so boot() proceeds past the dependency guard.
if (!class_exists('ACF', false)) {
eval('class ACF {}');
}
Plugin::instance()->boot();
self::assertNotFalse(
has_action('init', ['WPMultiCity\\Taxonomy', 'do_register']),
'boot() must register Taxonomy on init'
);
self::assertNotFalse(
has_action('init', ['WPMultiCity\\PostType', 'do_register']),
'boot() must register PostType on init'
);
self::assertNotFalse(
has_action('acf/init', ['WPMultiCity\\FieldGroup', 'do_register']),
'boot() must register FieldGroup on acf/init'
);
}
}
Note: Plugin::instance() is a singleton — tests share it. The Brain Monkey Actions registry is reset between tests via Monkey\tearDown(), so duplicate add_action calls across tests are harmless within a single test run.
- Step 15.2: Run — expect failure
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit --filter "PluginBootTest::test_boot_attaches_init_and_acf_init_hooks_when_acf_present"
Expected: FAIL — has_action returns false because Plugin::boot() currently has empty body (only the dependency check + comment).
Task 16: Plugin::boot() — GREEN (call the three register() methods)
Files:
-
Modify:
includes/class-plugin.php:37-45(boot method body) -
Step 16.1: Update boot() to invoke the three register() methods
Replace the boot() method body in includes/class-plugin.php:
public function boot(): void
{
if (!$this->dependencies_satisfied()) {
add_action('admin_notices', [$this, 'render_dependency_notice']);
return;
}
Taxonomy::register();
PostType::register();
FieldGroup::register();
}
- Step 16.2: Run — expect all tests pass
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit
Expected: OK (8 tests, ≥35 assertions). All four test files green.
- Step 16.3: Commit
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add tests/PluginBootTest.php includes/class-plugin.php
git commit -m "feat(wpmc S2): Plugin::boot() wires Taxonomy + PostType + FieldGroup"
Task 17: Update README roadmap status
Files:
-
Modify:
README.mdlines 22-29 (roadmap table) -
Step 17.1: Mark S2 as done in the roadmap
In README.md, change the S2 row:
| S2 | Taxonomy `town` + CPT `shortcode-town` + ACF `list_shortcode` | cp-q7g | open |
to:
| S2 | Taxonomy `town` + CPT `shortcode-town` + ACF `list_shortcode` | cp-q7g | done |
And update the S1 status row from in progress to done:
| S1 | Скелет (header, autoload, bootstrap) | cp-8ci | in progress |
to:
| S1 | Скелет (header, autoload, bootstrap) | cp-8ci | done |
- Step 17.2: Commit
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git add README.md
git commit -m "docs(wpmc): mark S1+S2 done in README roadmap"
Task 18: Final verification
Files: none
- Step 18.1: Full PHPUnit run
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
vendor/bin/phpunit
Expected: OK (8 tests, ≥35 assertions), exit code 0, no risky / no warnings.
- Step 18.2: PHP syntax lint on every new file
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
for f in includes/class-taxonomy.php includes/class-post-type.php includes/class-field-group.php includes/class-plugin.php tests/TestCase.php tests/TaxonomyTest.php tests/PostTypeTest.php tests/FieldGroupTest.php tests/PluginBootTest.php tests/bootstrap.php; do
php -l "$f"
done
Expected: No syntax errors detected for every file.
- Step 18.3: Git log review
cd "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city"
git log --oneline
Expected: chain of commits from chore: init wp-multi-city … through docs(wpmc): mark S1+S2 done …, each commit doing one logical thing.
- Step 18.4: Close beads task
bd close cp-q7g --reason "S2 done: taxonomy town + CPT shortcode-town + ACF list_shortcode registered with unit tests (Brain Monkey, 8 tests green)"
Expected: cp-q7g closed; bd ready now lists cp-arq (S3 shortcode_dmr) as the next ready front under epic cp-cw4.
Acceptance map (spec → tasks)
| Spec requirement | Task |
|---|---|
| Brain Monkey + Mockery dev deps | 1 |
| PHPUnit config + bootstrap + TestCase | 1 |
Taxonomy town with hierarchical=false, show_in_rest=true, public=false, meta_box_cb=false |
2-5 |
CPT shortcode-town with public=false, show_ui=true, supports=['title'], menu_position=8 |
6-9 |
ACF group group_wpmc_shortcode_rows titled WPMultiCity / Shortcode rows |
13-14 |
Repeater list_shortcode with sub-fields town (taxonomy, return_format=id) and text (textarea) |
13-14 |
| Group location = post_type==shortcode-town | 13-14 |
| Graceful no-op when ACF function missing | 12, 14 |
Plugin::boot() wires up all three registrations |
15-16 |
| README roadmap status updated | 17 |
| Full green test run | 18 |
| Close cp-q7g | 18 |