Skip to content

Commit 2ea1ebe

Browse files
committed
feat!: migrate pre-1.0 installs to opt-in use_slug
The pre-1.0 plugin always used the assigned page's slug as the CPT URL base. As of 1.0.0 it's opt-in via the per-CPT use_slug option. Without a migration, existing sites would silently switch from /<page-slug>/<post> to /<cpt-default>/<post> on upgrade — breaking every published URL. Adds a Migrator that runs once on plugins_loaded (via Plugin::init): walks the page-for-CPT mapping and enables use_slug for every CPT that doesn't already have an explicit value. Tracked through a pfcpt_db_version option so it short-circuits on subsequent loads. Cleaned up by uninstall.php.
1 parent 773019c commit 2ea1ebe

6 files changed

Lines changed: 137 additions & 0 deletions

File tree

release-please-config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
{
1111
"type": "generic",
1212
"path": "plugin.php"
13+
},
14+
{
15+
"type": "generic",
16+
"path": "src/Lifecycle/Migrator.php"
1317
}
1418
]
1519
}

src/Container.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use n5s\PageForCustomPostType\Integration\WordPressSeo;
1616
use n5s\PageForCustomPostType\Integration\Wpml;
1717
use n5s\PageForCustomPostType\Lifecycle\LifecycleManager;
18+
use n5s\PageForCustomPostType\Lifecycle\Migrator;
1819
use n5s\PageForCustomPostType\PostType\PostType;
1920
use WP_Query;
2021
use wpdb;
@@ -59,6 +60,8 @@ public function __construct()
5960

6061
SettingsValidator::class => static fn (): SettingsValidator => new SettingsValidator(),
6162

63+
Migrator::class => static fn (): Migrator => new Migrator(),
64+
6265
Handler::class => fn (): Handler => new Handler(
6366
$this->get(Api::class)
6467
),

src/Lifecycle/Migrator.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace n5s\PageForCustomPostType\Lifecycle;
6+
7+
use n5s\PageForCustomPostType\Core\Api;
8+
9+
/**
10+
* Runs one-shot data migrations on plugin upgrade.
11+
*
12+
* The current schema version is stored in the `pfcpt_db_version` option.
13+
* Migrations short-circuit once that option is at or above the target.
14+
*/
15+
final class Migrator
16+
{
17+
private const DB_VERSION_OPTION = 'pfcpt_db_version';
18+
private const CURRENT_VERSION = '1.0.0'; // x-release-please-version
19+
20+
public function migrate(): void
21+
{
22+
$installed = get_option(self::DB_VERSION_OPTION, '');
23+
$installed = \is_string($installed) ? $installed : '';
24+
25+
if (version_compare($installed, self::CURRENT_VERSION, '>=')) {
26+
return;
27+
}
28+
29+
if ($installed === '') {
30+
$this->migrateToOptInUseSlug();
31+
}
32+
33+
update_option(self::DB_VERSION_OPTION, self::CURRENT_VERSION);
34+
}
35+
36+
/**
37+
* Pre-0.6.0 always used the assigned page's slug as the CPT URL base.
38+
* From 0.6.0 it's opt-in via the per-CPT use_slug option. Preserve the
39+
* old URLs for upgrading sites by enabling use_slug on every assigned
40+
* CPT that doesn't already have an explicit value.
41+
*/
42+
private function migrateToOptInUseSlug(): void
43+
{
44+
$mapping = get_option(Api::OPTION_PAGE_IDS, []);
45+
if (!\is_array($mapping)) {
46+
return;
47+
}
48+
49+
foreach (array_keys($mapping) as $postType) {
50+
if (!\is_string($postType) || $postType === '') {
51+
continue;
52+
}
53+
54+
$optionName = Api::OPTION_PREFIX . $postType . Api::OPTION_SUFFIX_USE_SLUG;
55+
if (get_option($optionName) === false) {
56+
update_option($optionName, '1');
57+
}
58+
}
59+
}
60+
}

src/Plugin.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use n5s\PageForCustomPostType\Integration\WordPressSeo;
1616
use n5s\PageForCustomPostType\Integration\Wpml;
1717
use n5s\PageForCustomPostType\Lifecycle\LifecycleManager;
18+
use n5s\PageForCustomPostType\Lifecycle\Migrator;
1819
use n5s\PageForCustomPostType\PostType\PostType;
1920

2021
/**
@@ -67,6 +68,7 @@ public function init(): self
6768
return $this;
6869
}
6970

71+
$this->container->get(Migrator::class)->migrate();
7072
$this->registerHooks();
7173
$this->initialized = true;
7274

tests/Integration/MigratorTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace n5s\PageForCustomPostType\Tests\Integration;
6+
7+
use n5s\PageForCustomPostType\Lifecycle\Migrator;
8+
use n5s\PageForCustomPostType\Tests\Fixtures\TestCase;
9+
10+
class MigratorTest extends TestCase
11+
{
12+
private Migrator $migrator;
13+
14+
protected function setUp(): void
15+
{
16+
parent::setUp();
17+
$this->migrator = new Migrator();
18+
}
19+
20+
public function testFreshInstallSetsDbVersionWithoutTouchingUseSlug(): void
21+
{
22+
delete_option('pfcpt_db_version');
23+
delete_option('pages_for_custom_post_type');
24+
delete_option('page_for_book_use_slug');
25+
26+
$this->migrator->migrate();
27+
28+
$this->assertNotEmpty(get_option('pfcpt_db_version'));
29+
$this->assertFalse(get_option('page_for_book_use_slug'));
30+
}
31+
32+
public function testUpgradeFromPreUseSlugEnablesUseSlugForAssignedCPTs(): void
33+
{
34+
$this->createFixtures();
35+
delete_option('pfcpt_db_version');
36+
delete_option('page_for_' . self::BOOK_POST_TYPE . '_use_slug');
37+
delete_option('page_for_' . self::BIKE_POST_TYPE . '_use_slug');
38+
39+
$this->migrator->migrate();
40+
41+
$this->assertEquals('1', get_option('page_for_' . self::BOOK_POST_TYPE . '_use_slug'));
42+
$this->assertEquals('1', get_option('page_for_' . self::BIKE_POST_TYPE . '_use_slug'));
43+
$this->assertNotEmpty(get_option('pfcpt_db_version'));
44+
}
45+
46+
public function testMigrationDoesNotOverwriteExplicitlySetUseSlug(): void
47+
{
48+
$this->createFixtures();
49+
delete_option('pfcpt_db_version');
50+
update_option('page_for_' . self::BOOK_POST_TYPE . '_use_slug', '0');
51+
52+
$this->migrator->migrate();
53+
54+
$this->assertEquals('0', get_option('page_for_' . self::BOOK_POST_TYPE . '_use_slug'));
55+
}
56+
57+
public function testMigrationIsIdempotent(): void
58+
{
59+
$this->createFixtures();
60+
update_option('pfcpt_db_version', '0.6.0');
61+
delete_option('page_for_' . self::BOOK_POST_TYPE . '_use_slug');
62+
63+
$this->migrator->migrate();
64+
65+
$this->assertFalse(get_option('page_for_' . self::BOOK_POST_TYPE . '_use_slug'));
66+
}
67+
}

uninstall.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
unset($pfcptMapping, $pfcptPostType);
2929

3030
delete_option('pages_for_custom_post_type');
31+
delete_option('pfcpt_db_version');
3132

3233
// Catch any orphaned rows in wp_options for CPTs no longer present in the
3334
// aggregated mapping (e.g. a CPT was unregistered without cleanup).

0 commit comments

Comments
 (0)