1391 lines
39 KiB
Markdown
1391 lines
39 KiB
Markdown
# S3 — `[shortcode_dmr]` + helpers 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:** Implement the multi-city content substitution core: shortcode `[shortcode_dmr id=N]` plus public helpers `wpmc_current_town`, `wpmc_get_term_by_slug`, `wpmc_alt_get_field`, `wpmc_remove_domain_link`.
|
||
|
||
**Architecture:** Static `WPMultiCity\Helpers` class for implementation. Static `WPMultiCity\ShortcodeDmr` class registers and renders the shortcode. `includes/wpmc-functions.php` defines thin procedural `wpmc_*` wrappers, loaded unconditionally from the main plugin file. `Plugin::boot()` adds `ShortcodeDmr::register()` alongside the existing three S2 registrations.
|
||
|
||
**Tech Stack:** PHP 8.0+, WordPress 6.0+, ACF Pro, PHPUnit 9.6, Brain Monkey 2.7, Mockery 1.6, Patchwork (for `function_exists` shimming).
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-05-12-s3-shortcode-dmr-helpers-design.md`
|
||
|
||
**Beads task:** `cp-arq` (epic `cp-cw4`).
|
||
|
||
---
|
||
|
||
## Working Directory
|
||
|
||
All paths relative to:
|
||
|
||
```
|
||
/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city
|
||
```
|
||
|
||
Run shell commands from that directory (quote it for shells that can't handle the Cyrillic/spaces).
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
```
|
||
wp-multi-city/
|
||
├── wp-multi-city.php # MODIFY — require wpmc-functions.php
|
||
├── includes/
|
||
│ ├── class-plugin.php # MODIFY — boot() also calls ShortcodeDmr::register()
|
||
│ ├── class-helpers.php # CREATE — WPMultiCity\Helpers static utility
|
||
│ ├── class-shortcode-dmr.php # CREATE — WPMultiCity\ShortcodeDmr
|
||
│ └── wpmc-functions.php # CREATE — procedural wpmc_* wrappers
|
||
└── tests/
|
||
├── TestCase.php # MODIFY — reset caches in setUp
|
||
├── HelpersTest.php # CREATE — all helper tests
|
||
├── ShortcodeDmrTest.php # CREATE — shortcode handler tests
|
||
└── PluginBootShortcodeDmrTest.php # CREATE — boot wires ShortcodeDmr
|
||
```
|
||
|
||
Boundaries:
|
||
- `Helpers` — pure utility (no hooks of its own).
|
||
- `ShortcodeDmr` — only the shortcode lifecycle (`register`, `do_register`, `render`).
|
||
- `wpmc-functions.php` — only public procedural shims; never grows past one-line wrappers.
|
||
|
||
---
|
||
|
||
## Task 1: Helpers scaffold + per-request cache plumbing
|
||
|
||
**Files:**
|
||
- Create: `includes/class-helpers.php`
|
||
- Modify: `tests/TestCase.php`
|
||
|
||
This task only creates the empty class with `reset_cache()` and wires it into TestCase setUp so subsequent tests start clean. No public methods yet — they get added with their own RED→GREEN cycles in Tasks 2-7.
|
||
|
||
- [ ] **Step 1.1: Create skeleton class**
|
||
|
||
`includes/class-helpers.php`:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity;
|
||
|
||
final class Helpers
|
||
{
|
||
private static ?string $current_town_memo = null;
|
||
private static bool $current_town_resolved = false;
|
||
private static array $term_cache = [];
|
||
|
||
public static function reset_cache(): void
|
||
{
|
||
self::$current_town_memo = null;
|
||
self::$current_town_resolved = false;
|
||
self::$term_cache = [];
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 1.2: Wire reset into TestCase::setUp**
|
||
|
||
Open `tests/TestCase.php` and add a `Helpers::reset_cache()` call after `Monkey\Functions\stubTranslationFunctions()`. The full file becomes:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity\Tests;
|
||
|
||
use Brain\Monkey;
|
||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||
use WPMultiCity\Helpers;
|
||
|
||
abstract class TestCase extends BaseTestCase
|
||
{
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
Monkey\setUp();
|
||
Monkey\Functions\stubTranslationFunctions();
|
||
Helpers::reset_cache();
|
||
}
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
Monkey\tearDown();
|
||
parent::tearDown();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 1.3: Run full suite — must still be green (9 tests)**
|
||
|
||
```bash
|
||
vendor/bin/phpunit
|
||
```
|
||
|
||
Expected: OK (9 tests, 43 assertions).
|
||
|
||
- [ ] **Step 1.4: Commit**
|
||
|
||
```bash
|
||
git add includes/class-helpers.php tests/TestCase.php
|
||
git commit -m "feat(wpmc S3): Helpers scaffold + reset_cache wiring"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: WP_Term test stub + `Helpers::get_term_by_slug` RED test
|
||
|
||
**Files:**
|
||
- Modify: `tests/bootstrap.php`
|
||
- Create: `tests/HelpersTest.php`
|
||
|
||
`WP_Term` is a WordPress core class that's not available in unit-test context. We add a minimal stub class in `tests/bootstrap.php` so test code can do `new \WP_Term()` naturally.
|
||
|
||
- [ ] **Step 2.1: Add WP_Term stub to tests/bootstrap.php**
|
||
|
||
Open `tests/bootstrap.php` and append (after the existing autoloader+register_autoloader lines):
|
||
|
||
```php
|
||
if (!class_exists('WP_Term', false)) {
|
||
eval('class WP_Term { public int $term_id = 0; public string $slug = ""; public string $taxonomy = ""; }');
|
||
}
|
||
```
|
||
|
||
Final `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();
|
||
|
||
if (!class_exists('WP_Term', false)) {
|
||
eval('class WP_Term { public int $term_id = 0; public string $slug = ""; public string $taxonomy = ""; }');
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2.2: Write failing test**
|
||
|
||
`tests/HelpersTest.php`:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity\Tests;
|
||
|
||
use WPMultiCity\Helpers;
|
||
|
||
final class HelpersTest extends TestCase
|
||
{
|
||
public function test_get_term_by_slug_returns_wp_term_when_found(): void
|
||
{
|
||
$term = new \WP_Term();
|
||
$term->term_id = 42;
|
||
$term->slug = 'tyumen';
|
||
$term->taxonomy = 'town';
|
||
|
||
\Brain\Monkey\Functions\expect('get_term_by')
|
||
->once()
|
||
->with('slug', 'tyumen', 'town')
|
||
->andReturn($term);
|
||
|
||
$result = Helpers::get_term_by_slug('tyumen');
|
||
|
||
self::assertInstanceOf(\WP_Term::class, $result);
|
||
self::assertSame(42, $result->term_id);
|
||
self::assertSame('tyumen', $result->slug);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2.2: Run — expect failure (method does not exist)**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_get_term_by_slug_returns_wp_term_when_found
|
||
```
|
||
|
||
Expected: Error — `Call to undefined method WPMultiCity\Helpers::get_term_by_slug`.
|
||
|
||
---
|
||
|
||
## Task 3: `Helpers::get_term_by_slug` — GREEN + cache test
|
||
|
||
**Files:**
|
||
- Modify: `includes/class-helpers.php`
|
||
- Modify: `tests/HelpersTest.php`
|
||
|
||
- [ ] **Step 3.1: Add method to Helpers**
|
||
|
||
Append to `includes/class-helpers.php` inside the class (before the closing brace):
|
||
|
||
```php
|
||
public static function get_term_by_slug(string $slug, string $taxonomy = 'town'): ?\WP_Term
|
||
{
|
||
$key = $taxonomy . '|' . $slug;
|
||
if (array_key_exists($key, self::$term_cache)) {
|
||
return self::$term_cache[$key];
|
||
}
|
||
$term = \get_term_by('slug', $slug, $taxonomy);
|
||
return self::$term_cache[$key] = ($term instanceof \WP_Term) ? $term : null;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3.2: Run — expect PASS**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_get_term_by_slug_returns_wp_term_when_found
|
||
```
|
||
|
||
Expected: OK (1 test, 3 assertions).
|
||
|
||
- [ ] **Step 3.3: Add cache-hit + not-found tests**
|
||
|
||
Append to the `HelpersTest` class body:
|
||
|
||
```php
|
||
public function test_get_term_by_slug_returns_null_when_not_found(): void
|
||
{
|
||
\Brain\Monkey\Functions\expect('get_term_by')
|
||
->once()
|
||
->with('slug', 'nonexistent', 'town')
|
||
->andReturn(false);
|
||
|
||
self::assertNull(Helpers::get_term_by_slug('nonexistent'));
|
||
}
|
||
|
||
public function test_get_term_by_slug_caches_results_within_request(): void
|
||
{
|
||
\Brain\Monkey\Functions\expect('get_term_by')
|
||
->once()
|
||
->with('slug', 'tyumen', 'town')
|
||
->andReturn(false);
|
||
|
||
Helpers::get_term_by_slug('tyumen');
|
||
Helpers::get_term_by_slug('tyumen');
|
||
Helpers::get_term_by_slug('tyumen');
|
||
|
||
// Mockery ->once() at teardown verifies the cache stopped repeated calls.
|
||
self::assertTrue(true);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3.4: Run — both new tests must pass**
|
||
|
||
```bash
|
||
vendor/bin/phpunit tests/HelpersTest.php
|
||
```
|
||
|
||
Expected: OK (3 tests, ≥4 assertions).
|
||
|
||
- [ ] **Step 3.5: Commit**
|
||
|
||
```bash
|
||
git add includes/class-helpers.php tests/HelpersTest.php
|
||
git commit -m "feat(wpmc S3): Helpers::get_term_by_slug with per-request cache"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: `Helpers::current_town` — RED test (URI matches existing term)
|
||
|
||
**Files:**
|
||
- Modify: `tests/HelpersTest.php`
|
||
|
||
- [ ] **Step 4.1: Add failing test**
|
||
|
||
Append to `HelpersTest`:
|
||
|
||
```php
|
||
public function test_current_town_returns_slug_when_uri_matches_existing_town(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/tyumen/some-service/';
|
||
|
||
\Brain\Monkey\Functions\expect('get_terms')
|
||
->once()
|
||
->with('town', \Mockery::on(function ($args) {
|
||
return ($args['hide_empty'] ?? null) === false
|
||
&& ($args['fields'] ?? null) === 'id=>slug';
|
||
}))
|
||
->andReturn([10 => 'spb', 11 => 'tyumen']);
|
||
|
||
self::assertSame('tyumen', Helpers::current_town());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4.2: Run — expect FAIL (method does not exist)**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_current_town_returns_slug_when_uri_matches_existing_town
|
||
```
|
||
|
||
Expected: Error — `Call to undefined method WPMultiCity\Helpers::current_town`.
|
||
|
||
---
|
||
|
||
## Task 5: `Helpers::current_town` — GREEN minimal (URI match path only)
|
||
|
||
**Files:**
|
||
- Modify: `includes/class-helpers.php`
|
||
|
||
- [ ] **Step 5.1: Add method that handles URI-match path only**
|
||
|
||
Append to Helpers class:
|
||
|
||
```php
|
||
public static function current_town(): ?string
|
||
{
|
||
if (self::$current_town_resolved) {
|
||
return self::$current_town_memo;
|
||
}
|
||
|
||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||
$first_segment = self::first_path_segment($uri);
|
||
|
||
$resolved = null;
|
||
if ($first_segment !== null) {
|
||
$terms = \get_terms('town', [
|
||
'hide_empty' => false,
|
||
'fields' => 'id=>slug',
|
||
]);
|
||
if (is_array($terms) && in_array($first_segment, $terms, true)) {
|
||
$resolved = $first_segment;
|
||
}
|
||
}
|
||
|
||
if ($resolved === null) {
|
||
$resolved = \apply_filters('wpmc_main_town_slug', null);
|
||
if (!is_string($resolved) || $resolved === '') {
|
||
$resolved = null;
|
||
}
|
||
}
|
||
|
||
$resolved = \apply_filters('wpmc_current_town_slug', $resolved);
|
||
if (!is_string($resolved) || $resolved === '') {
|
||
$resolved = null;
|
||
}
|
||
|
||
self::$current_town_memo = $resolved;
|
||
self::$current_town_resolved = true;
|
||
return $resolved;
|
||
}
|
||
|
||
private static function first_path_segment(string $uri): ?string
|
||
{
|
||
$path = strtok($uri, '?');
|
||
if ($path === false) {
|
||
return null;
|
||
}
|
||
$parts = array_values(array_filter(explode('/', $path), static fn(string $p) => $p !== ''));
|
||
return $parts[0] ?? null;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5.2: Run — expect PASS**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_current_town_returns_slug_when_uri_matches_existing_town
|
||
```
|
||
|
||
Expected: OK (1 test, 1 assertion).
|
||
|
||
- [ ] **Step 5.3: Commit**
|
||
|
||
```bash
|
||
git add includes/class-helpers.php tests/HelpersTest.php
|
||
git commit -m "feat(wpmc S3): Helpers::current_town URI-match path"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: `Helpers::current_town` — add fallback + filter tests
|
||
|
||
**Files:**
|
||
- Modify: `tests/HelpersTest.php`
|
||
|
||
- [ ] **Step 6.1: Add 5 more tests (no-match→null, main_town_slug fallback, URI wins over fallback, current_town_slug override, memoization)**
|
||
|
||
Append to HelpersTest class:
|
||
|
||
```php
|
||
public function test_current_town_returns_null_when_uri_does_not_match_and_no_filter(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/some-random-page/';
|
||
|
||
\Brain\Monkey\Functions\expect('get_terms')
|
||
->andReturn([10 => 'spb', 11 => 'tyumen']);
|
||
|
||
self::assertNull(Helpers::current_town());
|
||
}
|
||
|
||
public function test_current_town_uses_main_town_slug_filter_when_uri_has_no_match(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/';
|
||
|
||
\Brain\Monkey\Functions\expect('get_terms')
|
||
->andReturn([10 => 'spb', 11 => 'tyumen']);
|
||
\Brain\Monkey\Filters\expectApplied('wpmc_main_town_slug')
|
||
->once()
|
||
->with(null)
|
||
->andReturn('spb');
|
||
|
||
self::assertSame('spb', Helpers::current_town());
|
||
}
|
||
|
||
public function test_current_town_uri_match_wins_over_main_town_filter(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||
|
||
\Brain\Monkey\Functions\expect('get_terms')
|
||
->andReturn([10 => 'spb', 11 => 'tyumen']);
|
||
// main_town filter should not be consulted when URI matches.
|
||
\Brain\Monkey\Filters\expectApplied('wpmc_main_town_slug')->never();
|
||
|
||
self::assertSame('tyumen', Helpers::current_town());
|
||
}
|
||
|
||
public function test_current_town_slug_filter_overrides_final_value(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/';
|
||
|
||
\Brain\Monkey\Functions\expect('get_terms')
|
||
->andReturn([]);
|
||
\Brain\Monkey\Filters\expectApplied('wpmc_current_town_slug')
|
||
->once()
|
||
->andReturn('override-slug');
|
||
|
||
self::assertSame('override-slug', Helpers::current_town());
|
||
}
|
||
|
||
public function test_current_town_memoizes_within_request(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||
|
||
\Brain\Monkey\Functions\expect('get_terms')
|
||
->once()
|
||
->andReturn([10 => 'spb', 11 => 'tyumen']);
|
||
|
||
Helpers::current_town();
|
||
Helpers::current_town();
|
||
Helpers::current_town();
|
||
|
||
self::assertTrue(true);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6.2: Run — expect all 5 new tests pass**
|
||
|
||
```bash
|
||
vendor/bin/phpunit tests/HelpersTest.php
|
||
```
|
||
|
||
Expected: OK (8 tests, ≥9 assertions). Failure in any of these 5 = bug in Task 5 implementation.
|
||
|
||
- [ ] **Step 6.3: Commit**
|
||
|
||
```bash
|
||
git add tests/HelpersTest.php
|
||
git commit -m "test(wpmc S3): current_town fallback + filter + memoization coverage"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: `Helpers::alt_get_field` — RED test (string value goes through do_shortcode)
|
||
|
||
**Files:**
|
||
- Modify: `tests/HelpersTest.php`
|
||
|
||
- [ ] **Step 7.1: Add failing test**
|
||
|
||
Append to HelpersTest:
|
||
|
||
```php
|
||
public function test_alt_get_field_applies_do_shortcode_to_string_values(): void
|
||
{
|
||
\Brain\Monkey\Functions\when('function_exists')->alias(function ($name) {
|
||
return $name === 'get_field';
|
||
});
|
||
\Brain\Monkey\Functions\expect('get_field')
|
||
->once()
|
||
->with('headline', 5, true)
|
||
->andReturn('Hello [bar]');
|
||
\Brain\Monkey\Functions\expect('do_shortcode')
|
||
->once()
|
||
->with('Hello [bar]')
|
||
->andReturn('Hello World');
|
||
|
||
self::assertSame('Hello World', Helpers::alt_get_field('headline', 5));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 7.2: Run — expect FAIL**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_alt_get_field_applies_do_shortcode_to_string_values
|
||
```
|
||
|
||
Expected: Error — undefined method.
|
||
|
||
---
|
||
|
||
## Task 8: `Helpers::alt_get_field` — GREEN
|
||
|
||
**Files:**
|
||
- Modify: `includes/class-helpers.php`
|
||
|
||
- [ ] **Step 8.1: Add method**
|
||
|
||
Append to Helpers class:
|
||
|
||
```php
|
||
public static function alt_get_field(string $selector, $post_id = false, bool $format_value = true)
|
||
{
|
||
$post_id = \apply_filters('wpmc_alt_get_field_post_id', $post_id, $selector);
|
||
if (!\function_exists('get_field')) {
|
||
return null;
|
||
}
|
||
$value = \get_field($selector, $post_id, $format_value);
|
||
return is_string($value) ? \do_shortcode($value) : $value;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 8.2: Run — expect PASS**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_alt_get_field_applies_do_shortcode_to_string_values
|
||
```
|
||
|
||
Expected: OK (1 test, 1 assertion).
|
||
|
||
- [ ] **Step 8.3: Add 3 more tests (non-string, filter, ACF missing)**
|
||
|
||
Append:
|
||
|
||
```php
|
||
public function test_alt_get_field_returns_arrays_as_is(): void
|
||
{
|
||
\Brain\Monkey\Functions\when('function_exists')->alias(fn($n) => $n === 'get_field');
|
||
\Brain\Monkey\Functions\expect('get_field')
|
||
->once()
|
||
->andReturn(['a', 'b']);
|
||
\Brain\Monkey\Functions\expect('do_shortcode')->never();
|
||
|
||
self::assertSame(['a', 'b'], Helpers::alt_get_field('list'));
|
||
}
|
||
|
||
public function test_alt_get_field_post_id_filter_overrides_caller_id(): void
|
||
{
|
||
\Brain\Monkey\Functions\when('function_exists')->alias(fn($n) => $n === 'get_field');
|
||
\Brain\Monkey\Filters\expectApplied('wpmc_alt_get_field_post_id')
|
||
->once()
|
||
->with(5, 'title')
|
||
->andReturn(99);
|
||
\Brain\Monkey\Functions\expect('get_field')
|
||
->once()
|
||
->with('title', 99, true)
|
||
->andReturn(null);
|
||
|
||
Helpers::alt_get_field('title', 5);
|
||
self::assertTrue(true);
|
||
}
|
||
|
||
public function test_alt_get_field_returns_null_when_acf_get_field_missing(): void
|
||
{
|
||
\Brain\Monkey\Functions\when('function_exists')->justReturn(false);
|
||
\Brain\Monkey\Functions\expect('get_field')->never();
|
||
|
||
self::assertNull(Helpers::alt_get_field('anything'));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 8.4: Run — expect all 4 alt_get_field tests pass**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter alt_get_field
|
||
```
|
||
|
||
Expected: OK (4 tests, ≥4 assertions).
|
||
|
||
- [ ] **Step 8.5: Commit**
|
||
|
||
```bash
|
||
git add includes/class-helpers.php tests/HelpersTest.php
|
||
git commit -m "feat(wpmc S3): Helpers::alt_get_field with do_shortcode + filter"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: `Helpers::remove_domain_link` — full TDD cycle (5 cases at once)
|
||
|
||
**Files:**
|
||
- Modify: `tests/HelpersTest.php`
|
||
- Modify: `includes/class-helpers.php`
|
||
|
||
This helper is small and the cases are independent — TDD as one RED→GREEN pair with five assertions, then commit.
|
||
|
||
- [ ] **Step 9.1: Add failing test (all 5 cases)**
|
||
|
||
Append to HelpersTest:
|
||
|
||
```php
|
||
public function test_remove_domain_link_handles_various_url_shapes(): void
|
||
{
|
||
self::assertSame('/foo/bar/', Helpers::remove_domain_link('https://site.com/foo/bar/'));
|
||
self::assertSame('/foo?q=1#a', Helpers::remove_domain_link('http://site.com/foo?q=1#a'));
|
||
self::assertSame('/foo/', Helpers::remove_domain_link('/foo/'));
|
||
self::assertSame('mailto:x@y.com', Helpers::remove_domain_link('mailto:x@y.com'));
|
||
self::assertSame('not a url', Helpers::remove_domain_link('not a url'));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 9.2: Run — expect FAIL (method undefined)**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_remove_domain_link_handles_various_url_shapes
|
||
```
|
||
|
||
Expected: Error — `Call to undefined method`.
|
||
|
||
- [ ] **Step 9.3: Implement method**
|
||
|
||
Append to Helpers:
|
||
|
||
```php
|
||
public static function remove_domain_link(string $url): string
|
||
{
|
||
$parsed = parse_url($url);
|
||
if ($parsed === false || empty($parsed['scheme'])) {
|
||
return $url;
|
||
}
|
||
$scheme = strtolower((string) $parsed['scheme']);
|
||
if ($scheme !== 'http' && $scheme !== 'https') {
|
||
return $url;
|
||
}
|
||
$path = $parsed['path'] ?? '';
|
||
$query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
|
||
$fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
|
||
return $path . $query . $fragment;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 9.4: Run — expect PASS**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_remove_domain_link_handles_various_url_shapes
|
||
```
|
||
|
||
Expected: OK (1 test, 5 assertions).
|
||
|
||
- [ ] **Step 9.5: Commit**
|
||
|
||
```bash
|
||
git add includes/class-helpers.php tests/HelpersTest.php
|
||
git commit -m "feat(wpmc S3): Helpers::remove_domain_link via parse_url"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: `ShortcodeDmr` scaffold + register hook test (TDD pair)
|
||
|
||
**Files:**
|
||
- Create: `tests/ShortcodeDmrTest.php`
|
||
- Create: `includes/class-shortcode-dmr.php`
|
||
|
||
- [ ] **Step 10.1: RED — write failing test**
|
||
|
||
`tests/ShortcodeDmrTest.php`:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity\Tests;
|
||
|
||
use WPMultiCity\ShortcodeDmr;
|
||
|
||
final class ShortcodeDmrTest extends TestCase
|
||
{
|
||
public function test_register_attaches_init_action(): void
|
||
{
|
||
ShortcodeDmr::register();
|
||
|
||
self::assertNotFalse(
|
||
has_action('init', [ShortcodeDmr::class, 'do_register']),
|
||
'ShortcodeDmr::register() must hook init'
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 10.2: Run — expect FAIL (class not found)**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter "ShortcodeDmrTest::test_register_attaches_init_action"
|
||
```
|
||
|
||
Expected: Error — `Class "WPMultiCity\ShortcodeDmr" not found`.
|
||
|
||
- [ ] **Step 10.3: GREEN — create class**
|
||
|
||
`includes/class-shortcode-dmr.php`:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity;
|
||
|
||
final class ShortcodeDmr
|
||
{
|
||
public const SHORTCODE = 'shortcode_dmr';
|
||
|
||
private static array $render_cache = [];
|
||
|
||
public static function reset_cache(): void
|
||
{
|
||
self::$render_cache = [];
|
||
}
|
||
|
||
public static function register(): void
|
||
{
|
||
\add_action('init', [self::class, 'do_register']);
|
||
}
|
||
|
||
public static function do_register(): void
|
||
{
|
||
\add_shortcode(self::SHORTCODE, [self::class, 'render']);
|
||
}
|
||
|
||
public static function render($atts = [], $content = null): string
|
||
{
|
||
// Implementation in Tasks 12-16.
|
||
return '';
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 10.4: Wire reset into TestCase::setUp**
|
||
|
||
Open `tests/TestCase.php`. Add `ShortcodeDmr::reset_cache()` after `Helpers::reset_cache()`. The file becomes:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity\Tests;
|
||
|
||
use Brain\Monkey;
|
||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||
use WPMultiCity\Helpers;
|
||
use WPMultiCity\ShortcodeDmr;
|
||
|
||
abstract class TestCase extends BaseTestCase
|
||
{
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
Monkey\setUp();
|
||
Monkey\Functions\stubTranslationFunctions();
|
||
Helpers::reset_cache();
|
||
ShortcodeDmr::reset_cache();
|
||
}
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
Monkey\tearDown();
|
||
parent::tearDown();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 10.5: Run — expect PASS**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter "ShortcodeDmrTest::test_register_attaches_init_action"
|
||
```
|
||
|
||
Expected: OK (1 test, 1 assertion).
|
||
|
||
- [ ] **Step 10.6: Commit**
|
||
|
||
```bash
|
||
git add tests/ShortcodeDmrTest.php tests/TestCase.php includes/class-shortcode-dmr.php
|
||
git commit -m "feat(wpmc S3): ShortcodeDmr scaffold + register init hook"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: `ShortcodeDmr::do_register` calls `add_shortcode` (TDD pair)
|
||
|
||
**Files:**
|
||
- Modify: `tests/ShortcodeDmrTest.php`
|
||
|
||
- [ ] **Step 11.1: Add failing test**
|
||
|
||
Append to ShortcodeDmrTest:
|
||
|
||
```php
|
||
public function test_do_register_registers_shortcode(): void
|
||
{
|
||
\Brain\Monkey\Functions\expect('add_shortcode')
|
||
->once()
|
||
->with('shortcode_dmr', [ShortcodeDmr::class, 'render']);
|
||
|
||
ShortcodeDmr::do_register();
|
||
|
||
self::assertTrue(true);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 11.2: Run — expect PASS (do_register already calls add_shortcode from Task 10's stub)**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_do_register_registers_shortcode
|
||
```
|
||
|
||
Expected: OK (1 test, 1 assertion). The stub from Task 10 already implements the correct behavior; this test is the regression lock.
|
||
|
||
- [ ] **Step 11.3: Commit (test-only)**
|
||
|
||
```bash
|
||
git add tests/ShortcodeDmrTest.php
|
||
git commit -m "test(wpmc S3): lock add_shortcode call in ShortcodeDmr::do_register"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 12: `render` — RED test (matching row returns text)
|
||
|
||
**Files:**
|
||
- Modify: `tests/ShortcodeDmrTest.php`
|
||
|
||
- [ ] **Step 12.1: Add failing test**
|
||
|
||
Append:
|
||
|
||
```php
|
||
public function test_render_returns_text_when_row_matches_current_town(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||
\Brain\Monkey\Functions\when('get_terms')->justReturn([10 => 'spb', 11 => 'tyumen']);
|
||
|
||
$term = new \WP_Term();
|
||
$term->term_id = 11;
|
||
$term->slug = 'tyumen';
|
||
\Brain\Monkey\Functions\when('get_term_by')->alias(function ($field, $slug, $tax) use ($term) {
|
||
return ($field === 'slug' && $slug === 'tyumen' && $tax === 'town') ? $term : false;
|
||
});
|
||
|
||
\Brain\Monkey\Functions\expect('get_field')
|
||
->once()
|
||
->with('list_shortcode', 42)
|
||
->andReturn([
|
||
['town' => 10, 'text' => 'СПб-текст'],
|
||
['town' => 11, 'text' => 'Тюмень-текст'],
|
||
]);
|
||
|
||
$result = ShortcodeDmr::render(['id' => 42]);
|
||
|
||
self::assertSame('Тюмень-текст', $result);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 12.2: Run — expect FAIL (render still returns '')**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_render_returns_text_when_row_matches_current_town
|
||
```
|
||
|
||
Expected: FAIL — `Failed asserting that '' matches expected 'Тюмень-текст'`.
|
||
|
||
---
|
||
|
||
## Task 13: `render` — GREEN minimal implementation
|
||
|
||
**Files:**
|
||
- Modify: `includes/class-shortcode-dmr.php`
|
||
|
||
- [ ] **Step 13.1: Replace render method body**
|
||
|
||
Replace the `render()` method in `includes/class-shortcode-dmr.php` with:
|
||
|
||
```php
|
||
public static function render($atts = [], $content = null): string
|
||
{
|
||
$atts = \shortcode_atts(['id' => 0], (array) $atts);
|
||
$id = (int) $atts['id'];
|
||
if ($id <= 0) {
|
||
return '';
|
||
}
|
||
|
||
$town_slug = Helpers::current_town();
|
||
if ($town_slug === null) {
|
||
return '';
|
||
}
|
||
|
||
$cache_key = $id . '|' . $town_slug;
|
||
if (array_key_exists($cache_key, self::$render_cache)) {
|
||
return self::$render_cache[$cache_key];
|
||
}
|
||
|
||
$term = Helpers::get_term_by_slug($town_slug);
|
||
if ($term === null) {
|
||
return self::$render_cache[$cache_key] = '';
|
||
}
|
||
|
||
$rows = \get_field('list_shortcode', $id);
|
||
if (!is_array($rows) || $rows === []) {
|
||
return self::$render_cache[$cache_key] = '';
|
||
}
|
||
|
||
foreach ($rows as $row) {
|
||
if (!is_array($row) || !isset($row['town'])) {
|
||
continue;
|
||
}
|
||
if ((int) $row['town'] === (int) $term->term_id) {
|
||
$text = isset($row['text']) ? (string) $row['text'] : '';
|
||
return self::$render_cache[$cache_key] = $text;
|
||
}
|
||
}
|
||
|
||
return self::$render_cache[$cache_key] = '';
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 13.2: Run — expect PASS**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_render_returns_text_when_row_matches_current_town
|
||
```
|
||
|
||
Expected: OK (1 test, 1 assertion).
|
||
|
||
- [ ] **Step 13.3: Commit**
|
||
|
||
```bash
|
||
git add tests/ShortcodeDmrTest.php includes/class-shortcode-dmr.php
|
||
git commit -m "feat(wpmc S3): ShortcodeDmr::render resolves text by current town"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 14: `render` — edge cases (id<=0, current_town null, term null, empty rows, no match)
|
||
|
||
**Files:**
|
||
- Modify: `tests/ShortcodeDmrTest.php`
|
||
|
||
- [ ] **Step 14.1: Add 5 edge-case tests**
|
||
|
||
Append to ShortcodeDmrTest:
|
||
|
||
```php
|
||
public function test_render_returns_empty_when_id_is_zero(): void
|
||
{
|
||
\Brain\Monkey\Functions\expect('get_field')->never();
|
||
self::assertSame('', ShortcodeDmr::render(['id' => 0]));
|
||
}
|
||
|
||
public function test_render_returns_empty_when_current_town_is_null(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/no-matching-segment/';
|
||
\Brain\Monkey\Functions\when('get_terms')->justReturn([10 => 'spb']);
|
||
\Brain\Monkey\Functions\expect('get_field')->never();
|
||
|
||
self::assertSame('', ShortcodeDmr::render(['id' => 42]));
|
||
}
|
||
|
||
public function test_render_returns_empty_when_term_lookup_fails(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
||
\Brain\Monkey\Functions\when('get_term_by')->justReturn(false);
|
||
\Brain\Monkey\Functions\expect('get_field')->never();
|
||
|
||
self::assertSame('', ShortcodeDmr::render(['id' => 42]));
|
||
}
|
||
|
||
public function test_render_returns_empty_when_rows_are_empty(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
||
$term = new \WP_Term();
|
||
$term->term_id = 11;
|
||
$term->slug = 'tyumen';
|
||
\Brain\Monkey\Functions\when('get_term_by')->justReturn($term);
|
||
\Brain\Monkey\Functions\expect('get_field')->once()->andReturn([]);
|
||
|
||
self::assertSame('', ShortcodeDmr::render(['id' => 42]));
|
||
}
|
||
|
||
public function test_render_returns_empty_when_no_row_matches(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||
\Brain\Monkey\Functions\when('get_terms')->justReturn([10 => 'spb', 11 => 'tyumen']);
|
||
$term = new \WP_Term();
|
||
$term->term_id = 11;
|
||
$term->slug = 'tyumen';
|
||
\Brain\Monkey\Functions\when('get_term_by')->justReturn($term);
|
||
\Brain\Monkey\Functions\expect('get_field')
|
||
->once()
|
||
->andReturn([['town' => 10, 'text' => 'Только СПб']]);
|
||
|
||
self::assertSame('', ShortcodeDmr::render(['id' => 42]));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 14.2: Run — expect all 5 new tests pass**
|
||
|
||
```bash
|
||
vendor/bin/phpunit tests/ShortcodeDmrTest.php
|
||
```
|
||
|
||
Expected: OK (8 tests, ≥8 assertions).
|
||
|
||
- [ ] **Step 14.3: Commit**
|
||
|
||
```bash
|
||
git add tests/ShortcodeDmrTest.php
|
||
git commit -m "test(wpmc S3): ShortcodeDmr::render edge-case coverage"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 15: `render` — cache test
|
||
|
||
**Files:**
|
||
- Modify: `tests/ShortcodeDmrTest.php`
|
||
|
||
- [ ] **Step 15.1: Add cache test**
|
||
|
||
Append:
|
||
|
||
```php
|
||
public function test_render_caches_result_per_id_and_town(): void
|
||
{
|
||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
||
$term = new \WP_Term();
|
||
$term->term_id = 11;
|
||
$term->slug = 'tyumen';
|
||
\Brain\Monkey\Functions\when('get_term_by')->justReturn($term);
|
||
|
||
\Brain\Monkey\Functions\expect('get_field')
|
||
->once()
|
||
->with('list_shortcode', 42)
|
||
->andReturn([['town' => 11, 'text' => 'Тюмень-текст']]);
|
||
|
||
ShortcodeDmr::render(['id' => 42]);
|
||
ShortcodeDmr::render(['id' => 42]);
|
||
ShortcodeDmr::render(['id' => 42]);
|
||
|
||
self::assertTrue(true);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 15.2: Run — expect PASS (cache implemented in Task 13)**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter test_render_caches_result_per_id_and_town
|
||
```
|
||
|
||
Expected: OK (1 test, 1 assertion).
|
||
|
||
- [ ] **Step 15.3: Commit**
|
||
|
||
```bash
|
||
git add tests/ShortcodeDmrTest.php
|
||
git commit -m "test(wpmc S3): lock per-request render cache for ShortcodeDmr"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 16: `Plugin::boot()` wires `ShortcodeDmr::register()` (TDD pair)
|
||
|
||
**Files:**
|
||
- Create: `tests/PluginBootShortcodeDmrTest.php`
|
||
- Modify: `includes/class-plugin.php`
|
||
|
||
- [ ] **Step 16.1: RED — add failing test in a new file (existing PluginBootTest stays untouched)**
|
||
|
||
`tests/PluginBootShortcodeDmrTest.php`:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity\Tests;
|
||
|
||
use WPMultiCity\Plugin;
|
||
|
||
final class PluginBootShortcodeDmrTest extends TestCase
|
||
{
|
||
public function test_boot_attaches_shortcode_dmr_init_action(): void
|
||
{
|
||
if (!class_exists('ACF', false)) {
|
||
eval('class ACF {}');
|
||
}
|
||
|
||
$plugin = Plugin::instance();
|
||
$reflection = new \ReflectionClass($plugin);
|
||
$prop = $reflection->getProperty('booted');
|
||
$prop->setValue($plugin, false);
|
||
|
||
$plugin->boot();
|
||
|
||
self::assertNotFalse(
|
||
has_action('init', ['WPMultiCity\\ShortcodeDmr', 'do_register']),
|
||
'boot() must register ShortcodeDmr on init'
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 16.2: Run — expect FAIL**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter "PluginBootShortcodeDmrTest::test_boot_attaches_shortcode_dmr_init_action"
|
||
```
|
||
|
||
Expected: FAIL — `has_action` returns false because `boot()` doesn't call `ShortcodeDmr::register()` yet.
|
||
|
||
- [ ] **Step 16.3: GREEN — add register call to Plugin::boot()**
|
||
|
||
Open `includes/class-plugin.php`. Inside `boot()`, after the existing three `register()` calls, add `ShortcodeDmr::register();` so the final block looks like:
|
||
|
||
```php
|
||
Taxonomy::register();
|
||
PostType::register();
|
||
FieldGroup::register();
|
||
ShortcodeDmr::register();
|
||
```
|
||
|
||
- [ ] **Step 16.4: Run — expect PASS**
|
||
|
||
```bash
|
||
vendor/bin/phpunit --filter "PluginBootShortcodeDmrTest::test_boot_attaches_shortcode_dmr_init_action"
|
||
```
|
||
|
||
Expected: OK (1 test, 1 assertion).
|
||
|
||
- [ ] **Step 16.5: Commit**
|
||
|
||
```bash
|
||
git add tests/PluginBootShortcodeDmrTest.php includes/class-plugin.php
|
||
git commit -m "feat(wpmc S3): Plugin::boot() wires ShortcodeDmr"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 17: Procedural `wpmc-functions.php` + main file include
|
||
|
||
**Files:**
|
||
- Create: `includes/wpmc-functions.php`
|
||
- Modify: `wp-multi-city.php`
|
||
|
||
Procedural shims are not strictly tested by themselves — they're one-line passthroughs. But we add a smoke test in the existing HelpersTest to confirm the file loads and `wpmc_current_town()` returns the same value as `Helpers::current_town()`.
|
||
|
||
- [ ] **Step 17.1: Create wpmc-functions.php**
|
||
|
||
`includes/wpmc-functions.php`:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
use WPMultiCity\Helpers;
|
||
|
||
if (!function_exists('wpmc_current_town')) {
|
||
function wpmc_current_town(): ?string
|
||
{
|
||
return Helpers::current_town();
|
||
}
|
||
}
|
||
|
||
if (!function_exists('wpmc_get_term_by_slug')) {
|
||
function wpmc_get_term_by_slug(string $slug, string $taxonomy = 'town'): ?\WP_Term
|
||
{
|
||
return Helpers::get_term_by_slug($slug, $taxonomy);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('wpmc_alt_get_field')) {
|
||
function wpmc_alt_get_field(string $selector, $post_id = false, bool $format_value = true)
|
||
{
|
||
return Helpers::alt_get_field($selector, $post_id, $format_value);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('wpmc_remove_domain_link')) {
|
||
function wpmc_remove_domain_link(string $url): string
|
||
{
|
||
return Helpers::remove_domain_link($url);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 17.2: Modify wp-multi-city.php to require the file**
|
||
|
||
Open `wp-multi-city.php`. After the `register_autoloader()` line (line 23) and before the `register_activation_hook` line, add:
|
||
|
||
```php
|
||
require_once WPMC_PLUGIN_DIR . 'includes/wpmc-functions.php';
|
||
```
|
||
|
||
So the relevant block reads:
|
||
|
||
```php
|
||
require_once WPMC_PLUGIN_DIR . 'includes/class-plugin.php';
|
||
\WPMultiCity\Plugin::register_autoloader();
|
||
require_once WPMC_PLUGIN_DIR . 'includes/wpmc-functions.php';
|
||
|
||
register_activation_hook(__FILE__, ['\\WPMultiCity\\Activator', 'activate']);
|
||
```
|
||
|
||
- [ ] **Step 17.3: Add procedural-API smoke test**
|
||
|
||
Append to `tests/HelpersTest.php`:
|
||
|
||
```php
|
||
public function test_wpmc_procedural_functions_proxy_to_helpers(): void
|
||
{
|
||
require_once dirname(__DIR__) . '/includes/wpmc-functions.php';
|
||
|
||
self::assertTrue(function_exists('wpmc_current_town'));
|
||
self::assertTrue(function_exists('wpmc_get_term_by_slug'));
|
||
self::assertTrue(function_exists('wpmc_alt_get_field'));
|
||
self::assertTrue(function_exists('wpmc_remove_domain_link'));
|
||
|
||
// remove_domain_link is pure (no WP function dependencies) — exercise the proxy.
|
||
self::assertSame('/foo/', wpmc_remove_domain_link('https://site.com/foo/'));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 17.4: Run full suite**
|
||
|
||
```bash
|
||
vendor/bin/phpunit
|
||
```
|
||
|
||
Expected: OK (≥21 tests, no risky/warnings). Count varies depending on which subagent reordered tests; the important number is the assertion count growing monotonically from 43 (S2) to whatever S3 contributes.
|
||
|
||
- [ ] **Step 17.5: PHP lint the new file**
|
||
|
||
```bash
|
||
php -l includes/wpmc-functions.php
|
||
```
|
||
|
||
Expected: `No syntax errors detected`.
|
||
|
||
- [ ] **Step 17.6: Commit**
|
||
|
||
```bash
|
||
git add includes/wpmc-functions.php wp-multi-city.php tests/HelpersTest.php
|
||
git commit -m "feat(wpmc S3): procedural wpmc_* API + smoke test"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 18: Update README roadmap
|
||
|
||
**Files:**
|
||
- Modify: `README.md`
|
||
|
||
- [ ] **Step 18.1: Mark S3 done**
|
||
|
||
In `README.md`, change the S3 row from:
|
||
|
||
```
|
||
| S3 | `[shortcode_dmr id=N]` + хелперы (`current_town`, `_alt_get_field`, `remove_domain_link`) | cp-arq | open |
|
||
```
|
||
|
||
to:
|
||
|
||
```
|
||
| S3 | `[shortcode_dmr id=N]` + хелперы (`current_town`, `_alt_get_field`, `remove_domain_link`) | cp-arq | done |
|
||
```
|
||
|
||
- [ ] **Step 18.2: Commit**
|
||
|
||
```bash
|
||
git add README.md
|
||
git commit -m "docs(wpmc): mark S3 done in README roadmap"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 19: Final verification
|
||
|
||
**Files:** none
|
||
|
||
- [ ] **Step 19.1: Full PHPUnit run**
|
||
|
||
```bash
|
||
vendor/bin/phpunit
|
||
```
|
||
|
||
Expected: OK with all tests green, no risky/no warnings. Count expectation: 9 (S2) + 27 (S3) − overlaps = approximately **27 new tests** for S3 → ~36 total, ≥90 assertions.
|
||
|
||
- [ ] **Step 19.2: PHP lint every new/modified PHP file**
|
||
|
||
```bash
|
||
for f in includes/class-helpers.php includes/class-shortcode-dmr.php includes/wpmc-functions.php includes/class-plugin.php wp-multi-city.php tests/HelpersTest.php tests/ShortcodeDmrTest.php tests/PluginBootShortcodeDmrTest.php tests/TestCase.php; do
|
||
php -l "$f"
|
||
done
|
||
```
|
||
|
||
Expected: `No syntax errors detected` × 9.
|
||
|
||
- [ ] **Step 19.3: Working tree clean**
|
||
|
||
```bash
|
||
git status
|
||
```
|
||
|
||
Expected: `nothing to commit, working tree clean`. If a Yandex.Disk conflict copy appears (`* (2).php`), restore from HEAD as documented in S2's recovery note.
|
||
|
||
- [ ] **Step 19.4: Git log review**
|
||
|
||
```bash
|
||
git log --oneline | head -25
|
||
```
|
||
|
||
Expected to see the chain of S3 commits in order: spec → Helpers scaffold → get_term_by_slug → current_town → alt_get_field → remove_domain_link → ShortcodeDmr scaffold → register → render TDD pair → render edge cases → render cache lock → Plugin::boot wire → procedural API → README.
|
||
|
||
- [ ] **Step 19.5: Close beads task**
|
||
|
||
```bash
|
||
bd close cp-arq --reason "S3 done: wpmc_current_town/_get_term_by_slug/_alt_get_field/_remove_domain_link + [shortcode_dmr id=N] handler. ~27 new Brain Monkey tests. Plugin::boot wires ShortcodeDmr. Procedural API at wpmc-functions.php."
|
||
```
|
||
|
||
Expected: cp-arq closed. `bd ready` now lists `cp-83m` (S4) under epic `cp-cw4`.
|
||
|
||
---
|
||
|
||
## Acceptance map (spec → tasks)
|
||
|
||
| Spec requirement | Task(s) |
|
||
|---|---|
|
||
| `wpmc_current_town()` returns slug from URI match | 4, 5 |
|
||
| `wpmc_current_town()` fallback to `wpmc_main_town_slug` filter | 6 |
|
||
| `wpmc_current_town()` `wpmc_current_town_slug` final filter | 6 |
|
||
| `wpmc_current_town()` per-request memoization | 6 |
|
||
| `wpmc_get_term_by_slug()` with cache | 2, 3 |
|
||
| `[shortcode_dmr id=N]` returns text for matching row | 12, 13 |
|
||
| `[shortcode_dmr id=N]` returns '' on edge cases | 14 |
|
||
| `[shortcode_dmr id=N]` per-request cache | 15 |
|
||
| `wpmc_alt_get_field()` do_shortcode for strings, identity for arrays | 7, 8 |
|
||
| `wpmc_alt_get_field()` `wpmc_alt_get_field_post_id` filter | 8 |
|
||
| `wpmc_alt_get_field()` graceful when `get_field` missing | 8 |
|
||
| `wpmc_remove_domain_link()` strips scheme+host, preserves query+fragment | 9 |
|
||
| `wpmc_remove_domain_link()` no-op for mailto/tel/relative | 9 |
|
||
| `Plugin::boot()` wires `ShortcodeDmr::register()` | 16 |
|
||
| Procedural API loaded unconditionally from main file | 17 |
|
||
| README roadmap updated | 18 |
|
||
| Full green suite + lint + close beads | 19 |
|