1096 lines
35 KiB
Markdown
1096 lines
35 KiB
Markdown
# 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`:
|
|
|
|
```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**
|
|
|
|
```bash
|
|
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
|
|
<?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
|
|
<?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
|
|
<?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)**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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
|
|
<?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)**
|
|
|
|
```bash
|
|
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
|
|
<?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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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):
|
|
|
|
```php
|
|
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)**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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
|
|
<?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)**
|
|
|
|
```bash
|
|
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
|
|
<?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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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):
|
|
|
|
```php
|
|
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)**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```php
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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
|
|
<?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**
|
|
|
|
```bash
|
|
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
|
|
<?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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```bash
|
|
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)**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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)**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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
|
|
<?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**
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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.md` lines 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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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 |
|