diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index ab43cc0..a3a3fe2 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -52,6 +52,8 @@ jobs: - test:wordpress-seo - test:polylang - test:autodescription + - test:wpml + - test:advanced-custom-fields include: - php: '8.2' wp: 'trunk' @@ -87,6 +89,8 @@ jobs: run: PLUGINS=wordpress-seo php -d pcov.enabled=1 vendor/bin/phpunit --testsuite=plugin-integration --coverage-php=build/coverage-wordpress-seo.cov - name: Coverage (autodescription) run: PLUGINS=autodescription php -d pcov.enabled=1 vendor/bin/phpunit --testsuite=plugin-integration --coverage-php=build/coverage-autodescription.cov + - name: Coverage (advanced-custom-fields) + run: PLUGINS=advanced-custom-fields php -d pcov.enabled=1 vendor/bin/phpunit --testsuite=plugin-integration --coverage-php=build/coverage-advanced-custom-fields.cov - name: Merge coverage run: | composer require --dev phpunit/phpcov --quiet diff --git a/composer.json b/composer.json index 04520e7..9029e6c 100644 --- a/composer.json +++ b/composer.json @@ -91,8 +91,10 @@ "@test:wordpress-seo", "@test:polylang", "@test:wpml", - "@test:autodescription" + "@test:autodescription", + "@test:advanced-custom-fields" ], + "test:advanced-custom-fields": "PLUGINS=advanced-custom-fields phpunit --testsuite=unit,integration,plugin-integration", "test:autodescription": "PLUGINS=autodescription phpunit --testsuite=unit,integration,plugin-integration", "test:core": "phpunit --testsuite=unit,integration", "test:coverage": "php -d pcov.enabled=1 phpunit --testsuite=unit,integration --coverage-text", diff --git a/src/Container.php b/src/Container.php index 311894e..1637912 100644 --- a/src/Container.php +++ b/src/Container.php @@ -10,6 +10,7 @@ use n5s\PageForCustomPostType\Core\RewriteManager; use n5s\PageForCustomPostType\Frontend\Handler; use n5s\PageForCustomPostType\Frontend\QueryFilter; +use n5s\PageForCustomPostType\Integration\AdvancedCustomFields; use n5s\PageForCustomPostType\Integration\Autodescription; use n5s\PageForCustomPostType\Integration\Polylang; use n5s\PageForCustomPostType\Integration\WordPressSeo; @@ -141,6 +142,9 @@ public function __construct() ), // Integration composites + AdvancedCustomFields\AdvancedCustomFields::class => fn (): AdvancedCustomFields\AdvancedCustomFields => new AdvancedCustomFields\AdvancedCustomFields( + $this->get(Api::class) + ), Polylang\Polylang::class => fn (): Polylang\Polylang => new Polylang\Polylang( $this->get(Polylang\UrlTranslation::class), $this->get(Polylang\Translation::class), diff --git a/src/Integration/AdvancedCustomFields/AdvancedCustomFields.php b/src/Integration/AdvancedCustomFields/AdvancedCustomFields.php index a2ae755..250177e 100644 --- a/src/Integration/AdvancedCustomFields/AdvancedCustomFields.php +++ b/src/Integration/AdvancedCustomFields/AdvancedCustomFields.php @@ -4,14 +4,86 @@ namespace n5s\PageForCustomPostType\Integration\AdvancedCustomFields; -add_action('acf/include_location_rules', static function (int $acfMajorVersion): void { - if ($acfMajorVersion !== 5) { - return; +use n5s\PageForCustomPostType\Core\Api; +use n5s\PageForCustomPostType\Integration\IntegrationInterface; + +/** + * Advanced Custom Fields integration. + * + * Extends the built-in `page_type` location rule with `_page` values so + * field groups can target PFCPT-bound pages. + */ +final class AdvancedCustomFields implements IntegrationInterface +{ + public function __construct( + private readonly Api $api + ) { + } + + public function isSupported(): bool + { + return \function_exists('acf_get_location_type'); + } + + public function registerHooks(): void + { + add_filter('acf/location/rule_values/type=page_type', [$this, 'addPageTypeValues'], 10, 2); + add_filter('acf/location/match_rule/type=page_type', [$this, 'matchPageType'], 10, 4); + } + + /** + * Add `_page` options to the Page Type rule values dropdown. + * + * @param array $values + * @param array $rule + * @return array + */ + public function addPageTypeValues(array $values, array $rule): array + { + foreach (array_keys($this->api->getPageIds()) as $postType) { + $postTypeObject = get_post_type_object($postType); + + if ($postTypeObject && \is_string($postTypeObject->labels->archives)) { + $values[$postType . '_page'] = $postTypeObject->labels->archives; + } + } + + return $values; } - require_once __DIR__ . '/LocationPageType.php'; + /** + * Match a `page_type == _page` rule against the current screen. + * + * @param array $rule + * @param array $screen + * @param array $fieldGroup + */ + public function matchPageType(bool $match, array $rule, array $screen, array $fieldGroup): bool + { + if ($match) { + return true; + } + + if (!isset($screen['post_id'])) { + return false; + } + + $post = get_post($screen['post_id']); + + if (!$post instanceof \WP_Post) { + return false; + } - $store = acf_get_store('location-types'); - $locationType = new LocationPageType(); - $store->set($locationType->name, $locationType); -}); + foreach ($this->api->getPageIds() as $postType => $pageId) { + if ($rule['value'] !== $postType . '_page') { + continue; + } + + $result = ($pageId === $post->ID); + + return ($rule['operator'] === '!=') ? !$result : $result; + } + + return false; + } +} diff --git a/src/Integration/AdvancedCustomFields/LocationPageType.php b/src/Integration/AdvancedCustomFields/LocationPageType.php deleted file mode 100644 index dd38a8e..0000000 --- a/src/Integration/AdvancedCustomFields/LocationPageType.php +++ /dev/null @@ -1,119 +0,0 @@ -api = Plugin::getInstance()->getApi(); - } - - /** - * Match the location rule against the current screen. - * - * @param array $rule - * @param array $screen - * @param array $fieldGroup - */ - // phpcs:ignore Syde.Functions.ArgumentTypeDeclaration.NoArgumentType - public function match($rule, $screen, $fieldGroup): bool - { - $match = parent::match($rule, $screen, $fieldGroup); - - if ($match) { - return $match; - } - - // Check screen args - if (!isset($screen['post_id'])) { - return false; - } - - $postId = $screen['post_id']; - - if (!\is_int($postId) && !$postId instanceof \WP_Post) { - return false; - } - - $post = get_post($postId); - - if (!$post instanceof \WP_Post) { - return false; - } - - $pageIds = $this->api->getPageIds(); - - if (empty($pageIds)) { - return false; - } - - $result = null; - - foreach ($pageIds as $postType => $pageId) { - if ($rule['value'] === $postType . '_page') { - $result = ($pageId === $post->ID); - break; - } - } - - if ($result === null) { - return false; - } - - // Reverse result for "!=" operator - if ($rule['operator'] === '!=') { - return !$result; - } - - return $result; - } - - /** - * Get available values for the location rule. - * - * @param array $rule - * @return array - */ - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps,Syde.Functions.ArgumentTypeDeclaration.NoArgumentType - public function get_values($rule): array - { - $parentValues = parent::get_values($rule); - - $values = []; - - foreach ($parentValues as $key => $value) { - if (\is_string($key) && \is_string($value)) { - $values[$key] = $value; - } - } - - $postTypes = array_keys($this->api->getPageIds()); - - if (empty($postTypes)) { - return $values; - } - - foreach ($postTypes as $postType) { - $postTypeObject = get_post_type_object($postType); - - if ($postTypeObject && \is_string($postTypeObject->labels->archives)) { - $values[$postType . '_page'] = $postTypeObject->labels->archives; - } - } - - return $values; - } -} diff --git a/src/Plugin.php b/src/Plugin.php index d2abcae..59e6074 100755 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -9,6 +9,7 @@ use n5s\PageForCustomPostType\Core\RewriteManager; use n5s\PageForCustomPostType\Frontend\Handler; use n5s\PageForCustomPostType\Frontend\QueryFilter; +use n5s\PageForCustomPostType\Integration\AdvancedCustomFields; use n5s\PageForCustomPostType\Integration\Autodescription; use n5s\PageForCustomPostType\Integration\IntegrationInterface; use n5s\PageForCustomPostType\Integration\Polylang; @@ -184,6 +185,7 @@ public function onTemplateRedirect(): void public function getIntegrations(): array { return [ + AdvancedCustomFields\AdvancedCustomFields::class, Polylang\Polylang::class, WordPressSeo\WordPressSeo::class, Wpml\Wpml::class, diff --git a/tests/Integration/Integration/AdvancedCustomFieldsTest.php b/tests/Integration/Integration/AdvancedCustomFieldsTest.php new file mode 100644 index 0000000..ca25c76 --- /dev/null +++ b/tests/Integration/Integration/AdvancedCustomFieldsTest.php @@ -0,0 +1,146 @@ +_page` values on the Page Type location rule. + */ +#[RequiresFunction('acf_get_location_type')] +class AdvancedCustomFieldsTest extends TestCase +{ + private const RULE_TYPE = 'page_type'; + + protected function setUp(): void + { + if (!\function_exists('acf_get_location_type')) { + $this->markTestSkipped('Advanced Custom Fields is not installed.'); + } + + parent::setUp(); + $this->createFixtures(); + $this->configureStaticFrontPage(); + } + + public function testIsSupported(): void + { + $acf = new AdvancedCustomFields(new Api()); + + $this->assertTrue($acf->isSupported()); + } + + public function testRegisterHooksAttachesLocationFilters(): void + { + $acf = new AdvancedCustomFields(new Api()); + $acf->registerHooks(); + + $this->assertNotFalse(has_filter('acf/location/rule_values/type=page_type', [$acf, 'addPageTypeValues'])); + $this->assertNotFalse(has_filter('acf/location/match_rule/type=page_type', [$acf, 'matchPageType'])); + } + + public function testPageTypeValuesIncludeCustomPostTypePages(): void + { + $values = $this->applyValuesFilter(); + + $this->assertArrayHasKey('book_page', $values); + $this->assertArrayHasKey('bike_page', $values); + } + + public function testPageTypeValuesPreserveCoreOptions(): void + { + $values = $this->applyValuesFilter(); + + foreach (['front_page', 'posts_page', 'top_level', 'parent', 'child'] as $key) { + $this->assertArrayHasKey($key, $values); + } + } + + public function testMatchRuleReturnsTrueForBoundPage(): void + { + $matched = $this->applyMatchFilter('==', 'book_page', $this->homeForBookId); + + $this->assertTrue($matched); + } + + public function testMatchRuleReturnsFalseForUnrelatedPage(): void + { + $matched = $this->applyMatchFilter('==', 'book_page', $this->staticFrontPageId); + + $this->assertFalse($matched); + } + + public function testMatchRuleInvertsForNotEqualsOperator(): void + { + $matched = $this->applyMatchFilter('!=', 'book_page', $this->homeForBookId); + + $this->assertFalse($matched); + } + + public function testMatchRuleShortCircuitsWhenCoreAlreadyMatched(): void + { + $matched = acf_match_location_rule( + ['param' => self::RULE_TYPE, 'operator' => '==', 'value' => 'front_page'], + ['post_id' => $this->staticFrontPageId], + [] + ); + + $this->assertTrue($matched); + } + + public function testMatchRuleReturnsFalseWhenPostIdMissing(): void + { + $matched = acf_match_location_rule( + ['param' => self::RULE_TYPE, 'operator' => '==', 'value' => 'book_page'], + [], + [] + ); + + $this->assertFalse($matched); + } + + public function testMatchRuleReturnsFalseWhenPostDoesNotExist(): void + { + // get_post() returns null for a non-existent ID. + $matched = $this->applyMatchFilter('==', 'book_page', 999999); + + $this->assertFalse($matched); + } + + public function testMatchRuleReturnsFalseForUnknownPostTypeValue(): void + { + $matched = $this->applyMatchFilter('==', 'movie_page', $this->homeForBookId); + + $this->assertFalse($matched); + } + + /** + * @return array + */ + private function applyValuesFilter(): array + { + return acf_get_location_rule_values([ + 'param' => self::RULE_TYPE, + 'operator' => '==', + 'value' => '', + ]); + } + + private function applyMatchFilter(string $operator, string $value, int $postId): bool + { + return acf_match_location_rule( + ['param' => self::RULE_TYPE, 'operator' => $operator, 'value' => $value], + ['post_id' => $postId], + [] + ); + } +} diff --git a/tests/Integration/PluginTest.php b/tests/Integration/PluginTest.php index a59c3f7..cfc7ced 100644 --- a/tests/Integration/PluginTest.php +++ b/tests/Integration/PluginTest.php @@ -7,6 +7,7 @@ use n5s\PageForCustomPostType\Container; use n5s\PageForCustomPostType\Core\Api; use n5s\PageForCustomPostType\Core\RewriteManager; +use n5s\PageForCustomPostType\Integration\AdvancedCustomFields\AdvancedCustomFields; use n5s\PageForCustomPostType\Integration\IntegrationInterface; use n5s\PageForCustomPostType\Plugin; use n5s\PageForCustomPostType\Tests\Fixtures\TestCase; @@ -62,6 +63,7 @@ public function testGetIntegrationsReturnsArrayOfIntegrationClassStrings(): void $this->assertIsArray($integrations); $this->assertNotEmpty($integrations); + $this->assertContains(AdvancedCustomFields::class, $integrations); foreach ($integrations as $integration) { $this->assertIsString($integration); diff --git a/tests/Unit/ContainerTest.php b/tests/Unit/ContainerTest.php index 8f1166b..fbc9031 100644 --- a/tests/Unit/ContainerTest.php +++ b/tests/Unit/ContainerTest.php @@ -8,6 +8,7 @@ use n5s\PageForCustomPostType\Container; use n5s\PageForCustomPostType\Core\Api; use n5s\PageForCustomPostType\Core\RewriteManager; +use n5s\PageForCustomPostType\Integration\AdvancedCustomFields\AdvancedCustomFields; use n5s\PageForCustomPostType\Tests\Fixtures\TestCase; class ContainerTest extends TestCase @@ -27,6 +28,13 @@ public function testGetReturnsCorrectTypeForApi(): void $this->assertInstanceOf(Api::class, $service); } + public function testGetReturnsCorrectTypeForAdvancedCustomFieldsIntegration(): void + { + $service = $this->container->get(AdvancedCustomFields::class); + + $this->assertInstanceOf(AdvancedCustomFields::class, $service); + } + public function testGetReturnsSameInstanceOnRepeatedCalls(): void { $first = $this->container->get(Api::class); @@ -39,6 +47,7 @@ public function testHasReturnsTrueForKnownServices(): void { $this->assertTrue($this->container->has(Api::class)); $this->assertTrue($this->container->has(RewriteManager::class)); + $this->assertTrue($this->container->has(AdvancedCustomFields::class)); } public function testHasReturnsFalseForUnknownServices(): void diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 46d8721..445b5dd 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -20,6 +20,7 @@ 'wordpress-seo' => 'wordpress-seo/wp-seo.php', 'polylang' => 'polylang/polylang.php', 'autodescription' => 'autodescription/autodescription.php', + 'advanced-custom-fields' => 'advanced-custom-fields/acf.php', ]; $requestedPlugins = array_filter(explode(',', getenv('PLUGINS') ?: '')); $plugins = array_values(array_filter(array_map(static function (string $p) use ($availablePlugins): ?string {